前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9275
  • Agent工具调用的安全防护要点
  • 搜索延迟拆解:毫秒级预算分配实战
  • 推理模型延迟预算分析
  • 推理 Effort 档位实际控制什么
  • 推理模型性价比曲线:如何算清每分正确率成本
  • 为什么推理能力在数学好过代码好过文本
  • 通过所有测试的代码仍可能让AI代理崩溃
  • Cursor 修 IDOR 易漏同文件其他路由
  • 可信数据是AI Agent规模化的关键瓶颈
  • Anthropic为Agent引入梦境推理能力引热议
  • Python 中缓存 LLM 响应:细节决定成败
  • Harness Engineering:打造 AI 编程助手的工作环境
  • 加向量数据库前,先用 NumPy 搞定 Embedding
  • 自实现令牌桶限流:比等 429 更优
  • 长时 AI 任务用后台任务队列:状态机比框架重要
  • asyncio 并发调用 40 个 LLM:代码少但坑多
  • AI 项目中 API 密钥的泄密风险与防护层级
  • 电话Agent工程实践:SIP/WebRTC音频路径全解析
  • pgvector索引调优:HNSW与IVFFlat成本精确分析
  • 多租户应用按客户计费:成本与可计费用量必须分开
  • PDF 解析难点深度剖析:字符间距阈值是万恶之源
  • Hugging Face开源OlmoEarth文本嵌入
  • 阿里开源 Qwen3.8:2.4T MoE、激活 95B、256K 上下文
  • MiniMax H3:单个 Transformer 替代视频生成完整流水线
  • DeepSeek V4 Pro 正式版发布:多项测试接近 Fable 5 水平
  • AI编程助手Lovable完成新一轮4亿美元融资,估值达133亿美元
  • MiniMax Music 3.0:开放权重生产级音乐生成模型
  • Microsoft 发布 MindTopo:VLMs 空间推理能力新基准
  • Grok 4.6 发布:剑指 GPT-5.6 Sol,主打长时间 Agent 任务
  • Grok 4.6 中文详解:训练数据、Agent 能力边界与定价
  • 市场份额报告:Google Gemini 份额从 12% 跌至 1.9%
  • 用Embeddings+Reranking+LLM构建可靠的内容分类流水线
  • 推理冷启动从10分钟降至秒级:容器镜像瘦身实战
  • Anthropic研究揭示:Claude Code旧版权限提示97%被机械通过
  • DeepSeek-V4-Pro-0813 悄然上线,支持思考与非思考模式
  • AI 生图 Prompt 审核实战:Node.js 调用 Chat JSON Schema 方案
  • 企业AI分析的隐藏陷阱:语义漂移问题深度剖析
  • AI 正在消除软件工程中层:代码看不懂、没人负责的团队困境
  • Qwen3.8-2.4T-A95B 模型发布
  • TraceMotive:本地优先的AI代理执行追踪调试工具
  • AI编程工具正在离开IDE:终端原生Agent工作流崛起
  • 个人开发者用AI编程的项目架构经验
  • FastAPI五个安全漏洞发现与修复全过程
  • 长文档AI审核的审计设计:Map-Reduce优于检索增强
  • AI Agent辅助发现SharePoint RCE漏洞链(CVSS 9.1)
  • 我用Claude Code将API的P99延迟降低一半
  • AI Agent读了你的secrets并删了生产数据库——PocketOS事故详解
  • 用聊天模型做金融内容审核:结构化输出设计实践
  • Prompt注入攻击原理与防御实践指南
  • 大规模漏洞扫描活动泛滥,攻击者冒充ClaudeBot等AI爬虫
  • 谷歌DeepMind发布手语转文本模型SL2T
  • 已加载 51 / 9275
8.0
热点
AI SCORE
技术实践2026-08-13 00:30

PDF 解析难点深度剖析:字符间距阈值是万恶之源

