1"""Japan MHLW Medical Facility Open Data Actor.
2
3厚生労働省 医療情報ネット オープンデータ (e-Govデータポータル経由) から
4病院・診療所・歯科・薬局の施設情報を検索する。
5CSV ZIP は data.e-gov.go.jp の固定 resource URL から取得。
6"""
7from __future__ import annotations
8
9import asyncio
10import csv
11import io
12import json
13import os
14import zipfile
15from typing import Any
16
17import httpx
18from apify import Actor
19
20
21DEFAULT_ZIP_URL = (
22 "https://data.e-gov.go.jp/data/dataset/321fdf20-5f6a-49e5-bcab-35d81d652c65"
23 "/resource/af88450b-049c-4deb-8dc9-327312d877e1/download/e-gov20251201.zip"
24)
25
26
27CSV_MAP = {
28 "hospital": "01-1_hospital_facility_info_",
29 "clinic": "02-1_clinic_facility_info_",
30 "dental": "03-1_dental_facility_info_",
31 "maternity": "04_maternity_home_",
32 "pharmacy": "05_pharmacy_",
33}
34
35PREF_CODES = {
36 "北海道": "01", "青森県": "02", "岩手県": "03", "宮城県": "04", "秋田県": "05",
37 "山形県": "06", "福島県": "07", "茨城県": "08", "栃木県": "09", "群馬県": "10",
38 "埼玉県": "11", "千葉県": "12", "東京都": "13", "神奈川県": "14", "新潟県": "15",
39 "富山県": "16", "石川県": "17", "福井県": "18", "山梨県": "19", "長野県": "20",
40 "岐阜県": "21", "静岡県": "22", "愛知県": "23", "三重県": "24", "滋賀県": "25",
41 "京都府": "26", "大阪府": "27", "兵庫県": "28", "奈良県": "29", "和歌山県": "30",
42 "鳥取県": "31", "島根県": "32", "岡山県": "33", "広島県": "34", "山口県": "35",
43 "徳島県": "36", "香川県": "37", "愛媛県": "38", "高知県": "39", "福岡県": "40",
44 "佐賀県": "41", "長崎県": "42", "熊本県": "43", "大分県": "44", "宮崎県": "45",
45 "鹿児島県": "46", "沖縄県": "47",
46}
47
48
49def _find_csv(zf: zipfile.ZipFile, kind: str) -> str:
50 prefix = CSV_MAP[kind]
51 for n in zf.namelist():
52 if n.endswith(".csv") and n.startswith(prefix):
53 return n
54 raise KeyError(f"CSV not found for kind={kind}: {zf.namelist()[:5]}")
55
56
57async def main() -> None:
58 async with Actor:
59 run_input = await Actor.get_input() or {}
60 facility_type = (run_input.get("facilityType") or "hospital").lower()
61 prefecture = (run_input.get("prefecture") or "").strip()
62 keyword = (run_input.get("keyword") or "").strip()
63 limit = int(run_input.get("limit") or 20)
64 zip_url = run_input.get("zipUrl") or os.environ.get("MHLW_ZIP_URL") or DEFAULT_ZIP_URL
65
66 if facility_type not in CSV_MAP:
67 raise ValueError(f"facilityType must be one of {list(CSV_MAP)}")
68
69 Actor.log.info(f"Downloading MHLW open data ZIP: {zip_url}")
70 async with httpx.AsyncClient(timeout=180, follow_redirects=True,
71 headers={"User-Agent": "Mozilla/5.0"}) as client:
72 resp = await client.get(zip_url)
73 resp.raise_for_status()
74 content = resp.content
75
76 zf = zipfile.ZipFile(io.BytesIO(content))
77 csv_name = _find_csv(zf, facility_type)
78 Actor.log.info(f"Parsing {csv_name}")
79
80 pref_code = PREF_CODES.get(prefecture, "") if prefecture else ""
81 results = []
82 with zf.open(csv_name) as f:
83 wrapper = io.TextIOWrapper(f, encoding="utf-8", errors="replace")
84 reader = csv.reader(wrapper)
85 header = [h.lstrip("\ufeff").strip() for h in next(reader)]
86 col = {h: i for i, h in enumerate(header)}
87 i_name = col.get("正式名称", 1)
88 i_kana = col.get("正式名称(フリガナ)", 2)
89 i_addr = col.get("所在地", 9)
90 i_pref = col.get("都道府県コード", 7)
91 i_city = col.get("市区町村コード", 8)
92 i_lat = col.get("所在地座標(緯度)", 10)
93 i_lon = col.get("所在地座標(経度)", 11)
94
95 for row in reader:
96 if pref_code and row[i_pref] != pref_code:
97 continue
98 name = row[i_name] if i_name < len(row) else ""
99 kana = row[i_kana] if i_kana < len(row) else ""
100 addr = row[i_addr] if i_addr < len(row) else ""
101 if keyword and keyword not in name and keyword not in addr and keyword not in kana:
102 continue
103 results.append({
104 "facilityType": facility_type,
105 "id": row[0] if row else "",
106 "name": name,
107 "kana": kana,
108 "address": addr,
109 "prefectureCode": row[i_pref],
110 "cityCode": row[i_city],
111 "latitude": row[i_lat] if i_lat < len(row) else "",
112 "longitude": row[i_lon] if i_lon < len(row) else "",
113 })
114 if len(results) >= limit:
115 break
116
117 Actor.log.info(f"Got {len(results)} facilities")
118 if not results:
119 results = [{"__meta__": "1", "facilityType": facility_type,
120 "prefecture": prefecture, "keyword": keyword, "count": "0"}]
121 await Actor.push_data(results)
122 Actor.log.info(f"Pushed {len(results)} rows")
123
124
125if __name__ == "__main__":
126 asyncio.run(main())