戰(zhàn):從PDF到結(jié)構(gòu)化Markdown與JSON的文檔解析指南)
做文檔處理這些年我越來越覺得PDF這玩意兒就是個(gè)“格式牢籠”。表面看是個(gè)文件里面文字表格圖片都擺得整整齊齊可真要把它里面的內(nèi)容抽出來用能讓人頭疼到懷疑人生。這個(gè)月我集中測(cè)了一款叫docling的開源文檔解析工具GitHub上熱度漲得很快做RAG、文檔問答和知識(shí)庫的同行都在安利所以我把一周多的實(shí)測(cè)過程和踩坑記錄整理出來給想用文檔解析做信息抽取或者自動(dòng)化處理的朋友做個(gè)參考。1. 文檔解析這件事痛點(diǎn)比你想的多1.1 從一道“格式毒藥”說起先說說我們平時(shí)遇到的坑。很多人覺得PDF轉(zhuǎn)Word、PDF轉(zhuǎn)文本是再簡(jiǎn)單不過的事隨便找個(gè)在線工具點(diǎn)一下轉(zhuǎn)換完事。但只要你處理過幾十份真實(shí)業(yè)務(wù)文檔就知道這里面的水深得很。最典型的問題是“文字順序錯(cuò)亂”PDF里兩欄排版的論文轉(zhuǎn)出來之后左右兩邊的內(nèi)容攪在一起像一團(tuán)亂麻表格稍微復(fù)雜一點(diǎn)轉(zhuǎn)出來要么擠成一堆要么直接變成一行一行的殘廢文本還有那些帶腳注、眉頁、頁碼的合同和報(bào)告轉(zhuǎn)完之后這些標(biāo)注全混到正文里了。為什么會(huì)這樣因?yàn)镻DF本質(zhì)上不是一種“文檔編輯格式”它存的是每個(gè)字符在頁面上的坐標(biāo)位置有點(diǎn)像一張拍好的照片而不是排版原稿。機(jī)器拿到PDF之后如果想從里面讀出“這段是標(biāo)題那段是正文這個(gè)表格是兩行三列”它必須自己去推斷。傳統(tǒng)轉(zhuǎn)換工具的思路大多是“按坐標(biāo)抓字拼串”效果自然就看運(yùn)氣了。我見過有同行用正則去PDF里抽表格遇到稍微規(guī)整的還行遇到跨頁的、帶樣式的基本就是災(zāi)難現(xiàn)場(chǎng)。1.2 Docling的思路先讀懂文檔再輸出結(jié)構(gòu)docling這個(gè)項(xiàng)目給我的第一印象是它換了一條路不是“提取文字”而是“理解文檔”。你要拿一份PDF給它它會(huì)先對(duì)整個(gè)頁面做布局分析識(shí)別出哪個(gè)區(qū)域是標(biāo)題、哪個(gè)區(qū)域是正文、哪個(gè)區(qū)域是表格、哪個(gè)區(qū)域是圖片然后再把這些區(qū)域里的內(nèi)容按照閱讀順序組織起來最終輸出成結(jié)構(gòu)化的Markdown或者JSON。也就是說它做了一個(gè)“人眼看文檔”的動(dòng)作先看懂版式再翻譯成機(jī)器能直接用的結(jié)構(gòu)。docling是IBM開源出來的目前支持PDF、Word、PPT、Excel、HTML和圖片等常見輸入格式輸出上最常用的是Markdown和JSON兩種。對(duì)于做知識(shí)庫、做文檔問答、做自動(dòng)化歸檔的小伙伴來說它解決了一個(gè)核心問題文檔里的信息不再是“人知道但機(jī)器拿不到”的狀態(tài)而是可以變成帶層級(jí)、帶語義、帶坐標(biāo)的數(shù)據(jù)。這篇文章后面我會(huì)重點(diǎn)講PDF場(chǎng)景因?yàn)檫@個(gè)最能反映一個(gè)文檔解析工具的真實(shí)水準(zhǔn)。2. 安裝部署與前置依賴比想象中省事2.1 三步完成環(huán)境準(zhǔn)備docling用起來的前置條件不復(fù)雜核心就是Python環(huán)境加pip安裝。建議你用一個(gè)干凈的虛擬環(huán)境避免和項(xiàng)目里其他依賴沖突。我第一次裝的時(shí)候直接用全局環(huán)境結(jié)果把opencv的版本搞亂了后面排查了半天才意識(shí)到是環(huán)境問題。python -m venv .venv source .venv/bin/activate pip install docling[ocr]這里有個(gè)小細(xì)節(jié)要說明docling是核心包[ocr]這個(gè)擴(kuò)展會(huì)額外幫你裝好OCR相關(guān)的依賴默認(rèn)帶的是EasyOCR。如果你不裝OCRdocling也能處理數(shù)字原生的PDF但遇到掃描件、圖片型PDF就會(huì)束手無策。我個(gè)人的建議是哪怕暫時(shí)用不到也先把OCR擴(kuò)展裝上因?yàn)闃I(yè)務(wù)的文檔類型永遠(yuǎn)是變化的等真遇到一份沒法復(fù)制文字的掃描合同時(shí)再補(bǔ)裝反而會(huì)耽誤事。安裝完成后你在終端里直接敲docling --help就能看到命令幫助說明裝好了。如果是在Python腳本里用導(dǎo)入DocumentConverter的時(shí)候沒有報(bào)錯(cuò)環(huán)境就算準(zhǔn)備完成了。整個(gè)過程從零到能用實(shí)測(cè)十分鐘以內(nèi)。2.2 OCR引擎該不該裝裝哪個(gè)docling底層的OCR實(shí)現(xiàn)有兩種比較主流的選法一種是它默認(rèn)集成的EasyOCR另一種是Tesseract。這兩者沒有絕對(duì)的好壞取決于你的使用場(chǎng)景。對(duì)比項(xiàng)EasyOCRTesseract安裝方式跟隨docling[ocr]自動(dòng)安裝需要在系統(tǒng)里單獨(dú)裝Tesseract程序中文支持內(nèi)置效果不錯(cuò)需要額外下載中文語言包識(shí)別精度對(duì)清晰文檔較高對(duì)舊版掃描件和特殊字體更穩(wěn)資源占用加載模型耗內(nèi)存較多相對(duì)輕量CPU上也能跑配置方式在Python里設(shè)EasyOcrOptions在Python里設(shè)TesseractOcrOptions我自己的實(shí)測(cè)感受是如果文檔是中文掃描件EasyOCR開箱即用的體驗(yàn)會(huì)更好如果處理的是英文歷史文獻(xiàn)、老報(bào)紙這種對(duì)比強(qiáng)烈的黑白掃描件Tesseract有時(shí)候反而更能抗噪。你完全可以在代碼里根據(jù)文件類型動(dòng)態(tài)選擇OCR引擎docling的Pipeline選項(xiàng)里預(yù)留了靈活的切換接口。2.3 首次運(yùn)行前心里有個(gè)底第一次跑docling的時(shí)候你可能會(huì)有種“卡住”的錯(cuò)覺其實(shí)它在下載模型。docling核心的布局分析模型和表格結(jié)構(gòu)模型會(huì)從HuggingFace模型倉庫拉到本地緩存目錄一般在~/.cache/huggingface下。整個(gè)過程取決于網(wǎng)絡(luò)狀況快的話一兩分鐘慢的話可能要等一會(huì)兒。如果發(fā)現(xiàn)幾百M(fèi)的模型下不動(dòng)可以提前把緩存目錄共享到內(nèi)網(wǎng)或者設(shè)置HF_HOME環(huán)境變量指向你指定的模型目錄。另外關(guān)于硬件配置docling在CPU上確實(shí)能跑但速度和體驗(yàn)完全兩回事。我用一臺(tái)普通的MacBook Pro處理一份40頁的PDFCPU模式下大概要一分鐘左右換到帶CUDA的GPU機(jī)器同一個(gè)文件壓縮到十幾秒。如果你的項(xiàng)目里文檔量很大強(qiáng)烈建議用GPU環(huán)境。內(nèi)存方面普通文檔8GB夠用但如果是高分辨率掃描件開了OCR16GB會(huì)比較穩(wěn)。3. 核心實(shí)操?gòu)腜DF到結(jié)構(gòu)化Markdown3.1 一行命令快速上手docling的命令行入口做得非常簡(jiǎn)單開箱即用這個(gè)感受非常強(qiáng)。不用寫任何代碼就能把一個(gè)PDF轉(zhuǎn)成Markdown和JSON。docling demo.pdf --to markdown --output ./out執(zhí)行完之后./out目錄下會(huì)生成兩個(gè)文件demo.md和demo.json。demo.md是給人看的Markdown文本里面標(biāo)題、表格、列表結(jié)構(gòu)都是規(guī)整的demo.json是給程序用的結(jié)構(gòu)化數(shù)據(jù)。我第一次處理一份帶復(fù)雜表格的年度報(bào)告時(shí)看到Markdown里表格被完整還原成markdown表格格式說實(shí)話還是挺驚喜的——這比傳統(tǒng)工具直接把表格拍平成散文字強(qiáng)太多了。如果你處理的PDF是掃描件記得加上--ocr參數(shù)如果表格比較密集還可以用--table-mode accurate讓表格識(shí)別走更精細(xì)的模式。官方默認(rèn)的模式是快速的在表格簡(jiǎn)單時(shí)速度優(yōu)先但表格復(fù)雜的時(shí)候我又測(cè)過用accurates模式漏格和錯(cuò)格的情況明顯減少。3.2 用Python API定制你的轉(zhuǎn)換流程命令行適合臨時(shí)用用真正集成到業(yè)務(wù)系統(tǒng)里還是走Python API更靈活。docling的接口設(shè)計(jì)不算復(fù)雜核心就是DocumentConverter這個(gè)類。from docling.document_converter import DocumentConverter converter DocumentConverter() result converter.convert(demo.pdf) doc result.document # 導(dǎo)出Markdown with open(demo.md, w, encodingutf-8) as f: f.write(doc.export_to_markdown()) # 導(dǎo)出JSON with open(demo.json, w, encodingutf-8) as f: import json json.dump(doc.export_to_dict(), f, ensure_asciiFalse, indent2)這段代碼幾分鐘就能跑通。我實(shí)際開發(fā)的時(shí)候還會(huì)在轉(zhuǎn)換前后加一些統(tǒng)計(jì)信息和異常處理比如記錄converter.convert()的耗時(shí)、返回狀態(tài)等方便排查問題。值得留意的是convert()方法返回的Document對(duì)象里不僅包含Markdown導(dǎo)出結(jié)果還保留著整個(gè)文檔的層級(jí)結(jié)構(gòu)、閱讀順序和布局元素信息。你要做信息抽取的時(shí)候不需要自己再寫一堆正則去猜哪里是標(biāo)題直接從JSON里按元素類型過濾就行。3.3 表格識(shí)別這是它的強(qiáng)項(xiàng)但也不是神表格解析是docling的招牌功能之一。它內(nèi)部用了專門的表格結(jié)構(gòu)模型能從PDF頁面上把表格邊框、單元格、跨行跨列的信息摳出來還原成帶有行列語義的結(jié)構(gòu)化表格。我拿一個(gè)七列十五行的數(shù)據(jù)表來測(cè)它能準(zhǔn)確把表頭識(shí)別出來文本內(nèi)容也能按單元格對(duì)號(hào)入座Markdown渲染出來基本和原版一致。不過把丑話說在前面它面對(duì)那種“反人類”的復(fù)雜表格時(shí)還是會(huì)翻車。比如帶合并單元格的復(fù)雜表頭、嵌套表格、跨頁斷開的表格偶爾會(huì)出現(xiàn)格子錯(cuò)位、內(nèi)容串行。我的處理習(xí)慣是表格數(shù)量少但精度要求高的文檔轉(zhuǎn)完必看一遍Markdown對(duì)一致性要求極其嚴(yán)格的數(shù)據(jù)比如財(cái)報(bào)里的數(shù)字表再加一步程序校驗(yàn)而不是盲目信任輸出。如果你在代碼里想調(diào)整表格識(shí)別級(jí)別可以通過Pipeline選項(xiàng)設(shè)置from docling.document_converter import DocumentConverter, PdfFormatOption from docling.datamodel.base_models import InputFormat from docling.datamodel.pipeline_options import PdfPipelineOptions pipeline_options PdfPipelineOptions() pipeline_options.table_mode accurate # 或者 fast converter DocumentConverter( format_options{ InputFormat.PDF: PdfFormatOption(pipeline_optionspipeline_options) } )3.4 JSON輸出給下游開發(fā)者的“富礦”可能很多朋友第一次用docling只盯著Markdown看把JSON當(dāng)成了副產(chǎn)品。實(shí)際上JSON才是真正的寶貝。docling輸出的JSON里文檔被拆成了帶類型的元素列表比如title、paragraph、table、list、code每個(gè)元素還附帶bbox坐標(biāo)框信息以及它在頁面閱讀順序中的位置。這意味著你完全可以基于這份JSON把一份PDF“翻譯”成一份按閱讀順序排列的內(nèi)容索引。這個(gè)特性在RAG場(chǎng)景里太好用了。傳統(tǒng)做法是拿PDF轉(zhuǎn)出的一堆文本直接塞進(jìn)向量庫但往往噪音太大、結(jié)構(gòu)丟失。用docling的JSON處理后你可以按元素粒度做切片給標(biāo)題、段落、表格分別打標(biāo)簽再灌進(jìn)向量庫。檢索的時(shí)候用戶問表里的數(shù)據(jù)你直接命中表格元素問某個(gè)標(biāo)題下的內(nèi)容你命中最接近的標(biāo)題塊。這些在之前都是要花大量精力去清洗才能做到的效果。4. 進(jìn)階玩法與性能調(diào)優(yōu)4.1 批量轉(zhuǎn)換讓腳本替你做臟活真正落地的時(shí)候沒人會(huì)一份一份手動(dòng)跑命令行都是直接上一個(gè)目錄扔進(jìn)去批量處理。批量轉(zhuǎn)換的代碼不復(fù)雜但有幾個(gè)細(xì)節(jié)值得關(guān)注。import os from pathlib import Path from docling.document_converter import DocumentConverter converter DocumentConverter() input_dir Path(./pdfs) output_dir Path(./output) output_dir.mkdir(exist_okTrue) for pdf_file in input_dir.glob(*.pdf): try: result converter.convert(str(pdf_file)) out_md output_dir / f{pdf_file.stem}.md out_md.write_text(result.document.export_to_markdown(), encodingutf-8) print(f處理完成: {pdf_file.name}) except Exception as e: print(f處理失敗: {pdf_file.name}, 原因: {e})這里第一個(gè)坑是內(nèi)存問題。如果一次性處理上千份PDF持續(xù)調(diào)用convert()可能會(huì)讓內(nèi)存占用慢慢增長(zhǎng)。我實(shí)際驗(yàn)證后發(fā)現(xiàn)長(zhǎng)時(shí)間批量跑任務(wù)時(shí)最好每處理完一批就gc.collect()強(qiáng)制回收一次或者干脆把進(jìn)程拆成按固定數(shù)量文檔處理的子任務(wù)處理完一批自動(dòng)重啟。第二個(gè)坑是斷點(diǎn)續(xù)跑。批量任務(wù)跑一半掛了是很正常的事情輸出文件已存在就直接跳過能省下一大半重試時(shí)間。4.2 自定義Pipeline按需取舍加速docling默認(rèn)的處理流程是“布局分析表格識(shí)別OCR全開”但很多場(chǎng)景下你并不需要每一樣都跑。比如你的PDF是純文字報(bào)表沒有復(fù)雜版面那布局分析可以保留默認(rèn)如果是掃描件但沒有表格就可以把表格識(shí)別關(guān)掉只保留OCR和文本抽取速度能提升不少。Pipeline的定制入口在上面已經(jīng)見過PdfPipelineOptions里除了table_mode還有幾個(gè)常用的開關(guān)pipeline_options.do_ocr True # 是否啟用OCR pipeline_options.do_table_structure False # 是否啟用表格結(jié)構(gòu)識(shí)別 pipeline_options.do_code False # 是否識(shí)別代碼塊我第一次優(yōu)化一個(gè)掃描合同批量解析任務(wù)時(shí)把表格結(jié)構(gòu)識(shí)別關(guān)掉之后處理速度幾乎快了一倍。所以建議你拿到一批新文檔時(shí)先抽一兩份樣本看看文檔里到底有哪些元素再?zèng)Q定Pipeline的開關(guān)組合。工具是死的需求是活的這比盲目追求最高精度劃算得多。4.3 性能對(duì)比與調(diào)參經(jīng)驗(yàn)性能這個(gè)事做技術(shù)的都關(guān)心。我同一份126頁的PDF分別在CPU和GPU上跑了一遍結(jié)果很直觀CPU耗時(shí)大約3分半GPU耗時(shí)不到1分鐘。如果你的機(jī)器沒有GPU但又要處理大量掃描件建議先優(yōu)化一個(gè)參數(shù)降低輸入圖片的分辨率。OCR處理高分辨率掃描件時(shí)每頁的圖像解壓和預(yù)處理非常消耗CPU把掃描分辨率控制在150dpi到200dpi之間清晰度足夠速度卻會(huì)好看很多。還有一個(gè)容易被忽略的點(diǎn)是并發(fā)線程數(shù)。docling內(nèi)部用了并行處理默認(rèn)參數(shù)在某些容器環(huán)境下可能不合理。如果發(fā)現(xiàn)CPU占用一直上不去或者反過來內(nèi)存一直在漲可以在代碼里看看進(jìn)程實(shí)際起的線程數(shù)太高就手動(dòng)限制一下。我在一個(gè)4核CPU的云主機(jī)上跑任務(wù)時(shí)把并行度調(diào)到2到3整體吞吐反而比默認(rèn)全開更穩(wěn)定。5. 常見問題排查實(shí)錄5.1 OCR中文識(shí)別不出來怎么辦這是我在測(cè)試群和評(píng)論區(qū)看到最多的問題。很多人裝了docling去轉(zhuǎn)中文掃描件結(jié)果輸出里中文變成了亂碼或直接消失。原因多半是OCR引擎的語言參數(shù)沒有設(shè)置。docling默認(rèn)的EasyOCR語言列表里包含英文但中文需要你顯式添加。from docling.datamodel.pipeline_options import EasyOcrOptions ocr_options EasyOcrOptions(lang[en, ch_sim]) pipeline_options.ocr_options ocr_options如果你用的是Tesseract語言參數(shù)是eng和chi_sim同時(shí)要確保系統(tǒng)里已經(jīng)安裝了對(duì)應(yīng)的語言包。這個(gè)參數(shù)加好之后中文識(shí)別率會(huì)有質(zhì)的變化。另外掃描件的質(zhì)量也很影響OCR效果我試過一份對(duì)比度很差的快遞底單加對(duì)比度之后識(shí)別率立竿見影。如果原圖已經(jīng)糊成一團(tuán)換任何引擎都救不回來。5.2 表格從中間開始亂掉表格整體沒問題但到了中間或者跨頁的位置就開始錯(cuò)位、串行這個(gè)問題我在處理長(zhǎng)表格時(shí)也遇到過。通常原因是PDF里表格被分頁拆開了docling在識(shí)別跨頁表格時(shí)需要把上下兩段拼接起來一旦表頭或邊界判斷有一點(diǎn)偏差后面的單元格就全亂了。處理這類文檔我的經(jīng)驗(yàn)是先試試切換table_mode從fast切到accurate表格結(jié)構(gòu)模型有更高概率把表頭與數(shù)據(jù)行對(duì)應(yīng)正確如果PDF本身是掃描件先跑一遍OCR再進(jìn)表格識(shí)別比直接看掃描圖識(shí)別強(qiáng)很多。還有一種情況是文檔里某些表格本身就沒有明顯邊框線這種“無框表格”機(jī)器識(shí)別難度本就很高別把期望拉滿必要時(shí)刻用人工復(fù)核收尾。5.3 轉(zhuǎn)換速度慢、內(nèi)存漲得快速度慢通常集中在兩個(gè)環(huán)節(jié)一是大量掃描件走OCR二是超大文檔一次性轉(zhuǎn)換。拿我那份300頁的行業(yè)報(bào)告來說直接整本丟進(jìn)docling中途內(nèi)存一度逼近極限屬實(shí)壓力拉滿。解決辦法很樸素拆分。從源頭把PDF按章節(jié)拆成幾十個(gè)獨(dú)立小文件再逐一轉(zhuǎn)換最后合并Markdown。這樣節(jié)省內(nèi)存更重要的是哪怕中間某個(gè)文件失敗了重跑一個(gè)片段就行不用全量再來。另外內(nèi)存持續(xù)上漲還有一個(gè)隱蔽原因批量循環(huán)里不斷創(chuàng)建新的DocumentConverter實(shí)例。正確做法是不管處理多少文件都復(fù)用一個(gè)converter實(shí)例避免重復(fù)加載模型、反復(fù)開辟內(nèi)存。5.4 離線環(huán)境模型加載不了如果你在內(nèi)網(wǎng)部署沒有外網(wǎng)權(quán)限首次運(yùn)行docling大概率會(huì)卡在模型下載。這里的核心思路是“提前把模型準(zhǔn)備好讓程序走本地加載”。第一步在一臺(tái)能聯(lián)網(wǎng)的同架構(gòu)機(jī)器上跑一次轉(zhuǎn)換把~/.cache/huggingface目錄整體打包拷到目標(biāo)機(jī)器第二步在目標(biāo)機(jī)器上設(shè)置環(huán)境變量HF_HOME指向解壓后的目錄讓docling啟動(dòng)時(shí)直接讀本地模型。export HF_HOME/data/huggingface_cache export HF_HUB_OFFLINE1設(shè)置好后docling不會(huì)再嘗試聯(lián)網(wǎng)模型加載只在本地緩存里找。需要注意的是docling版本升級(jí)后模型緩存的結(jié)構(gòu)可能略有變化離線部署時(shí)盡量保持目標(biāo)機(jī)器的包版本和模型來源機(jī)器一致不容易踩“版本不匹配”的坑。這個(gè)方法也適用于有網(wǎng)絡(luò)隔離要求的企業(yè)內(nèi)部環(huán)境操作起來很實(shí)用。這幾天實(shí)測(cè)下來我對(duì)docling最大的感受是它不是在幫你“轉(zhuǎn)換文件”而是真的在嘗試讓機(jī)器“看懂”文檔。它并不完美復(fù)雜表格、老舊掃描件照樣會(huì)翻車但只要搭配好OCR開關(guān)、表格模式、批量拆分這幾招它完全能撐起一條自動(dòng)化的文檔處理流水線。最后分享一個(gè)自己的小技巧文檔頁數(shù)多的時(shí)候不要整本丟進(jìn)去先按章節(jié)拆分再逐段交給docling速度會(huì)快很多出問題也只影響局部。希望這篇實(shí)測(cè)記錄能幫你少踩幾個(gè)坑讓文檔解析這件事不再那么“玄學(xué)”。