Research-Stack/4-Infrastructure/shim/arxiv_oaipmh_harvest.py
allaun 77488ac0ae feat(lean): close gaussian_line_integral_unit_dir + consolidate infrastructure
Lean proof fixes:
- N3L_Energy.lean: fully close gaussian_line_integral_unit_dir (nlinarith+hab
  for unit-circle quadratic, sqrt_mul+neg_div for integral_gaussian_1d match,
  exp_sum_of_sq order fix, add_assoc for h_gauss_shift, sq_sqrt for field_simp,
  sq_abs for perpDistance hd)
- Add Adapters/AlphaProofNexus: 12 Erdos/graph adapter stubs (AlphaProof nexus)
- Add Adapters/ErgodicAdditive.lean, SidonMatroid.lean
- Add AntiDiophantine.lean, EffectiveBoundDQ.lean, PVGS_DQ_Bridge.lean
- Add FormalConjectures/Util/ProblemImports.lean
- Add RRC/EntropyCandidates/Candidates.lean
- Add OTOM external project (lakefile.toml, lake-manifest.json, lean-toolchain)

Infrastructure:
- Add 4-Infrastructure/shim/: 17 Python probes (RRC manifold, Sidon kernel,
  Wannier, arxiv harvest, math_symbols DB, coverage density, geometric entropy)
- Add 4-Infrastructure/NoDupeLabs/: Node server + package files
- Add 6-Documentation/docs/specs/DP_RRC_RECEIPT_ENCODING_SPEC.md
- Add fix_offloat.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 16:53:23 -05:00

174 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""
arxiv_oaipmh_harvest.py — Harvest arXiv math papers via OAI-PMH into arxiv DB.
Usage:
python3 4-Infrastructure/shim/arxiv_oaipmh_harvest.py --set=math:math:NT
python3 4-Infrastructure/shim/arxiv_oaipmh_harvest.py --set=math:math:NT --set=math:math:CO
python3 4-Infrastructure/shim/arxiv_oaipmh_harvest.py --all-math
python3 4-Infrastructure/shim/arxiv_oaipmh_harvest.py --append # just append, no DROP
"""
from __future__ import annotations
import argparse
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
from pathlib import Path
from urllib.request import urlopen, Request
OAI_BASE = "https://oaipmh.arxiv.org/oai"
NEON_HOST = "neon-64gb"
CONTAINER = "arxiv-pg"
DB = "arxiv"
BATCH_INSERT = 100
def ssh_psql(sql: str, timeout: int = 300) -> str:
result = subprocess.run([
"ssh", NEON_HOST,
f"podman exec -i {CONTAINER} psql -U postgres -d {DB} -t -A"
], input=sql, capture_output=True, text=True, timeout=timeout)
return result.stdout.strip()
def setup_table(append: bool = False):
if not append:
ssh_psql("DROP TABLE IF EXISTS arxiv_papers;")
ssh_psql("""
CREATE TABLE IF NOT EXISTS arxiv_papers (
paper_id TEXT PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
categories TEXT NOT NULL DEFAULT '',
authors TEXT NOT NULL DEFAULT '',
abstract TEXT NOT NULL DEFAULT ''
);
""")
print("Table arxiv_papers ready", file=sys.stderr)
def fetch_records(set_spec: str, resumption_token: str | None = None) -> tuple[list[dict], str | None]:
if resumption_token:
url = f"{OAI_BASE}?verb=ListRecords&resumptionToken={resumption_token}"
else:
url = f"{OAI_BASE}?verb=ListRecords&metadataPrefix=arXivRaw&set={set_spec}"
req = Request(url, headers={"User-Agent": "ResearchStack/1.0 arxiv-harvester"})
with urlopen(req, timeout=120) as resp:
data = resp.read()
root = ET.fromstring(data)
ns = {
"oai": "http://www.openarchives.org/OAI/2.0/",
"arxiv": "http://arxiv.org/OAI/arXivRaw/",
}
records = []
error = root.find(".//oai:error", ns)
if error is not None:
print(f" OAI error: {error.text}", file=sys.stderr)
return records, None
for rec in root.findall(".//oai:record", ns):
header = rec.find("oai:header", ns)
if header is not None and header.get("status", "") == "deleted":
continue
metadata = rec.find("oai:metadata/arxiv:arXivRaw", ns)
if metadata is None:
continue
paper_id = metadata.findtext("arxiv:id", "", ns).strip()
if not paper_id:
continue
records.append({
"paper_id": paper_id,
"title": metadata.findtext("arxiv:title", "", ns).strip(),
"categories": metadata.findtext("arxiv:categories", "", ns).strip(),
"authors": metadata.findtext("arxiv:authors", "", ns).strip(),
"abstract": metadata.findtext("arxiv:abstract", "", ns).strip(),
})
token_el = root.find(".//oai:resumptionToken", ns)
next_token = token_el.text.strip() if token_el is not None and token_el.text else None
return records, next_token
def escape_sql(s: str) -> str:
return s.replace("'", "''").replace("\\", "\\\\")
def insert_batch(records: list[dict]):
if not records:
return 0
values = []
for r in records:
pid = escape_sql(r["paper_id"])
title = escape_sql(r["title"][:1000])
cats = escape_sql(r["categories"][:500])
authors = escape_sql(r["authors"][:500])
abstract = escape_sql(r["abstract"][:5000])
values.append(f"('{pid}','{title}','{cats}','{authors}','{abstract}')")
sql = (f"INSERT INTO arxiv_papers (paper_id, title, categories, authors, abstract) VALUES "
+ ','.join(values)
+ " ON CONFLICT (paper_id) DO UPDATE SET "
+ "title=EXCLUDED.title, categories=EXCLUDED.categories, "
+ "authors=EXCLUDED.authors, abstract=EXCLUDED.abstract")
ssh_psql(sql, timeout=120)
return len(records)
def harvest(set_spec: str):
print(f"Harvesting set: {set_spec}", file=sys.stderr)
token = None
total = 0
batch = []
t0 = time.time()
page = 0
while True:
records, token = fetch_records(set_spec, token)
batch.extend(records)
total += len(records)
page += 1
if len(batch) >= BATCH_INSERT or (not records and token is None):
insert_batch(batch)
batch.clear()
elapsed = time.time() - t0
rate = total / elapsed if elapsed > 0 else 0
if page % 10 == 0:
print(f" {total} records, {elapsed:.0f}s ({rate:.0f}/s), more={'yes' if token else 'no'}", file=sys.stderr)
if token is None:
break
time.sleep(0.5)
if batch:
insert_batch(batch)
elapsed = time.time() - t0
print(f" Done: {total} records in {elapsed:.0f}s", file=sys.stderr)
def main():
ap = argparse.ArgumentParser(description="Harvest arXiv papers via OAI-PMH")
ap.add_argument("--set", action="append", default=[], help="OAI set spec")
ap.add_argument("--all-math", action="store_true", help="Harvest all math")
ap.add_argument("--append", action="store_true", help="Don't DROP table first")
args = ap.parse_args()
sets = args.set
if args.all_math:
sets = ["math:math"]
if not sets:
sets = ["math:math:NT"]
setup_table(append=args.append)
for s in sets:
harvest(s)
count = ssh_psql("SELECT COUNT(*) FROM arxiv_papers")
print(f"\nTotal rows: {count}", file=sys.stderr)
if __name__ == "__main__":
main()