全球各国省市数据JSON:结构设计、清洗校验与前端加载实践
简介全球各国省市数据以单个JSON文件整理而成面向地理信息处理、数据分析、地图应用开发及Web前端数据联动等场景适合需要全球行政区划基础数据的中高级开发者直接取用。包体为1个json文件压缩后大小约82KB结构清晰按国家ID、国家名组织省份列表省份下含城市列表且国、省、市节点均保持唯一ID便于快速检索与关联。已有1595人学习下载口碑较为稳定。数据中无下属城市分类的省份直接以省名为市解析时须注意该约定部分行政区划可能随时间或政策调整建议在实际项目中结合时效性自行验证更新。借助这份数据可快速搭建地区选择联动组件、跨国统计对比、导航定位基础地理层或地理教学示例省去手动整理多源行政数据的繁琐步骤有效提升原型开发与数据实验效率。1. 一份「全球各国省市数据.json」为什么值得认真对待很多系统的地址能力都是从一份静态 JSON 起步的登录注册页的三级联动下拉、下单时的行政区选择、后台按地区筛选报表、大屏地图的省份配色分组背后都在消费同一类数据。实际项目里各页面各做一套地址数据的情况很常见结果就是同一个用户在不同页面看到的省市叫法不一致编码对不上有的页面还停留在行政区划变更前的旧版本。「全球各国省市数据.json」这类文件的价值就是把这种乱象收口成一个单一数据源一份 JSON全局引用按需裁剪谁要用谁从这里取。适合它的场景非常明确——业务需要全球范围的行政区域基础信息但不需要实时地图服务只需要一份稳定、可检索、可更新、能进版本管理的数据资产。要注意的是它不等于实时地图数据不含路网、POI、天气或实时坐标。它的价值来自结构可靠、编码统一、更新可追踪而不是数据本身有多“新”。2. 全球省市数据结构三级嵌套与编号规范先立住再动手拿到一份“全球各国省市数据.json”第一步不是数它有多少条而是确认层级模型。大多数文件按“国家 → 一级行政区 → 城市”三级组织但也有的只到省级有的还带了第四级。层级多不代表好代表的是后续每一次遍历、索引、缓存都要为多余层级付出代价。先看清结构再谈使用是这套数据能不能落地的前提。2.1 三级嵌套与扁平展开两种模型怎么选三级嵌套结构是“国家 → 一级行政区 → 城市”的树形组织方式。它的直接好处是渲染联动下拉时可以直接遍历地图按国家聚合时也省事。但当你需要“给定城市编码快速找到所属国家”时树形结构要逐层向上找索引和缓存都比较别扭。扁平展开结构则是一行一个区域记录每条记录同时携带国家编码、一级行政区编码、城市编码。这样进 SQL、建索引、做字符串检索都很顺手但渲染树形组件前要先分组而且分组后的排序、键值稳定性都要额外处理。实践中我更建议源文件保持嵌套结构。因为不同业务要的层级不一样扁平化操作很容易在裁剪时丢掉层级关系真需要扁平时让消费方自己展开或者在发布脚本里产出一个独立的扁平衍生包。标题里“省市”两个字本身就是树形语义直接上扁平结构反而和习惯不符。{ code: XX, name: 示例国, nameEn: Demo Country, province: [ { code: XX-01, name: 示例省, nameEn: Demo Province, city: [ { code: XX-01001, name: 示例市, nameEn: Demo City } ] } ] }这段示例里的 XX 是占位国家码生产数据建议用 ISO 3166-1 的两位字母码province 和 city 用数组而不是对象一是保留顺序二是避免键名里出现特殊字符。city 层没有继续嵌套因为第四级数据多数业务用不到需要时再扩展即可。城市编码“XX-01001”是手工约定国家码 省序号 三位城市序号这样在同一个国家内不会撞码。2.2 编号规范三种编码方案怎么选才不返工编码是这个数据文件里最不能妥协的部分。结构可以随时改编码一旦定错后面所有对接、对账、缓存都会连锁出问题。第一层国家编码直接用 ISO 3166-1 alpha-2 两位字母码。它短、可读、外部系统普遍支持。个别来源会混入三位数字码或三位字母码导入时必须统一成两位字母码并把原码保留在一个 alias 字段里方便后续对账。第二层一级行政区编码业界常见的写法是“国家码-地区码”比如 XX-01、XX-02 这种形态。它最大的优点是不同国家间不会撞码。单独用数字序号会很容易跟其他国家重单独用名称编码又会引入拼音缩写和英文缩写混用的问题。第三层城市编码最麻烦。国际上很多数据源并没有统一的城市行政区划码常见做法是用州名加城市名做键单一国家内部则可能有成熟的行政区划码体系。做全球统一时折中最稳的方案是“国家码 一级行政区码 序号”也就是 2.1 里“XX-01001”的由来。自增全局 ID 看似省事但自增码不可读、且跨版本不稳定不推荐。还有一个原则编码发布后尽量只增不改。真要改就当作换新码处理旧码保留在 alias 字段里别直接删。线上历史订单、报表分组、用户地址都挂在旧码上删了就是事故。2.3 数据来源与清洗聚合多来源后必做的三件事不做点清洗就直接用全球数据大概率会在三个月后暴雷。常见数据来源包括开放地理数据、统计部门发布的行政区划代码、社区维护的地理数据仓库。多来源合并时必做三件事。第一统一编码。把所有来源的国家码、地区码、城市码按 2.2 的规范对齐。第二去重合并。同一个城市在两个来源里叫法不同、编码不同按名称和坐标双重判断去重。第三补充 aliases 与生效日期。行政区划是随时间变动的源数据里最好带一个生效日期字段哪怕很多消费方不用它也是你判断“哪个版本最新”的依据。下面是一段极简的去重清洗脚本骨架按“国家码 省码 城市码”做唯一键def clean(raw_list): seen set() result [] for item in raw_list: key (item[countryCode], item[provinceCode], item[cityCode]) if key in seen: continue seen.add(key) item[countryCode] item[countryCode].upper() result.append(item) return result代码逻辑很简单先用三元组做唯一键过滤重复项再把国家码统一转成大写避免两位字母码大小写混用。这个脚本只做演示真实清洗还要处理名称归一化、别名合并、行政区划变更后的旧码归档。清洗完的数据单独存一份不要直接覆盖原始文件后面排查问题还要回看原样。3. 字段设计与业务裁剪让一份 JSON 同时喂饱选择器、地图和搜索数据结构定好后下一个问题是字段。全球省市数据的原始文件往往包含大量字段但前端选择器只需要“名字 编码”地图可视化需要“名字 编码 中心点”搜索需要“中文名 英文名 别名”。一份 JSON 要让多种消费方共用字段设计就要有取舍。3.1 一套最小且可扩展的字段集我给这类数据文件建议的最小字段集是code、name、nameEn、localName、level、parentCode、aliases、center、timezone。其中 code、name、level 是必填parentCode 在三层以上结构里推荐必填其余按业务裁剪。字段太多会让文件体积成倍膨胀尤其 aliases 和 timezone 这种字段每个节点都带会显著增加体积。一个务实的办法是公开的 source.json 保留全量字段发布时产出一个精简业务包。裁剪白名单通常保留 code、name、nameEn、level、parentCode、aliases地图业务额外保留 center时区功能需要时再单独拉扩展包。字段类型必填主要用途体积影响codestring是唯一标识、联动、接口传参小namestring是界面展示小nameEnstring否英文展示、国际搜索中localNamestring否本地语言展示中levelnumber是区分国家/省/市极小parentCodestring推荐扁平化、面包屑小aliasesstring[]否搜索别名、旧名较大center[number, number]否地图打点中timezonestring否时区功能较大从表里能看出真正决定体积的是可选字段。裁剪时按“没有它业务会不会挂”来判断比按“这个字段以后可能有用”来判断靠谱得多。3.2 用递归裁剪脚本生成业务包写一个递归裁剪脚本只保留当前业务需要的字段。这样 source.json 保持全量业务包保持精简两边互不污染。// pick-fields.cjs const fs require(fs); const ALLOW new Set([code, name, nameEn, aliases, level, parentCode]); function pick(node) { const out {}; for (const key of ALLOW) { if (node[key] ! undefined) out[key] node[key]; } if (Array.isArray(node.province)) { out.province node.province.map(pick); } if (Array.isArray(node.city)) { out.city node.city.map(pick); } return out; } const source JSON.parse(fs.readFileSync(source.json, utf-8)); const output source.map(pick); fs.writeFileSync(dist/app.min.json, JSON.stringify(output));这段脚本维护成本很低。ALLOW 用白名单而不是黑名单将来 source.json 里新增字段时业务包不会不知不觉变大。递归结构只处理 province 和 city 两个数组属性层级再多时按同样模式扩展即可。写完后对比一下 dist/app.min.json 和 source.json 的体积能直观看到裁剪效果。裁剪后要做一个验证确保没有误删关键字段对比裁剪前后所有 code 的数量。可以手工用 jq 或简单脚本统计数量不一致说明递归有问题。3.3 入库前校验编码唯一性、父子关系与空节点业务包生成后入库前必须过一道校验。常见的脏数据有几种编码重复、省编码不是国家编码的子码、城市没有归属的省、name 为空。这些在业务里不会马上暴露但会在联动、搜索、地图聚合时慢慢显现。// validate.cjs function assert(cond, msg) { if (!cond) throw new Error(msg); } function validateCountry(country) { assert(country.code country.name, 国家节点缺字段: ${country.code}); for (const province of country.province || []) { assert(province.code.startsWith(country.code -), 省份编码不匹配国家: ${province.code}); assert(province.name, 省份缺名称: ${province.code}); for (const city of province.city || []) { assert(city.code.startsWith(province.code), 城市编码不匹配省份: ${city.code}); assert(city.name, 城市缺名称: ${city.code}); } } }遍历所有国家节点执行上述校验任何一个断言失败都会直接抛错并给出具体编码方便定位。注意 province 和 city 都可能缺失所以用了|| []做兜底。这种“宁可启动失败也不放脏数据上线”的态度后面能省掉大量排查工作。4. 前端与 Node 端载入全球省市 JSON三种加载方案与缓存细节数据文件准备好了接下来是怎么把它弄进运行环境。浏览器和 Node 的诉求完全不同浏览器在意首屏体积和缓存Node 在意内存占用和读取效率。同一份 JSON两端的加载方案要分开设计。4.1 浏览器端fetch 动态拉取 vs 打包进 bundle浏览器端最常见的两种方式是 fetch 动态拉取和把 JSON 打进打包产物。fetch 的好处是数据更新不触发前端发版发一个新版 JSON 文件资源地址不变浏览器下次请求就拿到新内容缺点是首次进入页面时多一次网络往返弱网环境下联动下拉会明显变慢。打包进 bundle 的好处是加载时机和页面同步没有额外请求缺点是 JSON 会进产物缓存数据更新必须跟着发版走而且会污染前端包体积。这两个方案没有绝对好坏取决于更新频率。如果选 fetch建议配合浏览器的 HTTP 缓存// 带缓存策略的载入函数 async function loadGeo(url) { const res await fetch(url, { cache: force-cache }); if (!res.ok) throw new Error(Geo load failed: ${res.status}); return res.json(); }这里 cache 选force-cache意思是浏览器有缓存就用缓存不重新校验没有缓存才请求。适合“版本文件名带 hash、内容不可变”的场景。如果你的部署环境会覆盖同名文件建议改成no-cache配合 ETag让服务器决定是否返回新内容。参数本身不复杂关键是和部署方式匹配。4.2 Node 端一次 parse 与按国家分片的选择Node 端直接JSON.parse(fs.readFileSync(...))是最省事的做法但在全球全量数据上内存占用会明显偏高。Node 的 JSON.parse 是一次性把整个文件读进内存再构建对象树服务端如果只是启动时读一次还好频繁重启会放大这个开销。更推荐的做法是在发布阶段就把数据按国家拆成独立文件例如geo/xx.json、geo/yy.json服务端需要哪个国家就读取哪个文件。这样每次解析的数据量可控还能配合缓存变量做进程内的热缓存。// geo-loader.cjs const fs require(fs); const path require(path); const cache new Map(); function loadCountry(code) { if (cache.has(code)) return cache.get(code); const file path.join(__dirname, geo, ${code.toLowerCase()}.json); const data JSON.parse(fs.readFileSync(file, utf-8)); cache.set(code, data); return data; }Map 缓存避免了重复读盘和重复解析。注意 key 用小写统一防止调用方传入 XX 和 xx 造成缓存双份。如果文件更新了要保证进程内缓存失效常见做法是给缓存加版本号或直接用文件名里的版本段作为 key。4.3 运行时过滤语言切换、别名搜索与懒加载运行时过滤要解决两件事一是界面用中文还是英文展示二是搜索时中英文别名都能命中。语言切换不建议在运行时遍历整个树做翻译而是数据层本身就带 name 和 nameEn展示时按当前语言取字段。搜索建议在加载时把 name、nameEn、aliases 拼成一个可检索字符串缓存起来function buildSearchIndex(node) { const parts [node.name, node.nameEn, ...(node.aliases || [])]; return parts.join( ).toLowerCase(); }搜索时对这个字符串做 includes 判断能覆盖绝大多数中英文混搜场景。缺点是每次搜索都是全量遍历数据量大时建议把索引单独存成一个文件或者只在首屏加载常用国家数据。懒加载的顺序一般按业务热度排交易量前几名的国家先加载其他区域等用户展开时再 fetch这样首屏体积基本不受影响。5. 全球省市数据 JSON 的五个典型坑代码混用、缓存过期、排序与空值这套数据本身不难难的是那些埋在对接细节里的坑。下面五条是我见过频率最高的问题每一条都按“现象 → 原因 → 解决”说清楚基本覆盖了从导入到上线的完整链路。5.1 编码混用导致联动错乱现象同一份数据里一部分国家用两位字母码一部分用三位数字码。页面联动时省下拉能正常带出但传给后端后匹配不到对应城市。原因多来源聚合时没有统一编码规范清洗脚本只做了去重没做编码映射。解决导入前做白名单校验把所有国家码统一成两位字母码并保留原码字段。校验可以沿用 3.3 的 assert 思路发现不符合规范的直接报错而不是静默放行。这条成本最低回报最高。5.2 行政区划变更后缓存不更新现象线上地址选择器还在展示一年前已经撤销的行政区。后台的编码映射表却是新版本两边对不上。原因静态 JSON 被直接打进前端资源包文件名没变浏览器和 CDN 命中旧缓存。行政区域数据属于低频变动但不代表不变尤其县区级调整时有发生。解决最稳妥的方式是给产物文件名加内容哈希内容变了文件名就变直接从根源上避开缓存问题。更轻量一点的做法是给数据文件加版本号参数但版本号依赖手动维护容易漏。我现在的习惯是发布脚本自动算哈希不手动管版本。5.3 中文排序翻车码点排序不等于拼音排序现象地址列表按名称排序结果“示例市”排在了“示例省”前面而且顺序看起来完全没有规律。原因JavaScript 的默认Array.sort()按 UTF-16 码点排序这个顺序跟拼音顺序、笔画顺序都无关在中文语境里几乎总是错的。解决用Intl.Collator配合拼音排序规则const collator new Intl.Collator(zh-Hans-u-co-pinyin, { sensitivity: base }); list.sort((a, b) collator.compare(a.name, b.name));如果业务层面更看重稳定性也可以直接按 code 排序因为行政区划码本身就是按区域关系编排的顺序通常比拼音更符合业务预期。5.4 校验不严导致空省、空城市入库现象SQL 导入后部分记录省市字段为空地图打点漂到海里后台报表按地区分组时出现一条空的“未知区域”。原因JSON 里部分节点缺字段导入脚本没有做约束检查数据库表结构又没有非空约束空值一路溜进来。解决入库前跑 3.3 的校验脚本每个节点必须有 name每个省必须有归属国家每个城市必须有归属省。数据库层面给 code、name、parentCode 全部加上 NOT NULL 约束给 code 加唯一索引双保险。5.5 搜索命中差中文名搜不到外文城市现象用户搜中文名能搜到搜英文名或当地语言名称时结果为空。原因数据文件只保留了单一语言的 name 字段没有 nameEn 和 aliases。国际业务场景下这几乎是必然踩的坑。解决在字段设计阶段就加入 nameEn 和 aliases并在搜索索引里把两个字段一起拼进去。aliases 至少包含历史名称和常见外语名。解决完搜索问题后这条数据对地图、对报表也更有价值因为展示时可以用本地语言。6. 进阶技巧用一段发布脚本管理 source.json 与业务产物的关系数据文件维护到后期最怕的不是改数据而是不知道哪些产物是当前线上在用的版本。我现在的习惯是source.json 永远保持全量、原始、不做加工所有业务包都由发布脚本从 source.json 派生出来文件名带内容哈希线上只消费 dist 目录。// release.cjs const fs require(fs); const crypto require(crypto); const source JSON.parse(fs.readFileSync(source.json, utf-8)); function hashText(text) { return crypto.createHash(md5).update(text).digest(hex).slice(0, 8); } function outputWithHash(payload, filename, dir dist) { const text JSON.stringify(payload); const file ${filename}.${hashText(text)}.json; fs.writeFileSync(${dir}/${file}, text); return file; } outputWithHash(source, app-full); // 这里再调用业务裁剪函数输出 app.min 和各国家分片这段脚本的核心价值是把“发布动作”变成“一条命令”。文件名里的 hash 由内容计算而来内容没变 hash 不变内容变了文件名自动变天然解决了 5.2 的缓存坑。每次发布后dist 目录里的文件列表就是当前的版本清单回滚也简单找到上一批 hash 文件重新切过去就行。我的教训是千万不要在 source.json 上做手工修改再另存为业务包。一旦手工介入source 和 product 之间的关系就断了下次改数据时你会搞不清线上跑的是哪个版本。脚本化以后source 是唯一输入dist 是唯一产物出问题先查脚本再查 source排查路径短很多。希望这套从结构设计、字段裁剪到发布脚本的思路能帮你在拿到“全球各国省市数据.json”时少走几步弯路。数据文件看起来是简单的静态资源但它值得你用工程化的方式对待。本文还有配套的精品资源点击获取