#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Генератор синтетических прайсов клиник (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, json, os, random, subprocess, 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.pdfgen import canvas from reportlab.pdfbase import pdfmetrics; from reportlab.pdfbase.ttfonts import TTFont 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))