Skip to content

Commit f44f3a5

Browse files
authored
Merge pull request #5 from HaishuoFang/fix/table-extraction
Fix table extraction from arXiv HTML papers
2 parents 466bd7f + e66640b commit f44f3a5

2 files changed

Lines changed: 144 additions & 17 deletions

File tree

src/arxiv2md/markdown.py

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,33 @@ def _serialize_table(table: Tag, *, remove_inline_citations: bool = False) -> st
297297
return f"$$ {eqn_text} $$"
298298

299299
rows = []
300-
for row in table.find_all("tr", recursive=False):
301-
cells = row.find_all(["th", "td"], recursive=False)
302-
if not cells:
303-
continue
304-
values = []
305-
for cell in cells:
306-
cell_text = _cleanup_inline_text(_serialize_inline(cell, remove_inline_citations=remove_inline_citations)).replace("\n", "<br>")
307-
values.append(cell_text)
308-
rows.append(values)
300+
# Find rows in tbody, thead, tfoot, or directly in table
301+
# Handle nested structure where rows might be inside tbody/thead/tfoot
302+
tbody_elements = table.find_all(["tbody", "thead", "tfoot"], recursive=False)
303+
304+
if tbody_elements:
305+
# Table has tbody/thead/tfoot structure - find rows within them
306+
for tbody in tbody_elements:
307+
for row in tbody.find_all("tr", recursive=False):
308+
cells = row.find_all(["th", "td"], recursive=False)
309+
if not cells:
310+
continue
311+
values = []
312+
for cell in cells:
313+
cell_text = _cleanup_inline_text(_serialize_inline(cell, remove_inline_citations=remove_inline_citations)).replace("\n", "<br>")
314+
values.append(cell_text)
315+
rows.append(values)
316+
else:
317+
# Table has no tbody/thead/tfoot - find rows directly in table
318+
for row in table.find_all("tr", recursive=False):
319+
cells = row.find_all(["th", "td"], recursive=False)
320+
if not cells:
321+
continue
322+
values = []
323+
for cell in cells:
324+
cell_text = _cleanup_inline_text(_serialize_inline(cell, remove_inline_citations=remove_inline_citations)).replace("\n", "<br>")
325+
values.append(cell_text)
326+
rows.append(values)
309327

310328
if not rows:
311329
return ""
@@ -323,18 +341,40 @@ def _serialize_table(table: Tag, *, remove_inline_citations: bool = False) -> st
323341

324342

325343
def _serialize_figure(figure: Tag, *, remove_inline_citations: bool = False) -> str:
344+
# Check if this is a table figure (ltx_table class)
345+
figure_classes = " ".join(figure.get("class", []))
346+
is_table_figure = "ltx_table" in figure_classes
347+
326348
caption_tag = figure.find("figcaption")
327349
caption = _normalize_text(_serialize_inline(caption_tag, remove_inline_citations=remove_inline_citations)) if caption_tag else ""
328-
img = figure.find("img")
329-
src = img.get("src") if img else None
330-
alt = img.get("alt") if img else None
331350

332351
lines = []
333-
if caption:
334-
lines.append(f"Figure: {caption}")
335-
if src:
336-
image_label = alt or "Image"
337-
lines.append(f"{image_label}: {src}")
352+
353+
if is_table_figure:
354+
# Handle table figures - find and serialize the embedded table
355+
# Note: fix_tabular_tables strips attributes, so search for any table element
356+
table = figure.find("table")
357+
if table:
358+
table_md = _serialize_table(table, remove_inline_citations=remove_inline_citations)
359+
if caption:
360+
lines.append(f"**{caption}**")
361+
if table_md:
362+
lines.append(table_md)
363+
elif caption:
364+
# Fallback if no table found but has caption
365+
lines.append(f"Table: {caption}")
366+
else:
367+
# Handle regular image figures
368+
img = figure.find("img")
369+
src = img.get("src") if img else None
370+
alt = img.get("alt") if img else None
371+
372+
if caption:
373+
lines.append(f"Figure: {caption}")
374+
if src:
375+
image_label = alt or "Image"
376+
lines.append(f"{image_label}: {src}")
377+
338378
return "\n".join(lines).strip()
339379