dev.to · AI#PDF#文本解析#底层原理
Editor brief · 编辑速览

PDF 内部是绘制指令序列,文件中根本没有「Invoice」这个词,只有 Inv、kerning 参数、oice 的拼接——解析器必须靠阈值判断是单词内间距还是单词间空格,而这个阈值必然有失败案例。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

PDF 解析:为何它依然困难

PDF 提取之所以困难,不是因为解析器做得差,而是因为你想要的东西——按顺序排列的文本、成段的段落——根本不是文件里实际存放的内容,而且在很多文档中,这些信息根本无法精确还原。

文件里实际有什么

PDF 页面内容流是一系列绘图操作符,定义在 ISO 32000-1 中。涉及文本的操作符数量很少:BT 开启一个文本对象,Tf 选择字体和字号,Td 和 Tm 定位文本矩阵,Tj 和 TJ 输出字符串,ET 结束对象。一行文档的输出大概长这样:

BT
  /F1 11 Tf
  72 720 Td
  [(Inv) 20 (oice) -250 (T) 80 (otal:)] TJ
  180 0 Td
  (1,240.00) Tj
ET

仔细看这段代码,因为 PDF 提取的一切问题都从中而来。文件里并没有"Invoice"这个单词——只有 Inv,然后是一个 20 单位字距调整,再是 oice。TJ 数组里的数字让笔尖在 em 的千分比上向后移动,以收紧字母对。解析器必须对每一个调整做出判断:它是单词内部的字距调整,还是单词之间的空格,但文件本身并没有说明这一点。这个判断依赖一个阈值,而阈值在两个方向上都存在失败案例:阈值太低会得到"Inv oice",太高则得到"InvoiceTotal"。

"Total:"和"1,240.00"之间也没有空格字符,只有一次 Td 将笔尖向右移动了 180 单位。这个间隙究竟是单个空格、制表位,还是两个表格列之间的边界,完全是从几何位置推断出来的。

ToUnicode 问题

Tj 字符串中的字节不是 Unicode,而是字符码,需要到所选字体的编码表中查找。对于嵌入的字体子集来说,这个编码表是生成文档的应用程序自行决定的——通常是只包含文档所用字形的密集编号,所以码值 1 可能对应的是任意一个碰巧最先出现的字母。

PDF 可能携带一个 /ToUnicode CMap,将这些码值映射回 Unicode。但这是可选的。当它缺失时——在使用老版本 TeX 工具链、某些 CAD 和报表工具,以及经过重写器处理过的文件中很常见——提取过程没有任何方式可以还原文本。pdfminer.six 会明确标注这一点,对无法映射的码值输出 (cid:34) 风格的标记;其他库可能直接返回原始码点,解码后看起来像合理的乱码,没有任何"这是文本吗"的断言能将其捕获,除非你检查字母分布。

连字是同样问题的微缩版本。格式正确的 /ToUnicode 会将 fi 连字字形映射到 U+FB01,这是一个真实存在的字符,你的分词器、搜索引擎和去重哈希都会将其视为与"fi"不同的内容。用 unicodedata.normalize("NFKC", text) 规范化可以将它分解回两个 ASCII 字母,这就是为什么规范形式应该在提取后立即应用,而不是在查询时才处理。

为此问题值得建立一个诊断方法,因为症状太容易被忽略。取一页提取出的文本,统计其中有多少字符落在你实际使用语言字符集之外,然后与空格字符的比例做比较。字体映射损坏的页面通常空格比例看起来正常——定位是准确的,只是码值到字符的映射出了问题——但字母分布异常,出现真实单词中永远不会出现的字母序列。这个组合比任何基于长度的检查都更能检测出问题,而且它是唯一能捕获"缺失的映射产生了字母而非 cid 标记"这种情况的检测方式。

阅读顺序并没有被存储

