# Vykony — stahování a zpracování zdravotních výkonů ## Přehled Kompletní pipeline pro stahování zdravotních výkonů ze **szv.mzd.gov.cz** do MongoDB, včetně detailů každého výkonu a exportu do XLSX reportu. ``` szv.mzd.gov.cz │ ├── /Vykon/ → stahni_vykony.py → MongoDB: vykony + vykony_historie └── /Vykon/Detail/{id}/ → stahni_detaily.py → MongoDB: detaily + detaily_historie ↓ report_vykony.py → vykony_report.xlsx ``` --- ## Skripty | Skript | Co dělá | |--------|---------| | `stahni_vykony.py` | Stahuje seznam všech výkonů (přehledová data), ukládá do `vykony` s historií | | `stahni_detaily.py` | Stahuje detailní stránku každého výkonu, ukládá do `detaily` s historií | | `report_vykony.py` | Exportuje výkony + detaily do XLSX, jeden list na odbornost | | `debug_detail.py` | Ladící skript — stáhne HTML jednoho výkonu a vypíše co parser vidí | ### Doporučené pořadí spuštění ```bash python stahni_vykony.py # 1. seznam výkonů (~4246 kusů, ~1 min) python stahni_detaily.py # 2. detaily (~4246 HTTP requestů, ~10–20 min) python report_vykony.py # 3. export do XLSX ``` --- ## Nastavení skriptů Každý skript má na začátku blok konfiguračních proměnných — **není potřeba sahat do kódu**: ### `stahni_detaily.py` ```python KOLIK = 0 # 0 = vše; jinak max. počet ke stažení (pro testování: 10) FORCE = False # True = přestáhni i už stažené záznamy WORKERS = 5 # počet paralelních vláken (doporučeno 5–20) ``` ### `report_vykony.py` ```python ODBORNOSTI = ["001", "002"] # [] = všechny odbornosti VYSTUP = Path(__file__).parent / "vykony_report.xlsx" ``` --- ## MongoDB **Host:** `192.168.1.76:27017` **DB:** `zdravotni_vykony` **GUI:** MongoDB Compass (zdarma, od MongoDB Inc.) ### Kolekce `vykony` — aktuální přehledový stav Vždy obsahuje poslední verzi každého výkonu. Hodnoty jsou zkrácené (kódy bez popisů). | Pole | Typ | Popis | |------|-----|-------| | `cislo_vykonu` | str | Unikátní klíč (např. `01021`) | | `odbornost` | str | Kód odbornosti (např. `001`) | | `nazev_vykonu` | str | Název výkonu | | `kategorie` | str | Kód kategorie (např. `P`) | | `typ_vykonu` | str | Kód typu (např. `A`) | | `doba_trvani` | float\|null | Minuty | | `omezeni_mistem` | str\|null | Kód omezení (např. `A`) | | `omezeni_frekvenci` | str\|null | Např. `1/1 den` | | `prime_naklady` | float\|null | Body | | `osobni` | float\|null | Body | | `body_rezijni` | float\|null | Body | | `body_celkem` | float\|null | Body | | `revize` | datetime\|null | Datum poslední revize výkonu | | `detail_url` | str\|null | URL detailu na szv.mzd.gov.cz | | `_aktivni` | bool | `false` pokud výkon zmizel ze scrape | | `_platny_od` | datetime | Kdy tato verze začala platit | | `_scraped_at` | datetime | Čas posledního úspěšného scrape | | `_deaktivovano` | datetime | Kdy byl výkon deaktivován (jen pokud `_aktivni=false`) | **Indexy:** `cislo_vykonu` (unique), `odbornost`, `_aktivni`, `_platny_od` --- ### Kolekce `vykony_historie` — archiv změn přehledu Každý záznam = jedna historická verze výkonu (stav před změnou). Obsahuje stejná pole jako `vykony`, navíc: | Pole | Typ | Popis | |------|-----|-------| | `_platny_do` | datetime | Kdy tato verze přestala platit | | `_zmenena_pole` | list[str] | Která pole se změnila (nebo `["_aktivni"]` při deaktivaci) | **Indexy:** `cislo_vykonu`, `_platny_do`, `_zmenena_pole` --- ### Kolekce `detaily` — aktuální detailní stav Obsahuje kompletní data z detailní stránky každého výkonu. Hodnoty jsou dlouhé (s popisy). Skalární pole + vnořené sub-tabulky jako pole objektů. **Skalární pole:** | Pole | Typ | Popis | |------|-----|-------| | `cislo_vykonu` | str | Unikátní klíč, propojení s `vykony` | | `nazev` | str | Plný název výkonu | | `kategorie` | str | Dlouhý popis (např. `P - hrazen plně`) | | `typ_formulare` | str | Např. `ambulantní` | | `omezeni_mistem` | str | Dlouhý popis (např. `A - pouze ambulantně`) | | `omezeni_frekvenci` | str | Např. `1/1 den` | | `doba_trvani` | float\|null | Minuty | | `nepocitat_rezii` | bool | | | `popis` | str | Obecný popis (často prázdný) | | `poznamka` | str | Poznámka | | `podminky` | str | Podmínky pro vykázání | | `cim_zacina` | str | Popis začátku výkonu | | `obsah_rozsah` | str | Obsah a rozsah výkonu | | `cim_konci` | str | Popis konce výkonu | | `body_prime` | float\|null | Bodová hodnota — přímé náklady | | `body_osobni` | float\|null | Bodová hodnota — osobní náklady | | `body_rezijni` | float\|null | Bodová hodnota — režijní náklady | | `body_celkem` | float\|null | Bodová hodnota — celkem | | `detail_url` | str | URL zdroje | | `_platny_od` | datetime | Kdy byla tato verze detailu uložena | | `_scraped_at` | datetime | Čas posledního scrape | **Sub-tabulky (pole objektů):** | Pole | Klíče objektů | |------|---------------| | `autorska_odbornost` | Kód, Název, Pořadí, Sazba režie | | `dalsi_odbornost` | Kód, Název, Sazba režie | | `nositele` | Kategorie, Funkce, Praxe, Cas, Bodyaktualni, Poznamka | | `materialy` | Kód, Název, Doplněk, Množství, Jednotka, Cena, DPH %, Body | | `pripravky` | Kód, Název, Doplněk, ATC, Omezení, Množství, Jednotka, Cena, Body | | `pristroje` | Kód, Název, D.Ž., N.Ú., D.P., DPH %, Body | | `zum` | Kód, Název | | `zulp` | Kód, Název | | `bodova_hodnota` | Přímé, Osobní, Režijní, Celkem (1 řádek, flatten na skalární body_*) | **Indexy:** `cislo_vykonu` (unique), `_scraped_at`, `_platny_od` --- ### Kolekce `detaily_historie` — archiv změn detailů Každý záznam = stav detailu před změnou. Obsahuje stejná pole jako `detaily`, navíc: | Pole | Typ | Popis | |------|-----|-------| | `_platny_do` | datetime | Kdy tato verze přestala platit | | `_zmenena_pole` | list[str] | Která pole se změnila | **Indexy:** `cislo_vykonu`, `_platny_do`, `_zmenena_pole` --- ## Logika detekce změn Stejná logika platí pro oba páry kolekcí (`vykony`/`vykony_historie` i `detaily`/`detaily_historie`): 1. Načte celou aktivní kolekci do paměti (`cislo_vykonu → dokument`) 2. Pro každý nově stažený záznam: - **Nový** → vloží s `_platny_od = run_at` - **Změněn** (liší se v některém z `COMPARE_FIELDS`) → stará verze jde do historie s `_platny_do` + `_zmenena_pole`, nová verze se zapíše - **Nezměněn** → jen aktualizuje `_scraped_at` 3. Výkony co v novém scrape chybí → `_aktivni = False`, stará verze do historie (jen `vykony`) --- ## Technické poznámky ### Proč ne MVCGridHandler.axd? Ajax endpoint (`/MVCGridHandler.axd?Name=VykonGrid&pageSize=9999`) ignoruje `pageSize` a vrací max 50 záznamů. Navíc po vyčerpání dat cyklí od začátku. Správný endpoint je přímo stránka `/Vykon/`. ### HTML struktura detailní stránky - Hlavní tabulka: `` - Řádky: `` (ne `
labelhodnota` jak by se čekalo) - Sub-tabulky: `` uvnitř `
` - Parser iteruje `find_all("tr", recursive=False)` — bez rekurze nevleze do vnořených tabulek ### Encoding Server vrací latin znaky i při UTF-8 hlavičce → `resp.apparent_encoding` místo `resp.encoding`. ### Rate limiting `stahni_detaily.py` dělá `time.sleep(0.1)` po každém requestu per worker. Při 20 workerech ≈ 200 requestů/s — server to bez problémů zvládá. --- ## Typické MongoDB dotazy ```js // Všechny aktivní výkony odbornosti 001 db.vykony.find({ odbornost: "001", _aktivni: true }) // Detail konkrétního výkonu db.detaily.findOne({ cislo_vykonu: "01021" }) // Výkony s podmínkami (neprázdné pole) db.detaily.find({ podminky: { $ne: "" } }) // Výkony které se někdy změnily db.vykony_historie.distinct("cislo_vykonu") // Historie konkrétního výkonu (detail) db.detaily_historie.find({ cislo_vykonu: "01021" }).sort({ _platny_do: 1 }) // Co se změnilo při posledním runu db.detaily_historie.find({ _platny_do: { $gte: ISODate("2026-06-01") } }) // Výkony s nositelem kategorie L3 db.detaily.find({ "nositele.Kategorie": "L3" }) // Výkony s materiálem (neprázdné materialy) db.detaily.find({ "materialy.0": { $exists: true } }) // Deaktivované výkony db.vykony.find({ _aktivni: false }) ``` --- ## XLSX Report Skript `report_vykony.py` generuje `vykony_report.xlsx`: - Jeden list na odbornost (název listu: `Odbornost 001` atd.) - Výkony řazené podle čísla výkonu - Více nositelů → více řádků pro jeden výkon (střídavé zbarvení po výkonech) - Sloupec **Postup výkonu** = čím začíná + obsah a rozsah + čím končí (odděleno prázdným řádkem) - Záhlaví zmrazeno (freeze panes) **Sloupce reportu:** Číslo výkonu · Název · Kategorie · Typ formuláře · Doba trvání · Omezení místem · Omezení frekvencí · Nepočítat režii · Body přímé · Body osobní · Body režijní · Body celkem · Postup výkonu · Nositel kategorie · Nositel funkce · Nositel čas · Nositel body --- ## Závislosti ``` requests beautifulsoup4 lxml pymongo openpyxl ```