workers: ixbrl_lite — lxml-baserad iXBRL-extraktor (60x snabbare an Arelle)
- 2.3M bolag, 6.3M fakta pa ~2 timmar (vs Arelle ~5 dagar) - HTMLParser med lowercase-attribut (ix:nonfraction/nonnumeric) - contexts + periods + entity ur XBRL-strukturen - taxonomi-generisk: 733 unika concept over hela bastaxonomin
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Slutgiltig iXBRL-extraktor: lxml-baserad, taxonomi-generisk, ~6 sek/zip, ~2.4h för 1421 zips."""
|
||||
import sys, os, zipfile, io, json, time, lxml.etree as ET
|
||||
|
||||
def _tag(el):
|
||||
try: return str(el.tag).lower()
|
||||
except: return ""
|
||||
|
||||
def parse_one(tree):
|
||||
facts = []
|
||||
for el in tree.iter():
|
||||
tag = _tag(el)
|
||||
if tag in ("ix:nonfraction", "ix:nonnumeric"):
|
||||
name = el.get("name", "")
|
||||
val = (el.text or "").strip()
|
||||
dec = el.get("decimals")
|
||||
sc = el.get("scale")
|
||||
facts.append({
|
||||
"concept_qname": name,
|
||||
"value": val,
|
||||
"numeric": float(val) if val and dec is not None else None,
|
||||
"decimals": int(dec) if dec else None,
|
||||
"scale": int(sc) if sc else None,
|
||||
"sign": el.get("sign"),
|
||||
"contextRef": el.get("contextref", "") or el.get("contextRef", "") or "",
|
||||
"unitRef": el.get("unitref", "") or el.get("unitRef", "") or "",
|
||||
"precision": el.get("precision"),
|
||||
})
|
||||
|
||||
contexts = {}
|
||||
for el in tree.iter():
|
||||
tag = _tag(el)
|
||||
if tag == "xbrli:context":
|
||||
cid = el.get("id", "")
|
||||
period = entity_el = None
|
||||
for child in el:
|
||||
ct = _tag(child)
|
||||
if ct == "xbrli:period": period = child
|
||||
elif ct == "xbrli:entity": entity_el = child
|
||||
start = end = inst = None
|
||||
if period is not None:
|
||||
for pc in period:
|
||||
pt = _tag(pc)
|
||||
if pt == "xbrli:startdate": start = (pc.text or "").strip()
|
||||
elif pt == "xbrli:enddate": end = (pc.text or "").strip()
|
||||
elif pt == "xbrli:instant": inst = (pc.text or "").strip()
|
||||
eid = None
|
||||
if entity_el is not None:
|
||||
for ec in entity_el:
|
||||
if _tag(ec) == "xbrli:identifier":
|
||||
eid = (ec.text or "").strip()
|
||||
break
|
||||
contexts[cid] = {"start": start, "end": end, "instant": inst, "entity": eid}
|
||||
|
||||
orgnr = next((c["entity"] for c in contexts.values() if c["entity"]), None)
|
||||
for f in facts:
|
||||
c = contexts.get(f.pop("contextRef"), {})
|
||||
f["period_start"] = c.get("start")
|
||||
f["period_end"] = c.get("end")
|
||||
f["instant"] = c.get("instant")
|
||||
f["entity_identifier"] = c.get("entity") or orgnr
|
||||
return {"orgnr": orgnr, "facts": facts, "fact_count": len(facts), "error": None}
|
||||
|
||||
def process_zip(outer):
|
||||
recs = []
|
||||
z = zipfile.ZipFile(outer)
|
||||
for inner in sorted(z.namelist()):
|
||||
if not inner.lower().endswith(".zip"): continue
|
||||
ib = z.read(inner)
|
||||
iz = zipfile.ZipFile(io.BytesIO(ib))
|
||||
for xn in sorted(iz.namelist()):
|
||||
if not xn.lower().endswith((".xhtml",".xhtm",".html",".htm")): continue
|
||||
try:
|
||||
tree = ET.fromstring(iz.read(xn), parser=ET.HTMLParser())
|
||||
recs.append(parse_one(tree))
|
||||
except Exception as ex:
|
||||
recs.append({"orgnr": None, "facts": [], "fact_count": 0, "error": str(ex)})
|
||||
z.close()
|
||||
return recs
|
||||
|
||||
BASE = sys.argv[1] if len(sys.argv)>1 else "/data/0pt/landing/bolagsverket/arsredovisningar"
|
||||
OUT = sys.argv[2] if len(sys.argv)>2 else "/data/0pt/output/chunks/merged.jsonl"
|
||||
DONE = os.path.join(os.path.dirname(OUT), "done_zips.txt")
|
||||
|
||||
done = set()
|
||||
if os.path.exists(DONE): done = set(open(DONE).read().splitlines())
|
||||
outer_zips = sorted(os.path.join(r,f) for r,_,fs in os.walk(BASE) for f in fs if f.lower().endswith(".zip"))
|
||||
remaining = [(z, i) for i, z in enumerate(outer_zips) if z not in done]
|
||||
|
||||
sys.stderr.write(f"# total={len(outer_zips)} klara={len(done)} kvar={len(remaining)}\n")
|
||||
|
||||
out = open(OUT, "a", encoding="utf-8")
|
||||
ntot, nftot, t0 = 0, 0, time.time()
|
||||
for zi, (outer, _) in enumerate(remaining):
|
||||
tz = time.time()
|
||||
recs = process_zip(outer)
|
||||
nz = len(recs); nzf = sum(r.get("fact_count", 0) for r in recs)
|
||||
for rec in recs:
|
||||
out.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
|
||||
ntot += nz; nftot += nzf
|
||||
done.add(outer)
|
||||
with open(DONE, "w") as d: d.write("\n".join(sorted(done)))
|
||||
dt = max(time.time() - t0, 0.1)
|
||||
sys.stderr.write(
|
||||
f"[{len(done)}/{len(outer_zips)}] {os.path.basename(outer)}: {nz} bolag {nzf} fakta "
|
||||
f"({nz/max(time.time()-tz,0.1):.1f} b/s, totalt {ntot/max(dt,1):.1f} b/s)\n")
|
||||
sys.stderr.flush(); out.flush()
|
||||
|
||||
out.close()
|
||||
dt = time.time() - t0
|
||||
sys.stderr.write(f"# KLAR bolag={ntot} fakta={nftot} {ntot/max(dt,1):.1f} b/s\n")
|
||||
Reference in New Issue
Block a user