#!/usr/bin/env python3 """Генератор синтетических прайсов клиник (RU/KZ/EN) + эталонная нормализация + справочник канонов. Выдаёт: fixtures/raw/* — 6 форматов «сырых» прайсов (Excel/CSV/txt/PNG/PDF) fixtures/normalized/golden.json — эталон: каждая строка → канон (оракул для TDD ETL) contracts/service_canon.json — справочник канонических услуг (seed для нормализации) Детерминированно (seed=42). """ import csv import json import os import random import subprocess import sys ROOT = sys.argv[1] if len(sys.argv) > 1 else "." RAW = os.path.join(ROOT, "fixtures/raw") NORM = os.path.join(ROOT, "fixtures/normalized") CONTRACTS = os.path.join(ROOT, "contracts") for d in (RAW, NORM, CONTRACTS): os.makedirs(d, exist_ok=True) random.seed(42) def ensure(pkg, mod=None): try: __import__(mod or pkg) except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pkg]) # Справочник канонических медуслуг. lang-тегированные синонимы — материал для нормализации. CANONS = [ { "id": "cbc", "ru": "Общий анализ крови", "kz": "Қанның жалпы анализі", "en": "Complete blood count (CBC)", "category": "Лаборатория", "code": "B03.016.003", "unit": "1 иссл.", "price": (1200, 4500), "syn": [ ("ОАК", "ru"), ("Общий анализ крови (развернутый)", "ru"), ("Клинический анализ крови", "ru"), ("КАК", "ru"), ("CBC", "en"), ("Қанның жалпы талдауы", "kz"), ], }, { "id": "uzi_abd", "ru": "УЗИ органов брюшной полости", "kz": "Құрсақ қуысы ағзаларының УДЗ", "en": "Abdominal ultrasound", "category": "Диагностика/УЗИ", "code": "A04.16.001", "unit": "1 иссл.", "price": (6000, 13000), "syn": [ ("УЗИ ОБП", "ru"), ("УЗИ брюшной полости", "ru"), ("Ультразвуковое исследование брюшной полости", "ru"), ("Құрсақ қуысы УДЗ", "kz"), ("Abdominal US", "en"), ], }, { "id": "therapist", "ru": "Приём врача-терапевта", "kz": "Терапевт дәрігердің қабылдауы", "en": "General practitioner consultation", "category": "Приём специалистов", "code": "B01.047.001", "unit": "1 приём", "price": (4000, 9000), "syn": [ ("Прием терапевта", "ru"), ("Консультация терапевта первичная", "ru"), ("Терапевт қабылдауы", "kz"), ("GP visit", "en"), ], }, { "id": "ecg", "ru": "Электрокардиография (ЭКГ)", "kz": "Электрокардиография (ЭКГ)", "en": "Electrocardiography (ECG)", "category": "Диагностика", "code": "A05.10.006", "unit": "1 иссл.", "price": (2000, 5000), "syn": [ ("ЭКГ", "ru"), ("ЭКГ с расшифровкой", "ru"), ("Электрокардиография", "ru"), ("ECG", "en"), ("ЭКГ + описание", "ru"), ], }, { "id": "glucose", "ru": "Глюкоза крови", "kz": "Қандағы глюкоза", "en": "Blood glucose", "category": "Лаборатория", "code": "B03.016.006", "unit": "1 иссл.", "price": (800, 2200), "syn": [ ("Глюкоза", "ru"), ("Сахар крови", "ru"), ("Қандағы глюкоза", "kz"), ("Glucose", "en"), ], }, { "id": "cardiologist", "ru": "Консультация кардиолога", "kz": "Кардиолог кеңесі", "en": "Cardiologist consultation", "category": "Приём специалистов", "code": "B01.015.001", "unit": "1 приём", "price": (5000, 12000), "syn": [ ("Прием кардиолога", "ru"), ("Кардиолог консультация первичная", "ru"), ("Кардиолог қабылдауы", "kz"), ], }, { "id": "mri_brain", "ru": "МРТ головного мозга", "kz": "Бас миының МРТ", "en": "Brain MRI", "category": "Диагностика/МРТ", "code": "A05.23.009", "unit": "1 иссл.", "price": (18000, 35000), "syn": [ ("МРТ г/м", "ru"), ("Магнитно-резонансная томография головного мозга", "ru"), ("Бас миы МРТ", "kz"), ("Brain MRI", "en"), ], }, { "id": "biochem", "ru": "Биохимический анализ крови", "kz": "Қанның биохимиялық анализі", "en": "Blood chemistry panel", "category": "Лаборатория", "code": "B03.016.004", "unit": "1 иссл.", "price": (3500, 9000), "syn": [ ("Биохимия крови", "ru"), ("Б/х крови", "ru"), ("Биохимический анализ крови (базовый)", "ru"), ("Қан биохимиясы", "kz"), ], }, { "id": "tooth_ext", "ru": "Удаление зуба", "kz": "Тісті жұлу", "en": "Tooth extraction", "category": "Стоматология", "code": "A16.07.001", "unit": "1 зуб", "price": (6000, 15000), "syn": [ ("Удаление зуба простое", "ru"), ("Экстракция зуба", "ru"), ("Тіс жұлу", "kz"), ("Tooth removal", "en"), ], }, { "id": "xray_chest", "ru": "Рентген грудной клетки", "kz": "Кеуде клеткасының рентгені", "en": "Chest X-ray", "category": "Диагностика", "code": "A06.09.007", "unit": "1 иссл.", "price": (2500, 6000), "syn": [ ("Рентгенография ОГК", "ru"), ("Флюорография", "ru"), ("Кеуде рентгені", "kz"), ("Chest X-ray", "en"), ], }, { "id": "tsh", "ru": "Тиреотропный гормон (ТТГ)", "kz": "Тиреотропты гормон (ТТГ)", "en": "Thyroid-stimulating hormone (TSH)", "category": "Лаборатория", "code": "B03.016.021", "unit": "1 иссл.", "price": (1500, 4000), "syn": [("ТТГ", "ru"), ("Тиреотропный гормон", "ru"), ("TSH", "en"), ("ТТГ гормоны", "kz")], }, { "id": "urinalysis", "ru": "Общий анализ мочи", "kz": "Зәрдің жалпы анализі", "en": "Urinalysis", "category": "Лаборатория", "code": "B03.016.010", "unit": "1 иссл.", "price": (900, 2500), "syn": [ ("ОАМ", "ru"), ("Общий анализ мочи", "ru"), ("Зәр анализі", "kz"), ("Urine test", "en"), ], }, { "id": "gyn", "ru": "Приём гинеколога", "kz": "Гинеколог қабылдауы", "en": "Gynecologist consultation", "category": "Приём специалистов", "code": "B01.001.001", "unit": "1 приём", "price": (5000, 11000), "syn": [ ("Прием гинеколога", "ru"), ("Консультация гинеколога первичная", "ru"), ("Гинеколог кеңесі", "kz"), ], }, { "id": "covid_pcr", "ru": "ПЦР на COVID-19", "kz": "COVID-19 ПТР", "en": "COVID-19 PCR test", "category": "Лаборатория", "code": "A26.08.027", "unit": "1 иссл.", "price": (7000, 16000), "syn": [ ("ПЦР COVID-19", "ru"), ("ПЦР-тест на коронавирус", "ru"), ("COVID PCR", "en"), ("COVID ПТР", "kz"), ], }, { "id": "vit_d", "ru": "Витамин D (25-OH)", "kz": "D дәрумені (25-OH)", "en": "Vitamin D (25-OH)", "category": "Лаборатория", "code": "B03.016.115", "unit": "1 иссл.", "price": (6000, 12000), "syn": [ ("25-OH витамин D", "ru"), ("Витамин Д", "ru"), ("Vitamin D", "en"), ("D витамині", "kz"), ], }, ] CLINICS = [ "Клиника «Аль-Шифа»", "Медцентр «Сункар»", "Diagnostic Lab Astana", "Клиника «Береке»", "Медцентр «Тенгри»", ] def price(c): return random.randint(c["price"][0] // 100, c["price"][1] // 100) * 100 def variant(c): return random.choice([(c["ru"], "ru")] + c["syn"]) GOLDEN = [] def build(file_name, clinic, n): rows = [] for c in random.sample(CANONS, k=min(n, len(CANONS))): raw_name, lang = variant(c) p = price(c) rows.append({"raw_name": raw_name, "price": p, "unit": c["unit"]}) GOLDEN.append( { "source_file": file_name, "clinic": clinic, "raw_name": raw_name, "lang": lang, "canon_id": c["id"], "canonical_ru": c["ru"], "canonical_kz": c["kz"], "canonical_en": c["en"], "category": c["category"], "code": c["code"], "price": p, "currency": "KZT", "unit": c["unit"], } ) return rows ensure("openpyxl") from openpyxl import Workbook # 1) чистый Excel rows = build("clinic_alpha_clean.xlsx", CLINICS[0], 9) wb = Workbook() ws = wb.active ws.title = "Прайс" ws.append(["Наименование услуги", "Цена, тг"]) for r in rows: ws.append([r["raw_name"], r["price"]]) wb.save(f"{RAW}/clinic_alpha_clean.xlsx") # 2) грязный Excel rows = build("clinic_beta_dirty.xlsx", CLINICS[1], 9) wb = Workbook() ws = wb.active ws.title = "PRICE 2026" ws.append(["Прайс-лист медцентра «Сункар» (с 01.06.2026)"]) ws.append([]) ws.append(["№", "Услуга / Қызмет", "Стоимость (тенге)", "Ед."]) secs = ["— Лаборатория / Зертхана —", "— Диагностика —", "— Приём специалистов —"] i = 1 for k, r in enumerate(rows): if k % 3 == 0 and secs: ws.append([secs.pop(0), None, None, None]) val = f"от {r['price']}" if random.random() < 0.3 else r["price"] ws.append([i, r["raw_name"], val, r["unit"]]) i += 1 wb.save(f"{RAW}/clinic_beta_dirty.xlsx") # 3) CSV ; rows = build("clinic_gamma.csv", CLINICS[2], 8) with open(f"{RAW}/clinic_gamma.csv", "w", encoding="utf-8", newline="") as f: w = csv.writer(f, delimiter=";") w.writerow(["service", "price_kzt", "unit"]) for r in rows: w.writerow([r["raw_name"], r["price"], r["unit"]]) # 4) грязный текст rows = build("clinic_delta_messy.txt", CLINICS[4], 7) with open(f"{RAW}/clinic_delta_messy.txt", "w", encoding="utf-8") as f: f.write("МЕДЦЕНТР «ТЕНГРИ» — действующие цены\n\n") for r in rows: sep = random.choice([" — ", " ... ", ": ", " "]) cur = random.choice(["тг", "тенге", "₸", "KZT"]) f.write(f"{r['raw_name']}{sep}{r['price']} {cur} ({r['unit']})\n") f.write("\n* Цены справочные, уточняйте в регистратуре. Бағалар анықтамалық.\n") # 5) PNG-скан rows = build("clinic_epsilon_scan.png", CLINICS[3], 9) ensure("Pillow", "PIL") from PIL import Image, ImageDraw, ImageFont def font(sz): for p in [ "/System/Library/Fonts/Supplemental/Arial.ttf", "/Library/Fonts/Arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ]: if os.path.exists(p): return ImageFont.truetype(p, sz) return ImageFont.load_default() img = Image.new("RGB", (900, 720), (250, 248, 244)) d = ImageDraw.Draw(img) d.text((40, 30), "Клиника «Береке» — прайс-лист", fill=(20, 20, 20), font=font(34)) d.line((40, 85, 860, 85), fill=(180, 180, 180), width=2) y = 110 for r in rows: d.text((50, y), r["raw_name"][:42], fill=(30, 30, 30), font=font(23)) d.text((720, y), f"{r['price']} тг", fill=(30, 30, 30), font=font(23)) y += 62 img = img.rotate(-1.4, expand=False, fillcolor=(250, 248, 244)) img.save(f"{RAW}/clinic_epsilon_scan.png") # 6) PDF rows = build("clinic_zeta_prices.pdf", CLINICS[2], 10) pdf = "skip" try: ensure("reportlab") from reportlab.lib.pagesizes import A4 from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas fp = next( ( p for p in [ "/Library/Fonts/Arial.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ] if os.path.exists(p) ), None, ) if fp: pdfmetrics.registerFont(TTFont("U", fp)) c = canvas.Canvas(f"{RAW}/clinic_zeta_prices.pdf", pagesize=A4) c.setFont("U", 18) c.drawString(60, 790, "Diagnostic Lab Astana — Price list / Прайс") c.setFont("U", 12) y = 750 for r in rows: c.drawString(60, y, r["raw_name"][:55]) c.drawRightString(540, y, f"{r['price']} KZT") y -= 28 c.save() pdf = "ok" except Exception as e: pdf = f"skip ({e})" # эталон нормализации + справочник with open(f"{NORM}/golden.json", "w", encoding="utf-8") as f: json.dump(GOLDEN, f, ensure_ascii=False, indent=2) canon_out = [ { "canon_id": c["id"], "canonical_ru": c["ru"], "canonical_kz": c["kz"], "canonical_en": c["en"], "category": c["category"], "code": c["code"], "unit": c["unit"], "synonyms": [{"text": t, "lang": l} for t, l in c["syn"]], } for c in CANONS ] with open(f"{CONTRACTS}/service_canon.json", "w", encoding="utf-8") as f: json.dump(canon_out, f, ensure_ascii=False, indent=2) print("PDF:", pdf) print("raw:", sorted(os.listdir(RAW))) print("golden rows:", len(GOLDEN), "| canons:", len(CANONS))