简介:《我看见的世界》是李飞飞(Fei-Fei Li)亲笔撰写的AI主题著作,这份PDF电子书面向人工智能爱好者、科技从业者及关注技术社会影响的读者,以作者个人视角串联起机器学习、深度学习等核心概念。资源包共1个PDF文件,大小约4.39MB,含完整目录、版权页及正文,适合在电脑或移动设备上直接阅读。已有8662人学习下载。书中以2018年李飞飞赴美国国会作证的真实经历开篇,展现科学家面对AI伦理议题的紧张与担当,并由此延伸出AI定义、发展历史、伦理政策、斯坦福教育实践及医疗交通等应用案例。读者既能从中感受一位移民科学家从求学到引领AI研究的成长历程,也能获得关于AI现状、社会影响与未来趋势的深度认知,是一份兼具专业洞察与人文温度的阅读资源。
1. 从一份编号 PDF 读起:我看见的世界,首先是一道数据题
资料目录里出现【R213】The Worlds I See 我看见的世界.pdf 这样的文件时,大多数人习惯双击打开读两页;我习惯先问一句:它能不能被程序稳定解析。这是一位华人计算机视觉学者写的个人叙事,书名中英成对,文件里还夹着归档编号,但 PDF 内部是什么状态——有没有文本层、字体嵌没嵌、元数据和文件名一不一致——不打开看永远不知道。把一份 PDF 从“能打开”推进到“能检索、能抽取、能归档”,是整个知识库建设里最磨人也最容易被低估的一步。下面把这类编号书单 PDF 的检查、抽文、归档和排错流程完整拆开。
2. 为什么 PDF 是“伪结构化”档案:先搞懂它到底存了什么
PDF 是文档领域里最特殊的常见格式。你以为是“一页书”,程序看到的是一串对象和流;你以为有“标题、段落、列表”, PDF 里多数时候只有坐标、字体和绘制指令。它不是一份可编辑文档,而是一份把排版结果定格下来的快照。处理《我看见的世界》这类中英双语的电子书 PDF 时尤其明显:英文原版和中文译本可能是同一套排版字体,也可能中文部分直接用了未嵌入的 CID 字体,所有解析问题都从这层“伪结构化”开始。
2.1 PDF 的三层物理结构:对象、内容流、字符映射
从物理层面看,一份标准 PDF 至少由三层组成。最底层是语法层,文件被切成一个个 object,有数字编号,有类型,内容存放在 stream 里;中间层是页面内容流,里面是BT/ET、Tj/TJ、Tm这类绘图操作符,它们负责描述“在哪个坐标、用哪个字体、画什么字”;最上层才是语义层,由字体对象、字符编码表和 ToUnicode CMap 组成,负责把内容流里的字符编码映射成真实 Unicode 文字。
我拿到文件会先把这三层分开看。文本提取看起来是读取文字,实际是顺着“页面 → 内容流 → 字体 → CMap → Unicode”这条链路逐层打通。很多 PDF 打开时显示正常,却抽不出文本,就是因为显示只依赖前两层,而提取依赖第三层。中英混排的书更容易踩中这个问题:中文部分如果用了Type0字体却没有完整的 ToUnicode 表,阅读器靠字形轮廓照样渲染,抽取程序却拿不到任何有效字符。这不是工具不行,是文件本身缺了语义层。
2.2 五条命令给 PDF 做“体检”:先看再抽,别一上来就解析
在写任何提取脚本前,我习惯先用一组工具把文件底细摸清楚。五条命令就能覆盖大多数判断。
# 1) 基本信息:页数、PDF 版本、页面尺寸、是否带标签 pdfinfo "R213_The_Worlds_I_See.pdf" # 2) 字体清单:看中文字体是否嵌入、是否有 ToUnicode 映射 pdffonts "R213_The_Worlds_I_See.pdf" # 3) 结构健康度:检查交叉引用表和对象流是否损坏 qpdf --check "R213_The_Worlds_I_See.pdf" # 4) 图像资源:统计扫描图数量,判断是否混入图片层 pdfimages -list "R213_The_Worlds_I_See.pdf" # 5) 元数据:看 Title、Author 字段与文件名是否一致 exiftool "R213_The_Worlds_I_See.pdf"这五条的输出各看一个侧面。pdfinfo最该看的是页数和页面尺寸,如果一本书英文版和中文版页数差异很大,很可能中文版是重新排版的;pdffonts看的是字体列里有没有(emb)标记,没有emb就说明字体未嵌入,换一台机器显示和提取都可能变样;qpdf --check最直接,输出里有CHECK OK就说明结构健康;pdfimages -list用来分辨它是不是扫描版,统计出的图像数量如果接近页数,说明这本书大概率没有文本层,后面得走 OCR;exiftool看元数据,但要注意,文件名的作者字段和内部Author字段经常不是一回事。
我用表把这五条命令对应到一个快速判断逻辑上,实际处理时不用每条都跑,按怀疑方向挑着看就行。
| 命令 | 核心输出 | 判断结论 |
|---|---|---|
pdfinfo | Pages、Page size | 页数异常则可能中英版本混排 |
pdffonts | emb 标记、CMap 信息 | 字体未嵌入则提取部署环境会翻车 |
qpdf --check | 对象结构错误列表 | 有错误先修复再抽文本 |
pdfimages -list | 图像数量、尺寸 | 图片数约等于页数则该走 OCR |
exiftool | Title、Author、Producer | 元数据与文件名不一致则要建映射 |
2.3 归档编号【R213】应该放在哪一层,而不是文件名里
我注意到这类文件命名喜欢把编号、中英文名、作者字段全拼进文件名,例如【R213】The Worlds I See 我看见的世界.pdf。这种命名有个隐患:文件名是给人看的,不是给程序看的。方括号在 shell 里有特殊含义,作者字段会随文件重命名丢失,编号和内容没有建立稳定关联。常见做法是把【R213】作为外部索引主键,放进一个独立的元数据表里,而不是依赖文件名本身。
我更推荐在文档库目录下维护一份R213_meta.csv,字段至少包括doc_id、file_name、title_en、title_zh、author、pages、source_path。这样即使文件在团队协同时被重命名、移动、压缩,编号和内容的关系仍然可追踪。至于 PDF 内部的元数据,能不动就不动,盲目用工具改写 Title 字段可能破坏数字签名,这是一条容易被忽略的边界。
3. 从双语 PDF 里稳定抽正文:像拆电路一样做逐块提取
体检通过之后才进入提取阶段。对【R213】这种书,目标不是把整本书倒成一大段文字,而是稳定、可回溯地抽到每一页的有效内容。PDF 文本提取有两条主流路线,选哪条取决于你要的是“快”还是“可控”。
3.1 两条提取路线:poppler 与 PyMuPDF 的取舍
poppler 是命令行工具集,pdftotext一条命令就能出全文,参数简单,适合批量预览;PyMuPDF 是 Python 库,能拿到页面块级结构、坐标、字体信息,适合需要精细控制的场景。我自己的习惯是先用pdftotext -layout跑一遍,看清这本书的文本层质量,再决定要不要写 PyMuPDF 脚本。两者的核心差异在排序和粒度。
| 维度 | pdftotext | PyMuPDF |
|---|---|---|
| 上手成本 | 一条命令 | 约 20 行脚本 |
| 输出粒度 | 按页输出纯文本 | 按 block / line / word 输出 |
| 页码坐标 | 不保留 | 每个块都带 bbox 坐标 |
| 双栏排版 | 依赖-layout参数 | 可由sort=True控制 |
| 适合场景 | 快速探查文本层 | 结构化归档、切章、溯源 |
3.2 最小提取脚本:保留页码锚点的按块抽取
我用 PyMuPDF 跑过很多次这类双语电子书,下面这个脚本是我会保留在工具库里的最小版本。它的核心不是“把文字拿出来”,而是“把文字按块拿准”。
import fitz # PyMuPDF doc = fitz.open("R213_The_Worlds_I_See.pdf") out_lines = [] for page_no in range(len(doc)): page = doc[page_no] # sort=True 让块按自上而下、自左而右排序,双栏书也能基本还原阅读顺序 blocks = page.get_text("blocks", sort=True) for block in blocks: x0, y0, x1, y1, text, block_no, block_type = block if block_type != 0: # 0 文本块,1 图片块,2 其他 continue if y0 < 60 or y1 > page.rect.height - 60: continue # 过滤页眉页脚 text = " ".join(text.split()) # 压缩块内多余换行 out_lines.append(f"[p{page_no + 1}] {text}") with open("extracted_text.txt", "w", encoding="utf-8") as fp: fp.write("\n".join(out_lines))这里有两个参数值得说清楚。sort=True控制块的排序顺序,PDF 页面里的文字块本来没有天然顺序,这个参数会按坐标做一次排序,对双语双栏的书效果最明显;坐标过滤条件y0 < 60和y1 > page.rect.height - 60是经验值,专门对付页眉页码。如果这本书的页眉文字位置偏中间,不要死守 60,可以先跑一遍看坐标分布再调。
3.3 双语混排的三个处理细节:顺序、空白、断词
中英混排的书在提取时比单一语言麻烦在三个点。第一是顺序:中文段落和英文段落可能左右分栏,也可能上下分栏,sort=True只能处理相对规整的排版,遇到嵌套混排还是要按坐标手动分桶;第二是空白:英文单词被 PDF 内容流拆成字符后,简单压缩空格会把The Worlds I See拼成TheWorldsISee,所以" ".join(text.split())里的空格处理其实是分语言做的,中文行可以紧凑拼接,英文行必须保留单词间隔;第三是断词:中文标点全角、英文标点半角,在行尾换行时容易出现半截标点,后续做章节切分时要注意按语言分别清洗。
4. 把三百页的书拆成可检索文档库:目录树、章节化 Markdown 与索引表
文本抽出来只是第一步,真正有落地价值的是把这份抽文结果组织成一个团队可复用的文档库。对【R213】来说,最终产物不是一份extracted_text.txt,而是一个目录结构清晰、每章可独立检索、每段能回溯页码的资料库。
4.1 目录树先按编号建,不要按书名建
我在资料库里习惯用编号做第一层目录。book_library/R213/下面放五个东西:raw/存原始 PDF,text/存全量提取文本,chapters/存章节化后的 Markdown,index/存索引表,meta/存元数据信息。为什么不用书名?因为同一本书可能有英文原版、中译本、影印版,按书名建目录会分裂成好几处,按编号建目录则天然聚合。
book_library/ └── R213/ ├── raw/R213_The_Worlds_I_See.pdf ├── text/extracted_text.txt ├── chapters/R213_ch01_扉页.md ├── chapters/R213_ch02_序言.md ├── chapters/R213_ch03_正文前段.md ├── index/R213_chapter_index.csv └── meta/R213_meta.csv这套结构的优点是任何人接手时,只看目录就知道哪个文件是原始素材、哪个是加工产物。正文里写得再花哨的 PDF,落到这个结构里都只剩一个编号加一堆可追溯的文本文件。
4.2 按 PDF 自带目录切章节,落成带页码锚点的 Markdown
很多电子书 PDF 内部本来就带书签目录,PyMuPDF 可以直接读取。下面的脚本用get_toc()拿到章节层级,再按章节切分文本块,输出带页码锚点的 Markdown 文件。
import fitz from pathlib import Path PDF_PATH = Path("book_library/R213/raw/R213_The_Worlds_I_See.pdf") OUT_DIR = Path("book_library/R213/chapters") OUT_DIR.mkdir(parents=True, exist_ok=True) doc = fitz.open(PDF_PATH) toc = doc.get_toc() # 条目结构:(层级, 标题, 页码) # 把“页码”转成“页面索引”,PDF 书签页码从 1 开始 items = [(level, title, page - 1) for level, title, page in toc] for i, (level, title, start_page) in enumerate(items): end_page = items[i + 1][2] if i + 1 < len(items) else len(doc) lines = [f"# {title}", f"> 来源页:p{start_page + 1} - p{end_page}", ""] for pno in range(start_page, end_page): page_text = doc[pno].get_text("text") lines.append(page_text) fname = f"R213_ch{i + 1:02d}_{title[:12]}.md" (OUT_DIR / fname).write_text("\n".join(lines), encoding="utf-8")这段脚本的关键在get_toc()拿到的页码是 PDF 内部定义的书签页码,不是文件物理页码,两者常有偏移,一定要先做page - 1的转换。切章边界来自目录,如果 PDF 本身没有书签,就需要退回到按页文本密度来估计章节位置,代码逻辑就不一样了。
4.3 索引表兜住所有“对不上”的情况
切出来的章节一定会和原书目录有偏差,这不奇怪,很多电子书的书签页码是错的。我通常在生成章节的同时落一张R213_chapter_index.csv,把每个章节文件的文件名、起始页码、结束页码、章节标题记录下来。这张表以后有两个用处:一是快速回答“这句话在第几页”,二是排查书签偏移时不用重新跑全量提取。
doc_id,chapter_file,title,start_page,end_page R213,R213_ch01_扉页.md,扉页,1,3 R213,R213_ch02_序言.md,序言,4,9 R213,R213_ch03_正文前段.md,正文前段,10,86索引表一旦建立,后续所有检索、引用、溯源都以它为准。别再相信自己眼睛看到的页码,要以脚本打印出来的物理页为准,这是避坑的基础。
5. 四个高频翻车点:PDF 解析的踩坑现场与对策
处理【R213】这类双语 PDF 的过程里,我踩过不少坑,也帮人排过不少错,现象各异,根因大多集中在四类。
5.1 扫描版 PDF 直接抽文本,抽出来全是空白
现象:pdftotext输出文件只有零星几个字,甚至完全空白,但 PDF 用阅读器打开展示正常。原因:这份 PDF 本身就是整页扫描图,文字全在图像里,没有文本层。判定方法是用前面的pdfimages -list,如果图片数量接近页数,几乎可以确认是扫描版。解决:先转图像再 OCR,我一般用 Tesseract 加中文语言包,命令大致是tesseract page_001.png out -l chi_sim+eng。注意识别结果还是纯文本,没有坐标信息,所以 OCR 版的归档无法像原生文本版那样精确按块切章。
5.2 中文字体未嵌入,抽取出的汉字变成方框或乱码
现象:pdffonts里中文相关字体没有(emb)标记,抽取出来的内容不是“看见”,而是????或方框。原因:PDF 字体未嵌入,文本提取依赖字体内部的编码映射,映射缺失时程序只能拿到空码位。解决:先回到pdffonts看字体名,如果字体本身能识别,可以尝试qpdf --decrypt解开权限限制后再提取;如果内容流里的字符映射本身就是空的,那只能靠 OCR 兜底,这条没有后悔药。
5.3 页眉页脚混进正文,切章锚点整体错位
现象:切出来的章节 Markdown 顶部总是混着书名、页码,甚至把章标题都冲掉。原因:我在 3.2 脚本里用坐标过滤了页眉页脚,但不同书页页眉位置不同,一旦某页页眉正好压在正文块中间,过滤条件就失效了。解决:不要只靠单一y坐标阈值,先对整本书跑一遍块坐标分布,统计页眉页脚的 y 值范围,再按每一节的模式过滤。判断标准很简单:页眉在全书几百页里通常是同一位置,找出它们共同的 y 区间即可。
5.4 文件名里的【R213】在批处理脚本里惹出怪问题
现象:一段遍历*.pdf的 shell 脚本,跑到这本带方括号的 PDF 时要么找不到文件,要么传参被拆断。原因:方括号在 shell glob 里有字符类的含义,文件名里的[会让部分脚本把它当成模式而不是普通字符。解决:给文件路径加引号只是第一步,还得用find ... -print0配合while IFS= read -r -d ''处理文件名。我自己在这种场景下会把索引文件单独命名成R213_The_Worlds_I_See.pdf,去掉方括号,同时在元数据表里保留原始文件名,两边互不干扰。
6. 让这份 PDF 真正进入团队知识库:切块、溯源与最后一公里
文本抽完、章节切完,资料库已经可以用了。但如果要给检索或问答系统用,还差最后一步:把章节文本切成语义完整的块,并给每个块打上来源标记。
6.1 按章节边界切块,而不是按固定 token 数切
很多检索系统默认按定长切块,比如每 500 字一刀。这对技术手册勉强可用,但对《我看见的世界》这类带有叙事性的书是场灾难——一个完整的故事被硬生生切断,检索时前后文拼不起来。我习惯按章节边界切,再用段落做二次分块。
from pathlib import Path chapter_dir = Path("book_library/R213/chapters") for md_file in sorted(chapter_dir.glob("R213_ch*.md")): text = md_file.read_text(encoding="utf-8") blocks = text.split("\n\n") # 按空行切段 chunk, chunk_size = [], 0 for block in blocks: block_len = len(block.strip()) if chunk_size + block_len > 800 and chunk: # 一段完整语料落袋 print(f"{md_file.name}: {''.join(chunk)[:30]}...") chunk, chunk_size = [], 0 chunk.append(block) chunk_size += block_len这里的 800 只是一个建议值,叙事类文本我一般控制在 600 到 1000 字之间,太长检索不精准,太短上下文不全。关键是切块逻辑必须尊重原文的空行标记,空行往往是段落边界,也是语义边界。
6.2 每个块都带上一张“来源身份证”
切出来的块如果只有正文,检索到了也不知道出处。我的做法是在每块开头加三行元数据:章节文件名、页码区间、原书标题。这样下游无论接数据库还是接大模型检索,回答问题时都能溯源到具体位置。习惯上我更愿意在归档阶段多写几行字段,而不是等翻车后再回查。以前我图省事做过一次无标记切块,结果一条错误引用追着改了三轮,从那以后,所有块都带来源标记就成了固定动作。
一位熟悉 PDF 处理的朋友说过一句话:PDF 解析不是技术题,是耐心题。大多数问题都能靠“先体检、再提取、后归档”的流程挡在门外。这套流程跑顺之后,收益是长期的。希望帮到你。
本文还有配套的精品资源,点击获取