340380

tests/test_markdown.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,90 @@ def test_math_and_tables_render() -> None:
2626
assert "| 1 | 2 |" in markdown
2727
assert "$$" in markdown
2828
assert "E = mc^2" in markdown
29+
30+
31+
def test_table_with_tbody() -> None:
32+
"""Test that tables with tbody/thead/tfoot structure are correctly converted."""
33+
html = """
34+
<table class="ltx_tabular">
35+
<tbody>
36+
<tr><th>Model</th><th>Accuracy</th></tr>
37+
<tr><td>Llama-7B</td><td>70.12</td></tr>
38+
<tr><td>Llama-13B</td><td>72.39</td></tr>
39+
</tbody>
40+
</table>
41+
"""
42+
43+
markdown = convert_fragment_to_markdown(html)
44+
45+
# Should contain table structure
46+
assert "| Model | Accuracy |" in markdown
47+
assert "| --- | --- |" in markdown
48+
assert "| Llama-7B | 70.12 |" in markdown
49+
assert "| Llama-13B | 72.39 |" in markdown
50+
51+
52+
def test_table_with_thead_tbody() -> None:
53+
"""Test that tables with thead and tbody are correctly converted."""
54+
html = """
55+
<table class="ltx_tabular">
56+
<thead>
57+
<tr><th>Method</th><th>Result</th></tr>
58+
</thead>
59+
<tbody>
60+
<tr><td>Prune SW</td><td>0.0%</td></tr>
61+
<tr><td>Prune Non-SW</td><td>68.5%</td></tr>
62+
</tbody>
63+
</table>
64+
"""
65+
66+
markdown = convert_fragment_to_markdown(html)
67+
68+
# Should contain table structure
69+
assert "| Method | Result |" in markdown
70+
assert "| Prune SW | 0.0% |" in markdown
71+
assert "| Prune Non-SW | 68.5% |" in markdown
72+
73+
74+
def test_table_inside_figure() -> None:
75+
"""Test that tables wrapped in figure elements (ltx_table) are correctly converted."""
76+
html = """
77+
<figure class="ltx_table" id="S3.T1">
78+
<table class="ltx_tabular ltx_centering ltx_guessed_headers ltx_align_middle">
79+
<thead class="ltx_thead">
80+
<tr class="ltx_tr">
81+
<th class="ltx_td ltx_align_left ltx_th ltx_th_column">Model</th>
82+
<th class="ltx_td ltx_align_center ltx_th ltx_th_column">Arc-c</th>
83+
<th class="ltx_td ltx_align_center ltx_th ltx_th_column">Arc-e</th>
84+
</tr>
85+
</thead>
86+
<tbody class="ltx_tbody">
87+
<tr class="ltx_tr">
88+
<td class="ltx_td ltx_align_left">Original</td>
89+
<td class="ltx_td ltx_align_center">41.81</td>
90+
<td class="ltx_td ltx_align_center">75.29</td>
91+
</tr>
92+
<tr class="ltx_tr">
93+
<td class="ltx_td ltx_align_left">Prune SW</td>
94+
<td class="ltx_td ltx_align_center">19.80</td>
95+
<td class="ltx_td ltx_align_center">39.60</td>
96+
</tr>
97+
</tbody>
98+
</table>
99+
<figcaption class="ltx_caption ltx_centering">
100+
<span class="ltx_tag ltx_tag_table">Table 1: </span>
101+
<span class="ltx_text ltx_font_bold">Super Weight Importance</span>.
102+
Pruning the super weight significantly impairs quality.
103+
</figcaption>
104+
</figure>
105+
"""
106+
107+
markdown = convert_fragment_to_markdown(html)
108+
109+
# Should contain the caption
110+
assert "Table 1:" in markdown
111+
assert "Super Weight Importance" in markdown
112+
# Should contain the actual table data
113+
assert "| Model | Arc-c | Arc-e |" in markdown
114+
assert "| Original | 41.81 | 75.29 |" in markdown
115+
assert "| Prune SW | 19.80 | 39.60 |" in markdown

0 commit comments

Comments
 (0)