261 lines
9.7 KiB
Python
261 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Parser DASTA XML (Datový standard MZ ČR, verze DS 03.01.01).
|
||
|
||
Rozebírá laboratorní zprávy (typ odesílatele LB) do strukturovaných dat.
|
||
Soubory jsou v kódování windows-1250 (uvedeno v XML deklaraci) a obsahují
|
||
DOCTYPE odkaz na lokální DTD (ds030101.dtd), který při parsování ignorujeme.
|
||
|
||
Struktura DASTA dávky (zjednodušeně):
|
||
dasta kořen, hlavička dávky (datum, verze, odesílatel)
|
||
zdroj_is informační systém, který dávku vytvořil
|
||
pm (icp) příjemce zprávy (ordinace) + adresa (a typ="P")
|
||
is (icp, ico) odesílatel = laboratoř + adresa (a typ="O")
|
||
ip (id_pac) pacient: rodné číslo, jméno, dat. narození, sex
|
||
pv / p pojišťovna (kodpoj, typpoj)
|
||
dg / dgz / diag diagnózy
|
||
v blok výsledků
|
||
vr (klic_nclp...) jeden laboratorní výsledek (analyt)
|
||
nazev_lclp název položky (WBC, RBC, ...)
|
||
dat_du/dat_pl/dat_vv odběr / příjem / vydání výsledku
|
||
autor validující lékař
|
||
vrn číselný výsledek
|
||
hodnota naměřená hodnota
|
||
nazvy@jednotka měrná jednotka
|
||
skala s1..s8 referenční pásma (meze)
|
||
interpret_g_z grafická interpretace | | * | |
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from dataclasses import dataclass, field, asdict
|
||
from pathlib import Path
|
||
from xml.etree import ElementTree as ET
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Datové třídy
|
||
# ---------------------------------------------------------------------------
|
||
@dataclass
|
||
class Vysledek:
|
||
klic_nclp: str # kód NČLP (Národní číselník laboratorních položek)
|
||
nazev: str # lokální název položky (WBC, RBC, ...)
|
||
hodnota: str | None
|
||
jednotka: str | None
|
||
dat_odber: str | None # dat_du – datum a čas odběru
|
||
dat_vydani: str | None # dat_vv – datum a čas vydání výsledku
|
||
autor: str | None
|
||
stav: str | None # stav_vys (A = definitivní)
|
||
typ_kvant: str | None # priznak_kvant (R = reálné číslo)
|
||
interpret: str | None # grafická interpretace mezí
|
||
meze: list[str] = field(default_factory=list) # s1..s8
|
||
|
||
@property
|
||
def referencni_mez(self) -> str | None:
|
||
"""Klinicky relevantní referenční rozmezí = s4 (dolní) až s5 (horní)."""
|
||
if len(self.meze) >= 5:
|
||
return f"{self.meze[3]} – {self.meze[4]}"
|
||
return None
|
||
|
||
|
||
@dataclass
|
||
class Pacient:
|
||
id_pac: str | None
|
||
rodne_cislo: str | None
|
||
jmeno: str | None
|
||
prijmeni: str | None
|
||
datum_narozeni: str | None
|
||
sex: str | None
|
||
pojistovna: str | None
|
||
typ_pojisteni: str | None
|
||
diagnozy: list[str] = field(default_factory=list)
|
||
vysledky: list[Vysledek] = field(default_factory=list)
|
||
|
||
|
||
@dataclass
|
||
class Adresa:
|
||
jmeno: str | None = None
|
||
radky: list[str] = field(default_factory=list)
|
||
psc: str | None = None
|
||
mesto: str | None = None
|
||
|
||
|
||
@dataclass
|
||
class DastaZprava:
|
||
ozn_soub: str | None
|
||
id_soubor: str | None
|
||
datum_vytvoreni: str | None
|
||
verze_ds: str | None
|
||
typ_odesilatele: str | None
|
||
zdroj_program: str | None
|
||
zdroj_verze: str | None
|
||
prijemce_icp: str | None
|
||
prijemce: Adresa | None
|
||
odesilatel_icp: str | None
|
||
odesilatel_ico: str | None
|
||
odesilatel: Adresa | None
|
||
pacienti: list[Pacient] = field(default_factory=list)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pomocné funkce
|
||
# ---------------------------------------------------------------------------
|
||
def _text(el, tag: str) -> str | None:
|
||
"""Vrátí text potomka `tag` nebo None."""
|
||
if el is None:
|
||
return None
|
||
child = el.find(tag)
|
||
return child.text.strip() if (child is not None and child.text) else None
|
||
|
||
|
||
def _parse_adresa(el) -> Adresa | None:
|
||
if el is None:
|
||
return None
|
||
a = el.find("a")
|
||
if a is None:
|
||
return None
|
||
radky = [v for v in (_text(a, "adr"), _text(a, "dop1"), _text(a, "dop2")) if v]
|
||
return Adresa(
|
||
jmeno=_text(a, "jmeno"),
|
||
radky=radky,
|
||
psc=_text(a, "psc"),
|
||
mesto=_text(a, "mesto"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Hlavní parser
|
||
# ---------------------------------------------------------------------------
|
||
def parse_dasta(cesta: str | Path) -> DastaZprava:
|
||
cesta = Path(cesta)
|
||
# XML obsahuje DOCTYPE s odkazem na DTD; vypneme načítání externích entit
|
||
# tím, že parsujeme bez resolveru. ElementTree DTD ignoruje automaticky.
|
||
raw = cesta.read_bytes()
|
||
# ElementTree si kódování přečte z XML deklarace (<?xml ... encoding=...?>).
|
||
root = ET.fromstring(raw)
|
||
|
||
zdroj = root.find("zdroj_is")
|
||
pm = root.find("pm")
|
||
is_el = root.find("is")
|
||
|
||
zprava = DastaZprava(
|
||
ozn_soub=root.get("ozn_soub"),
|
||
id_soubor=root.get("id_soubor"),
|
||
datum_vytvoreni=root.get("dat_vb"),
|
||
verze_ds=root.get("verze_ds"),
|
||
typ_odesilatele=root.get("typ_odesm"),
|
||
zdroj_program=zdroj.get("kod_prog") if zdroj is not None else None,
|
||
zdroj_verze=zdroj.get("verze_prog") if zdroj is not None else None,
|
||
prijemce_icp=pm.get("icp") if pm is not None else None,
|
||
prijemce=_parse_adresa(pm),
|
||
odesilatel_icp=is_el.get("icp") if is_el is not None else None,
|
||
odesilatel_ico=is_el.get("ico") if is_el is not None else None,
|
||
odesilatel=_parse_adresa(is_el),
|
||
)
|
||
|
||
if is_el is None:
|
||
return zprava
|
||
|
||
for ip in is_el.findall("ip"):
|
||
# pojišťovna – bere se z elementu <p> (přímo nebo uvnitř <pv>)
|
||
p = ip.find("p")
|
||
if p is None:
|
||
pv = ip.find("pv")
|
||
p = pv.find("p") if pv is not None else None
|
||
|
||
pacient = Pacient(
|
||
id_pac=ip.get("id_pac"),
|
||
rodne_cislo=_text(ip, "rodcis"),
|
||
jmeno=_text(ip, "jmeno"),
|
||
prijmeni=_text(ip, "prijmeni"),
|
||
datum_narozeni=_text(ip, "dat_dn"),
|
||
sex=_text(ip, "sex"),
|
||
pojistovna=_text(p, "kodpoj") if p is not None else None,
|
||
typ_pojisteni=_text(p, "typpoj") if p is not None else None,
|
||
)
|
||
|
||
# diagnózy
|
||
dg = ip.find("dg")
|
||
if dg is not None:
|
||
for diag in dg.iter("diag"):
|
||
if diag.text:
|
||
pacient.diagnozy.append(diag.text.strip())
|
||
|
||
# výsledky
|
||
v = ip.find("v")
|
||
if v is not None:
|
||
for vr in v.findall("vr"):
|
||
vrn = vr.find("vrn")
|
||
nazvy = vrn.find("nazvy") if vrn is not None else None
|
||
skala = vrn.find("skala") if vrn is not None else None
|
||
|
||
meze = []
|
||
interpret = None
|
||
if skala is not None:
|
||
for i in range(1, 9):
|
||
meze.append(_text(skala, f"s{i}") or "")
|
||
interpret = _text(skala, "interpret_g_z")
|
||
|
||
pacient.vysledky.append(Vysledek(
|
||
klic_nclp=vr.get("klic_nclp"),
|
||
nazev=_text(vr, "nazev_lclp"),
|
||
hodnota=_text(vrn, "hodnota") if vrn is not None else None,
|
||
jednotka=nazvy.get("jednotka") if nazvy is not None else None,
|
||
dat_odber=_text(vr, "dat_du"),
|
||
dat_vydani=_text(vr, "dat_vv"),
|
||
autor=_text(vr, "autor"),
|
||
stav=vr.get("stav_vys"),
|
||
typ_kvant=vrn.get("priznak_kvant") if vrn is not None else None,
|
||
interpret=interpret,
|
||
meze=meze,
|
||
))
|
||
|
||
zprava.pacienti.append(pacient)
|
||
|
||
return zprava
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Výpis přehledu
|
||
# ---------------------------------------------------------------------------
|
||
def vypis_prehled(z: DastaZprava) -> None:
|
||
print("=" * 70)
|
||
print(f"DASTA dávka {z.ozn_soub} (DS {z.verze_ds}, typ {z.typ_odesilatele})")
|
||
print(f"Vytvořeno: {z.datum_vytvoreni}")
|
||
print(f"Program: {z.zdroj_program} {z.zdroj_verze}")
|
||
print(f"ID souboru: {z.id_soubor}")
|
||
print("-" * 70)
|
||
if z.odesilatel:
|
||
print(f"Odesílatel (laboratoř) IČP {z.odesilatel_icp} IČO {z.odesilatel_ico}")
|
||
print(f" {z.odesilatel.jmeno} | {', '.join(z.odesilatel.radky)}")
|
||
print(f" {z.odesilatel.psc} {z.odesilatel.mesto}")
|
||
if z.prijemce:
|
||
print(f"Příjemce (ordinace) IČP {z.prijemce_icp}")
|
||
print(f" {z.prijemce.jmeno} | {', '.join(z.prijemce.radky)}")
|
||
print("=" * 70)
|
||
|
||
for pac in z.pacienti:
|
||
print(f"\nPacient: {pac.prijmeni} {pac.jmeno} r.č. {pac.rodne_cislo}"
|
||
f" nar. {pac.datum_narozeni} {pac.sex}")
|
||
print(f" Pojišťovna {pac.pojistovna} (typ {pac.typ_pojisteni})"
|
||
f" Diagnózy: {', '.join(pac.diagnozy) or '—'}")
|
||
print(f" Výsledků: {len(pac.vysledky)}")
|
||
print()
|
||
print(f" {'Položka':<14}{'Hodnota':>10} {'Jedn.':<9}"
|
||
f"{'Ref. mez':<18}{'Interpr.':<12}NČLP")
|
||
print(" " + "-" * 75)
|
||
for vys in pac.vysledky:
|
||
ref = vys.referencni_mez or ""
|
||
interp = (vys.interpret or "").strip()
|
||
print(f" {vys.nazev or '':<14}{vys.hodnota or '':>10} "
|
||
f"{vys.jednotka or '':<9}{ref:<18}{interp:<12}{vys.klic_nclp}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) > 1:
|
||
cesta = sys.argv[1]
|
||
else:
|
||
cesta = r"u:\Dropbox\Ordinace\pomoc\DASTA\RLB05E6T.xml"
|
||
zprava = parse_dasta(cesta)
|
||
vypis_prehled(zprava)
|