File size: 11,222 Bytes
256c9c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | #!/usr/bin/env python3
"""
Extract structure from a parsed paper text.
Usage:
python extract_structure.py <paper_text.md> <output_dir>
Outputs:
{output_dir}/sections/ — individual section files
{output_dir}/algorithms/ — extracted algorithm boxes
{output_dir}/equations/ — extracted numbered equations
{output_dir}/tables/ — extracted tables
{output_dir}/footnotes.md — all footnotes collected
"""
import re
import sys
from pathlib import Path
def identify_sections(text: str) -> list[dict]:
"""Identify section boundaries using heading patterns.
Detects:
- Markdown headings (# , ## , ### )
- Numbered headings (1. Introduction, 2.1 Related Work)
- ALL CAPS headings (INTRODUCTION, RELATED WORK)
"""
lines = text.split("\n")
sections = []
current_section = None
current_lines = []
# Patterns for section headings
md_heading = re.compile(r"^(#{1,4})\s+(.+)$")
numbered_heading = re.compile(
r"^(\d+(?:\.\d+)*)\s+([A-Z][A-Za-z\s:,\-]+)$"
)
allcaps_heading = re.compile(r"^([A-Z][A-Z\s]{4,})$")
def save_current():
if current_section and current_lines:
sections.append({
"title": current_section,
"content": "\n".join(current_lines).strip(),
})
for line in lines:
heading = None
# Check markdown heading
m = md_heading.match(line)
if m:
heading = m.group(2).strip()
# Check numbered heading
if not heading:
m = numbered_heading.match(line.strip())
if m:
heading = f"{m.group(1)} {m.group(2).strip()}"
# Check ALL CAPS heading (only for longer titles to avoid false positives)
if not heading:
m = allcaps_heading.match(line.strip())
if m and len(m.group(1).strip()) > 5:
heading = m.group(1).strip().title()
if heading:
save_current()
current_section = heading
current_lines = []
else:
current_lines.append(line)
save_current()
return sections
def extract_algorithms(text: str) -> list[dict]:
"""Extract algorithm boxes from the paper.
Looks for patterns like:
Algorithm 1: Name
...algorithm body...
(ends at next section heading or next Algorithm block)
"""
algorithms = []
# Pattern: "Algorithm N" possibly followed by colon and name
pattern = re.compile(
r"(Algorithm\s+\d+[:\.]?\s*[^\n]*)\n(.*?)(?=Algorithm\s+\d+[:\.]|^#{1,4}\s|\Z)",
re.DOTALL | re.MULTILINE,
)
for match in pattern.finditer(text):
title = match.group(1).strip()
body = match.group(2).strip()
if body:
algorithms.append({
"title": title,
"content": body,
})
return algorithms
def extract_equations(text: str) -> list[dict]:
"""Extract numbered equations.
Looks for:
- LaTeX equation environments: \\begin{equation}...\\end{equation}
- Display math with numbering: $$ ... $$ (N)
- Inline equation references: (1), (2), Eq. 1, Equation 1
- Markdown math blocks
"""
equations = []
# LaTeX equation environments
latex_eq = re.compile(
r"\\begin\{(?:equation|align|gather)\*?\}(.*?)\\end\{(?:equation|align|gather)\*?\}",
re.DOTALL,
)
for i, match in enumerate(latex_eq.finditer(text)):
equations.append({
"number": i + 1,
"content": match.group(1).strip(),
"raw": match.group(0),
})
# Display math with parenthesized numbers: $$ formula $$ (N)
display_math = re.compile(r"\$\$(.*?)\$\$\s*\((\d+)\)", re.DOTALL)
for match in display_math.finditer(text):
equations.append({
"number": int(match.group(2)),
"content": match.group(1).strip(),
"raw": match.group(0),
})
# Lines that look like equations with numbers at the end: formula (N)
numbered_line = re.compile(r"^(.+?)\s+\((\d+)\)\s*$", re.MULTILINE)
for match in numbered_line.finditer(text):
content = match.group(1).strip()
num = int(match.group(2))
# Only include if it looks like an equation (has math-like characters)
if any(c in content for c in "=+∑∏∫_^{}\\√∞"):
if not any(eq["number"] == num for eq in equations):
equations.append({
"number": num,
"content": content,
"raw": match.group(0),
})
# Sort by equation number
equations.sort(key=lambda e: e["number"])
return equations
def extract_tables(text: str) -> list[dict]:
"""Extract tables from the paper text.
Looks for:
- Markdown tables (pipes)
- Table captions (Table N: ...)
- Structured text that looks like a table
"""
tables = []
# Find table captions and associated content
table_caption = re.compile(
r"(Table\s+\d+[:\.]?\s*[^\n]*)\n(.*?)(?=Table\s+\d+[:\.]|^#{1,4}\s|\Z)",
re.DOTALL | re.MULTILINE,
)
for match in table_caption.finditer(text):
caption = match.group(1).strip()
body = match.group(2).strip()
# Check if the body contains table-like content (pipes, tabs, or aligned columns)
if "|" in body or "\t" in body or re.search(r"\s{3,}", body):
tables.append({
"caption": caption,
"content": body[:2000], # limit size
})
# Also find markdown tables without explicit captions
md_table = re.compile(r"(\|[^\n]+\|\n\|[-:\s|]+\|\n(?:\|[^\n]+\|\n)*)", re.MULTILINE)
for match in md_table.finditer(text):
table_text = match.group(1).strip()
if not any(table_text in t["content"] for t in tables):
tables.append({
"caption": "Untitled table",
"content": table_text,
})
return tables
def extract_footnotes(text: str) -> list[dict]:
"""Extract footnotes from the paper."""
footnotes = []
# Pattern: footnote markers like ¹, ², ³ or [1], [2] at start of line
fn_pattern = re.compile(
r"(?:^|\n)[\s]*(?:[\u00b9\u00b2\u00b3\u2074-\u2079]|\[(\d+)\]|(\d+)\.)[\s]+(.+?)(?=\n[\s]*(?:[\u00b9\u00b2\u00b3\u2074-\u2079]|\[\d+\]|\d+\.)\s|\n\n|\Z)",
re.DOTALL,
)
for match in fn_pattern.finditer(text):
content = match.group(0).strip()
if len(content) > 10: # skip very short matches that are likely false positives
footnotes.append(content)
# Also look for explicit footnote sections
fn_section = re.compile(
r"(?:footnote|note)s?\s*:?\s*\n(.*?)(?=\n#{1,4}\s|\Z)",
re.DOTALL | re.IGNORECASE,
)
for match in fn_section.finditer(text):
content = match.group(1).strip()
if content and content not in footnotes:
footnotes.append(content)
return [{"content": fn} for fn in footnotes]
def save_list_to_dir(items: list[dict], output_dir: Path, name_key: str = "title"):
"""Save a list of extracted items as individual files."""
output_dir.mkdir(parents=True, exist_ok=True)
for i, item in enumerate(items):
# Create a clean filename
name = item.get(name_key, item.get("caption", f"item_{i+1}"))
name = str(name)
clean_name = re.sub(r"[^\w\s-]", "", name)
clean_name = re.sub(r"\s+", "_", clean_name).strip("_").lower()
if not clean_name:
clean_name = f"item_{i+1}"
clean_name = clean_name[:80] # limit filename length
filepath = output_dir / f"{i+1:02d}_{clean_name}.md"
content = f"# {name}\n\n{item.get('content', item.get('raw', ''))}\n"
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
def main():
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <paper_text.md> <output_dir>", file=sys.stderr)
sys.exit(1)
paper_path = Path(sys.argv[1])
output_dir = Path(sys.argv[2])
if not paper_path.exists():
print(f"ERROR: {paper_path} does not exist", file=sys.stderr)
sys.exit(1)
print(f"Extracting structure from: {paper_path}")
text = paper_path.read_text(encoding="utf-8")
print(f" Total characters: {len(text):,}")
# Extract sections
print("\n--- Extracting sections ---")
sections = identify_sections(text)
if sections:
save_list_to_dir(sections, output_dir / "sections")
print(f" Found {len(sections)} sections:")
for s in sections:
print(f" - {s['title']} ({len(s['content'])} chars)")
else:
print(" WARNING: No sections detected. The paper text may not have clear headings.")
# Save the entire text as a single section
(output_dir / "sections").mkdir(parents=True, exist_ok=True)
(output_dir / "sections" / "01_full_text.md").write_text(text, encoding="utf-8")
# Extract algorithms
print("\n--- Extracting algorithm boxes ---")
algorithms = extract_algorithms(text)
if algorithms:
save_list_to_dir(algorithms, output_dir / "algorithms")
print(f" Found {len(algorithms)} algorithms:")
for a in algorithms:
print(f" - {a['title']}")
else:
print(" No algorithm boxes found.")
# Extract equations
print("\n--- Extracting equations ---")
equations = extract_equations(text)
if equations:
save_list_to_dir(equations, output_dir / "equations", name_key="number")
print(f" Found {len(equations)} numbered equations")
else:
print(" No numbered equations found (may be inline or in non-standard format).")
# Extract tables
print("\n--- Extracting tables ---")
tables = extract_tables(text)
if tables:
save_list_to_dir(tables, output_dir / "tables", name_key="caption")
print(f" Found {len(tables)} tables:")
for t in tables:
print(f" - {t['caption']}")
else:
print(" No tables found.")
# Extract footnotes
print("\n--- Extracting footnotes ---")
footnotes = extract_footnotes(text)
footnotes_path = output_dir / "footnotes.md"
if footnotes:
with open(footnotes_path, "w", encoding="utf-8") as f:
f.write("# Footnotes\n\n")
for i, fn in enumerate(footnotes):
f.write(f"## Footnote {i + 1}\n\n{fn['content']}\n\n---\n\n")
print(f" Found {len(footnotes)} footnotes")
else:
with open(footnotes_path, "w", encoding="utf-8") as f:
f.write("# Footnotes\n\nNo footnotes extracted.\n")
print(" No footnotes found.")
# Summary
print(f"\n--- Extraction Summary ---")
print(f" Sections: {len(sections)}")
print(f" Algorithms: {len(algorithms)}")
print(f" Equations: {len(equations)}")
print(f" Tables: {len(tables)}")
print(f" Footnotes: {len(footnotes)}")
print(f" Output dir: {output_dir}")
print(f"\nDone.")
if __name__ == "__main__":
main()
|