area.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. """
  2. 区域数据服务(基于 city.json 省/市/区三级数据)
  3. 数据格式:省(id/name/pid) → 市(id/name/pid/sons) → 区县名称字符串数组
  4. 直辖市特征:省名 == 市名(如"北京市"省下有唯一城市"北京市")
  5. """
  6. import json
  7. import os
  8. from collections import namedtuple
  9. from typing import Optional, Tuple, Dict, Any, List, Set
  10. import logging
  11. log = logging.getLogger('city')
  12. # ---------- 返回类型 ----------
  13. AreaMatch = namedtuple('AreaMatch', ['province_id', 'city_id', 'province', 'city'])
  14. AreaMatch.__new__.__annotations__ = {
  15. 'province_id': int,
  16. 'city_id': int,
  17. 'province': str,
  18. 'city': str,
  19. }
  20. # ---------- 常量 ----------
  21. _PROVINCE_SUFFIXES = ['壮族自治区', '回族自治区', '维吾尔自治区', '自治区', '省', '市']
  22. _CITY_SUFFIXES = [
  23. # 民族自治州复合后缀(按长度降序,确保最长匹配优先)
  24. '朝鲜族自治州', '土家族苗族自治州', '藏族羌族自治州',
  25. '哈尼族彝族自治州', '布依族苗族自治州', '苗族侗族自治州',
  26. '壮族苗族自治州', '傣族景颇族自治州', '蒙古族藏族自治州',
  27. '柯尔克孜自治州', '哈萨克自治州', '蒙古自治州',
  28. '藏族自治州', '彝族自治州', '白族自治州', '傣族自治州',
  29. '傈僳族自治州', '回族自治州',
  30. '自治州', '地区', '盟', '市',
  31. ]
  32. _DISTRICT_SUFFIXES = [
  33. '自治县', '林区', '特区', '区', '县', '市', '旗',
  34. '朝鲜族自治县', '回族彝族自治县', '彝族回族苗族自治县',
  35. '哈尼族彝族傣族自治县',
  36. ]
  37. # 虚拟城市名称别名(仅加入省级→市 Trie,不加入全局 Trie 以避免多省冲突)
  38. _VIRTUAL_CITY_ALIASES = {
  39. '省直辖县级行政区划': ['省直辖县', '省辖县', '直辖县', '辖县'],
  40. '自治区直辖县级行政区划': ['兵团城市'],
  41. }
  42. # ---------- 工具函数 ----------
  43. def _strip_suffix(name, suffixes):
  44. """尝试去掉后缀得到简称,去掉最长的匹配"""
  45. for s in sorted(suffixes, key=len, reverse=True):
  46. if name.endswith(s) and len(name) > len(s):
  47. return name[:-len(s)]
  48. return name
  49. class _TrieNode:
  50. """前缀树节点(支持多值共存)"""
  51. __slots__ = ('children', 'values')
  52. def __init__(self) -> None:
  53. self.children: Dict[str, '_TrieNode'] = {}
  54. # 多值: [(type, id, city_id), ...]
  55. # city_id: province=0, city=c_id, district=所属city_id
  56. self.values: List[Tuple[str, int, int]] = []
  57. class _Trie:
  58. """
  59. 统一前缀树,省/市/区 逐字链式构造
  60. 插入模式:
  61. - insert(word, type, id, city_id): 从 root 插入
  62. - insert_from(node, word, type, id, city_id): 从指定节点继续插入
  63. 搜索规则:
  64. - 逐字遍历 addr,沿途收集最佳匹配
  65. - 停止位检查 city_id 唯一性:多种 city_id → 歧义 → None
  66. """
  67. _TYPE_PRIORITY = {'district': 3, 'city': 2, 'province': 1}
  68. def __init__(self) -> None:
  69. self.root = _TrieNode()
  70. # ---------- 插入 ----------
  71. @staticmethod
  72. def _add_value(node: _TrieNode, entry_type: str, entry_id: int, city_id: int) -> None:
  73. """在节点上添加值(仅去重,冲突由 search 停止位检测)"""
  74. for t, eid, cid in node.values:
  75. if t == entry_type and eid == entry_id:
  76. return # 完全相同,忽略
  77. node.values.append((entry_type, entry_id, city_id))
  78. def insert(self, word: str, entry_type: str, entry_id: int,
  79. city_id: int = 0) -> List['_TrieNode']:
  80. """从 root 插入,返回叶节点列表"""
  81. node = self.root
  82. for ch in word:
  83. if ch not in node.children:
  84. node.children[ch] = _TrieNode()
  85. node = node.children[ch]
  86. _Trie._add_value(node, entry_type, entry_id, city_id)
  87. return [node]
  88. @staticmethod
  89. def insert_from(from_nodes: List['_TrieNode'],
  90. word: str, entry_type: str, entry_id: int,
  91. city_id: int = 0) -> List['_TrieNode']:
  92. """从指定节点列表继续逐字插入,返回新叶节点列表"""
  93. leaves = []
  94. for start in from_nodes:
  95. node = start
  96. for ch in word:
  97. if ch not in node.children:
  98. node.children[ch] = _TrieNode()
  99. node = node.children[ch]
  100. _Trie._add_value(node, entry_type, entry_id, city_id)
  101. leaves.append(node)
  102. return leaves
  103. # ---------- 搜索 ----------
  104. def _best_value(self, node: _TrieNode) -> Optional[Tuple[str, int]]:
  105. """获取节点上最佳值(district > city > province)"""
  106. best = None
  107. best_pri = 0
  108. for t, eid, _ in node.values:
  109. pri = self._TYPE_PRIORITY.get(t, 0)
  110. if pri > best_pri:
  111. best_pri = pri
  112. best = (t, eid)
  113. return best
  114. def search(self, text: str) -> Optional[Tuple[str, int]]:
  115. """
  116. 逐字遍历 text,返回沿途最佳匹配 (type, id)
  117. - 记录停止节点(沿途最后有值的节点)
  118. - 停止位检查:节点上所有 city_id 是否一致
  119. - 一致 → 返回停止节点最佳值
  120. - 不一致(多种 city_id)→ 歧义 → None
  121. """
  122. if not text:
  123. return None
  124. node = self.root
  125. stop_node = None
  126. for ch in text:
  127. if ch not in node.children:
  128. break # 路径断了
  129. node = node.children[ch]
  130. if self._best_value(node) is not None:
  131. stop_node = node
  132. if stop_node is None:
  133. return None
  134. # 停止位 city_id 唯一性检查
  135. if len(stop_node.values) > 1:
  136. city_ids = set(cid for _, _, cid in stop_node.values)
  137. if len(city_ids) > 1:
  138. return None # 多种 city_id → 歧义
  139. return self._best_value(stop_node)
  140. class AreaService:
  141. """区域数据服务,从 city.json 加载并构建多级索引"""
  142. def __init__(self, area_data_json: str, addr_prefix_json: Optional[str] = None) -> None:
  143. area_data_json = os.path.normpath(area_data_json)
  144. log.info(f'area service loading from {area_data_json}')
  145. with open(area_data_json, 'r', encoding='utf-8') as f:
  146. data = json.load(f)
  147. # 数据容器
  148. self._province_map: Dict[int, Dict[str, Any]] = {} # province_id -> {id, name, short_name}
  149. self._city_map: Dict[int, Dict[str, Any]] = {} # city_id -> {id, name, short_name, province_id}
  150. # 层级索引
  151. self._addr_prefix_map: Dict[str, int] = {} # 手工前缀 -> city_id
  152. self._area_trie: _Trie = _Trie() # 统一前缀树:省/市/区 -> id
  153. self._build_maps(data)
  154. self._build_indexes()
  155. # 加载可选的地址前缀映射
  156. if addr_prefix_json:
  157. self._load_addr_prefix_map(addr_prefix_json)
  158. log.info(
  159. f'area service loaded: {len(self._province_map)} provinces, '
  160. f'{len(self._city_map)} cities, {len(self._districts)} districts'
  161. )
  162. # ---------- 内部构建 ----------
  163. def _build_maps(self, data: List[Dict[str, Any]]) -> None:
  164. """从 JSON 构建省/市/区映射"""
  165. self._districts: List[Tuple[str, int]] = [] # (区县名, city_id)
  166. for prov in data:
  167. p_id = prov['id']
  168. p_name = prov['name']
  169. p_short = _strip_suffix(p_name, _PROVINCE_SUFFIXES)
  170. self._province_map[p_id] = {
  171. 'id': p_id, 'name': p_name, 'short_name': p_short,
  172. }
  173. for city in prov.get('sons', []):
  174. c_id = city['id']
  175. c_name = city['name']
  176. c_short = _strip_suffix(c_name, _CITY_SUFFIXES)
  177. self._city_map[c_id] = {
  178. 'id': c_id, 'name': c_name, 'short_name': c_short,
  179. 'province_id': p_id,
  180. }
  181. # 收集区县
  182. for dist_name in city.get('sons', []):
  183. self._districts.append((dist_name, c_id))
  184. def _build_indexes(self) -> None:
  185. """
  186. 构建统一前缀树,三种插入模式:
  187. 1. 省:从 root 插入(全称 + 简称)
  188. 2. 市:从省叶节点继续插入(层级链)+ 从 root 直接插入(全局 fallback)
  189. 3. 区:从 root 直接插入(全局 fallback)
  190. """
  191. # 预计算:直辖市的 p_id 集合(不插入省节点,直接插入市节点)
  192. municipality_pids: Set[int] = set()
  193. for c_id, city in self._city_map.items():
  194. p_id = city['province_id']
  195. if city['name'] == self._province_map[p_id]['name']:
  196. municipality_pids.add(p_id)
  197. # 1. 省级(全称 + 简称)
  198. # 直辖市不插入省节点,直接插入市节点(避免省/市同名导致 trie 停在 province)
  199. prov_leaves: Dict[int, List] = {} # p_id → [叶节点]
  200. for p_id, prov in self._province_map.items():
  201. if p_id in municipality_pids:
  202. continue # 直辖市跳过省插入
  203. leaves = self._area_trie.insert(prov['name'], 'province', p_id, 0)
  204. if prov['short_name'] and prov['short_name'] != prov['name'] and len(prov['short_name']) > 1:
  205. leaves += self._area_trie.insert(prov['short_name'], 'province', p_id, 0)
  206. prov_leaves[p_id] = leaves
  207. # 2. 市级
  208. city_leaves: Dict[int, List] = {} # c_id → [叶节点](用于区县层级链)
  209. for c_id, city in self._city_map.items():
  210. if city['name'] in ('市辖区',):
  211. continue
  212. p_id = city['province_id']
  213. c_name = city['name']
  214. c_short = city['short_name']
  215. # 直辖市:不经过省节点,直接从 root 插入市
  216. if p_id in municipality_pids:
  217. direct = self._area_trie.insert(c_name, 'city', c_id, c_id)
  218. city_leaves[c_id] = direct
  219. if c_short and c_short != c_name and len(c_short) > 1:
  220. city_leaves[c_id] += self._area_trie.insert(c_short, 'city', c_id, c_id)
  221. # 虚拟城市别名
  222. if c_name in _VIRTUAL_CITY_ALIASES:
  223. for alias in _VIRTUAL_CITY_ALIASES[c_name]:
  224. city_leaves[c_id] += self._area_trie.insert(alias, 'city', c_id, c_id)
  225. continue
  226. # 普通城市:从省叶节点继续插入(层级链:省→市)
  227. if p_id in prov_leaves and c_name:
  228. chained = self._area_trie.insert_from(prov_leaves[p_id], c_name, 'city', c_id, c_id)
  229. city_leaves[c_id] = chained
  230. if c_short and c_short != c_name and len(c_short) > 1:
  231. city_leaves[c_id] += self._area_trie.insert_from(prov_leaves[p_id], c_short, 'city', c_id, c_id)
  232. # 虚拟城市别名也从省叶节点链式插入
  233. if c_name in _VIRTUAL_CITY_ALIASES:
  234. for alias in _VIRTUAL_CITY_ALIASES[c_name]:
  235. city_leaves[c_id] += self._area_trie.insert_from(prov_leaves[p_id], alias, 'city', c_id, c_id)
  236. # 从 root 直接插入(全局 fallback)
  237. if c_name:
  238. direct = self._area_trie.insert(c_name, 'city', c_id, c_id)
  239. if c_id not in city_leaves:
  240. city_leaves[c_id] = direct
  241. else:
  242. city_leaves[c_id] += direct
  243. # 市简称(单字简称不插入,避免歧义:如"北林区"→"北")
  244. if c_short and c_short != c_name and len(c_short) > 1:
  245. city_leaves[c_id] += self._area_trie.insert(c_short, 'city', c_id, c_id)
  246. # 虚拟城市别名
  247. if c_name in _VIRTUAL_CITY_ALIASES:
  248. for alias in _VIRTUAL_CITY_ALIASES[c_name]:
  249. city_leaves[c_id] += self._area_trie.insert(alias, 'city', c_id, c_id)
  250. # 3. 区级(city_id = 所属市 c_id)
  251. # 3a. 从市叶节点继续插入(层级链:市→区)
  252. for dist_name, c_id in self._districts:
  253. if c_id in city_leaves:
  254. self._area_trie.insert_from(city_leaves[c_id], dist_name, 'district', c_id, c_id)
  255. d_short = _strip_suffix(dist_name, _DISTRICT_SUFFIXES)
  256. if d_short and d_short != dist_name and len(d_short) > 1:
  257. self._area_trie.insert_from(city_leaves[c_id], d_short, 'district', c_id, c_id)
  258. # 3b. 从 root 直接插入(全局 fallback)
  259. for dist_name, c_id in self._districts:
  260. self._area_trie.insert(dist_name, 'district', c_id, c_id)
  261. d_short = _strip_suffix(dist_name, _DISTRICT_SUFFIXES)
  262. if d_short and d_short != dist_name and len(d_short) > 1:
  263. self._area_trie.insert(d_short, 'district', c_id, c_id)
  264. # ---------- 查询接口 ----------
  265. def _resolve(self, entry_type: str, entry_id: int) -> Optional[AreaMatch]:
  266. """将 trie 匹配结果转为 AreaMatch"""
  267. if entry_type == 'city':
  268. city = self._city_map[entry_id]
  269. prov = self._province_map.get(city['province_id'])
  270. return AreaMatch(
  271. prov['id'] if prov else 0, entry_id,
  272. prov['name'] if prov else '', city['name']
  273. )
  274. if entry_type == 'district':
  275. city = self._city_map.get(entry_id)
  276. if city:
  277. prov = self._province_map.get(city['province_id'])
  278. return AreaMatch(
  279. prov['id'] if prov else 0, entry_id,
  280. prov['name'] if prov else '', city['name']
  281. )
  282. return None
  283. def search_area(self, addr: str) -> Optional[AreaMatch]:
  284. """
  285. 从地址中解析省市信息
  286. addr 进前缀树,出来就是结果。
  287. """
  288. if not addr:
  289. return None
  290. # 1. 手工配置的前缀(最高优先级)
  291. for prefix, city_id in self._addr_prefix_map.items():
  292. if addr.startswith(prefix):
  293. city = self._city_map.get(city_id)
  294. if city:
  295. prov = self._province_map.get(city['province_id'])
  296. return AreaMatch(
  297. prov['id'] if prov else 0, city_id,
  298. prov['name'] if prov else '', city['name']
  299. )
  300. return AreaMatch(0, city_id, '', '')
  301. # 2. 统一前缀树匹配
  302. result = self._area_trie.search(addr)
  303. if not result:
  304. return None
  305. entry_type, entry_id = result
  306. # 仅匹配到省(非直辖市)→ 市未知
  307. if entry_type == 'province':
  308. return None
  309. return self._resolve(entry_type, entry_id)
  310. def get_area_info(self, area_id: int) -> Optional[Dict[str, Any]]:
  311. """
  312. 根据区域 id 获取信息
  313. 支持省级或市级 id。
  314. """
  315. area_id = int(area_id)
  316. city = self._city_map.get(area_id)
  317. if city:
  318. prov = self._province_map.get(city['province_id'])
  319. return {
  320. 'id': area_id, 'name': city['name'], 'level': 'city',
  321. 'province_id': city['province_id'],
  322. 'province_name': prov['name'] if prov else '',
  323. }
  324. prov = self._province_map.get(area_id)
  325. if prov:
  326. return {'id': area_id, 'name': prov['name'], 'level': 'province'}
  327. return None
  328. def get_province_list(self) -> List[Dict[str, Any]]:
  329. """获取所有省份列表"""
  330. return [
  331. {'id': p['id'], 'name': p['name'], 'short_name': p['short_name']}
  332. for p in self._province_map.values()
  333. ]
  334. def get_city_list(self, province_id: Optional[int] = None) -> List[Dict[str, Any]]:
  335. """获取城市列表,可按省份过滤"""
  336. if province_id:
  337. return [
  338. {'id': c['id'], 'name': c['name'], 'short_name': c['short_name'],
  339. 'province_id': c['province_id']}
  340. for c in self._city_map.values() if c['province_id'] == province_id
  341. ]
  342. return [
  343. {'id': c['id'], 'name': c['name'], 'short_name': c['short_name'],
  344. 'province_id': c['province_id']}
  345. for c in self._city_map.values()
  346. ]
  347. def get_district_list(self, city_id: Optional[int] = None) -> List[str]:
  348. """获取区县列表(字符串数组),可按城市 id 过滤"""
  349. if city_id is not None:
  350. return [name for name, cid in self._districts if cid == city_id]
  351. return [name for name, _ in self._districts]
  352. def batch_get_ids_by_names(self, pairs: List[Tuple[str, str]]) -> Dict[Tuple[str, str], Tuple[int, int]]:
  353. """
  354. 批量通过 (province_name, city_name) 查询 (province_id, city_id)
  355. Args:
  356. pairs: [(province_name, city_name), ...]
  357. Returns:
  358. {(province_name, city_name): (province_id, city_id), ...}
  359. 未找到的不会出现在结果中
  360. """
  361. if not pairs:
  362. return {}
  363. # 构建名称反查索引
  364. prov_name_to_id: Dict[str, int] = {
  365. prov['name']: pid for pid, prov in self._province_map.items()
  366. }
  367. city_name_to_id: Dict[Tuple[int, str], int] = {
  368. (city['province_id'], city['name']): cid
  369. for cid, city in self._city_map.items()
  370. }
  371. result: Dict[Tuple[str, str], Tuple[int, int]] = {}
  372. for p_name, c_name in pairs:
  373. if not p_name:
  374. continue
  375. p_id = prov_name_to_id.get(p_name)
  376. if p_id is None:
  377. continue
  378. c_id = city_name_to_id.get((p_id, c_name)) if c_name else 0
  379. result[(p_name, c_name)] = (p_id, c_id or 0)
  380. return result
  381. def _load_addr_prefix_map(self, json_path: str) -> None:
  382. """
  383. 从 JSON 文件加载手工配置的地址前缀映射
  384. JSON 格式: [{"prefix": "xxx", "province": "省全称", "city": "市全称"}, ...]
  385. 根据省全称和市全称查找对应的城市 id
  386. """
  387. json_path = os.path.normpath(json_path)
  388. with open(json_path, 'r', encoding='utf-8') as f:
  389. rules = json.load(f)
  390. # 构建名称反查 map
  391. prov_name_to_id: Dict[str, int] = {
  392. prov['name']: pid for pid, prov in self._province_map.items()
  393. }
  394. city_name_to_id: Dict[Tuple[int, str], int] = {
  395. (city['province_id'], city['name']): cid
  396. for cid, city in self._city_map.items()
  397. }
  398. self._addr_prefix_map = {}
  399. for rule in rules:
  400. prefix = rule.get('prefix', '')
  401. p_name = rule.get('province', '')
  402. c_name = rule.get('city', '')
  403. p_id = prov_name_to_id.get(p_name)
  404. if not p_id:
  405. log.warning(f'addr prefix rule: province not found: {p_name}')
  406. continue
  407. c_id = city_name_to_id.get((p_id, c_name))
  408. if not c_id:
  409. log.warning(f'addr prefix rule: city not found: {p_name} {c_name}')
  410. continue
  411. self._addr_prefix_map[prefix] = c_id
  412. log.info(f'loaded {len(self._addr_prefix_map)} addr prefix rules from {json_path}')