内容流按照生成器绘制它们的顺序输出。对于单栏报告来说,这通常是自上而下。但对于双栏论文、杂志排版,或者任何包含提要引用和侧栏的文档,顺序往往并非如此,简单的提取会交错地逐行输出各栏内容——产生局部可读但全局无意义的文本。这是最坏的结果,因为根本没有任何检测机制会发现它。

Tagged PDF(PDF/UA,以及 ISO 32000 第 14.7 节中的结构树)确实携带了逻辑顺序,而且当文档拥有它时,这个顺序是权威的。但大多数文档没有。其他一切都是几何重建:通过位置将文本片段聚合成块,对块排序,对块内的行排序。PyMuPDF 正是通过 page.get_text("dict") 暴露了这些信息,它返回块 → 行 → 片段,每个层级都有边界框,sort=True 标志让纯文本调用按位置排序而非按绘制顺序。

import fitz            # PyMuPDF

def two_column_aware(page, gutter_ratio=0.45):
    """Split blocks by which half of the page they start in, then read
    the left column top-to-bottom before the right one."""
    width = page.rect.width
    blocks = page.get_text("blocks")     # (x0,y0,x1,y1,text,bno,btype)
    left  = [b for b in blocks if b[0] < width * gutter_ratio]
    right = [b for b in blocks if b[0] >= width * gutter_ratio]
    order = sorted(left, key=lambda b: b[1]) + sorted(right, key=lambda b: b[1])
    return "\n".join(b[4] for b in order if b[6] == 0)   # 0 = text block

这个启发式方法很粗糙,但对于大量学术 PDF 来说已经足够了。关键在于,无论你是否意识到,你都在写布局启发式规则;把它写出来意味着当它出错时你可以调优它。

四种策略及其各自的失败场景

这些策略组合使用比相互竞争效果更好。在混合语料上存活的模式是级联:先尝试文本层,运行摄入检查中的提取断言,只对那些未能通过的文档才降级到 OCR 或视觉模型。这样一来,对于数字原生的主流文档,每页成本接近于零。

一个你可以运行的评测

其他地方发布的解析器排名都是关于别人文档的结果。构建一个由二三十页到五十页困难页面组成的金标准集——每种你实际见过的失败模式各一页——手动转录一次,然后用它来评分候选解析器:

import jiwer, json, pathlib

def score(parsers: dict, gold_dir="gold"):
    # gold/<name>.pdf next to gold/<name>.txt, transcribed by a human
    rows = []
    for txt in pathlib.Path(gold_dir).glob("*.txt"):
        pdf = txt.with_suffix(".pdf")
        want = jiwer.Compose([jiwer.ToLowerCase(),
                              jiwer.RemoveMultipleSpaces(),
                              jiwer.Strip()])(txt.read_text())
        for name, fn in parsers.items():
            got = want.__class__ and jiwer.Compose([
                jiwer.ToLowerCase(), jiwer.RemoveMultipleSpaces(), jiwer.Strip()
            ])(fn(pdf))
            rows.append({"doc": txt.stem, "parser": name,
                         "cer": jiwer.cer(want, got),
                         "wer": jiwer.wer(want, got)})
    print(json.dumps(rows, indent=1))

字符错误率是首要指标,因为词错误率会双重惩罚第一节中的空格插入决策——一次作为删除,一次作为插入——而一个每个字母都对但部分空格有误的解析器比这个数字所暗示的要有用得多。五十页手动转录的页面是一天的工作量,但它取代了所有关于解析器的争论,以一张表格呈现。

如果级联的最后一级是视觉模型读取页面图像,每页成本高度依赖于模型如何计收图像输入——有些按瓦片计费,有些按分辨率推导的 token 数量计费,这使得同一页在不同模型间费用差异很大。模型目录中各模型的输入定价是这项计算的依据。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
多租户应用按客户计费:成本与可计费用量必须分开
下一篇
Hugging Face开源OlmoEarth文本嵌入