將PDF文件和掃描圖像等非結構化文檔轉換為結構化或半結構化格式是人工智能的關鍵部分。然而,由于PDF的復雜性和PDF解析任務的復雜性,這一過程顯得神秘莫測。
在RAG(Retrieval-Augmented Generation)基建之PDF解析的“魔法”與“陷阱”中,我們介紹了PDF解析的主要任務,對現有方法進行了分類,并簡要介紹了每種方法。RAG基建之PDF解析的“無OCR”魔法之旅中介紹了端到端方法。
本篇咱們來聊聊PDF解析的“流水線”大冒險!想象一下,PDF文件就像一座神秘的迷宮,里面藏著各種文字、表格、公式和圖片。我們的任務就是把這些亂七八糟的東西整理得井井有條,變成結構化的數據。聽起來是不是有點像在迷宮里找寶藏?
基于流水線的方法將PDF解析任務視為一系列模型或算法的流水線,如下所示。
基于流水線的方法可以分為以下五個步驟:
- 預處理原始PDF文件:修復模糊或傾斜等問題。此步驟包括圖像增強、圖像方向校正等。
- 進行布局分析:主要包括視覺結構分析和語義結構分析。前者識別文檔的結構并勾勒出相似區域,后者為這些區域標注特定的文檔類型,如文本、標題、列表、表格、圖表等。此步驟還涉及分析頁面的閱讀順序。
- 分別處理布局分析中識別的不同區域:此過程包括理解表格、識別文本以及識別公式、流程圖和特殊符號等其他組件。
- 整合之前的結果:恢復頁面結構。
- 輸出結構化或半結構化信息:如Markdown、JSON或HTML。
PDF解析的“流水線”方法四大天王:
-
- Marker:輕量級“寶藏獵人”
Marker是個輕量級的工具,速度快得像閃電俠,但它也有點小毛病。比如,它不太擅長處理表格,尤其是表格標題,簡直像是迷路的小羊羔。不過,它對付公式倒是有一套,尤其是那些復雜的數學公式,Marker能用Texify模型把它們變成漂亮的LaTeX格式。可惜的是,它只懂英語,日語和印地語對它來說就像外星語。
- Marker:輕量級“寶藏獵人”
-
- Papermage:科學文檔的“多面手”
Papermage是個專門對付科學文檔的“多面手”。它不僅能把文檔拆分成各種元素(文字、圖表、表格等),還能靈活處理跨頁、跨列的復雜內容。它的設計非常模塊化,開發者可以輕松添加新功能,就像給樂高積木加新零件一樣簡單。不過,它目前還沒有并行處理的能力,解析速度有點慢,像是在用老式打字機敲代碼。
- Papermage:科學文檔的“多面手”
-
- Unstructured:全能型“迷宮大師”
Unstructured是個全能型選手,布局分析做得非常細致。它不僅能識別文字和表格,還能處理復雜的文檔結構。它的自定義能力也很強,開發者可以根據需要調整中間結果,就像在迷宮里隨時調整路線一樣靈活。不過,它在公式識別上表現一般,像是迷宮里的一塊“絆腳石”。
- Unstructured:全能型“迷宮大師”
接下來,本文將討論幾種具有代表性的基于流水線的PDF解析框架,并分享從中獲得的見解。
文章目錄
- Marker
- 整體流程
- 從Marker中獲得的見解
- Marker的缺點
- PaperMage
- 組件
- 基礎數據類
- 整體流程和代碼分析
- 句子分割
- 布局結構分析
- 邏輯結構分析
- 關于Papermage的見解和討論
- Unstructured
- 關于布局分析
- 關于自定義
- 關于表格檢測和識別
- 關于公式檢測和識別
- MinerU:基于管道的開源文檔解析框架
- MinerU 工作流程:
- MinerU所使用的主要模型和算法
- 評論
- 結論
Marker
Marker 是一個用于深度學習模型的流水線。它能夠將PDF、EPUB和MOBI文檔轉換為Markdown格式。
整體流程
Marker的整體流程分為以下四個步驟:
步驟1:使用PyMuPDF和OCR將頁面劃分為塊并提取文本。對應代碼如下:
def convert_single_pdf(fname: str,model_lst: List,max_pages=None,metadata: Optional[Dict]=None,parallel_factor: int = 1
) -> Tuple[str, Dict]:......doc = pymupdf.open(fname, filetype=filetype)if filetype != "pdf":conv = doc.convert_to_pdf()doc = pymupdf.open("pdf", conv)blocks, toc, ocr_stats = get_text_blocks(doc,tess_lang,spell_lang,max_pages=max_pages,parallel=int(parallel_factor * settings.OCR_PARALLEL_WORKERS))
步驟2:使用布局分割器對塊進行分類,并使用列檢測器對塊進行排序。對應代碼如下:
def convert_single_pdf(fname: str,model_lst: List,max_pages=None,metadata: Optional[Dict]=None,parallel_factor: int = 1
) -> Tuple[str, Dict]:......# 從列表中解包模型texify_model, layoutlm_model, order_model, edit_model = model_lstblock_types = detect_document_block_types(doc,blocks,layoutlm_model,batch_size=int(settings.LAYOUT_BATCH_SIZE * parallel_factor))# 查找頁眉和頁腳bad_span_ids = filter_header_footer(blocks)out_meta["block_stats"] = {"header_footer": len(bad_span_ids)}annotate_spans(blocks, block_types)# 如果設置了調試標志,則轉儲調試數據dump_bbox_debug_data(doc, blocks)blocks = order_blocks(doc,blocks,order_model,batch_size=int(settings.ORDERER_BATCH_SIZE * parallel_factor))......
步驟3:過濾頁眉和頁腳,修復代碼和表格塊,并應用Texify模型處理公式。對應代碼如下:
def convert_single_pdf(fname: str,model_lst: List,max_pages=None,metadata: Optional[Dict]=None,parallel_factor: int = 1
) -> Tuple[str, Dict]:......# 修復代碼塊code_block_count = identify_code_blocks(blocks)out_meta["block_stats"]["code"] = code_block_countindent_blocks(blocks)# 修復表格塊merge_table_blocks(blocks)table_count = create_new_tables(blocks)out_meta["block_stats"]["table"] = table_countfor page in blocks:for block in page.blocks:block.filter_spans(bad_span_ids)block.filter_bad_span_types()filtered, eq_stats = replace_equations(doc,blocks,block_types,texify_model,batch_size=int(settings.TEXIFY_BATCH_SIZE * parallel_factor))out_meta["block_stats"]["equations"] = eq_stats......
步驟4:使用編輯器模型對文本進行后處理。對應代碼如下:
def convert_single_pdf(fname: str,model_lst: List,max_pages=None,metadata: Optional[Dict]=None,parallel_factor: int = 1
) -> Tuple[str, Dict]:......# 復制以避免更改原始數據merged_lines = merge_spans(filtered)text_blocks = merge_lines(merged_lines, filtered)text_blocks = filter_common_titles(text_blocks)full_text = get_full_text(text_blocks)# 處理空塊的連接full_text = re.sub(r'\n{3,}', '\n\n', full_text)full_text = re.sub(r'(\n\s){3,}', '\n\n', full_text)# 將項目符號字符替換為 -full_text = replace_bullets(full_text)# 使用編輯器模型對文本進行后處理full_text, edit_stats = edit_full_text(full_text,edit_model,batch_size=settings.EDITOR_BATCH_SIZE * parallel_factor)out_meta["postprocess_stats"] = {"edit": edit_stats}return full_text, out_meta
從Marker中獲得的見解
到目前為止,我們已經介紹了Marker的整體流程。現在,讓我們討論從Marker中獲得的見解。
見解1:布局分析可以分為多個子任務。第一個子任務涉及調用PyMuPDF API獲取頁面塊。
def ocr_entire_page(page, lang: str, spellchecker: Optional[SpellChecker] = None) -> List[Block]:if settings.OCR_ENGINE == "tesseract":return ocr_entire_page_tess(page, lang, spellchecker)elif settings.OCR_ENGINE == "ocrmypdf":return ocr_entire_page_ocrmp(page, lang, spellchecker)else:raise ValueError(f"未知的OCR引擎 {settings.OCR_ENGINE}")def ocr_entire_page_tess(page, lang: str, spellchecker: Optional[SpellChecker] = None) -> List[Block]:try:full_tp = page.get_textpage_ocr(flags=settings.TEXT_FLAGS, dpi=settings.OCR_DPI, full=True, language=lang)blocks = page.get_text("dict", sort=True, flags=settings.TEXT_FLAGS