xlsongc commented on code in PR #73926:
URL: https://github.com/apache/airflow/pull/73926#discussion_r4291201551


##########
providers/common/ai/src/airflow/providers/common/ai/operators/document_loader.py:
##########
@@ -450,21 +453,77 @@ def _parse_pdf_stream(self, stream: BinaryIO) -> 
list[dict[str, Any]]:
                 documents.append({"text": text, "metadata": {"page_number": 
page_num + 1}})
         return documents
 
-    def _parse_docx_stream(self, stream: BinaryIO) -> list[dict[str, Any]]:
+    def _parse_docx_stream(self, stream: BinaryIO, *, source_hint: str) -> 
list[dict[str, Any]]:
         """
-        Parse a DOCX stream into documents.
+        Parse a DOCX stream into a single document.
 
-        Extracts paragraph text only. Tables, headers, footers, and footnotes
-        are not included. For richer DOCX parsing, plug in a dedicated
-        extraction tool (``Unstructured``, ``docling``) as a custom parser
-        backend.
+        Paragraphs and tables in the document body are extracted in document
+        order. Each table row becomes one "| cell | cell |" line, and a nested
+        table is flattened into its cell. Headers, footers, footnotes, content
+        controls, text boxes, and pending tracked insertions are not included.
         """
         try:
             from docx import Document
+            from docx.oxml.exceptions import InvalidXmlError
+            from docx.table import Table
         except ImportError as e:
             raise AirflowOptionalProviderFeatureException(e)
 
         doc = Document(stream)
-        paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
-        text = "\n\n".join(paragraphs)
+        blocks = []
+        for block in doc.iter_inner_content():
+            if isinstance(block, Table):
+                try:
+                    rows = self._get_docx_table_rows(block, Table)
+                except (ValueError, InvalidXmlError, RecursionError) as e:
+                    # Malformed table XML raises ValueError (a vMerge with no 
cell above) or
+                    # InvalidXmlError (a missing required attribute). 
python-docx walks vertical
+                    # merges up recursively, so one spanning about 1000 rows 
raises RecursionError.
+                    self.log.warning(
+                        "Skipping a table in %s that python-docx could not 
read: %r", source_hint, e
+                    )
+                    continue
+                text = "\n".join(f"| {' | '.join(cells)} |" for cells in rows)
+            else:
+                text = block.text
+            if text.strip():
+                blocks.append(text)
+        text = "\n\n".join(blocks)
         return [{"text": text, "metadata": {}}]
+
+    def _get_docx_table_rows(self, table: Table, table_cls: type[Table]) -> 
list[list[str]]:
+        # w:gridBefore, w:gridAfter and w:gridSpan come straight from the XML, 
so the empty
+        # cells added for them are capped at the table's column count.
+        column_count = len(table.columns)
+
+        rows = []
+        for row in table.rows:
+            cells: list[str] = [""] * min(row.grid_cols_before, column_count)
+            previous_cell = None
+            for cell in row.cells:
+                # python-docx repeats the same _Cell object for every grid 
column a horizontal
+                # merge spans: keep its text once and leave the other columns 
empty so later
+                # cells stay under their headers. A vertical merge repeats on 
each row it spans.
+                if cell is previous_cell:
+                    if len(cells) < column_count:
+                        cells.append("")
+                    continue
+                previous_cell = cell
+                cells.append(self._get_docx_cell_text(cell, table_cls))
+            cells.extend([""] * min(row.grid_cols_after, column_count))
+            if any(cells):
+                rows.append(cells)
+        return rows
+
+    def _get_docx_cell_text(self, cell: _Cell, table_cls: type[Table]) -> str:
+        parts = []
+        for item in cell.iter_inner_content():
+            if isinstance(item, table_cls):
+                text = "; ".join(" / ".join(cells) for cells in 
self._get_docx_table_rows(item, table_cls))

Review Comment:
   Done. The try/except is now a helper, `_get_docx_table_rows_or_none`, used 
for both top-level and nested tables, so an unreadable nested table is left out 
of its cell and the outer rows stay. 
`test_docx_unreadable_nested_table_skipped_with_warning` uses your example and 
expects `| Name | Qty |` and `| Apple |  |` plus the warning; it fails without 
the change. `InvalidXmlError` is passed down as `invalid_xml_error_cls`, like 
`table_cls`, and the docs now say only the nested table is left out.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to