DOCX Parsing#
MinerU converts DOCX files into a page-block list using a multi-stage pipeline implemented in DocxConverter. The pipeline entry point is office_docx_analyze(), which calls convert_binary() and then passes results to result_to_middle_json().
Pipeline Overview#
DOCX bytes
→ package_normalizer (ZIP-level repair)
→ Mammoth pre-parse (full package context, top-level tables only)
→ python-docx Document object + _walk_linear()
├── tables → _handle_tables() [Mammoth result or isolated XML fallback]
├── paragraphs → _handle_text_elements()
├── pictures → _handle_pictures()
└── textboxes, SDTs, DrawingML, equations, …
→ result_to_middle_json()
convert() orchestrates startup: it sanitizes the file bytes, runs the Mammoth pre-parse, builds the Document object, pre-scans heading-list numIds, and then calls _walk_linear() on document.element.body.
Stage 0: Package Normalization#
Before any parsing, _sanitize_missing_internal_relationships() passes the raw bytes through normalize_docx_package() in mineru/model/docx/package_normalizer.py. This ZIP-level repair step removes broken OPC relationships, skips corrupt or unreadable members (including embedded Office/OLE payloads under word/embeddings/), and allows downstream libraries to open documents that would otherwise raise exceptions.
Stage 1: Mammoth Pre-Parse (Tables)#
_preparse_tables_with_mammoth() uses Mammoth to convert only top-level <w:tbl> elements to HTML, with access to the full DOCX package context (numbering definitions, styles, relationships). This solves a core limitation of isolated XML parsing: without numbering.xml and styles.xml, table cells containing lists or images cause AttributeErrors.
Key steps :
- Mammoth converts the full document but
_mammoth_top_level_table_document()filters the AST to keep only top-levelTablenodes, avoiding the cost of converting all body content. - BeautifulSoup parses the HTML output and filters to top-level
<table>elements only (excluding tables nested inside other tables). _align_mammoth_tables_to_xml_tables()matches each Mammoth HTML table to its corresponding<w:tbl>body element using a lightweight signature (row count, cell count, image count, normalized text). This prevents extra tables generated from textboxes or compatibility structures from misaligning the index._inject_equations_into_table()re-walks the XML table and injects OMML formulas (as<eq>…</eq>LaTeX placeholders) into any cells where Mammoth silently dropped them. This is done cell-by-cell by comparing HTML and XML rows in lock-step.
The result is stored in self._mammoth_tables_html — a list aligned one-to-one with body <w:tbl> elements. None entries signal that no reliable Mammoth result exists for that table.
Stage 2: Table Dispatch in _handle_tables()#
When _walk_linear() encounters a tbl element, it calls _handle_tables():
- Mammoth path: If
_mammoth_table_idx < len(_mammoth_tables_html)and the corresponding entry is notNone, the pre-parsed HTML is consumed and passed to_normalize_table_colspans()before being appended as aBlockType.TABLEblock. - Isolated XML fallback: If the Mammoth list is exhausted or the entry is
None, the raw<w:tbl>XML is parsed in isolation via Mammoth'sbody_xml.reader()andconvert_document_element_to_html(). This fallback lacks full document context but handles documents where the pre-parse itself failed.
colspan Normalization#
_normalize_table_colspans() corrects mismatched colspan values that arise from DOCX's internal virtual grid (w:gridSpan). Mammoth maps gridSpan values directly to HTML colspan, but borderless or sparse tables often have rows where colspan sums don't match. The algorithm:
- Computes the effective column count per row (sum of all
colspanvalues). - Picks the most-frequent count as the target.
- Reduces
colspanon oversized rows by trimming the first cell withcolspan > 1.
Tables withrowspan > 1cells are skipped to avoid corrupting valid merged-cell layouts.
Stage 3: Non-Table Body Elements#
_walk_linear() dispatches each body element by tag :
| Tag | Handler | Output Block Types |
|---|---|---|
tbl | _handle_tables() | TABLE |
p (with pictures) | _handle_pictures() + _handle_text_elements() | IMAGE, TEXT |
p | _handle_text_elements() | TEXT, TITLE, EQUATION, CAPTION, LIST |
sdt | _handle_sdt_as_index() or paragraph walk | INDEX, TEXT |
_handle_text_elements() maps paragraph style IDs to block types (Title, Heading → TITLE; Normal/Paragraph/etc → TEXT; caption → CAPTION). It also handles inline equations via _handle_equations_in_text() (OMML → LaTeX) and rich text formatting (bold, italic, underline, strikethrough, sub/superscript) via _get_format_from_run().
Nested Table Preservation#
PR #5243 added level-aware table matching and a recursive XML fallback so that nested tables are not silently dropped when Mammoth alignment or the isolated fallback fails. Key changes:
- Matching and colspan normalization are now nesting-level-aware — nested rows/cells no longer pollute parent table structure.
- Equation injection preserves nested table HTML rather than flattening it.
- A recursive XML fallback converts nested tables even when the primary alignment path fails.
Error Visibility#
Table parse failures are caught broadly:
except Exception:
logger.debug("could not parse a table, broken docx table")
The exception details are not included in the log message, making silent failures hard to diagnose. When debugging table parsing issues, also check the debug logs from _preparse_tables_with_mammoth() ("Could not pre-parse tables…") and _normalize_table_colspans() ("Failed to normalize table colspans…").
Key Files#
| File | Role |
|---|---|
mineru/model/docx/docx_converter.py | Core conversion logic: DocxConverter, all _handle_* methods |
mineru/model/docx/package_normalizer.py | ZIP-level DOCX repair before parsing |
mineru/backend/office/docx_analyze.py | Pipeline entry point: office_docx_analyze() |
mineru/backend/office/office_magic_model.py | Block restructuring and image_base64 field assignment |