diff --git a/app/integrations/cities.py b/app/integrations/cities.py new file mode 100644 index 0000000..a91fa55 --- /dev/null +++ b/app/integrations/cities.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""美团城市字典(地级市)读取。 + +数据源:app/integrations/data/meituan_cities.json,由 tools/gen_meituan_cities.py 从 +美团官方「城市字典」Excel 生成(359 个地级市:city_id / 城市名 / 省份)。 +实测一个地级市 cityId 已覆盖其下辖县级市供给(徐州 → 邳州 / 新沂 / 睢宁 …),故无区县层级。 + +- ETL(scripts/pull_meituan_coupons.py)遍历 all_cities() 全量抓取入库。 +- 读取侧将来「按城市过滤」时,也从这里取 city_id ↔ 城市名映射。 +""" +from __future__ import annotations + +import functools +import json +from pathlib import Path + +_JSON = Path(__file__).resolve().parent / "data" / "meituan_cities.json" + + +@functools.lru_cache(maxsize=1) +def all_cities() -> list[dict]: + """全部城市 [{city_id, name, province}, ...](359 个地级市)。""" + return json.loads(_JSON.read_text(encoding="utf-8")) + + +@functools.lru_cache(maxsize=1) +def _by_id() -> dict[str, dict]: + return {c["city_id"]: c for c in all_cities()} + + +def city_name(city_id: str) -> str | None: + """city_id → 城市名(查不到返回 None)。""" + c = _by_id().get(city_id) + return c["name"] if c else None diff --git a/app/integrations/data/meituan_cities.json b/app/integrations/data/meituan_cities.json new file mode 100644 index 0000000..8431142 --- /dev/null +++ b/app/integrations/data/meituan_cities.json @@ -0,0 +1,1797 @@ +[ + { + "city_id": "3NUYJKKJXPHVNZUHFK3HWUDHNM", + "name": "宣城市", + "province": "安徽省" + }, + { + "city_id": "LXXSHOY7LNK74ZK2SKVUXFY72Q", + "name": "阜阳市", + "province": "安徽省" + }, + { + "city_id": "ZEBF2LBJOEHGM4XGFPNW4IHBIA", + "name": "合肥市", + "province": "安徽省" + }, + { + "city_id": "WADWEY3GR6IARJLSNGQWG2KI4E", + "name": "滁州市", + "province": "安徽省" + }, + { + "city_id": "Z62QL3X66AOT6MSY7LB6LVO4CI", + "name": "芜湖市", + "province": "安徽省" + }, + { + "city_id": "M6QCWRCECRT6ZEJ6RERR6IWHFI", + "name": "淮南市", + "province": "安徽省" + }, + { + "city_id": "UP2NBJACSAO7FUH4XQQDMBOUE4", + "name": "马鞍山市", + "province": "安徽省" + }, + { + "city_id": "WLL7BSHUBPTLTMITFITJX2GKWY", + "name": "蚌埠市", + "province": "安徽省" + }, + { + "city_id": "ECTBNJ4KNNHPGXEVI4TBJNU7BY", + "name": "亳州市", + "province": "安徽省" + }, + { + "city_id": "KNEGDNOSRGNQPAMOMH54ATLIUU", + "name": "六安市", + "province": "安徽省" + }, + { + "city_id": "YH53FRR55H6VA4KXW36OJ7RNSE", + "name": "宿州市", + "province": "安徽省" + }, + { + "city_id": "LH6LX5DUVFPOFCAQ6NEGPWCLBI", + "name": "淮北市", + "province": "安徽省" + }, + { + "city_id": "RIFXWJ46SEJXE7EP6HZXEUIALU", + "name": "铜陵市", + "province": "安徽省" + }, + { + "city_id": "FK6F7KMO4WNARZKUXMMSBWGATM", + "name": "安庆市", + "province": "安徽省" + }, + { + "city_id": "M3CHGNNUSFSPS5HRV3W7MCBWFI", + "name": "黄山市", + "province": "安徽省" + }, + { + "city_id": "XFI6PM7SRO6NHBGYQUZEEA2WT4", + "name": "池州市", + "province": "安徽省" + }, + { + "city_id": "D2ZILSASTTGEFQWZIDTHA7PWU4", + "name": "澳门", + "province": "澳门特别行政区" + }, + { + "city_id": "WKV2HMXUEK634WP64CUCUQGM64", + "name": "北京市", + "province": "北京市" + }, + { + "city_id": "HPMKHLM3QR6EZGMY7GEI4H3QYQ", + "name": "泉州市", + "province": "福建省" + }, + { + "city_id": "6V523YRU54Y3PEAM2XPADNJM2U", + "name": "福州市", + "province": "福建省" + }, + { + "city_id": "HH3ZZCERPVQIYUZPW4A2U4JKZI", + "name": "莆田市", + "province": "福建省" + }, + { + "city_id": "BQ5RWEJS4O7W27SQMLPMRIRJDU", + "name": "宁德市", + "province": "福建省" + }, + { + "city_id": "TDN7XDQMZEP6ZCK6UO3VVMCSMM", + "name": "三明市", + "province": "福建省" + }, + { + "city_id": "ZKNTORN6YTZL2BXRYUSRGV3CHU", + "name": "厦门市", + "province": "福建省" + }, + { + "city_id": "Y2DI2QLZN5NNACMD3KI2DBR4IA", + "name": "龙岩市", + "province": "福建省" + }, + { + "city_id": "C4RS32I6QAHWLP55UQWI3N5LLY", + "name": "南平市", + "province": "福建省" + }, + { + "city_id": "5T23WDEOAP7RYNU4JNL2ZG7PDQ", + "name": "漳州市", + "province": "福建省" + }, + { + "city_id": "UHZBROATFB2KWNMNLS23DPRJBY", + "name": "定西市", + "province": "甘肃省" + }, + { + "city_id": "WLPAHIUOIVKS644QSN4V5ZY5XQ", + "name": "金昌市", + "province": "甘肃省" + }, + { + "city_id": "J7TO3UHZ57ABNUKIQUBCIJZQFA", + "name": "白银市", + "province": "甘肃省" + }, + { + "city_id": "GX277SS75375VEFYVCEHBL6ISA", + "name": "临夏回族自治州", + "province": "甘肃省" + }, + { + "city_id": "ORA3R7F2LSJHUOCIDTZCP54G7Q", + "name": "张掖市", + "province": "甘肃省" + }, + { + "city_id": "L6VQYYOJNTHSLW5JCR5SXBITDM", + "name": "武威市", + "province": "甘肃省" + }, + { + "city_id": "65OXQPQVFXNOYHXTMCE2RNFRL4", + "name": "兰州市", + "province": "甘肃省" + }, + { + "city_id": "IUIZYQ7E2SPMEAIGOUUNZWBQOA", + "name": "天水市", + "province": "甘肃省" + }, + { + "city_id": "BYBRGRDRV4NKAWCU6GWBIVLX3Q", + "name": "酒泉市", + "province": "甘肃省" + }, + { + "city_id": "KDST2VRETG6WK5SMJO2G2FN2RU", + "name": "嘉峪关市", + "province": "甘肃省" + }, + { + "city_id": "R3Q2XWVFVF4T2ADZZZMPZJRWBA", + "name": "庆阳市", + "province": "甘肃省" + }, + { + "city_id": "7FWNT2TP66SU4QEP6IBBMZWFNE", + "name": "陇南市", + "province": "甘肃省" + }, + { + "city_id": "T33S2GYGPVHAL5SR2FLSPTJSBE", + "name": "平凉市", + "province": "甘肃省" + }, + { + "city_id": "RR6KAWBLOKD4H2UINKYPFCPXO4", + "name": "甘南藏族自治州", + "province": "甘肃省" + }, + { + "city_id": "QKX4DS3CTJJG7SFW5SBHPLD42I", + "name": "茂名市", + "province": "广东省" + }, + { + "city_id": "JJZ75A32XCQNZU4IN2ZCEUGN3M", + "name": "梅州市", + "province": "广东省" + }, + { + "city_id": "SJSOXOSJASLUT6LBH4E32SUKKQ", + "name": "清远市", + "province": "广东省" + }, + { + "city_id": "SQQWAN5BQOVSX55S7EPF7QHMAU", + "name": "珠海市", + "province": "广东省" + }, + { + "city_id": "NGRJMW6JMRS6U2KUJEONSORAEY", + "name": "韶关市", + "province": "广东省" + }, + { + "city_id": "FAUMIGOSOET4E5WR5BL6P3OZHA", + "name": "佛山市", + "province": "广东省" + }, + { + "city_id": "JSBIH55ICFZQ2D3LEV47YMZ2NI", + "name": "河源市", + "province": "广东省" + }, + { + "city_id": "KOYAYPD2DBLDCF2ZI5GCW4LF6Y", + "name": "中山市", + "province": "广东省" + }, + { + "city_id": "647JGFPUYM4VWVLZCPSHT63XKQ", + "name": "汕头市", + "province": "广东省" + }, + { + "city_id": "AMOIPZW3Q2NMTDSEREFVM4SV74", + "name": "深圳市", + "province": "广东省" + }, + { + "city_id": "XIHEJY4H2CDZJCLIXDIN36BKXQ", + "name": "广州市", + "province": "广东省" + }, + { + "city_id": "UZ6OT4CYUR42KCTTED2KJW6EGA", + "name": "东莞市", + "province": "广东省" + }, + { + "city_id": "RM3HLOIUEYKTQ5O2JSVEKDMFRY", + "name": "阳江市", + "province": "广东省" + }, + { + "city_id": "C6XNJAZA6N3NNUDBUU3JIZPTDI", + "name": "潮州市", + "province": "广东省" + }, + { + "city_id": "AUPF3G2ULSV4TDT4L3NMHRTY6Y", + "name": "揭阳市", + "province": "广东省" + }, + { + "city_id": "7HIITKBPRXTVBA2FBBN443XITQ", + "name": "云浮市", + "province": "广东省" + }, + { + "city_id": "TO6ILZ7MPJMJN3S7W2SXMIFQQY", + "name": "江门市", + "province": "广东省" + }, + { + "city_id": "DTTBMGCIOMPCZY5NETUEKWJ6PY", + "name": "汕尾市", + "province": "广东省" + }, + { + "city_id": "HJY7JYWBA6FQY42RYSKX3RZTRI", + "name": "湛江市", + "province": "广东省" + }, + { + "city_id": "SLICHB4FBDVDLI53MR74WVUUNI", + "name": "肇庆市", + "province": "广东省" + }, + { + "city_id": "ZPX4JXJVBBYSSD2KTWHAPXO6NE", + "name": "惠州市", + "province": "广东省" + }, + { + "city_id": "JH4Q44RQA4EZ3Q6MHQVEVE7KZQ", + "name": "百色市", + "province": "广西壮族自治区" + }, + { + "city_id": "H2JXFEJFIL4PPFMYOS4MHZ5IBM", + "name": "崇左市", + "province": "广西壮族自治区" + }, + { + "city_id": "SXIRRISUOEGBU335AWT2ZFL6A4", + "name": "贵港市", + "province": "广西壮族自治区" + }, + { + "city_id": "HEQHKC4KP7YGGYVBZM5JEUI5AQ", + "name": "北海市", + "province": "广西壮族自治区" + }, + { + "city_id": "SKGG7KMFKVDIDKRVQEPTS7SIE4", + "name": "贺州市", + "province": "广西壮族自治区" + }, + { + "city_id": "N4WR7CWCULNA5Z35OTDJSZYDCU", + "name": "钦州市", + "province": "广西壮族自治区" + }, + { + "city_id": "T4RXX2WY6WPQYZEUXVJNZBXQZU", + "name": "梧州市", + "province": "广西壮族自治区" + }, + { + "city_id": "3R23AS3EIY7EYE2D5MWWORZODI", + "name": "河池市", + "province": "广西壮族自治区" + }, + { + "city_id": "57SMWWCV7X44E256P4I23OQ3AA", + "name": "防城港市", + "province": "广西壮族自治区" + }, + { + "city_id": "YHGHVIQ37UCTNQ4JKPQEAUWIQA", + "name": "桂林市", + "province": "广西壮族自治区" + }, + { + "city_id": "MQJZTM455OKZLAN5WQYUTA5TDE", + "name": "柳州市", + "province": "广西壮族自治区" + }, + { + "city_id": "C6FZPLB4NJQ6VUPSKDJH3EWQDM", + "name": "玉林市", + "province": "广西壮族自治区" + }, + { + "city_id": "BFSU5W6E5XBIDFPQLVPGRDSATY", + "name": "南宁市", + "province": "广西壮族自治区" + }, + { + "city_id": "CKXOQUZDNOVNME3PEBOY2CULQQ", + "name": "来宾市", + "province": "广西壮族自治区" + }, + { + "city_id": "LV32FV6IQTKFR7JIBEQMHRUVCA", + "name": "贵阳市", + "province": "贵州省" + }, + { + "city_id": "F26RCNKMFTZONJCSJ5C6FHVY74", + "name": "毕节市", + "province": "贵州省" + }, + { + "city_id": "G2LMYRWVRCK7BTD2WM4NWX4SYM", + "name": "黔南布依族苗族自治州", + "province": "贵州省" + }, + { + "city_id": "MFSOO3NBMB2PVLIVSI5EJK7MWY", + "name": "黔西南布依族苗族自治州", + "province": "贵州省" + }, + { + "city_id": "KWUL44L7SEMJGIMXCWSSEB3OOA", + "name": "遵义市", + "province": "贵州省" + }, + { + "city_id": "T2P3OFGQZUGR7D6TGRCMDF22GI", + "name": "铜仁市", + "province": "贵州省" + }, + { + "city_id": "AGUFUANSZNGC4TMOPZO65IRSPI", + "name": "六盘水市", + "province": "贵州省" + }, + { + "city_id": "2XOCOSNUAK3J5QDGTKIBWKK7KU", + "name": "安顺市", + "province": "贵州省" + }, + { + "city_id": "6XRTSAEYJTA2UBKXO4XEPQE5ZY", + "name": "黔东南苗族侗族自治州", + "province": "贵州省" + }, + { + "city_id": "YRMKRP2GOE2VMRS73N4YRIZUHY", + "name": "三亚市", + "province": "海南省" + }, + { + "city_id": "CJRGVLBNLJAVBJ4ZKKIZ3FZ2LY", + "name": "白沙黎族自治县", + "province": "海南省" + }, + { + "city_id": "5XOUAJ5Z4J4K7SVIQGXL2OM2JQ", + "name": "保亭黎族苗族自治县", + "province": "海南省" + }, + { + "city_id": "2UFQ6A2QRJPXPH3VOYEAQHMVSQ", + "name": "海口市", + "province": "海南省" + }, + { + "city_id": "TGCVXVS4M7NDQM4ROUDCVI6I3A", + "name": "承德市", + "province": "河北省" + }, + { + "city_id": "JEUP6QWCOXPSM3SQTINQCJKIGM", + "name": "衡水市", + "province": "河北省" + }, + { + "city_id": "Z442MNCW6BO2BBHIRUPRBACXPI", + "name": "唐山市", + "province": "河北省" + }, + { + "city_id": "RFE6R34GD4FY3LUKC2ICSF6AFY", + "name": "张家口市", + "province": "河北省" + }, + { + "city_id": "CWJN55M73VZDCYJEQ7AHDBWGGY", + "name": "沧州市", + "province": "河北省" + }, + { + "city_id": "DFL4ES776ECRGBYNOPLWKB247I", + "name": "雄安新区", + "province": "河北省" + }, + { + "city_id": "ZLSXYY34IHBHIC2NOVPQQBFTBE", + "name": "保定市", + "province": "河北省" + }, + { + "city_id": "3DO6Z2QRJQFMPLLDS55PG7DSBU", + "name": "石家庄市", + "province": "河北省" + }, + { + "city_id": "PR57XT25LI3246VGASEPSHP63E", + "name": "邢台市", + "province": "河北省" + }, + { + "city_id": "SKYLNH737BS56TD452FOKYL36U", + "name": "邯郸市", + "province": "河北省" + }, + { + "city_id": "5PWPERL7GQKJD6QPLR2TWUUM7E", + "name": "秦皇岛市", + "province": "河北省" + }, + { + "city_id": "5T2TGV6SJFVL3MO7HIMN2KTQTA", + "name": "廊坊市", + "province": "河北省" + }, + { + "city_id": "ECSTLZ7GP7IX3MB5EVNKS47MLE", + "name": "焦作市", + "province": "河南省" + }, + { + "city_id": "VTWW34QB2F5Q4LW7ISNUMWX7GY", + "name": "开封市", + "province": "河南省" + }, + { + "city_id": "D2NUN47NY4Q55X3UED4JMSI6CM", + "name": "周口市", + "province": "河南省" + }, + { + "city_id": "TR3XJFQR4EFYRRIX7TUQF3B26Y", + "name": "郑州市", + "province": "河南省" + }, + { + "city_id": "CKJGF5S6XMHW5ZJEBU7MJC47QA", + "name": "新乡市", + "province": "河南省" + }, + { + "city_id": "IFASZ625MCFJQKPLJ7EA2SMJUU", + "name": "商丘市", + "province": "河南省" + }, + { + "city_id": "SKXPYKTTRG4YAUHE2HZXWRWXGM", + "name": "鹤壁市", + "province": "河南省" + }, + { + "city_id": "G5LXE74CUHO2K6BBRN7Q5DSRJY", + "name": "漯河市", + "province": "河南省" + }, + { + "city_id": "SLNOAFJV2LTBSH7SJCRXJA36K4", + "name": "驻马店市", + "province": "河南省" + }, + { + "city_id": "65WO7LH7CFDUGKYMXQLRAYKKWM", + "name": "安阳市", + "province": "河南省" + }, + { + "city_id": "RIX2X7FAVTZCAQ5RT2C2CWK22Q", + "name": "南阳市", + "province": "河南省" + }, + { + "city_id": "SZOW5OY3U54SSY4WRC65VJUNTI", + "name": "平顶山市", + "province": "河南省" + }, + { + "city_id": "7VPIDDUS4P2LSZ6Q5S57MAQDEM", + "name": "信阳市", + "province": "河南省" + }, + { + "city_id": "4VYCRORUOZ4DC2U6S3CT6H6KWE", + "name": "洛阳市", + "province": "河南省" + }, + { + "city_id": "M5WNO2BQ3UGLLHBCG4NCEFPP5U", + "name": "濮阳市", + "province": "河南省" + }, + { + "city_id": "LY3O6PBPIWETMA3ZOL6ETY5UB4", + "name": "三门峡市", + "province": "河南省" + }, + { + "city_id": "FXXJLIRE72LS2W4OWWQVJMRJHA", + "name": "许昌市", + "province": "河南省" + }, + { + "city_id": "5XH353QTY3VWF2KYOCZCL3TOXY", + "name": "牡丹江市", + "province": "黑龙江省" + }, + { + "city_id": "2KGRZKF6IECV2W7K5J64Y2LY4M", + "name": "齐齐哈尔市", + "province": "黑龙江省" + }, + { + "city_id": "FASGWS5ADVSTFGJG6TGBZPZP6Q", + "name": "鹤岗市", + "province": "黑龙江省" + }, + { + "city_id": "2O6CDIXSWIKBXILZEEPKCS7MVI", + "name": "双鸭山市", + "province": "黑龙江省" + }, + { + "city_id": "OZ2PTOBYTBG57XJZMIC23QFJKM", + "name": "佳木斯市", + "province": "黑龙江省" + }, + { + "city_id": "FO24MQMULT3J5JW64APNXSQEPU", + "name": "伊春市", + "province": "黑龙江省" + }, + { + "city_id": "ETZ2HYWVU6U7SKU6G4JAO64RUQ", + "name": "黑河市", + "province": "黑龙江省" + }, + { + "city_id": "PR7EJNBY2VZBEUT36JAWE3TM7I", + "name": "七台河市", + "province": "黑龙江省" + }, + { + "city_id": "HADAAVLERKIW4SQGCTQYGX4AL4", + "name": "哈尔滨市", + "province": "黑龙江省" + }, + { + "city_id": "CGTU45YC5C3JYLHMA47USDPA7Y", + "name": "大庆市", + "province": "黑龙江省" + }, + { + "city_id": "T4W7SQIPOM4EYMEFFRAB5BSTII", + "name": "鸡西市", + "province": "黑龙江省" + }, + { + "city_id": "TYGZHNQL6YT7CX6EEG5DJQQHMA", + "name": "绥化市", + "province": "黑龙江省" + }, + { + "city_id": "OOSJTSN2CVUUCKD6XAB7EYYIPY", + "name": "大兴安岭地区", + "province": "黑龙江省" + }, + { + "city_id": "I3YF3EKZHIZTN6TZOTYTGZ2UXQ", + "name": "随州市", + "province": "湖北省" + }, + { + "city_id": "ESGVBOSTHW7JWEVCGYJUTEHEBQ", + "name": "宜昌市", + "province": "湖北省" + }, + { + "city_id": "MTJRWJ53XBW5SBTWHKNNZDLM7U", + "name": "十堰市", + "province": "湖北省" + }, + { + "city_id": "PXZLF2ISKQL5ACM67ZCBNOGDT4", + "name": "黄石市", + "province": "湖北省" + }, + { + "city_id": "44RMTOEHPUFXBHZXX4IQ4IRZVQ", + "name": "荆州市", + "province": "湖北省" + }, + { + "city_id": "OTKZGG743NFC474ADMMRX4ZOOA", + "name": "鄂州市", + "province": "湖北省" + }, + { + "city_id": "EXOUAZAQ73OEFAK72CHQ32GQHQ", + "name": "恩施土家族苗族自治州", + "province": "湖北省" + }, + { + "city_id": "SUCY7I72QJDZD7EBFXREIQ67SI", + "name": "咸宁市", + "province": "湖北省" + }, + { + "city_id": "QENSGB5R7HGYDXCG2LQZQTO3TU", + "name": "荆门市", + "province": "湖北省" + }, + { + "city_id": "OHIWL6SAE2PR4EJR4BOMLAE6FU", + "name": "武汉市", + "province": "湖北省" + }, + { + "city_id": "ZXCE4WV2CDVPQTA4HAOVELQMNE", + "name": "襄阳市", + "province": "湖北省" + }, + { + "city_id": "KEFN5OPSS4ZZF6NU2TTL72S6HE", + "name": "孝感市", + "province": "湖北省" + }, + { + "city_id": "ROAHLMQ67H6M5NDFVXROJG723E", + "name": "黄冈市", + "province": "湖北省" + }, + { + "city_id": "YBEBX2YYN4WPBNH6Z6C73DNE7I", + "name": "张家界市", + "province": "湖南省" + }, + { + "city_id": "45XGRKYGSCPE5VNRYF4FVJGFMM", + "name": "株洲市", + "province": "湖南省" + }, + { + "city_id": "LK3SEIBRU7GTDT4J2EPTLIO33U", + "name": "永州市", + "province": "湖南省" + }, + { + "city_id": "R4YXFIK53W5E556BSGSBJWS4DM", + "name": "郴州市", + "province": "湖南省" + }, + { + "city_id": "PQDO3RNADWXX75OWZW2GSXJ4SE", + "name": "怀化市", + "province": "湖南省" + }, + { + "city_id": "RRRT6QOJYEJ432L3F76ZN5NHCA", + "name": "长沙市", + "province": "湖南省" + }, + { + "city_id": "B6WPNMCZ3ENQSV4NFY5MSTPDAM", + "name": "岳阳市", + "province": "湖南省" + }, + { + "city_id": "KNDZW5EHDPKP2DX7HBLKP4DYLM", + "name": "益阳市", + "province": "湖南省" + }, + { + "city_id": "PA2GHG3XZ7I47HTKZ4YAFH3OYY", + "name": "湘西土家族苗族自治州", + "province": "湖南省" + }, + { + "city_id": "I7CNIUA5PYV2EHDEW3RGYT2R4U", + "name": "邵阳市", + "province": "湖南省" + }, + { + "city_id": "SRI2SU4FN66FMJJCKQOCZD72ZY", + "name": "常德市", + "province": "湖南省" + }, + { + "city_id": "H7UHHJAMQUL7UA5QEUTGKNSL3A", + "name": "湘潭市", + "province": "湖南省" + }, + { + "city_id": "EFB255OBTB2BUDZENR5UVIC7ZQ", + "name": "衡阳市", + "province": "湖南省" + }, + { + "city_id": "RDMANB4KCM3OJSNVGZWVYVME6E", + "name": "娄底市", + "province": "湖南省" + }, + { + "city_id": "LD37PDU5OB4UAV5QDOBMKG5YTY", + "name": "吉林市", + "province": "吉林省" + }, + { + "city_id": "EO3GF4XNF5RXWRPUVAT3KTQO4U", + "name": "四平市", + "province": "吉林省" + }, + { + "city_id": "4GD7OS4CAQABH5YIWVK5SKGHMY", + "name": "通化市", + "province": "吉林省" + }, + { + "city_id": "6DRI2R5VAWMYJHJPCJKUNMDYEQ", + "name": "延边朝鲜族自治州", + "province": "吉林省" + }, + { + "city_id": "TVBCNVGUND4MOUOOUXFGX7DIUA", + "name": "松原市", + "province": "吉林省" + }, + { + "city_id": "JYY62HSKBUVK5OU7KGJDKQ4RTA", + "name": "白城市", + "province": "吉林省" + }, + { + "city_id": "QEDUHKMZ36CHJTKRD6O2ZPLNBU", + "name": "长春市", + "province": "吉林省" + }, + { + "city_id": "4EADVCBJMZ5UBH2FVRT6QCLS2U", + "name": "辽源市", + "province": "吉林省" + }, + { + "city_id": "EUQD5EGS2LR5KJSFNG6PPSIHHI", + "name": "白山市", + "province": "吉林省" + }, + { + "city_id": "YLTIISPCLBEGTZZX3WUWAD7WDE", + "name": "淮安市", + "province": "江苏省" + }, + { + "city_id": "L6U5DZP6MESXPMHOHCDMJS55O4", + "name": "宿迁市", + "province": "江苏省" + }, + { + "city_id": "HQMLYA7TDGMYQAXFCDUXBZPYHI", + "name": "镇江市", + "province": "江苏省" + }, + { + "city_id": "K6XJ4UN65ZD6XQKYEG5YN7HCRI", + "name": "盐城市", + "province": "江苏省" + }, + { + "city_id": "36I4X3EZZU4EHOSCLQI5OAKKBE", + "name": "南通市", + "province": "江苏省" + }, + { + "city_id": "S3GWFQU6QAVRDKLJT77LD6OFLE", + "name": "泰州市", + "province": "江苏省" + }, + { + "city_id": "IO6F4AFGAVIFRGYTZEC4TXM7W4", + "name": "无锡市", + "province": "江苏省" + }, + { + "city_id": "NUXNK2VOFSD2JFTEO2AMWX6NSU", + "name": "扬州市", + "province": "江苏省" + }, + { + "city_id": "TEVZU6CU6SK57HFW7DFNGMQ44A", + "name": "南京市", + "province": "江苏省" + }, + { + "city_id": "UTYSRBQ4FSB7XLWCF3Z2HTKNUA", + "name": "常州市", + "province": "江苏省" + }, + { + "city_id": "OCZOBCJDEXKE7KBN3BD7AYQG2Q", + "name": "徐州市", + "province": "江苏省" + }, + { + "city_id": "6LIBPJGZROLXE3CLZGJRYMYBOU", + "name": "连云港市", + "province": "江苏省" + }, + { + "city_id": "FS4PIU74F7QKYARDWR5ZMOLICI", + "name": "苏州市", + "province": "江苏省" + }, + { + "city_id": "R2F4OWUO65HYZW2IQIKINORZ7Y", + "name": "赣州市", + "province": "江西省" + }, + { + "city_id": "YW346BTN3VFYNRC3744UR5MZXY", + "name": "抚州市", + "province": "江西省" + }, + { + "city_id": "QR3FDR26U2EJIXOMBHL7IJLQSA", + "name": "南昌市", + "province": "江西省" + }, + { + "city_id": "OAJHJL7L7VNW2Q5UXRE7F4CUJQ", + "name": "九江市", + "province": "江西省" + }, + { + "city_id": "OMH7D45R4DX2KNHLV3G2UP56OY", + "name": "景德镇市", + "province": "江西省" + }, + { + "city_id": "YSB2PAEROB2IZSJZFVFH7KJPEI", + "name": "鹰潭市", + "province": "江西省" + }, + { + "city_id": "5OYAMNORCXKYA6UF7DW6KFFBIU", + "name": "上饶市", + "province": "江西省" + }, + { + "city_id": "SMHZOYKE7BXQJ2NT6Q24TFMLEQ", + "name": "吉安市", + "province": "江西省" + }, + { + "city_id": "2RZV26OUPKUHUJ5ZPB673VDGZU", + "name": "萍乡市", + "province": "江西省" + }, + { + "city_id": "232VHZEEZ6SXACE4AC5HQ4ZTFQ", + "name": "新余市", + "province": "江西省" + }, + { + "city_id": "QRLM74YXNDW2QDBWLTFGEMXK2I", + "name": "宜春市", + "province": "江西省" + }, + { + "city_id": "S6OHUVUKIIWPVMQD44RREUMNT4", + "name": "葫芦岛市", + "province": "辽宁省" + }, + { + "city_id": "DQQ4OIFUGFYJY3XZRK5VDWMLCA", + "name": "辽阳市", + "province": "辽宁省" + }, + { + "city_id": "S4YXGFEYXEUG6ISZ6O337OPVSI", + "name": "阜新市", + "province": "辽宁省" + }, + { + "city_id": "D3JHM7A4CG6RJMBD7YRDS5JOYU", + "name": "盘锦市", + "province": "辽宁省" + }, + { + "city_id": "VTRWMOSS6PCUYUAIPG6VPBKUUQ", + "name": "营口市", + "province": "辽宁省" + }, + { + "city_id": "ZPHFGWBIEVLKP5CVZNZUB3CRT4", + "name": "朝阳市", + "province": "辽宁省" + }, + { + "city_id": "NGYYULZ4UAGD3Q2PG726FFXSHU", + "name": "抚顺市", + "province": "辽宁省" + }, + { + "city_id": "Q5BRTSW752VSHIAKLLL7KL5TNA", + "name": "锦州市", + "province": "辽宁省" + }, + { + "city_id": "ZGV3WNPOSS7J4ZWBP6ZQG46BNM", + "name": "沈阳市", + "province": "辽宁省" + }, + { + "city_id": "XEX676YYMTYIV5QPIUZB4TA7IY", + "name": "本溪市", + "province": "辽宁省" + }, + { + "city_id": "PRTEQZMLNLQNZXJHRCYYLWZB4E", + "name": "丹东市", + "province": "辽宁省" + }, + { + "city_id": "4GN4WF6UQRFU64T4FVZPRDXRWQ", + "name": "鞍山市", + "province": "辽宁省" + }, + { + "city_id": "3QTZDFLJFSLLOOVCZ65PSDAVOU", + "name": "铁岭市", + "province": "辽宁省" + }, + { + "city_id": "CC4ZTMKKXI73ZEVT5QQTJN5SMM", + "name": "大连市", + "province": "辽宁省" + }, + { + "city_id": "3MBJEFDLAVOMQZ7L7CM5MNSYKA", + "name": "鄂尔多斯市", + "province": "内蒙古自治区" + }, + { + "city_id": "5NOS4YC5WO2IZCQPVB6MCBYDJ4", + "name": "呼和浩特市", + "province": "内蒙古自治区" + }, + { + "city_id": "OQNIP675H7L5R64652BH7KHUOQ", + "name": "通辽市", + "province": "内蒙古自治区" + }, + { + "city_id": "4WA6I63MGVINV5DNLNWRRHCDDM", + "name": "阿拉善盟", + "province": "内蒙古自治区" + }, + { + "city_id": "ELI6BDJBAN6RCYTETMK2EX2UKU", + "name": "乌兰察布市", + "province": "内蒙古自治区" + }, + { + "city_id": "PV5ZAAXFW2DZCVZKCF4I4KK7BQ", + "name": "巴彦淖尔市", + "province": "内蒙古自治区" + }, + { + "city_id": "YU6UUT6G6T6AMWTJFECDIUQFEQ", + "name": "乌海市", + "province": "内蒙古自治区" + }, + { + "city_id": "V4MYANW5QFZCXG3FIDPXA3HOTE", + "name": "呼伦贝尔市", + "province": "内蒙古自治区" + }, + { + "city_id": "S5DCFJWJ7J3MJSLY2PHKWLNPOQ", + "name": "包头市", + "province": "内蒙古自治区" + }, + { + "city_id": "LY7SAZRFSJJMRU3JEO5SKNKIVM", + "name": "兴安盟", + "province": "内蒙古自治区" + }, + { + "city_id": "NM2XP54CNQCFOILKACYEQWUSGM", + "name": "锡林郭勒盟", + "province": "内蒙古自治区" + }, + { + "city_id": "S5G3IO75IDEJPZQA6VFM3OYPDI", + "name": "赤峰市", + "province": "内蒙古自治区" + }, + { + "city_id": "UUFUUPM5RT6ZU5UKILQC5YQV54", + "name": "吴忠市", + "province": "宁夏回族自治区" + }, + { + "city_id": "VMSRLIATK44WQXQEWAL63AXJ3M", + "name": "固原市", + "province": "宁夏回族自治区" + }, + { + "city_id": "4GWWCAAKGNJV2SMQPSWWZNCGYY", + "name": "中卫市", + "province": "宁夏回族自治区" + }, + { + "city_id": "6IE7GEETBQEF7GUSGU2FLIUEEM", + "name": "石嘴山市", + "province": "宁夏回族自治区" + }, + { + "city_id": "VI4YIH3URSON4Q4MWOEESXJ56Q", + "name": "银川市", + "province": "宁夏回族自治区" + }, + { + "city_id": "SIE4ED6QWVRT727GEHWBFH3DAA", + "name": "海东市", + "province": "青海省" + }, + { + "city_id": "JNJH6OJZIOQKXDXWW5ZGEHG5MA", + "name": "海西蒙古族藏族自治州", + "province": "青海省" + }, + { + "city_id": "MJADYNCKQNDJU2TXACTDP5I52M", + "name": "海北藏族自治州", + "province": "青海省" + }, + { + "city_id": "LRGFXIVB6RJWQWYAFH7EIUHCPE", + "name": "黄南藏族自治州", + "province": "青海省" + }, + { + "city_id": "J4TG3PCK2ZEMNEUMIPZF32UNQY", + "name": "果洛藏族自治州", + "province": "青海省" + }, + { + "city_id": "2YS5POGG53LKZGFBIUMDWP57SM", + "name": "玉树藏族自治州", + "province": "青海省" + }, + { + "city_id": "GRZMJEZCA2DNCZK3O6ZSUHMRPM", + "name": "西宁市", + "province": "青海省" + }, + { + "city_id": "NBFQIACRBBCAH5AZWJ5LVT7AU4", + "name": "海南藏族自治州", + "province": "青海省" + }, + { + "city_id": "MQUKCLQ76P4FRRECDBA3HBKT7Q", + "name": "滨州市", + "province": "山东省" + }, + { + "city_id": "633FVSBDDBM5WSMXSKOCX6QC5I", + "name": "潍坊市", + "province": "山东省" + }, + { + "city_id": "4434FVT3PXLMV6UAWLEW6O3M5A", + "name": "菏泽市", + "province": "山东省" + }, + { + "city_id": "P7PK4UBVCOHW3PI6IPEIA54DLY", + "name": "济南市", + "province": "山东省" + }, + { + "city_id": "I5M6JGTGSQEWX6HL7E5I6GRBAY", + "name": "德州市", + "province": "山东省" + }, + { + "city_id": "V562AOMBVU5NG5GB3EPK6U42XY", + "name": "烟台市", + "province": "山东省" + }, + { + "city_id": "4OSPHTE5TD24J6DYGR6DXMEDKY", + "name": "淄博市", + "province": "山东省" + }, + { + "city_id": "227TLAVTUJABWPJD4S4ZECJ3FY", + "name": "临沂市", + "province": "山东省" + }, + { + "city_id": "DAEZKZU32ZAPJGUTA6LLGO3WTY", + "name": "聊城市", + "province": "山东省" + }, + { + "city_id": "LBRRK2EOYJN5MLYJWT4R3QBSXM", + "name": "东营市", + "province": "山东省" + }, + { + "city_id": "AB6PBGCDBTNTG4KUQROY2FJ4GY", + "name": "枣庄市", + "province": "山东省" + }, + { + "city_id": "EVANGU7WZCVRAAM6NWTDJVP7SU", + "name": "济宁市", + "province": "山东省" + }, + { + "city_id": "GNUEGWZ3OKRWAKKVJ5THHHX6YY", + "name": "泰安市", + "province": "山东省" + }, + { + "city_id": "F3VWSF4ART2FYYBBOZYWKRXTUI", + "name": "青岛市", + "province": "山东省" + }, + { + "city_id": "KD6MNWWLVKB4E655XMV6MMA3KE", + "name": "日照市", + "province": "山东省" + }, + { + "city_id": "LHYVF4LBCOZ34G3WNZYUVEIQGA", + "name": "威海市", + "province": "山东省" + }, + { + "city_id": "ENVYDMYGDO3BMDXSAVQYZXLX74", + "name": "阳泉市", + "province": "山西省" + }, + { + "city_id": "UXOUG4UIF7ZRJYCNMQJ3LDN5FY", + "name": "临汾市", + "province": "山西省" + }, + { + "city_id": "4NZPT6Z35BMYACJ2HZGHUWRJ6E", + "name": "吕梁市", + "province": "山西省" + }, + { + "city_id": "KSNXQME2A3VFHCE3DM3SFZKIJQ", + "name": "晋城市", + "province": "山西省" + }, + { + "city_id": "HDOX7WKYSHJKEHET6TUYMVCTMQ", + "name": "太原市", + "province": "山西省" + }, + { + "city_id": "DFJIZVXJGBGBIABPSL3DGMIDIE", + "name": "长治市", + "province": "山西省" + }, + { + "city_id": "5KQYYTJR2EMP653QIALMA6LXXI", + "name": "忻州市", + "province": "山西省" + }, + { + "city_id": "T76EOJA332RIHML7B6LYS5LF4U", + "name": "朔州市", + "province": "山西省" + }, + { + "city_id": "HVX67CKT5TS6GPRDFDYOOLK4PE", + "name": "大同市", + "province": "山西省" + }, + { + "city_id": "S4NGXQJDOH7E4IHDWOH3EK6IIE", + "name": "晋中市", + "province": "山西省" + }, + { + "city_id": "GWDLZXLAWU54FKQ6G3HRQRR7E4", + "name": "运城市", + "province": "山西省" + }, + { + "city_id": "3FFTTN5PPV7MBCE5AGY2NGYOOI", + "name": "安康市", + "province": "陕西省" + }, + { + "city_id": "GACVPL3SWO3ZKH73JMJV6YI4NY", + "name": "延安市", + "province": "陕西省" + }, + { + "city_id": "6KPS7VRMW57P2DAC6OPR4ISHQQ", + "name": "商洛市", + "province": "陕西省" + }, + { + "city_id": "OMMF6XLNDNYWG5TBSNTWO2ZJZ4", + "name": "渭南市", + "province": "陕西省" + }, + { + "city_id": "EALFXGMWYRS6E6TWXQ2K3YHV4M", + "name": "咸阳市", + "province": "陕西省" + }, + { + "city_id": "WFG7U6JNUWDIS5ZZYM4FSM5C64", + "name": "榆林市", + "province": "陕西省" + }, + { + "city_id": "R3VBMYTCF5LVHO35X3MYJQFOOE", + "name": "宝鸡市", + "province": "陕西省" + }, + { + "city_id": "RQOWP7C234IS4RKSHB26IYZ5IU", + "name": "西安市", + "province": "陕西省" + }, + { + "city_id": "K4YU6B4T5GZLRWVHGVCR3576HI", + "name": "铜川市", + "province": "陕西省" + }, + { + "city_id": "VVFVAPLKSCN5KN4Q6RK2GPGUUA", + "name": "汉中市", + "province": "陕西省" + }, + { + "city_id": "2QSF6IG3KMDXWO5VP7FXHMMKXA", + "name": "上海市", + "province": "上海市" + }, + { + "city_id": "X3JCRNIPTCUU6DGOFFJ4MUK37M", + "name": "眉山市", + "province": "四川省" + }, + { + "city_id": "GQ24IZNTZJ3PUB5FDAMDA4W7UI", + "name": "攀枝花市", + "province": "四川省" + }, + { + "city_id": "6B6WT62WHBZRHPQUT7BAD2N6ZI", + "name": "泸州市", + "province": "四川省" + }, + { + "city_id": "HJ35P4KXL442MLIII7PFFWUNAE", + "name": "雅安市", + "province": "四川省" + }, + { + "city_id": "K4A6VSJH2AJYT46LMUSVQZPCCU", + "name": "资阳市", + "province": "四川省" + }, + { + "city_id": "646ZNPATOOM3MHI3LDU6HI4KFI", + "name": "阿坝藏族羌族自治州", + "province": "四川省" + }, + { + "city_id": "MU735ZDBFPXRQDUZ3I35JK3XEU", + "name": "内江市", + "province": "四川省" + }, + { + "city_id": "4WPGGJ63USY77GSRN2PPFCYKPQ", + "name": "广安市", + "province": "四川省" + }, + { + "city_id": "O4FFS4DALDAAKIFAUH4F5V5VS4", + "name": "宜宾市", + "province": "四川省" + }, + { + "city_id": "IRFJVK2KXBE6BZ7CSN4UFCI624", + "name": "绵阳市", + "province": "四川省" + }, + { + "city_id": "NELFD7FEKKUNDJ46VLD55SMDCE", + "name": "甘孜藏族自治州", + "province": "四川省" + }, + { + "city_id": "TKMVEUPZSQCXNRZPBEIK3F45AI", + "name": "遂宁市", + "province": "四川省" + }, + { + "city_id": "VQW7DPB4KTUI65COJBO3NODU24", + "name": "巴中市", + "province": "四川省" + }, + { + "city_id": "STP4ELXTVGQSB572LFRFJRIUUY", + "name": "南充市", + "province": "四川省" + }, + { + "city_id": "6ST5EX2JVXUCLR5GP5VEFSKN5M", + "name": "成都市", + "province": "四川省" + }, + { + "city_id": "UWNFCMW3HYJRALQI2MJH6EM2O4", + "name": "德阳市", + "province": "四川省" + }, + { + "city_id": "J5ZYU7XRV6CHSJAGOPQKS5YXNA", + "name": "达州市", + "province": "四川省" + }, + { + "city_id": "KYJTF5S746T35RFBMR65BLGM6U", + "name": "凉山彝族自治州", + "province": "四川省" + }, + { + "city_id": "RDUXR23XROLB4NGRVDDLXFXDSE", + "name": "乐山市", + "province": "四川省" + }, + { + "city_id": "4TUBIBHMVESJUCGMTUSLJPHXWI", + "name": "广元市", + "province": "四川省" + }, + { + "city_id": "AXQL57AO27NCHYMEOLRHAAKMTA", + "name": "自贡市", + "province": "四川省" + }, + { + "city_id": "4RXX566RZORCXS6HLEX3DIICSM", + "name": "花莲县", + "province": "台湾" + }, + { + "city_id": "BD5Y7SISWSSQGP3HTVPU6TXAH4", + "name": "台东县", + "province": "台湾" + }, + { + "city_id": "I4DNWLECRYOJAZLGYQB7PBBJXQ", + "name": "台中市", + "province": "台湾" + }, + { + "city_id": "GIZQIESFOMAEQSDQKOEQ5RTTPA", + "name": "南投县", + "province": "台湾" + }, + { + "city_id": "MPM6M2C634FAW7KYG3KIHERDTU", + "name": "彰化县", + "province": "台湾" + }, + { + "city_id": "NH2NK6JVOBBYYTK2G53ADWLX4Y", + "name": "苗栗县", + "province": "台湾" + }, + { + "city_id": "DW2Q2R2UEEDNQA7IHBGYV423K4", + "name": "新竹市", + "province": "台湾" + }, + { + "city_id": "FX5AOPFRPHGHHYZB4XNIPXLNNM", + "name": "新北市", + "province": "台湾" + }, + { + "city_id": "UVNZNB6G4M35RV3IGRUN6OXMXE", + "name": "屏东县", + "province": "台湾" + }, + { + "city_id": "MPB3M2YK24ZORO3EDCJ6UDWIGQ", + "name": "基隆市", + "province": "台湾" + }, + { + "city_id": "EBHVITJPDHJEEMMZTM4TD4UVRU", + "name": "台北市", + "province": "台湾" + }, + { + "city_id": "FQS4PZNCTQEX6I5F34Z2AGJUZM", + "name": "高雄市", + "province": "台湾" + }, + { + "city_id": "WVOJ636Q7MGT6RMN6QYQ4SZWIE", + "name": "嘉义市", + "province": "台湾" + }, + { + "city_id": "QRLER4EEMMKYGQLER2RWEQA74E", + "name": "台南市", + "province": "台湾" + }, + { + "city_id": "K2LHF64R2P4OJ7MDHJ6J2NSTPE", + "name": "桃园市", + "province": "台湾" + }, + { + "city_id": "BILG6LJIWUCTXZ6CDPXYAVM6XI", + "name": "澎湖县", + "province": "台湾" + }, + { + "city_id": "3FYRA3O2HUMLIQAAJQCPX2TETE", + "name": "宜兰县", + "province": "台湾" + }, + { + "city_id": "4MW6X22PAPVMHB6SBGF3RYS324", + "name": "天津市", + "province": "天津市" + }, + { + "city_id": "YEYPP4SQOBXU5UCNDN7ORSR6DI", + "name": "拉萨市", + "province": "西藏自治区" + }, + { + "city_id": "UNE6UPENGWQDOWGABDJEAQ2FEY", + "name": "山南市", + "province": "西藏自治区" + }, + { + "city_id": "VDXKN2YCIUPOHBZKKQHPN6HJWE", + "name": "林芝市", + "province": "西藏自治区" + }, + { + "city_id": "EAWIMNI77H72EYSOAYV3M76CB4", + "name": "阿里地区", + "province": "西藏自治区" + }, + { + "city_id": "HCF6UHTXOOKIOA43AIJH3ARKXA", + "name": "昌都市", + "province": "西藏自治区" + }, + { + "city_id": "Y47QI3KJY352QV3VOPXHM2IDWU", + "name": "日喀则市", + "province": "西藏自治区" + }, + { + "city_id": "R4UWJX44GVAA54NFKHT4Y4ZC5A", + "name": "那曲市", + "province": "西藏自治区" + }, + { + "city_id": "2D37GB5XUALJDXONWJIGXV3QXU", + "name": "香港", + "province": "香港特别行政区" + }, + { + "city_id": "PA5W7Z255K3EGYA4LTA7BGLHRA", + "name": "巴音郭楞蒙古自治州", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "RJZOCU5ECQCOLCOCHJH2UNLQJM", + "name": "哈密市", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "RYK6AR3VDQJFXZLX3MHYFV5VAI", + "name": "塔城地区", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "MXUVGU5NPVTNINAKKPNLWUQ54Q", + "name": "博尔塔拉蒙古自治州", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "T5RKMSH5VIGRTHDZOKV2EIKPIM", + "name": "伊犁哈萨克自治州", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "NABMKFZPOUCZMS4TUVJSZ24DNA", + "name": "克孜勒苏柯尔克孜自治州", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "2YQBXWNFYVJX4NU6WXB5II6X34", + "name": "昌吉回族自治州", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "DBKCQCCQU2URQG2EP5ZNPKBETY", + "name": "乌鲁木齐市", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "DON6KYBCR2XJQOJQGOZTYV4RMM", + "name": "阿克苏地区", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "WKG47NEVYII5JISZJN7QSI2BDQ", + "name": "克拉玛依市", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "DWLK6D3OUOXOVQHSEJVTRIDRAI", + "name": "喀什地区", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "SAPKF2PQGJD4UMVJZTC3IZKI64", + "name": "阿勒泰地区", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "ARWSLGG54LGUGN3XMIWW76NW34", + "name": "吐鲁番市", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "VQCWAKL6ADHYFTSVPBGPDFSB2I", + "name": "北屯市", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "ES5A6MOROAG6F2XAQ25TYYPWUE", + "name": "铁门关市", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "36IUY52AESIPF4QEQAR2RTNQYA", + "name": "和田地区", + "province": "新疆维吾尔自治区" + }, + { + "city_id": "Z26KSUL6ULS65ITZNUCRRWBYJM", + "name": "文山壮族苗族自治州", + "province": "云南省" + }, + { + "city_id": "XBBUUATPBD2IUJR47FCKVUXB2I", + "name": "昭通市", + "province": "云南省" + }, + { + "city_id": "TK2LP3JCYYMPTV4OA3WJCH7UQ4", + "name": "怒江傈僳族自治州", + "province": "云南省" + }, + { + "city_id": "ISJ4FESOYKCQ5LXOQO6NL7TQHA", + "name": "曲靖市", + "province": "云南省" + }, + { + "city_id": "ELBUBVI5UMUIEQGIOAHPMESXFA", + "name": "西双版纳傣族自治州", + "province": "云南省" + }, + { + "city_id": "QHOYHEGZM4WSJZPFEU6FCBYIWY", + "name": "玉溪市", + "province": "云南省" + }, + { + "city_id": "4G4SPJ7MVHMYZAWMQ4642PMLVI", + "name": "保山市", + "province": "云南省" + }, + { + "city_id": "HCHRV2LGJ2TJ4X6BWNWI2IMID4", + "name": "普洱市", + "province": "云南省" + }, + { + "city_id": "IS4Q6NASBWHO3RFO7UCIWOXVI4", + "name": "昆明市", + "province": "云南省" + }, + { + "city_id": "TDJZOAZFQUQPYRR5BHOJTG6RWM", + "name": "红河哈尼族彝族自治州", + "province": "云南省" + }, + { + "city_id": "EIYC62RNU4SHQW3RLAMDPTQYTI", + "name": "大理白族自治州", + "province": "云南省" + }, + { + "city_id": "BA3XFPITAYKBUWDKU3QONRHBC4", + "name": "德宏傣族景颇族自治州", + "province": "云南省" + }, + { + "city_id": "6P6FFFO6C5MLNCVRJAJUICSVQI", + "name": "临沧市", + "province": "云南省" + }, + { + "city_id": "OXHMWH2TSIDI7BQ43EHAMXJ6N4", + "name": "丽江市", + "province": "云南省" + }, + { + "city_id": "QQPDT4LBI2K2KMBNXZ6YH7X2FI", + "name": "楚雄彝族自治州", + "province": "云南省" + }, + { + "city_id": "XBG4EJAWCRJL2TDPNJ23PTFYHQ", + "name": "迪庆藏族自治州", + "province": "云南省" + }, + { + "city_id": "DINNCH54AP74TJ62MICEYAZP74", + "name": "宁波市", + "province": "浙江省" + }, + { + "city_id": "XYTSLYGB2ETU6HG7GIXA7X5SOE", + "name": "嘉兴市", + "province": "浙江省" + }, + { + "city_id": "NNAALJZXGAWALR3LGE2V4UZT6U", + "name": "丽水市", + "province": "浙江省" + }, + { + "city_id": "H5UOJ5MQYJ737GS3TXYN2OJHUU", + "name": "杭州市", + "province": "浙江省" + }, + { + "city_id": "HG5VQGOMSCEGNXJXKO6XCNCHMY", + "name": "湖州市", + "province": "浙江省" + }, + { + "city_id": "LJ2SWEPRINTYDH5A2QHRMI5US4", + "name": "衢州市", + "province": "浙江省" + }, + { + "city_id": "TW4RRM62TDA7WWU77FDSLSAXGY", + "name": "台州市", + "province": "浙江省" + }, + { + "city_id": "HBBN247QZ6YUW5ZYSS6D7RVJCA", + "name": "绍兴市", + "province": "浙江省" + }, + { + "city_id": "GVFB23SJZGRPRXXTIKDCAOCDPI", + "name": "金华市", + "province": "浙江省" + }, + { + "city_id": "UEW4ENX7N7IFGFM7FD5SZ5GI7Y", + "name": "舟山市", + "province": "浙江省" + }, + { + "city_id": "HCCXS5DGRJQMYZMRLGVAMUIQEA", + "name": "温州市", + "province": "浙江省" + }, + { + "city_id": "FDGY55I6IHKY76E3MWDBOT2R6Y", + "name": "重庆市", + "province": "重庆市" + } +] \ No newline at end of file diff --git a/scripts/pull_meituan_coupons.py b/scripts/pull_meituan_coupons.py index aaefdc2..3bc2977 100644 --- a/scripts/pull_meituan_coupons.py +++ b/scripts/pull_meituan_coupons.py @@ -1,45 +1,74 @@ -"""美团 CPS 券定时抓取入库(北京试点)。 +"""美团 CPS 券定时抓取入库(全国 359 个地级市)。 -把 3 路券抓进 meituan_coupon 表,供「销量/佣金排序」从库里捞、本地排序,不再实时打美团: +把每个城市的 3 路券抓进 meituan_coupon 表,供「智能推荐 / 销量最高」从库里捞、本地排序, +不再实时打美团: 1. search_waimai : 到家/外卖, 搜「外卖」 翻到尽头 2. search_meishi : 到家/外卖, 搜「美食」 翻到尽头 3. store_supply : 到店, 多业务线供给(到餐+到综+酒店+门票) 翻到尽头 +城市来自 app/integrations/data/meituan_cities.json(美团官方城市字典,359 地级市)。 +实测一个地级市 cityId 已覆盖其下辖县级市(徐州 → 邳州/新沂…),故按地级市抓即可。 + +并发:城市级并发(每城内部仍顺序跑 3 路),主线程串行入库(Session 不跨线程)。 +实测单城全量 ~110s/~2300 条;15 并发抓完全国一轮 ~50–60min,402 占比 ~3% 且退避全消化。 +mentor 要求每 3h 全量一轮,窗口充裕,故默认 12 并发 + 启动错峰,把瞬时峰值与 402 压更低。 + 按 (source, product_view_sign) upsert 存最新态;last_seen 每轮刷新。带文件锁,防止 -上一轮没跑完下一轮又起(本地 5~10min、跨进程 cron 都安全)。 +上一轮没跑完下一轮又起。 用法: - # 单轮(打通验证 / 给 cron 用,线上每 1h 一次) + # 单轮全量(给 cron 用,线上每 3h 一次) python -m scripts.pull_meituan_coupons --once - # 本地循环(默认每 10min 一轮) - python -m scripts.pull_meituan_coupons --loop --interval 600 + # 常驻循环(每 3h 一轮) + python -m scripts.pull_meituan_coupons --loop --interval 10800 + + # 本地测试:只抓前 N 个城市 / 指定城市 + python -m scripts.pull_meituan_coupons --once --limit 3 + python -m scripts.pull_meituan_coupons --once --city-ids OCZOBCJDEXKE7KBN3BD7AYQG2Q + +部署(服务器):推荐 cron 跑 --once(每 3h),避免长驻进程孤儿: + 0 */3 * * * cd /path/to/app-server && .venv/bin/python -m scripts.pull_meituan_coupons --once >> data/etl.log 2>&1 """ from __future__ import annotations import argparse import hashlib +import logging import os import re import sys import tempfile +import threading import time +from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime, timedelta, timezone -# Windows 控制台按 UTF-8 输出中文/¥ +# Windows 控制台按 UTF-8 输出中文/¥;line_buffering=True 让 print 每行即时 flush—— +# 否则 stdout 重定向到 cron/后台日志文件时是块缓冲,要攒到 ~4KB 或进程退出才落盘, +# 常驻(--loop)时几乎看不到每轮进度。 try: - sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined] + sys.stdout.reconfigure(encoding="utf-8", line_buffering=True) # type: ignore[attr-defined] except Exception: # noqa: BLE001 pass -from sqlalchemy import delete, func, select -from sqlalchemy.dialects.postgresql import insert as pg_insert +from sqlalchemy import delete, func, select # noqa: E402 +from sqlalchemy.dialects.postgresql import insert as pg_insert # noqa: E402 -from app.db.session import SessionLocal -from app.integrations.meituan import MeituanCpsError, _call -from app.models.meituan_coupon import MeituanCoupon +from app.db.session import SessionLocal, engine # noqa: E402 +from app.integrations.cities import all_cities # noqa: E402 +from app.integrations.meituan import MeituanCpsError, _call # noqa: E402 +from app.models.meituan_coupon import MeituanCoupon # noqa: E402 + +# dev 默认 create_engine(echo=True) 会逐条打 SQL,本脚本逐城 upsert 会把日志刷爆; +# ETL 不需要 SQL 日志,显式关掉(入库不受影响;线上 prod 本就 echo=False)。 +engine.echo = False + +# 美团调用偶发错误(本机走代理高并发时的 SSL EOF / 上游 code=5 等)会被 meituan._call 的 +# logger.exception 打完整 traceback,并发抓取下单轮可刷数十 KB 日志。ETL 自己用 _STATS 统计 +# 「放弃页数」,无需逐条 traceback,故把美团 logger 压到 CRITICAL(线上直连少见此类错误)。 +logging.getLogger("shagua.meituan").setLevel(logging.CRITICAL) -CITY_BEIJING = "WKV2HMXUEK634WP64CUCUQGM64" QUERY_PATH = "/cps_open/common/api/v1/query_coupon" PAGE_SIZE = 20 MAX_PAGES = 80 # 单路安全上限(搜索 ~52 页、供给 ~70 页) @@ -49,7 +78,12 @@ RETRY = 7 # 无 sudo 部署时 data/ 常属 root,cps 写不了会导致每轮 PermissionError、cron 抓不进数据。 # 需要指定位置时用环境变量 MEITUAN_ETL_LOCK 覆盖。 LOCK_FILE = os.environ.get("MEITUAN_ETL_LOCK") or os.path.join(tempfile.gettempdir(), "meituan_etl.lock") -LOCK_STALE_SEC = 30 * 60 # 锁超过 30min 视为陈旧(进程异常退出残留),自动接管 +# 多城全量一轮 ~50–60min,锁陈旧阈值放宽到 90min,避免把「正在跑的轮次」误判为残留而接管。 +LOCK_STALE_SEC = 90 * 60 + +DEFAULT_CONCURRENCY = 12 # 并发城市数(实测 15 并发 402 占 3% 可退避消化;12 更稳,3h 窗口充裕) +STARTUP_STAGGER = 0.3 # 首批城市启动错峰间隔秒(削平瞬时峰值,实测能压低 402) +PRUNE_FAIL_RATIO_MAX = 0.05 # 失败城占比超此值则本轮跳过 prune(避免大面积抓取失败误删库) SOURCES = [ {"code": "search_waimai", "label": "外卖·搜外卖", "kind": "search", "platform": 1, "keyword": "外卖"}, @@ -58,33 +92,44 @@ SOURCES = [ "platform": 2, "biz_lines": [1, 2, 3, 4]}, ] +# 跨线程统计(并发抓取下汇总请求量 / 402 / 放弃页数,供观测限流) +_STATS_LOCK = threading.Lock() +_STATS = {"req": 0, "r402": 0, "err": 0} + + +def _bump(key: str) -> None: + with _STATS_LOCK: + _STATS[key] += 1 + # ───────────────────────── 美团调用 ───────────────────────── def _call_retry(body: dict) -> dict | None: - """打美团,402/频繁退避重试;其它错误打印并放弃本页。""" + """打美团,402/频繁退避重试;其它错误放弃本页。并发下不逐条 print(避免刷屏),计入 _STATS。""" for a in range(RETRY): + _bump("req") try: return _call(QUERY_PATH, body) except MeituanCpsError as e: msg = str(e) if "402" in msg or "频繁" in msg: + _bump("r402") time.sleep(2.5 * (a + 1)) continue - print(f" [warn] meituan: {msg[:80]}") + _bump("err") return None - except Exception as e: # noqa: BLE001 - print(f" [warn] {type(e).__name__}: {str(e)[:60]}") + except Exception: # noqa: BLE001 time.sleep(2.0 * (a + 1)) + _bump("err") return None -def _pull_search(platform: int, keyword: str) -> list[dict]: +def _pull_search(city_id: str, platform: int, keyword: str) -> list[dict]: rows: list[dict] = [] sid = None pg = 1 while pg <= MAX_PAGES: - body = {"platform": platform, "searchText": keyword, "cityId": CITY_BEIJING, "pageSize": PAGE_SIZE} + body = {"platform": platform, "searchText": keyword, "cityId": city_id, "pageSize": PAGE_SIZE} if sid: body["searchId"] = sid else: @@ -102,14 +147,14 @@ def _pull_search(platform: int, keyword: str) -> list[dict]: return rows -def _pull_supply(platform: int, biz_lines: list[int]) -> list[dict]: +def _pull_supply(city_id: str, platform: int, biz_lines: list[int]) -> list[dict]: rows: list[dict] = [] sid = None biz_param = [{"bizLine": b} for b in biz_lines] for _ in range(MAX_PAGES): body = { "multipleSupplyList": [{"platform": platform, "bizLineParamList": biz_param}], - "cityId": CITY_BEIJING, + "cityId": city_id, "sortField": 2, # 供给查询 sortField 必填;我们入库后本地再排,这里给个默认 "pageSize": PAGE_SIZE, } @@ -157,7 +202,7 @@ def _to_cents(yuan) -> int | None: return None -def _parse_item(item: dict, source: dict) -> dict | None: +def _parse_item(item: dict, source: dict, city_id: str) -> dict | None: cpd = item.get("couponPackDetail") or {} br = item.get("brandInfo") or {} ci = item.get("commissionInfo") or {} @@ -190,7 +235,7 @@ def _parse_item(item: dict, source: dict) -> dict | None: "source": source["code"], "platform": source["platform"], "biz_line": item.get("bizLine") or cpd.get("bizLine"), - "city_id": CITY_BEIJING, + "city_id": city_id, "product_view_sign": str(sign)[:128], "sku_view_id": cpd.get("skuViewId"), "name": (name[:256] or None), @@ -210,6 +255,27 @@ def _parse_item(item: dict, source: dict) -> dict | None: } +def _pull_one_city(city: dict, index: int, concurrency: int, stagger: float) -> list[dict]: + """抓单个城市的 3 路券并解析。worker 线程内执行(只抓取+解析,不碰 DB)。 + + 启动错峰:首批并发的 `concurrency` 个城市按 index 错开首请求,削平瞬时峰值(压低 402)。 + """ + if stagger: + time.sleep((index % concurrency) * stagger) + cid = city["city_id"] + parsed: list[dict] = [] + for src in SOURCES: + if src["kind"] == "search": + items = _pull_search(cid, src["platform"], src["keyword"]) + else: + items = _pull_supply(cid, src["platform"], src["biz_lines"]) + for it in items: + p = _parse_item(it, src, cid) + if p: + parsed.append(p) + return parsed + + # ───────────────────────── 入库(upsert) ───────────────────────── def _upsert(db, rows: list[dict], now: datetime) -> tuple[int, int]: @@ -272,30 +338,53 @@ def _release_lock() -> None: # ───────────────────────── 主流程 ───────────────────────── -def run_once(prune_hours: int = 24) -> None: +def run_once( + prune_hours: int = 24, + concurrency: int = DEFAULT_CONCURRENCY, + stagger: float = STARTUP_STAGGER, + cities: list[dict] | None = None, +) -> None: if not _acquire_lock(): print(f"[{datetime.now():%H:%M:%S}] 上一轮还在跑(锁占用),跳过本轮") return t0 = time.time() now = datetime.now(timezone.utc) + with _STATS_LOCK: + _STATS.update(req=0, r402=0, err=0) + cities = cities if cities is not None else all_cities() + n_city = len(cities) db = SessionLocal() try: - total = 0 - for src in SOURCES: - ts = time.time() - if src["kind"] == "search": - items = _pull_search(src["platform"], src["keyword"]) - else: - items = _pull_supply(src["platform"], src["biz_lines"]) - parsed = [p for p in (_parse_item(it, src) for it in items) if p] - up, dup = _upsert(db, parsed, now) - total += up - print(f" {src['label']:18} 抓{len(items):5} 解析{len(parsed):5} " - f"入库{up:5} (本源去重{dup:3}) {time.time() - ts:4.0f}s") - # 清理长期未再出现的陈旧券(美团 sign 轮换 / 券下架后的残留),默认 24h 宽限。 - # 护栏:仅在本轮确有入库(total>0)时才清理。美团整体故障时本轮可能 0 入库 - # (脚本不抛异常、只是抓回空),若仍照常 prune,连续故障会按 last_seen 把全表删空。 - if prune_hours and prune_hours > 0 and total > 0: + total_up = 0 + done = 0 + fails: list[str] = [] + # 城市级并发抓取(worker 只抓取+解析),主线程逐城串行入库(Session 不跨线程)。 + with ThreadPoolExecutor(max_workers=concurrency) as pool: + futs = { + pool.submit(_pull_one_city, city, i, concurrency, stagger): city + for i, city in enumerate(cities) + } + for fut in as_completed(futs): + city = futs[fut] + try: + parsed = fut.result() + except Exception as e: # noqa: BLE001 + fails.append(city["name"]) + print(f" [fail] {city['name']}: {type(e).__name__}: {str(e)[:60]}") + continue + up, _dup = _upsert(db, parsed, now) + total_up += up + done += 1 + if done % 20 == 0 or done == n_city: + print(f" [{done}/{n_city}] {city['name']:10} 抓{len(parsed):5} 入库{up:5} " + f"(累计入库 {total_up}, 用时 {time.time() - t0:.0f}s)") + + # 清理陈旧券(美团 sign 轮换 / 券下架残留),默认 24h 宽限。两道护栏防误删: + # ① total_up>0:本轮 0 入库(疑似上游整体故障,脚本不抛异常只抓回空)时跳过,否则 + # 会按 last_seen 把全表删空; + # ② 失败城占比 ≤5%:大面积城市抓取失败(限流/网络)时跳过,避免误删还在架上的券。 + fail_ratio = len(fails) / max(1, n_city) + if prune_hours and prune_hours > 0 and total_up > 0 and fail_ratio <= PRUNE_FAIL_RATIO_MAX: cutoff = now - timedelta(hours=prune_hours) pruned = db.execute( delete(MeituanCoupon).where(MeituanCoupon.last_seen < cutoff) @@ -303,36 +392,62 @@ def run_once(prune_hours: int = 24) -> None: db.commit() if pruned: print(f" 清理陈旧券(>{prune_hours}h 未再出现): {pruned} 条") - elif prune_hours and prune_hours > 0 and total == 0: - print(" 本轮 0 入库(疑似上游故障),跳过清理以防误删全表") + elif prune_hours and prune_hours > 0: + reason = "本轮 0 入库" if total_up == 0 else f"失败城占比 {fail_ratio:.0%}>{PRUNE_FAIL_RATIO_MAX:.0%}" + print(f" 跳过 prune({reason},防误删)") + cnt = db.execute(select(func.count()).select_from(MeituanCoupon)).scalar() - print(f"[{datetime.now():%H:%M:%S}] 本轮完成: 入库 {total} 条, 表总计 {cnt} 行, " - f"用时 {time.time() - t0:.0f}s") + with _STATS_LOCK: + req, r402, err = _STATS["req"], _STATS["r402"], _STATS["err"] + fail_tail = (": " + ",".join(fails[:10])) if fails else "" + print(f"[{datetime.now():%H:%M:%S}] 本轮完成: {done}/{n_city} 城, 入库 {total_up} 条, " + f"表总计 {cnt} 行, 请求 {req}(402 {r402} / 放弃 {err}), " + f"失败城 {len(fails)}{fail_tail}, 用时 {time.time() - t0:.0f}s") finally: db.close() _release_lock() +def _select_cities(limit: int, city_ids: str) -> list[dict]: + cities = all_cities() + if city_ids.strip(): + want = {c.strip() for c in city_ids.split(",") if c.strip()} + return [c for c in cities if c["city_id"] in want] + if limit and limit > 0: + return cities[:limit] + return cities + + def main() -> None: - ap = argparse.ArgumentParser(description="美团 CPS 券定时抓取入库") + ap = argparse.ArgumentParser(description="美团 CPS 券定时抓取入库(全国 359 地级市)") ap.add_argument("--once", action="store_true", help="只跑一轮(默认)") ap.add_argument("--loop", action="store_true", help="循环跑") - ap.add_argument("--interval", type=int, default=600, help="循环间隔秒(默认 600=10min)") + ap.add_argument("--interval", type=int, default=10800, + help="循环间隔秒(默认 10800=3h,对齐每 3h 全量一轮)") ap.add_argument("--prune-hours", type=int, default=24, help="清理超过 N 小时未再出现的陈旧券(默认 24;0=不清理)") + ap.add_argument("--concurrency", type=int, default=DEFAULT_CONCURRENCY, + help=f"并发城市数(默认 {DEFAULT_CONCURRENCY};实测 15 并发 402 占 3%% 可退避消化)") + ap.add_argument("--stagger", type=float, default=STARTUP_STAGGER, + help=f"首批城市启动错峰间隔秒(默认 {STARTUP_STAGGER};削平瞬时峰值压低 402)") + ap.add_argument("--limit", type=int, default=0, help="只抓前 N 个城市(测试用,0=全部)") + ap.add_argument("--city-ids", default="", help="只抓这些 cityId(逗号分隔,测试用,优先于 --limit)") args = ap.parse_args() + cities = _select_cities(args.limit, args.city_ids) + print(f"城市数: {len(cities)} / 并发: {args.concurrency} / 错峰: {args.stagger}s / 间隔: {args.interval}s") + if args.loop: print(f"循环模式: 每 {args.interval}s 一轮 (Ctrl-C 退出)") while True: try: - run_once(args.prune_hours) + run_once(args.prune_hours, args.concurrency, args.stagger, cities) except Exception as e: # noqa: BLE001 print(f"[{datetime.now():%H:%M:%S}] 本轮异常: {type(e).__name__}: {e}") _release_lock() time.sleep(args.interval) else: - run_once(args.prune_hours) + run_once(args.prune_hours, args.concurrency, args.stagger, cities) if __name__ == "__main__": diff --git a/tools/gen_meituan_cities.py b/tools/gen_meituan_cities.py new file mode 100644 index 0000000..231fb7e --- /dev/null +++ b/tools/gen_meituan_cities.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""从美团「城市字典」Excel 生成随仓库的 JSON(app/integrations/data/meituan_cities.json)。 + +本地一次性工具:美团每年更新城市字典时,把新 Excel 放进来重跑即可。 +Excel 不入库、不进仓库(二进制、需 openpyxl);生成的 JSON 随仓库提交,部署到服务器后 +ETL(scripts/pull_meituan_coupons.py)直接读它遍历全部城市。 + +字典口径:359 个地级市,经实测一个地级市 cityId 已覆盖其下辖县级市(如徐州→邳州/新沂), +故无需区县层级。 + +用法: + python tools/gen_meituan_cities.py ["城市字典xxx.xlsx" 路径] + (不传则用默认 e:\\codes\\城市字典2025 (1).xlsx) +""" +from __future__ import annotations + +import json +import os +import sys + +import openpyxl + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +DEFAULT_EXCEL = r"e:\codes\城市字典2025 (1).xlsx" +OUT = os.path.join(ROOT, "app", "integrations", "data", "meituan_cities.json") + + +def main() -> None: + excel = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_EXCEL + wb = openpyxl.load_workbook(excel, data_only=True) + ws = wb.worksheets[0] + rows = list(ws.iter_rows(values_only=True)) + header = rows[0] + + cities = [] + seen = set() + for r in rows[1:]: + cid = str(r[0]).strip() if r[0] else "" + name = str(r[1]).strip() if len(r) > 1 and r[1] else "" + prov = str(r[2]).strip() if len(r) > 2 and r[2] else "" + if not cid or cid in seen: + continue + seen.add(cid) + cities.append({"city_id": cid, "name": name, "province": prov}) + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + with open(OUT, "w", encoding="utf-8") as f: + json.dump(cities, f, ensure_ascii=False, indent=1) + + print(f"表头: {header}") + print(f"写出 {len(cities)} 城 → {OUT}") + print("样例:", cities[:3]) + + +if __name__ == "__main__": + main()