Files
2026-06-01 12:17:41 +02:00

263 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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ů, ~1020 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 520)
```
### `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: `<table class="detailTabulka">`
- Řádky: `<th>label</th><td>hodnota</td>` (ne `<td><td>` jak by se čekalo)
- Sub-tabulky: `<table class="VnitrniTabulka">` uvnitř `<td>`
- 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
```