#!/usr/bin/env python3
"""Parse PDFs under papers/ and write a spreadsheet summary."""
from pathlib import Path
import argparse
import csv
import sys
import zipfile
from html import escape
try:
import pandas as pd
except Exception:
pd = None
# Ensure repo root is on sys.path so sibling modules import correctly
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from parse_arbitrary_pdf_first_page import parse_arbitrary_pdf
FIELDNAMES = [
"filename",
"title",
"authors",
"affiliations",
"institution_set",
"abstract",
"pattern",
"parse_status",
"problem_solved",
"how_solved",
]
def _write_minimal_xlsx(rows: list[dict], out_xlsx: Path) -> None:
"""Write a basic Excel workbook using only the Python standard library."""
data = [FIELDNAMES] + [[str(row.get(field, "")) for field in FIELDNAMES] for row in rows]
def cell_ref(row_idx: int, col_idx: int) -> str:
letters = ""
col = col_idx
while col:
col, rem = divmod(col - 1, 26)
letters = chr(65 + rem) + letters
return f"{letters}{row_idx}"
sheet_rows = []
for r_idx, row in enumerate(data, start=1):
cells = []
for c_idx, value in enumerate(row, start=1):
cells.append(
f''
f"{escape(value)}"
""
)
sheet_rows.append(f'{"".join(cells)}
')
sheet_xml = (
''
''
''
''
f'{"".join(sheet_rows)}'
""
)
workbook_xml = (
''
''
''
""
)
rels_xml = (
''
''
''
""
)
workbook_rels_xml = (
''
''
''
""
)
content_types_xml = (
''
''
''
''
''
''
""
)
with zipfile.ZipFile(out_xlsx, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("[Content_Types].xml", content_types_xml)
zf.writestr("_rels/.rels", rels_xml)
zf.writestr("xl/workbook.xml", workbook_xml)
zf.writestr("xl/_rels/workbook.xml.rels", workbook_rels_xml)
zf.writestr("xl/worksheets/sheet1.xml", sheet_xml)
def summarize_folder(folder: Path, out_csv: Path, out_xlsx: Path | None = None):
rows = []
for p in sorted(folder.iterdir()):
if p.is_file() and p.suffix.lower() == ".pdf":
r = parse_arbitrary_pdf(p)
if r.get("success"):
authors = "; ".join(r.get("authors", []))
# join affiliations per author as 'Author -> A;B'
affs = []
for a, af in zip(r.get("authors", []), r.get("affiliations_per_author", [])):
affs.append(f"{a} -> {', '.join(af) if af else 'N/A'}")
affiliations = "; ".join(affs)
inst_set = "; ".join(r.get("institutions_set", []))
pattern = r.get("pattern")
abstract = r.get("abstract", "")
problem = r.get("problem_solved", "")
solution = r.get("how_solved", "")
else:
authors = ""
affiliations = ""
inst_set = ""
pattern = r.get("pattern")
abstract = r.get("abstract", "")
problem = ""
solution = ""
rows.append({
"filename": p.name,
"title": r.get("title", ""),
"authors": authors,
"affiliations": affiliations,
"institution_set": inst_set,
"abstract": abstract,
"pattern": pattern,
"parse_status": "success" if r.get("success") else r.get("reason", "failed"),
"problem_solved": problem,
"how_solved": solution,
})
# write CSV
with open(out_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
writer.writeheader()
for row in rows:
writer.writerow(row)
# optionally write Excel if pandas is available
if out_xlsx and pd is not None:
df = pd.DataFrame(rows)
df.to_excel(out_xlsx, index=False)
elif out_xlsx:
_write_minimal_xlsx(rows, out_xlsx)
if __name__ == "__main__":
base = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(
description="Extract paper metadata from the first page of a directory of PDFs."
)
parser.add_argument("--pdf-dir", type=Path, default=base / "papers")
parser.add_argument("--out-csv", type=Path, default=base / "data/papers_parsed.csv")
parser.add_argument("--out-xlsx", type=Path, default=base / "data/papers_parsed.xlsx")
args = parser.parse_args()
args.out_csv.parent.mkdir(parents=True, exist_ok=True)
args.out_xlsx.parent.mkdir(parents=True, exist_ok=True)
print(f"Parsing PDFs in {args.pdf_dir} -> {args.out_csv}")
summarize_folder(args.pdf_dir, args.out_csv, args.out_xlsx)
print("Done.")