← back to Uspto Trtyrap
scripts/load_trtyrap.py
164 lines
#!/usr/bin/env python3
"""
load_trtyrap.py — parse a USPTO TRTYRAP trademark-applications XML file into a
searchable SQLite DB (db/trtyrap.sqlite).
Streaming (xml.etree.iterparse on </case-file>) so a 377MB file never lands in RAM.
Extracts the core fields per trademark: serial/registration #, the word mark, filing +
registration dates, live status, owner (name/city/state/country), primary int'l class,
and the goods-&-services text. Idempotent (serial_number PRIMARY KEY, upsert).
Usage:
python3 scripts/load_trtyrap.py /path/to/apc*.xml [more.xml ...]
"""
from __future__ import annotations
import re
import sqlite3
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DB = ROOT / "db" / "trtyrap.sqlite"
BATCH = 2000
DDL = """
CREATE TABLE IF NOT EXISTS trademark (
serial_number TEXT PRIMARY KEY,
registration_number TEXT,
mark TEXT,
mark_norm TEXT,
filing_date TEXT,
registration_date TEXT,
status_code TEXT,
status_date TEXT,
attorney_name TEXT,
owner_name TEXT,
owner_city TEXT,
owner_state TEXT,
owner_country TEXT,
primary_class TEXT,
intl_classes TEXT,
goods_services TEXT
) WITHOUT ROWID;
"""
UPSERT = """
INSERT INTO trademark
(serial_number,registration_number,mark,mark_norm,filing_date,registration_date,
status_code,status_date,attorney_name,owner_name,owner_city,owner_state,owner_country,
primary_class,intl_classes,goods_services)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(serial_number) DO UPDATE SET
registration_number=excluded.registration_number, mark=excluded.mark,
mark_norm=excluded.mark_norm, filing_date=excluded.filing_date,
registration_date=excluded.registration_date, status_code=excluded.status_code,
status_date=excluded.status_date, attorney_name=excluded.attorney_name,
owner_name=excluded.owner_name, owner_city=excluded.owner_city,
owner_state=excluded.owner_state, owner_country=excluded.owner_country,
primary_class=excluded.primary_class, intl_classes=excluded.intl_classes,
goods_services=excluded.goods_services;
"""
def _t(el, path):
"""First matching descendant's text, or None."""
f = el.find(path)
return f.text.strip() if f is not None and f.text else None
def _regnum(s): # 0000000 = not registered
return None if not s or set(s) == {"0"} else s
def extract(cf) -> tuple:
hdr = cf.find("case-file-header")
mark = _t(hdr, "mark-identification") if hdr is not None else None
# goods & services = concat of statement texts whose type-code starts with GS
gs = []
for st in cf.findall("case-file-statements/case-file-statement"):
code = _t(st, "type-code") or ""
if code.startswith("GS"):
txt = _t(st, "text")
if txt:
gs.append(txt)
# first owner
ow = cf.find("case-file-owners/case-file-owner")
intl = [c.text.strip() for c in cf.findall("classifications/classification/international-code")
if c.text and c.text.strip()]
return (
_t(cf, "serial-number"),
_regnum(_t(cf, "registration-number")),
mark,
(mark or "").upper() or None,
_t(hdr, "filing-date") if hdr is not None else None,
_t(hdr, "registration-date") if hdr is not None else None,
_t(hdr, "status-code") if hdr is not None else None,
_t(hdr, "status-date") if hdr is not None else None,
_t(hdr, "attorney-name") if hdr is not None else None,
_t(ow, "party-name") if ow is not None else None,
_t(ow, "city") if ow is not None else None,
_t(ow, "state") if ow is not None else None,
_t(ow, "country") if ow is not None else None,
_t(cf, "classifications/classification/primary-code"),
";".join(dict.fromkeys(intl)) or None,
" | ".join(gs) or None,
)
def load_file(conn, path: Path) -> int:
cur = conn.cursor()
batch, n = [], 0
# iterparse streams; we clear each <case-file> after use to keep memory flat.
for _ev, cf in ET.iterparse(str(path), events=("end",)):
if cf.tag != "case-file":
continue
row = extract(cf)
if row[0]: # has a serial number
batch.append(row)
cf.clear()
if len(batch) >= BATCH:
cur.executemany(UPSERT, batch)
conn.commit()
n += len(batch)
batch.clear()
if n % 20000 == 0:
print(f" …{n:,} trademarks", flush=True)
if batch:
cur.executemany(UPSERT, batch)
conn.commit()
n += len(batch)
return n
def main(argv):
files = [Path(a) for a in argv]
files = [f for f in files if f.exists()]
if not files:
print("usage: load_trtyrap.py <file.xml> [...]", file=sys.stderr)
return 1
DB.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA synchronous=NORMAL;")
conn.executescript(DDL)
total = 0
for f in files:
print(f"Parsing {f.name} …", flush=True)
c = load_file(conn, f)
total += c
print(f" {f.name}: +{c:,} trademarks", flush=True)
print("Indexing (mark + owner + status) …", flush=True)
conn.execute("CREATE INDEX IF NOT EXISTS idx_mark ON trademark(mark_norm);")
conn.execute("CREATE INDEX IF NOT EXISTS idx_owner ON trademark(owner_name);")
conn.execute("CREATE INDEX IF NOT EXISTS idx_status ON trademark(status_code);")
conn.commit()
tot = conn.execute("SELECT COUNT(*) FROM trademark").fetchone()[0]
conn.close()
print(f"\nDONE. db/trtyrap.sqlite holds {tot:,} trademarks (this run +{total:,}).")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))