ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

省市区三级联动静态JSON数据:基于腾讯地图行政区划

省市区三级联动静态JSON数据:基于腾讯地图行政区划 省市区三级联动这套东西说起来是前端表单里的老面孔了但真正自己动手维护过一份完整数据的人并不多。大部分人的第一反应是找个现成的 npm 包或者直接调用某个地图开放平台的行政区划接口。省事的做法当然没问题可一旦遇到内网部署、离线环境、接口限流、字段不匹配这些状况你就会发现手里有一份自己掌控的 JSON 数据有多重要。这份基于腾讯地图行政区划整理出来的省市区三级 JSON本质上就是把全国的省、市、区县三层关系固化成一个静态文件前端拿到就能直接渲染不依赖任何实时请求也不受任何第三方接口配额的限制。它适合正在做后台管理系统、收货地址表单、门店归属选择、数据大屏地域筛选的开发者也适合初学 Vue 想找个真实数据集练手的朋友。接下来我把这套数据从来源、结构设计、清洗脚本、前端接入到踩过的坑完整地讲一遍。1. 先想清楚这份 JSON 到底要解决什么问题1.1 三级联动的三种数据获取路线对比做地域选择组件市面上能走的路其实就三条各自适用的场景差别很大选错了后面全是麻烦。第一条是实时调用地图开放平台的行政区划接口。用户每选一次省就发一次请求去拿市列表再选市又发一次请求拿区列表。好处是数据永远是最新的平台那边更新了你就跟着更新。坏处也非常明显每次交互都要等网络弱网环境下用户点一下要转圈两秒接口有配额量大一点就得买商业版最关键的是很多项目部署在纯内网压根连不出去。第二条是引入现成的第三方 npm 包。优点是接入快几分钟搞定。缺点是你不知道它的数据是哪一年的有些包最后一次发版还是好几年前行政区划调整之后它并不知道而且包体积往往做得很大包含了大量你用不到的字段打包进业务代码里白白撑大 bundle。第三条就是自己维护一份静态 JSON。数据文件放在项目里或者丢到 CDN、静态资源服务器上前端按需加载。这套方案的核心成本只在前期——你需要一次性把数据整理出来。整理完之后更新周期通常是半年到一年一次工作量极小但换来的是零延迟、零依赖、完全可控。我做过几个后台项目最后都收敛到了这条路线上。提示如果你的项目是面向 C 端、用户量很大、且对行政区划的时效性要求极高比如政务类应用那还是建议走接口方案或者建立自己的数据更新流水线。静态文件方案更适合 B 端后台、内部系统、离线部署这类场景。1.2 为什么选腾讯地图的行政区划作为底表选数据源这件事我试过好几个平台最后偏向腾讯地图原因有几个层面。腾讯位置服务的行政区划数据在层级划分上比较贴近国内实际使用习惯省、市、区县三级的归属关系清晰不像有些数据源会把开发区、高新区这类非标准行政区划混进来导致联动组件里冒出一堆奇怪的选项。它的行政区划编码采用的是国家标准的六位行政区划代码这个编码的好处是稳定——一个区县改名了编码通常不变你后面做数据比对和增量更新会轻松很多。另外它返回的数据里带有经纬度中心点坐标虽然三级联动本身用不上但如果后面要做门店地图打点、地址自动定位这份坐标就是白送的。还有一个很现实的原因腾讯位置服务的行政区划查询接口文档写得比较清楚参数和返回结构规整写脚本抓取的时候不用做太多兼容处理。有些平台返回的 JSON 结构嵌套极深字段命名也缺乏一致性清洗成本会高出一截。需要说明的是接口调用需要一个开发者 key这个 key 在腾讯位置服务官网注册账号后就能免费申请个人开发者有免费的日调用额度用来跑一次全量数据绰绰有余。跑完之后把结果落盘成 JSON后续就不需要再调接口了。注意key 属于个人或企业的账号凭证绑定的是你的账号和配额。不要使用网上流传的、来源不明的共享 key一来随时可能失效导致脚本跑一半断掉二来这类 key 的使用行为和配额归集都不受你控制。申请一个自己的免费 key成本几乎为零。2. 数据结构设计树形嵌套还是扁平表数据抓下来只是原料怎么组织成 JSON 才是真正决定后续好不好用的关键。这一步设计歪了前端写起来会很难受。2.1 两种结构的取舍逻辑最常见的两种组织方式一种是树形嵌套每个省节点下面直接挂一个children数组数组里是市市下面再挂区另一种是扁平数组所有节点平铺在一个大数组里靠parentCode字段维系父子关系。树形结构的优势是前端用起来直观。像 Element Plus 的el-cascader、Ant Design 的Cascader这类级联组件默认就吃children嵌套格式数据丢进去直接就能渲染一行转换代码都不用写。劣势是体积偏大因为每个节点都要重复写一遍键名全国三千多个区县节点光键名重复就占了不少字节另外如果你只想按省拆分文件做懒加载从树里拆分也比从扁平表里拆要麻烦一点。扁平结构的优势是紧凑、灵活。一个区县节点就是一个对象字段少、重复少整体体积能小个两三成按parentCode过滤就能得到任意一级的子节点做按需加载特别顺手要做数据比对、去重、校验也方便直接按 code 建索引就行。劣势是前端需要多做一层转换才能喂给级联组件。我最后的做法是两份都生成一份扁平的全量数据districts.flat.json作为数据源头和更新基准再基于它生成一份树形的districts.tree.json专供级联组件直接消费。两者由同一个脚本产出永远不会不同步。多占的那点体积换来的是前端零转换成本我觉得很值。2.2 我最终采用的字段清单字段设计上我的原则是够用就好但关键的几个一个都不能少。下面是我实际用的字段表。字段名类型说明是否必需codestring六位行政区划代码如110101是namestring短名称如东城区用于展示是fullnamestring完整名称如北京市东城区否parentCodestring父级代码省级为0或是levelnumber层级1 省 / 2 市 / 3 区县是initialstring名称首字母如D用于字母索引否pinyinstring全拼如dongchengqu否lngnumber中心点经度否latnumber中心点纬度否这里有几个点值得展开说一下。code一定要用字符串而不是数字。原因很简单行政区划代码虽然都是数字但它是标识符不是数值用数字类型会丢前导零虽然国内代码首位不为零但统一用字符串是更稳妥的习惯而且在 JS 里做对象键名索引时字符串处理更自然。level字段看起来可有可无实际上非常有用。前端做校验时经常需要判断用户选到第几级了有了这个字段一句node.level 3就搞定不用去数路径长度。做按需加载时也能靠它快速筛出某一层的数据。initial和pinyin属于可选增强。如果你的联动组件只做下拉用不上但如果要做城市列表页那种带右侧字母索引的交互或者要支持拼音搜索这两个字段能省掉你自己写拼音转换库的功夫。拼音数据可以在生成脚本里用现成的拼音库批量转换一次性的事。经纬度字段建议保留但注意精度。腾讯返回的是中心点坐标保留六位小数足够了再多就是浪费字节。实操心得如果你追求极致体积可以把fullname、pinyin、lng、lat全部砍掉只留code、name、parentCode、level四个字段。我实测过只留这四个字段的全量 JSON 大约在 250KB 上下gzip 之后不到 60KB对于绝大多数后台项目完全够用。3. 数据抓取与清洗从接口原始返回变成能用的 JSON这一步是整个流程里技术含量最高的地方也是最容易卡住的地方。3.1 抓取思路一次拉全量落盘不动腾讯位置服务的行政区划接口典型用法是先拿到全国的省级列表再对每个省调一次子级查询拿到市列表再对每个市调一次拿到区县列表。整个过程是典型的三层递归全国 34 个省级单位、约 340 个地级单位加起来大概需要发起三百多次请求。调用的时候有几个参数要注意。父级代码参数是逐级传递的省级那一次通常传一个约定的根值。返回体里一般包含状态码、状态描述和结果数组三部分结果数组里每个元素都带行政区划代码、名称、层级、中心点坐标这些字段。写脚本时第一件事就是校验状态码非成功状态直接中断并打印错误信息不要带着半截数据往下跑。关于频率三百多次请求如果在循环里无脑连发很容易触发平台的频率限制。稳妥的做法是在每次请求之间sleep一个合适的时间间隔比如 100 到 200 毫秒。按 200 毫秒算三百次请求也就一分钟左右完全可接受。另外建议给脚本加上重试机制单次请求失败就重试两到三次避免因为偶发的网络抖动导致整个流程白跑。注意跑脚本之前先把 key 写成环境变量不要硬编码在代码里然后提交到代码仓库。我见过太多人图省事把 key 写在源码里结果仓库一公开或者一离职交接key 就泄露了。用环境变量或者本地的配置文件加上.gitignore都是很基础但很有效的习惯。3.2 清洗脚本的关键逻辑拿到原始数据之后需要做几件事补齐层级、修正父级关系、处理特殊行政区、生成拼音、去重。下面是我用的脚本骨架Python 写的逻辑不复杂但几个处理点都很关键。import json import os import time import requests from pypinyin import lazy_pinyin, Style KEY os.environ.get(TENCENT_MAP_KEY) BASE https://apis.map.qq.com/ws/district/v1 SLEEP 0.2 def fetch_children(parent_code): 按父级代码拉取下一级行政区划失败重试三次 url f{BASE}/getchildren params {id: parent_code, key: KEY} for attempt in range(3): try: resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(status) 0: return data.get(result, [[]])[0] print(f接口返回异常: {data.get(message)}) except Exception as exc: print(f第 {attempt 1} 次请求失败: {exc}) time.sleep(SLEEP * 2) return [] def build_tree(): provinces fetch_children(0) # 拉省级列表 forest [] for prov in provinces: time.sleep(SLEEP) cities fetch_children(str(prov[id])) city_nodes [] for city in cities: time.sleep(SLEEP) districts fetch_children(str(city[id])) city_nodes.append({ code: str(city[id]), name: city[name], parentCode: str(prov[id]), level: 2, lng: city.get(location, {}).get(lng), lat: city.get(location, {}).get(lat), children: [ { code: str(d[id]), name: d[name], parentCode: str(city[id]), level: 3, lng: d.get(location, {}).get(lng), lat: d.get(location, {}).get(lat), } for d in districts ], }) forest.append({ code: str(prov[id]), name: prov[name], parentCode: 0, level: 1, children: city_nodes, }) return forest def flatten(forest): 把树拍平同时补上拼音和首字母 flat [] def walk(nodes): for node in nodes: item {k: v for k, v in node.items() if k ! children} py .join(lazy_pinyin(item[name])) item[pinyin] py item[initial] py[0].upper() if py else flat.append(item) if node.get(children): walk(node[children]) walk(forest) return flat if __name__ __main__: tree build_tree() flat flatten(tree) with open(districts.flat.json, w, encodingutf-8) as f: json.dump(flat, f, ensure_asciiFalse, separators(,, :)) with open(districts.tree.json, w, encodingutf-8) as f: json.dump(tree, f, ensure_asciiFalse, separators(,, :)) print(f省级 {len([n for n in flat if n[level] 1])} 个) print(f市级 {len([n for n in flat if n[level] 2])} 个) print(f区县 {len([n for n in flat if n[level] 3])} 个)脚本里有几个细节值得单独拎出来讲。第一time.sleep(SLEEP)的位置特意放在每次请求之前而不是之后这样即便中途异常退出也不会出现最后一次请求还没发就先 sleep的浪费。第二写文件时用了ensure_asciiFalse。这个参数不加中文全会被转成\uXXXX的转义形式文件体积能翻将近一倍人眼也没法直接读。加上之后是干净的中文出问题时可以肉眼扫一遍。第三separators(,, :)去掉了 JSON 默认的缩进和空格。缩进是给人看的但这份文件是给程序读的去掉缩进能省下不小的体积——三千多个节点的嵌套结构缩进带来的空白字符占比相当可观。第四扁平化的时候顺手算了拼音。lazy_pinyin返回的是拼音数组拼接之后取第一个字符转大写就是首字母。注意有些地名的首个汉字是多音字比如重庆的重自动转换可能给出zhong而不是chong。这类多音字地名数量不多但确实存在如果你的项目对拼音准确性要求高建议维护一份手动修正表在脚本跑完之后做一次覆盖替换。3.3 特殊行政区的处理直辖市、省直辖县级市和港澳台原始数据直接拍平之后你会遇到几类结构上的异常必须单独处理不然前端渲染出来会很怪。第一类是直辖市。北京、上海、天津、重庆这四个在行政层级上是省级下面直接就是区中间没有地级市这一层。如果你按标准三层去渲染用户选了北京市之后第二个下拉框会是空的第三个下拉框才出现区县。这个体验很别扭。常规做法是补一个虚拟的市级节点比如给北京市补一个 code 为110100、名称也叫北京市的虚拟市把北京的所有区挂到它下面。这样三层结构就完整了用户选北京市 → 北京市 → 东城区虽然看起来重复但交互逻辑是统一的。另一个做法是把虚拟市命名为市辖区视觉上更符合实际我个人更偏向这一种但要注意有些用户会疑惑市辖区是什么所以我在前端会加一层映射展示时把市辖区替换成所在省的简称。第二类是省直辖县级行政单位。像河南济源、湖北仙桃潜江天门、海南的部分县级市它们直接归省管中间没有地级市。处理思路和直辖市一样补一个虚拟的市级节点做容器。第三类是港澳台。数据结构和内地不太一样层级划分也有差异。建议单独拎出来处理不要硬套三层的模板实在不行就给它们补一层虚拟层级保证前端下拉框不会出现空选项。这一块的具体数据以你实际拉到的接口返回为准脚本里做好判空处理不要假设字段一定存在。第四类是名称重复和市辖区字样。有些接口返回里会出现名称为市辖区的节点它是行政编码上的一个占位实际展示时应该被过滤掉。另外像城区郊区这种通用名称在全国会重复出现很多次所以联动组件里永远不要用名称做值一定要用code做唯一标识。实操心得清洗完成后一定要跑一次自检——统计每个 level 的节点数和公开的行政区划数量对一下。省一级 34 个左右市一级 330 到 340 个之间区县一级 2800 到 2900 个之间。数量偏差超过几十个基本就是中间某次请求失败被静默吞掉了得重新跑。我在第一次跑脚本的时候就因为这个吃了亏某次请求超时返回了空数组脚本没报错直接继续最后少了十几个市前端用起来才发现某几个省下面全是空的。4. 前端落地Vue 项目里怎么把这份数据接进去数据准备好了接下来就是接入。这块的坑主要在体积和加载策略上。4.1 全量加载还是按需加载先说结论后台系统全量加载完全没问题面向 C 端或者移动端建议按省拆分做按需加载。全量加载的意思是组件初始化时一次性把整份 JSON 拉进来。用import静态引入的话数据会被打进 bundle用动态import()或者fetch请求静态资源的话数据是运行时加载的。我一般推荐后者因为静态引入会让首屏 bundle 明显变大而行政区划数据在页面刚打开时未必用得上。按需加载的意思是把全量数据按省拆成多个文件比如110000.json是北京市的用户选了省之后再去请求对应的文件。这么做的好处是首屏只加载一个省级列表体积几 KB加载极快坏处是每次切换省都要发一次请求如果静态资源服务器响应快这个延迟基本感知不到。拆分的脚本也很简单在上面那个 Python 脚本末尾加一段就行import collections grouped collections.defaultdict(list) for node in flat: if node[level] 1: grouped[node[code]].append(node) else: # 向上找到省级祖先 ... # 简单版本按 parentCode 链回溯 code_map {n[code]: n for n in flat} def root_of(code): node code_map[code] while node[parentCode] ! 0: node code_map[node[parentCode]] return node[code] buckets collections.defaultdict(list) for node in flat: if node[level] 1: buckets[node[code]].append(node) else: buckets[root_of(node[code])].append(node) os.makedirs(dist, exist_okTrue) for prov_code, nodes in buckets.items(): with open(fdist/{prov_code}.json, w, encodingutf-8) as f: json.dump(nodes, f, ensure_asciiFalse, separators(,, :))拆分之后每个省一个文件大的省像广东、四川文件也就二三十 KB小的省几 KB。前端只需要维护一份省级列表三十多条数据剩下的都按需拉。4.2 用 el-cascader 快速搭一个三级联动如果你用的是 Element Plus直接上el-cascader是最省事的树形数据丢进去就能用。template el-cascader v-modelselected :optionsoptions :propscascaderProps placeholder请选择省 / 市 / 区 clearable filterable changehandleChange / /template script setup import { ref, onMounted } from vue; const selected ref([]); const options ref([]); const cascaderProps { value: code, label: name, children: children, emitPath: true, }; onMounted(async () { const res await fetch(/static/districts.tree.json); options.value await res.json(); }); function handleChange(value) { if (!value || value.length 3) return; // value 是 code 数组比如 [110000, 110100, 110101] const [provinceCode, cityCode, districtCode] value; console.log(选中区县代码:, districtCode); } /scriptprops里的value、label、children三个映射一定要配因为我们用的是自定义字段名而不是 el-cascader 默认的value/label/children。配好之后组件就能正确识别层级关系。emitPath设置为true时v-model绑定的是完整路径的代码数组设为false时只返回最后一级的代码。我一般用true因为后端接口经常需要省市区三个代码并存。filterable打开之后用户可以直接输入关键字搜索这个在区县特别多的省份体验提升非常明显。不过要注意filterable默认只匹配label如果你想支持拼音搜索需要自己写filter-method用上我们前面生成的pinyin和initial字段。4.3 三个原生 select 联动的写法有些项目用不了组件库或者设计稿就是三个独立的下拉框那就得手写联动逻辑。核心就一句话上一级的选中值变化时重置并重新计算下一级的选项列表。template div classregion-picker select v-modelprovinceCode changeonProvinceChange option value请选择省份/option option v-forp in provinces :keyp.code :valuep.code {{ p.name }} /option /select select v-modelcityCode :disabled!provinceCode changeonCityChange option value请选择城市/option option v-forc in cities :keyc.code :valuec.code {{ c.name }} /option /select select v-modeldistrictCode :disabled!cityCode option value请选择区县/option option v-ford in districts :keyd.code :valued.code {{ d.name }} /option /select /div /template script setup import { ref, computed } from vue; import flatData from ./districts.flat.json; const provinceCode ref(); const cityCode ref(); const districtCode ref(); const provinces computed(() flatData.filter((n) n.level 1)); const cities computed(() provinceCode.value ? flatData.filter((n) n.level 2 n.parentCode provinceCode.value) : [] ); const districts computed(() cityCode.value ? flatData.filter((n) n.level 3 n.parentCode cityCode.value) : [] ); function onProvinceChange() { cityCode.value ; districtCode.value ; } function onCityChange() { districtCode.value ; } /script这里有个性能上的细节。flatData.filter()每次响应式触发都会全量遍历一遍三千多个元素。三千次遍历在现代浏览器里也就是零点几毫秒实际感知不到。但如果你有强迫症可以在组件初始化时先用一次遍历建好索引把结构改成{ 110000: [...] }这样的映射表后续查询就是 O(1) 的哈希查找。我一般不做这个优化除非数据量再大一个数量级。还有一个必须处理的点切换上级时一定要清空下级和更下级的选中值。不然会出现选了北京市朝阳区然后切到上海市区县那一栏还显示着朝阳区这种诡异状态。上面代码里的两个change处理函数就是干这个的。注意.json文件在 Vue 项目里可以直接import但不同构建工具对 JSON 的处理策略不一样。Vite 默认支持 JSON 导入但如果文件特别大超过几百 KB构建时可能会有性能告警。这种情况建议把 JSON 放到public目录里用fetch运行时加载不要走构建流程。5. 数据校验与更新让这份数据能长期活下去静态数据最大的问题是会过期所以必须有一套校验和更新机制不然两年之后你都不知道这份数据到底还能不能用。5.1 用 JSON Schema 做结构校验每次数据更新之后跑一次结构校验能挡掉绝大多数低级错误。JSON Schema 就是干这个的它用一段 JSON 描述另一段 JSON 应该长什么样。{ $schema: https://json-schema.org/draft/2020-12/schema, type: array, items: { type: object, required: [code, name, parentCode, level], properties: { code: { type: string, pattern: ^[0-9]{6}$ }, name: { type: string, minLength: 1 }, parentCode: { type: string }, level: { type: integer, enum: [1, 2, 3] } } } }校验的要点有三个。code必须是六位纯数字字符串这个正则能挡住编码截断或者类型错误的问题。level限定在 1 到 3 之间能发现层级计算错误。parentCode要求必须存在哪怕是省级也要给一个约定值比如0这样前端做过滤时逻辑统一不用写额外的分支判断。用 Python 的jsonschema库跑一下几行代码的事import json from jsonschema import validate, ValidationError with open(districts.flat.json, encodingutf-8) as f: data json.load(f) with open(schema.json, encodingutf-8) as f: schema json.load(f) try: validate(instancedata, schemaschema) print(结构校验通过) except ValidationError as e: print(校验失败:, e.message) print(出错位置:, e.absolute_path)除了结构校验还建议做一次关系完整性校验遍历所有非省级节点检查它的parentCode能不能在数据里找到对应的节点。找不到的就是孤儿节点说明父级缺失了。这个检查能抓出那种某次请求返回空导致整棵子树断链的问题。顺带说一句如果你做的是后端服务经常需要处理接口返回的 JSON特别是用 JMeter 做压测的时候用 JSON Extractor 提取响应里的字段之后想确认到底取到了什么值一个简单的办法是加一个 Debug Sampler或者把提取到的变量名写进后续请求的 URL 里观察。这类调试技巧和行政区划数据本身关系不大但本质上是同一件事JSON 的处理核心永远是先确认结构再确认值。5.2 版本管理和增量更新怎么做数据更新不用每次全量重跑重发那样动静太大。我的做法是给数据加一个版本号放在单独的文件里比如{ version: 2026.01, generatedAt: 2026-01-15T10:30:0008:00, counts: { province: 34, city: 337, district: 2843 } }version用年月做标识一眼能看出数据的新旧。counts是各级节点数量这个是最实用的健康指标——任何时候发现某个层级的数量突然掉了十几个立刻就知道出问题了。增量更新的话思路是把新旧两份扁平数据都按code建索引做一次集合比对变化类型判断方式处理建议新增节点新数据有、旧数据无直接追加注意父级是否也已存在删除节点旧数据有、新数据无谨慎处理确认不是接口漏返重命名code 相同、name 不同更新 name同时更新拼音归属变更code 相同、parentCode 不同需要同步调整树形结构这里面最容易误判的是删除节点。行政区划真正撤销的情况一年也没几个如果一次比对发现某省下面少了几十个区县八成不是真的撤销了而是抓取时那个请求失败了。所以比对结果里的删除项一定要人工扫一遍再确认。更新频率上我一般是一年跑一次全量或者在有明确行政区划调整消息的时候手动跑一次。日常用不着频繁更新因为行政区划调整的节奏本来就慢。5.3 前端缓存策略静态数据加载之后我建议在localStorage里加一层缓存把版本号一起存进去。每次加载时先请求版本文件版本一致就直接用缓存不一致再拉全量数据。这么做的收益在于用户第二次打开页面时行政区划数据是零请求的页面渲染更快。const VERSION_KEY region-data-version; const DATA_KEY region-data; async function loadRegions() { const meta await fetch(/static/region-meta.json).then((r) r.json()); const cachedVersion localStorage.getItem(VERSION_KEY); if (cachedVersion meta.version) { const cached localStorage.getItem(DATA_KEY); if (cached) return JSON.parse(cached); } const data await fetch(/static/districts.flat.json).then((r) r.json()); localStorage.setItem(VERSION_KEY, meta.version); localStorage.setItem(DATA_KEY, JSON.stringify(data)); return data; }要注意localStorage有容量限制一般 5MB 左右。全量行政区划 JSON 压缩前大概两三百 KB字符串化之后存进去没问题但如果你的字段特别多、还带经纬度和拼音可能会接近 1MB。这种情况要么精简字段要么只用sessionStorage做会话级缓存要么老实用浏览器 HTTP 缓存给静态资源服务器配上合适的Cache-Control头。6. 常见问题排查实录这一节全是我自己踩过的坑按出现频率从高到低排列。6.1 高频问题速查表现象可能原因排查方向某个省下面下拉框是空的抓取时该省请求失败被静默跳过检查节点数量统计重跑脚本直辖市只有两级缺少虚拟市级节点检查清洗脚本里的直辖市补全逻辑页面报 JSON 解析失败文件 BOM 头或编码不对确认文件是 UTF-8 无 BOM数据加载很慢全量文件太大改按省拆分或开启 gzip搜索不到某些地名拼音字段缺失或多音字转错检查拼音生成逻辑加手动修正表切换省份后区县没清空下级选中值未重置在 change 事件里清空下游所有值区县代码出现重复不同市的区县名称相同被当成同一项一定要用 code 做 key不要用 name6.2 几个印象深刻的排查过程编码问题的坑。有一次数据在新同事的机器上跑出来是乱码我这边完全正常。折腾半天发现是他用的编辑器保存文件时默认加了 BOM 头前端的JSON.parse遇到 BOM 直接抛错。解决办法很简单写文件时统一指定encodingutf-8Python 的这个参数不会写 BOM或者在读取端做一次replace(/^\uFEFF/, )兜底。这个坑看起来很低级但换机器、换系统、换编辑器的时候真的很容易复现。多音字地名的坑。重庆的重、厦门的厦、蚌埠的蚌这些地名的拼音自动转换经常出错。如果你的搜索功能依赖拼音用户搜chongqing搜不到重庆体验就很差。我的做法是维护一个几十条的手动修正字典键是地名值是正确拼音在脚本里生成完拼音之后做一次覆盖。这个字典一次写好后面一直能用。名称重复导致选中错乱的坑。早期我用name做option的value结果发现城关区这种名字在好几个市下面都有用户选了兰州的城关区回显的时候变成了拉萨的城关区。改回用code做 value 之后问题消失。这个教训很朴素但很值钱行政区划的唯一标识永远是代码不是名称。体积优化前后的对比。我最早生成的全量 JSON 带缩进、带中文转义、带全量经纬度和拼音文件大小接近 900KBgzip 之后还有 200 多 KB移动端加载肉眼可见地慢。后来做了三件事去掉缩进、ensure_asciiFalse、砍掉用不上的字段最终压到 260KB 左右gzip 后不到 60KB。优化前后差了十几倍这就是细节的威力。接口限流的坑。有一段时间脚本跑着跑着开始大面积失败打印出来的错误信息提示请求过于频繁。原因是我把sleep设得太短同时本地网络又跑了别的下载任务导致部分请求被限流。后来的做法是把间隔设成 200 毫秒并且加了连续失败三次就整体暂停三十秒再继续的退避逻辑之后就再没出过问题。6.3 几个值得记住的实操原则第一任何一次数据生成之后先跑数量统计再上线。省、市、区县三级各多少个这个数字扫一眼就知道数据完不完整比任何一个自动化测试都快。第二原始数据一定要存一份。别只存清洗后的结果把接口返回的原始 JSON 也归档下来加上日期。后面如果需要调整清洗逻辑不用重新调接口本地就能重跑省事也更稳。第三不要迷信自动化。增量更新比对出来的结果尤其是删除和归属变更这两类一定要人工看一遍。行政区划数据一年变动也就那么几次花十分钟人工过一遍比出事后再回滚要划算得多。第四版本号和数据一起走。数据文件更新了但版本号忘了改缓存策略就完全失效用户可能拿着去年的数据在跑。把版本号写进生成脚本自动输出别靠手工维护。最后分享一个小技巧。如果你想让这份数据既能在前端用又能在后端用比如后端也要做地址校验或者地域统计那建议在生成脚本里顺手多输出一份用制表符或者竖线分隔的 CSV。CSV 在某些数据分析场景下的可读性比 JSON 好得多直接丢进表格软件就能看也方便非技术同事确认数据有没有问题。我现在的流程里JSON 给程序读CSV 给人看两边用同一份源数据生成永远不会对不上。
返回列表