space ocr
指南文章价格文档
developer

把一页扫描件变成可以核对的 Markdown

把扫描件转成 Markdown 很容易做砸:标题被压平、表格变糊,也说不清哪一行来自哪里。这是一份关于保留版式的 Markdown OCR 的开发者指南——每个元素都带回坐标与 text_verified。

7 分钟阅读· 2026-07-28

把扫描件转成 Markdown,通常出于两个理由,而这两者要的东西并不完全一样。

一是发布:你想把文档放进 Wiki、文档站或仓库,并且希望标题还是标题。二是喂给模型:多数 LLM 流水线以 Markdown 为输入,因为语法本身承载结构——## 告诉模型这里是章节边界,管道表格告诉它这些单元格属于同一行。纯文本会把这些信息丢掉。

失败的方式也一样。标题被压成段落,文档站就成了一堵墙,检索用的分块也会在错误的位置断开。而且在这两种用途里,你拿到的通常只是一整串 Markdown,没有办法追问这一行来自页面的哪里

“保留版式”真正要保留的东西

一个可用的 Markdown 转换,需要正确完成四个彼此独立的判断:

  1. 阅读顺序 —— 多栏排版不能交错。这是朴素 OCR 输出最先崩掉的地方:引擎按检测顺序输出段落,而不是人阅读的顺序。
  2. 块类型 —— 这一行是标题、列表项、引用,还是普通段落。在扫描件上,仅凭字号并不可靠。
  3. 表格结构 —— 哪些单元格属于同一行,哪一行是表头,单元格折成两行时怎么处理。
  4. 不丢东西 —— 没人会注意到的失败。段落悄悄消失了,Markdown 看上去依然完好。

第四点最棘手,因为它在产物里是隐形的:丢了 5% 的转换结果,读起来完全通顺。

一次调用,以及返回什么

POST /ocr/markdown 接收一张图像,同时返回组装好的 Markdown 字符串,以及组装它所用的元素。

1
2
3
4
5
6
7
curl -X POST https://api.space-ocr.com/ocr/markdown \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "image": "https://example.com/report.jpg",
    "imageType": "url"
  }'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "status": "success",
  "data": {
    "markdown": "# 季度报告\n\n营收同比增长。\n\n| 项目 | 金额 |\n| --- | --- |\n| 营收 | 12,000 |",
    "review_summary": {
      "unit": "element",
      "total": 8,
      "boxed": 8,
      "text_verified": 7,
      "text_mismatch": 1,
      "needs_review": 1,
      "flagged": [{ "path": "elements[3].cells[1]", "reason": "text_mismatch" }],
      "recovered_blocks": 0,
      "token_coverage": 1.0
    },
    "elements": [
      {
        "type": "heading",
        "level": 1,
        "text": "季度报告",
        "bbox": { "xmin": 60, "ymin": 48, "xmax": 520, "ymax": 92 },
        "vertices": [{ "x": 60, "y": 48 }, "…"],
        "bbox_source": "token_id",
        "text_verified": true
      }
    ]
  }
}

markdown 是你直接粘进文档站的字符串。让结果可核对的,是 elements

每个元素,以及表格里的每个单元格,都带有 0–1000 归一化网格上的 bboxvertices,所以输出里的一个标题可以映射回原图上的一个矩形。归一化在工程上很重要:缩放之后位置依然对得上,生成缩略图后框还是准的。

元素类型如你所料:heading(带 level)、paragraphlist_itemblockquotecode_blockthematic_breaktable。表格带有 rowscolscells[],每个单元格有自己的 rowcolheadertext 和坐标——你可以直接渲染自己的表格,而不必把管道语法再解析回来。

text_verified:抓住“擅自改写”的那面旗

读页面的语言模型会悄悄地“改好”内容:把日期规范化、修掉它认为的错别字、展开缩写。做摘要没问题;但对于要落库的文档,这是看起来很称职的数据损坏

所以每个元素都带 text_verified。引擎取出该元素所主张的词元,查出在这些坐标上视觉 OCR 实际读到的文字,归一化后进行比对。true 表示两次独立判读一致;false 表示转写文字与页面上的文字不同——值照样返回,但被标记出来,而不是悄悄出错null 表示没有可比对的依据。

它不是准确率分数,也不该被当成准确率。它只回答一个很窄的问题:这条有没有和像素对过?

✓ Verified

悄悄的失败是有仪表的。 review_summary.token_coverage 是回收步骤之前模型所主张的页面词元占比。任何没有被任何元素主张的词元区间,都会以 bbox_source: "unclaimed_tokens" 的段落追加回来,review_summary.recovered_blocks 记录发生了几次。丢了段落的转换,会显示在数字里,而不是只体现在消失的正文里。

放在流水线的哪一环

做 RAG 摄取时,elements 比字符串更有用。按 heading 边界而不是字符数切块,得到的分块会与文档自身的章节对齐。给每个分块保留 bbox,引用就能指向原页面上的矩形,而不是一个页码。

如果是文档站或 Wiki,直接用 markdown,把元素作为审阅用的旁路数据保留。当有人对某个数字提出异议时,你可以在扫描件上把框打出来,而不是争论。

如果你根本不需要结构——建索引、检索、比对——Markdown 语法反而是噪音。那正是 POST /ocr/text 的用途:同一页以还原了阅读顺序的纯文本返回,每个区块同样带 text_verified

相关文章