Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-rag/src/lexigram/ai/rag/preprocessing/tables.py: 97%
95 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""
2Table extractor for extracting structured tables from documents.
4Note: This is a simplified implementation. In production,
5integrate with libraries like camelot, tabula, or deep learning models.
6"""
8from __future__ import annotations
10import re
11from typing import TYPE_CHECKING
13if TYPE_CHECKING:
14 from lexigram.ai.rag.preprocessing.types import (
15 DocumentType,
16 ExtractedTable,
17 TableFormat,
18 )
20from lexigram.ai.rag.preprocessing.base import AbstractPreprocessor
21from lexigram.ai.rag.preprocessing.document import PreprocessedDocument
22from lexigram.ai.rag.preprocessing.types import (
23 DocumentMetadata,
24 DocumentType,
25 ExtractedTable,
26 TableFormat,
27)
30class TableExtractor(AbstractPreprocessor):
31 """Table extractor for extracting structured tables from documents.
33 Note: This is a simplified implementation. In production,
34 integrate with libraries like camelot, tabula, or deep learning models.
35 """
37 def __init__(self, table_format: TableFormat = TableFormat.MARKDOWN):
38 """Initialize table extractor.
40 Args:
41 table_format: Format for extracted tables.
42 """
43 super().__init__("table_extractor")
44 self.table_format = table_format
46 async def preprocess(
47 self,
48 content: str,
49 **kwargs,
50 ) -> PreprocessedDocument:
51 """Extract tables from document.
53 Args:
54 content: Document content (HTML, Markdown, or text).
55 **kwargs: Additional parameters.
57 Returns:
58 Preprocessed document with extracted tables.
59 """
60 tables = self._extract_tables(content)
62 # Remove tables from text, replace with references
63 processed_text = content
64 for i, table in enumerate(tables):
65 processed_text = processed_text.replace(
66 self._get_table_marker(i),
67 f"[Table {i + 1}: {table.caption or 'Untitled'}]",
68 )
70 metadata = DocumentMetadata(
71 doc_type=self._detect_document_type(content),
72 word_count=len(processed_text.split()),
73 )
75 return PreprocessedDocument(
76 content=processed_text,
77 metadata=metadata,
78 tables=tables,
79 raw_content=content,
80 )
82 def _extract_tables(self, content: str) -> list[ExtractedTable]:
83 """Extract tables from content.
85 Args:
86 content: Document content.
88 Returns:
89 List of extracted tables.
90 """
91 tables = []
93 # Extract Markdown tables
94 markdown_tables = self._extract_markdown_tables(content)
95 tables.extend(markdown_tables)
97 # Extract HTML tables
98 html_tables = self._extract_html_tables(content)
99 tables.extend(html_tables)
101 return tables
103 def _extract_markdown_tables(self, content: str) -> list[ExtractedTable]:
104 """Extract tables from Markdown content.
106 Args:
107 content: Markdown content.
109 Returns:
110 List of extracted tables.
111 """
112 tables = []
114 # Simple Markdown table pattern: | col1 | col2 |
115 # Followed by separator: |------|------|
116 # Followed by rows: | val1 | val2 |
118 # Find table blocks
119 lines = content.split("\n")
120 i = 0
121 table_id = 0
123 while i < len(lines):
124 line = lines[i].strip()
126 # Check if line looks like a table row
127 if line.startswith("|") and line.endswith("|"):
128 # Potential table start
129 table_lines = [line]
130 i += 1
132 # Collect all consecutive table lines
133 while i < len(lines):
134 next_line = lines[i].strip()
135 if next_line.startswith("|") and next_line.endswith("|"):
136 table_lines.append(next_line)
137 i += 1
138 else:
139 break
141 # Parse table
142 if len(table_lines) >= 2: # At least header + separator
143 table = self._parse_markdown_table(table_lines, table_id)
144 if table:
145 tables.append(table)
146 table_id += 1
147 else:
148 i += 1
150 return tables
152 def _parse_markdown_table(
153 self,
154 lines: list[str],
155 table_id: int,
156 ) -> ExtractedTable | None:
157 """Parse Markdown table from lines.
159 Args:
160 lines: Table lines.
161 table_id: Table identifier.
163 Returns:
164 Extracted table or None.
165 """
166 if len(lines) < 2:
167 return None
169 # Parse headers
170 header_line = lines[0].strip("|").strip()
171 headers = list(map(str.strip, header_line.split("|")))
173 # Skip separator line (line 1)
174 # Parse rows
175 rows = []
176 for line in lines[2:]:
177 row_line = line.strip("|").strip()
178 row = list(map(str.strip, row_line.split("|")))
179 if len(row) == len(headers): # Valid row
180 rows.append(row)
182 return ExtractedTable(
183 table_id=f"table_{table_id}",
184 page_number=0, # Unknown for text content
185 headers=headers,
186 rows=rows,
187 format=TableFormat.MARKDOWN,
188 )
190 def _extract_html_tables(self, content: str) -> list[ExtractedTable]:
191 """Extract tables from HTML content.
193 Args:
194 content: HTML content.
196 Returns:
197 List of extracted tables.
198 """
199 tables = []
201 # Simple HTML table extraction using regex
202 # In production, use proper HTML parser like BeautifulSoup
203 table_pattern = r"<table[^>]*>(.*?)</table>"
204 table_matches = re.findall(table_pattern, content, re.DOTALL | re.IGNORECASE)
206 for i, table_html in enumerate(table_matches):
207 table = self._parse_html_table(table_html, i)
208 if table:
209 tables.append(table)
211 return tables
213 def _parse_html_table(
214 self,
215 html: str,
216 table_id: int,
217 ) -> ExtractedTable | None:
218 """Parse HTML table.
220 Args:
221 html: HTML table content.
222 table_id: Table identifier.
224 Returns:
225 Extracted table or None.
226 """
227 # Extract headers from <th> tags
228 header_pattern = r"<th[^>]*>(.*?)</th>"
229 header_matches = re.findall(header_pattern, html, re.DOTALL | re.IGNORECASE)
230 headers = list(map(self._clean_html, header_matches))
232 # Extract rows from <tr> tags
233 row_pattern = r"<tr[^>]*>(.*?)</tr>"
234 row_matches = re.findall(row_pattern, html, re.DOTALL | re.IGNORECASE)
236 rows = []
237 for row_html in row_matches:
238 # Extract cells from <td> tags
239 cell_pattern = r"<td[^>]*>(.*?)</td>"
240 cell_matches = re.findall(cell_pattern, row_html, re.DOTALL | re.IGNORECASE)
241 row = list(map(self._clean_html, cell_matches))
242 if row: # Valid row
243 rows.append(row)
245 if not headers or not rows:
246 return None
248 return ExtractedTable(
249 table_id=f"table_{table_id}",
250 page_number=0, # Unknown for HTML content
251 headers=headers,
252 rows=rows,
253 format=self.table_format,
254 )
256 def _clean_html(self, html_text: str) -> str:
257 """Clean HTML tags from text.
259 Args:
260 html_text: Text with HTML tags.
262 Returns:
263 Clean text.
264 """
265 # Remove HTML tags
266 clean = re.sub(r"<[^>]+>", "", html_text)
267 return clean.strip()
269 def _detect_document_type(self, content: str) -> DocumentType:
270 """Detect document type from content.
272 Args:
273 content: Document content.
275 Returns:
276 Detected document type.
277 """
278 if "<table>" in content.lower():
279 return DocumentType.HTML
280 if "|" in content and "|" in content.split("\n", maxsplit=1)[0]:
281 return DocumentType.MARKDOWN
282 return DocumentType.TEXT
284 def _get_table_marker(self, table_index: int) -> str:
285 """Get marker for table in content.
287 Args:
288 table_index: Index of table.
290 Returns:
291 Table marker string.
292 """
293 return f"[TABLE_MARKER_{table_index}]"