| |
| """ |
| 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 = [] |
|
|
| |
| 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 |
|
|
| |
| m = md_heading.match(line) |
| if m: |
| heading = m.group(2).strip() |
|
|
| |
| if not heading: |
| m = numbered_heading.match(line.strip()) |
| if m: |
| heading = f"{m.group(1)} {m.group(2).strip()}" |
|
|
| |
| 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 = 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_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 = 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), |
| }) |
|
|
| |
| 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)) |
| |
| 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), |
| }) |
|
|
| |
| 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 = [] |
|
|
| |
| 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() |
|
|
| |
| if "|" in body or "\t" in body or re.search(r"\s{3,}", body): |
| tables.append({ |
| "caption": caption, |
| "content": body[:2000], |
| }) |
|
|
| |
| 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 = [] |
|
|
| |
| 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: |
| footnotes.append(content) |
|
|
| |
| 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): |
| |
| 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] |
|
|
| 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):,}") |
|
|
| |
| 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.") |
| |
| (output_dir / "sections").mkdir(parents=True, exist_ok=True) |
| (output_dir / "sections" / "01_full_text.md").write_text(text, encoding="utf-8") |
|
|
| |
| 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.") |
|
|
| |
| 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).") |
|
|
| |
| 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.") |
|
|
| |
| 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.") |
|
|
| |
| 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() |
|
|