PDF parsing

PDF table recognition is not just converting to CSV: first, check the headers and boundaries

When you receive a scanned or typeset PDF financial report, run findtables() After directly exporting to CSV, you will very likely encounter three types of “sil

阅读文本 / Reading text
This paper uses synthetic samples to demonstrate the processing method. The code has been verified using examples in an isolated environment; this does not guarantee effectiveness for all real files or products. Before processing important files, please retain the original copies and verify them against the boundaries and acceptance criteria outlined in this document. When you receive a scanned or typeset PDF financial report, run find_tables() After directly exporting to CSV, you will very likely encounter three types of “silent errors”: a multi-page table is split into two separate tables, with the header missing on the second page; merged cells cause the entire column’s data to be misaligned; or the header is mistakenly recognized as a regular data row. These errors do not raise exceptions; they only become apparent during downstream data analysis or translation. The key to resolving this issue lies not in switching to a more powerful model, but in performing structural validation on the table object immediately after extraction. Why can't the output of find_tables be used directly? PyMuPDF's Page.find_tables() returns TableFinder object, which infers table regions based on page vector lines and text position heuristics. This inference process is purely geometric and does not understand semantics. This means: Statelessness : Table recognition on each page is performed independently. If the original table spans pages 3 and 4, the algorithm will treat it as two entirely unrelated tables. The table on page 4 may lack a header, or its header may actually be a continuation of the previous page. empty cell ambiguity : The PDF specification does not define a true concept of an “empty cell”; it only specifies that “there is no text object in that coordinate region.” When a cell’s content is empty, extract() may return None or an empty string (depending on the PyMuPDF version and parameter settings), which can cause the row–column alignment logic to fail. header determination missing : The algorithm cannot distinguish whether the first line is a header or data. For multi-line headers (such as “2023Year / First Quarter / Revenue”), the extraction results often end up as flattened string concatenations, thereby disrupting the hierarchical structure. Therefore, before feeding the table into the translation engine or database, a validation layer must be inserted to ensure the integrity of the table structure. synthesis example: Construct a sample table with defects To demonstrate the validation logic, we do not rely on external files; instead, we construct a PDF page containing typical issues in memory. If you wish to test with real files, please prepare a PDF that includes multi-page tables with some cells left blank, and ensure that it is neither encrypted nor corrupted. correctly outputs the expected : A table with 3 columns and 4 rows (including the header). The first row is the header ["Header A", "Header B", "Header C"] . The second through fourth lines contain the data. Among them (1, 1) position (i.e., row 2, column 2) should be a null value, used to test the logic for handling empty cells. Error/Boundary Input and Processing : 1. input : A certain cell contains no text objects at all in the PDF. - processes : The validator should detect None or an empty string, and depending on business rules, either fill in a default value, mark it as missing, or raise an error and terminate. Under no circumstances should unprocessed null values be allowed to enter the string operation pipeline. 2. input : The text in the first row of the table is the same font size as the body text, but in bold; the second row contains the actual column names. - processes : The header cannot be determined based on the text content alone. A secondary verification is required, combining font attributes or a predefined list of header keywords. If uncertainty remains, the original row should be retained and annotated with metadata for manual confirmation. This example illustrates that “success” in table extraction does not equate to “availability.” The validation logic must explicitly handle these edge cases. Python validation script: from extraction to assertion The following code demonstrates how to construct a PDF in memory and apply find_tables The result performs basic structural validation. Note: This example must be executed with the lock engaged. PyMuPDF>=1.24.0 running in an environment where the version is too old and may lack find_tables or related attributes. This code is for educational demonstration and has not been fully validated in a production environment. import fitz # PyMuPDF def create_synthetic_pdf(): """在内存中创建一个含简单表格的合成PDF,无需外部文件""" doc = fitz.open() page = doc.new_page() # 绘制一个3x4网格线作为表格边界 shape = page.new_shape() rect = fitz.Rect(50, 50, 300, 183) shape.draw_rect(rect) # 外框 # 横线 shape.draw_line(fitz.Point(50, 83), fitz.Point(300, 83)) shape.draw_line(fitz.Point(50, 116), fitz.Point(300, 116)) shape.draw_line(fitz.Point(50, 150), fitz.Point(300, 150)) # 竖线 shape.draw_line(fitz.Point(133, 50), fitz.Point(133, 183)) shape.draw_line(fitz.Point(216, 50), fitz.Point(216, 183)) shape.finish(color=(0, 0, 0), width=1) shape.commit() # 插入文本:注意(1,1)位置故意留空以测试空单元格处理 texts = [ (fitz.Point(60, 70), "Header A"), (fitz.Point(143, 70), "Header B"), (fitz.Point(226, 70), "Header C"), (fitz.Point(60, 103), "Data 1"), # (1,1) 留空 (fitz.Point(226, 103), "Data 3"), (fitz.Point(60, 136), "Data 4"), (fitz.Point(143, 136), "Data 5"), (fitz.Point(226, 136), "Data 6"), (fitz.Point(60, 170), "Data 7"), (fitz.Point(143, 170), "Data 8"), (fitz.Point(226, 170), "Data 9") ] for point, text in texts: page.insert_text(point, text, fontsize=10) return doc def validate_table_structure(doc): page = doc[0] tabs = page.find_tables() assert len(tabs.tables) == 1, f"期望1个表格,实际找到 {len(tabs.tables)} 个" table = tabs.tables[0] data = table.extract() # 返回二维列表 # 校验1: 行数与列数符合预期(含表头共4行) assert len(data) == 4, f"期望4行数据(含表头),实际 {len(data)} 行" assert all(len(row) == 3 for row in data), "存在行列数不一致的行" # 校验2: 空单元格处理(兼容None或空字符串) empty_cell = data[1][1] assert empty_cell is None or empty_cell == "", \ f"空单元格应为None或空串,实际为: {repr(empty_cell)}" # 校验3: 表头非空且不含纯数字(简易启发式) header = data[0] assert all(isinstance(h, str) and h.strip() and not h.strip().isdigit() for h in header), \ f"表头格式异常: {header}" print("结构校验通过。提取数据预览:") for row in data: print([c if c is not None else "<EMPTY>" for c in row]) # 执行校验 synthetic_doc = create_synthetic_pdf() validate_table_structure(synthetic_doc) synthetic_doc.close() This piece of code does not invoke any network services nor read local files; it is entirely self-contained. It validates three core aspects: the number of tables, dimensional consistency, and null-value safety. In real-world projects, you should replace assert is replaced with logging or exception handling, because production‑environment PDFs are far messier than synthetic samples. Acceptance Criteria and Manual Review Boundary code can intercept structural errors but cannot assess semantic correctness. The table below summarizes the division of labor between automated validation and manual review, to avoid over-relying on script-generated outputs: Observation of the phenomenon automated decision-making Manual review trigger condition Scope of Non-Applicability The first row on the second page of the multi-page table is exactly identical to the header text on the first page automatically removes duplicates and merges them into the same logical table The first-line text is similar but not identical (e.g., abbreviations, line breaks) In the scanned document, character discrepancies in the table header are due to OCR errors. All values in a certain column are either empty or identical Marked as a suspected merged cell, preserving the original structure This column visually appears to be an extension of the cell on the left complex nested table headers or multi-level classification labels extraction line count versus page visual line count deviation > 1 logs a warning and outputs the original bbox for debugging bias continues to occur or involves critical data regions decorative lines were mistakenly recognized as table borders header contains dynamic content such as date/version number Normal extraction, without special processing. In the same document, multiple tables have inconsistent header formats handwritten annotations cover the header area important distinction : The code above demonstrates “structural integrity validation,” not “end-to-end performance acceptance.” Even if all asserts pass, semantic errors such as column order reversals or loss of numerical units may still occur. For high-value documents, it is essential to sample and compare the original PDF with the extraction results. References and Next Steps PyMuPDF Page.find_tables Official Documentation: https://pymupdf.readthedocs.io/en/latest/page.html#Page.find_tables Next steps: Select the most complex PDF report at hand, run the aforementioned validation script, and record all assertion failure points. Adjust extraction parameters based on the type of failure (e.g., strategy 、 snap_tolerance ) or supplement with human review rules. The process is only integrated into the translation or analysis pipeline after five consecutive samples have passed verification. uses this checklist for document translation first ensures that the page order, source locations, and text integrity have been thoroughly checked, then submits the same de-identified sample to the translation process. After translation, it is still necessary to verify that the headings, body text, and figure captions correspond correctly; one should not rely solely on whether the text flows smoothly. OmniDoc (Product Entry) provides an entry point for translating multi-format documents, and the official website highlights its OCR, terminology database, and bilingual comparison features. For teams that wish to start processing directly from files, this is a path worth evaluating with real-world samples. Please use de-identified files that you are authorized to upload, and verify the results against the checklist provided in this document; we do not claim that OmniDoc utilizes the open-source libraries or internal algorithms referenced herein, nor do we substitute examples for product performance testing. - 1 -
Updated 2026-10-08