--- name: parsing-documents description: >- Converts PDFs, scans, and Word documents into text or markdown with the gaik toolkit's parsers, choosing the parser that will not silently destroy the structure the downstream task depends on. Use when reading a PDF or DOCX into text, pulling tables out of a document, running OCR on scans, feeding documents into a RAG pipeline or an LLM, deciding between PyMuPDF, Docling, and vision-LLM parsing, or when a parse appeared to succeed but the tables, columns, or whole pages came out wrong or empty. Also use when document parsing is costing more time or money than expected. Covers parser selection, per-page verification, and escalation from cheap local parsing to vision models. --- # Parsing documents with gaik ```bash pip install "gaik[parser]" # PyMuPDF, python-docx, Docling pip install "gaik[multimodal-parser]" # multi-provider vision parsing ``` ## Choose by what must survive, not by file type A parse that returns fluent, plausible text can still have destroyed the one thing the task needed. Decide first what has to still be true afterwards, then pick: | What must survive | Parser | Cost | |---|---|---| | Plain prose, simple single-column layout | `PyMuPDFParser` | free, local, milliseconds | | A Word document's text | `DocxParser` | free, local | | Text on scans / no text layer | `DoclingParser` (OCR) | slow on CPU, free | | **Table structure — merged cells, multi-row headers** | `MultimodalParser` or `VisionParser` | API call per page | | Images explained in place, for RAG chunks | `VisionPlusParser` | Docling + API call | | Docling quality without the local install | `DoclingApiClientParser` | needs `API_BASE` + `PASSWORD` | Escalate only when a check fails — start at the cheapest row that could plausibly work, verify (below), and move down one row if it did not. ## Why the cheap path scores zero on tables Measured on a public benchmark's table split (40 documents, GriTS and TEDS scored against ground-truth HTML table trees): | Parser | Output format | Table structure score | |---|---|---| | Vision-LLM parsing (`MultimodalParser`) | HTML `` | 0.90 – 0.96 | | Docling, serialized as HTML | HTML `
` | 0.89 | | Docling, as shipped (`use_markdown=True`) | markdown pipe table | **0.00** | | PyMuPDF | plain text | **0.00** | The zeros are not "much worse" — they are structurally unable to score, and that is the transferable point. **The output format decides what can survive.** A markdown pipe table has no way to express a merged cell or a two-row header, so a document containing one comes back looking clean and quietly wrong. Plain text loses column boundaries entirely. So the rule is not "always use vision". It is: if the tables carry merged cells or stacked headers, the parser must emit HTML — and among the paths that do, vision-LLM parsing led the specialized parser, with the gap widest on the messiest layouts. Treat those numbers as a dated snapshot on one corpus, not a constant. What generalizes is the format argument; re-measure the ranking on documents that look like yours. ## Parsing quality is usually a traceability decision, not an accuracy one The expensive counterexample, measured on an extraction task: feeding the model the native PDF, plain extracted text, or model-produced HTML gave **the same F1**. Parsing cost 22–25× the wall time and was 93% of total spend, and bought no accuracy at all. What it did buy was **bounding boxes**. When the parser returns coordinates for each element, a citation can be matched to a box afterwards, and a human reviewer clicks a highlighted region on the page. The model cannot invent that, and both halves stay independently checkable. Decide on that basis: - The deliverable is a reviewer clicking a highlighted box → pay for the rich parse. - Page-level evidence is enough, or nothing is reviewed by hand → do not. Feed the model the document and skip the parsing bill. ## Use the API correctly The class method and the module-level function of the same name **do not return the same type**, which is the easiest mistake to make here: ```python from gaik.software_components.parsers import PyMuPDFParser, parse_pdf text = PyMuPDFParser().parse_pdf("doc.pdf") # -> str result = parse_pdf("doc.pdf") # -> dict text = result["text_content"] ``` Every `parse_document` returns a dict, and the key differs by parser: ```python from gaik.software_components.parsers import ( DocxParser, DoclingParser, VisionPlusParser, MultimodalParser, ) DocxParser().parse_docx("doc.docx") # -> str DoclingParser().parse_document("scan.pdf")["text_content"] # OCR VisionPlusParser().parse_document("doc.pdf")["parsed_markdown"] # note: different key MultimodalParser(model_provider="openai").parse("doc.pdf") # -> ParseResult ``` `MultimodalParser` takes **keyword arguments only** and has no `config` parameter — it reads credentials from the environment. `ParseResult` is a plain dataclass with `raw_markdown`, `clean_markdown`, `html` (populated only when `create_html=True`) and `usage`; it has no `save()` method, so write the files yourself. `DoclingParser` has no `parse()` method. Set `merge_table=True` when a table runs across a page break — it instructs the model to stitch the halves back together, which no local parser can do. For which environment variables each provider needs, read `references/parser-selection.md`. ## Verify before building on the output Parsers fail quietly far more often than they raise, so check the output rather than the exception. Three checks catch nearly everything: **1. Emptiness, per page — never per document.** In one measured corpus 18 of 66 pages had no text layer, spread across half the documents. A document-level `if not text` check passes such a document as normal and those pages simply never reach the model: no error, no warning, no missing file. Loop the pages and assert each one produced characters; report which page numbers came back empty. **2. Table structure, if tables matter.** Search the output for `
` (or pipe rows). If the source has a merged cell and the output has no `
`, the structure is already gone — escalate rather than patch the text. **3. A known token round-trip.** Pick a handful of values you can see in the document — a total, an invoice number, a date — and assert they appear in the parsed text. This catches column-collapse and page-drop, which both otherwise read as fine prose. ## Gotchas - `parse_document` returns `text_content` on `PyMuPDFParser` and `DoclingParser`, but `parsed_markdown` on `VisionPlusParser` and `DoclingApiClientParser`. Same method name, different key — read the dict, don't assume. - Docling on CPU runs roughly 20–30 s/page. A GPU makes it faster but does **not** change its accuracy, so never reach for Docling to improve a *quality* result you measured on CPU — the number will be identical. - `DoclingParser` requires the `parser` extra, not `parser-cpu`. - Vision and audio components only accept OpenAI/Azure credentials and raise `NotImplementedError` for native Anthropic or Google. `MultimodalParser` is the multi-provider path. - On Windows, write parsed output with `encoding="utf-8"` explicitly. `Path.write_text()` defaults to the platform codepage, which raises on characters a document parser routinely produces — and a crashed write downstream looks exactly like a bad parse. - A parser returning fluent text is not evidence it read the whole document. Only the per-page check is.