资讯详情 小程序地址解析实战:从非结构化文本到省市区结构化字段
📅 2026/10/12 4:03:20
简介这份资源面向微信小程序开发者与电商、物流场景的后端工程师聚焦用户收货地址非标准化带来的处理难题借助腾讯云地址解析API将杂乱文本自动识别为省、市、区等结构化信息。压缩包共18个文件以6个js脚本、5个json配置、3个wxss样式与2个wxml页面为主另有少量系统隐藏文件整体约16KB体积轻巧便于直接导入微信开发者工具运行调试。代码中已实现HMAC-SHA1签名与Base64编码等关键环节并预留确认按钮事件供二次开发读者可据此理解API接入、请求构造、响应解析与异常处理的完整链路。目前已有8239人学习下载适合希望快速掌握地址识别与格式化、提升小程序交互体验的开发者参考借鉴。1. 小程序里粘贴一段地址怎么自动拆出省市区用户在小程序里粘贴一段收货地址格式千奇百怪「张三 13800138000 浙江省杭州市余杭区文一西路 969 号 3 号楼 502」「李四广东深圳南山区科技园南区 8 栋电话 139xxxx」「王五 北京朝阳区望京 SOHO T1 座 2201」。你要的不是把这段文字存下来而是拆成province / city / district / detail / name / phone六个字段再按统一格式回填到表单里。这件事靠正则基本是玄学——「北京」既是省又是市「深圳」不带「市」字「余杭区」前面可能没有「杭州市」。标题里的 AddressParseTest 就是围绕这个场景做的最小验证工程调用腾讯云地址解析 API把一段非结构化中文地址标准化成结构化字段再在小程序侧做校验和回填。适合正在做电商下单页、寄件页、CRM 客户录入的开发者尤其是被「用户乱填地址导致快递退回」折磨过的人。2. 地址解析 API 的输入输出与选型理由2.1 为什么不用正则和本地词库先说结论地址解析这件事本地词库能覆盖 80%剩下 20% 会让你在凌晨两点改代码。原因有三个。第一行政区划是动态的。某地撤县设区、某镇并入街道本地词库半年不更新就开始出错而这类变更每年都有。第二中文地址的省略和倒装太多。「杭州余杭文一西路」省略了「市」和「区」「浙江杭州」省略了「省」用户还会把手机号写在最前面。第三同名地名。全国叫「城关区」的区不止一个叫「西湖区」的也不止一个只靠字符串匹配无法消歧必须结合上下文和行政区划树。腾讯云的地址解析接口本质是一个 NLP 模型加行政区划库输入一段文本输出结构化的省市区街道和置信度。它的价值不在于「能拆」而在于「拆错了会告诉你置信度低」让你有机会让用户二次确认而不是默默存一个错地址。2.2 接口的请求与返回结构常见做法是走 HTTP POST请求体里放一段文本返回里带结构化结果。下面是我一般会先写的最小验证脚本用 Python 跑通再往小程序里搬因为小程序调试网络请求比较麻烦先用脚本确认接口行为能省很多时间。# address_parse_test.py # 用途在本地验证地址解析接口的输入输出确认字段名和置信度含义 import json import hashlib import hmac import time import base64 import urllib.request # 这两个值从控制台获取不要硬编码进小程序前端 SECRET_ID your_secret_id SECRET_KEY your_secret_key ENDPOINT address.tencentcloudapi.com SERVICE address ACTION ParseAddress VERSION 2021-01-01 def sign(key, msg): return hmac.new(key, msg.encode(utf-8), hashlib.sha256).digest() def build_headers(payload): timestamp int(time.time()) date time.strftime(%Y-%m-%d, time.gmtime(timestamp)) # 步骤1拼接规范请求串 hashed_payload hashlib.sha256(payload.encode(utf-8)).hexdigest() canonical ( POST\n/\n\n content-type:application/json; charsetutf-8\n fhost:{ENDPOINT}\n fx-tc-action:{ACTION.lower()}\n f\ncontent-type;host;x-tc-action\n{hashed_payload} ) # 步骤2拼接待签名字符串 credential_scope f{date}/{SERVICE}/tc3_request string_to_sign ( TC3-HMAC-SHA256\n f{timestamp}\n f{credential_scope}\n hashlib.sha256(canonical.encode(utf-8)).hexdigest() ) # 步骤3计算签名 secret_date sign((TC3 SECRET_KEY).encode(utf-8), date) secret_service sign(secret_date, SERVICE) secret_signing sign(secret_service, tc3_request) signature hmac.new( secret_signing, string_to_sign.encode(utf-8), hashlib.sha256 ).hexdigest() # 步骤4拼接 Authorization authorization ( TC3-HMAC-SHA256 fCredential{SECRET_ID}/{credential_scope}, SignedHeaderscontent-type;host;x-tc-action, fSignature{signature} ) return { Authorization: authorization, Content-Type: application/json; charsetutf-8, Host: ENDPOINT, X-TC-Action: ACTION, X-TC-Timestamp: str(timestamp), X-TC-Version: VERSION, } def parse_address(text): payload json.dumps({Text: text}, ensure_asciiFalse) headers build_headers(payload) req urllib.request.Request( fhttps://{ENDPOINT}, datapayload.encode(utf-8), headersheaders, methodPOST ) with urllib.request.urlopen(req, timeout5) as resp: return json.loads(resp.read().decode(utf-8)) if __name__ __main__: result parse_address(张三 13800138000 浙江省杭州市余杭区文一西路969号3号楼502) print(json.dumps(result, ensure_asciiFalse, indent2))这段代码做了四件事构造 TC3 签名、拼请求头、发 POST、打印结果。签名部分是最容易翻车的地方canonical字符串里的换行和分号一个都不能错x-tc-action必须小写SignedHeaders的顺序要和canonical里一致。参数上Text是唯一必填项长度上限一般在 200 字符左右超长要先截断或分段。返回里你会看到Province、City、District、Street、Detail这类字段以及一个Confidence或类似的置信度值。2.3 返回字段怎么映射到小程序表单拿到返回后不要直接回填先做一层映射和兜底。下面这张表是我在项目里固定下来的字段对应关系避免前端各处取值不一致。接口返回字段小程序表单字段兜底策略Provinceprovince为空时取 City 前两位匹配Citycity直辖市时与 Province 相同Districtdistrict为空时留空不猜Streetstreet可为空Detaildetail拼接 Street 后的剩余部分Confidence不展示低于阈值时弹确认框置信度这个字段很关键。我的经验是高于 0.9 直接回填0.6 到 0.9 之间回填但高亮让用户确认低于 0.6 就只填 detail省市区让用户自己选。这样既省了用户的手动选择又不会因为模型猜错导致快递退回。3. 在小程序里跑通解析与回填的完整链路3.1 小程序端不能直接调接口这是第一个必须讲清楚的点小程序的wx.request不能直接携带你的密钥去调云 API密钥一旦打进小程序包等于公开。正确做法是自己搭一层后端中转小程序把地址文本发给你的服务器服务器签名后调云 API再把结构化结果返回给小程序。后端可以用任何你熟悉的语言Node.js 写起来最快。下面是一个最小中转服务的核心逻辑。// server.js —— 小程序地址解析中转Node.js Express const express require(express); const crypto require(crypto); const axios require(axios); const app express(); app.use(express.json()); const SECRET_ID process.env.TC_SECRET_ID; const SECRET_KEY process.env.TC_SECRET_KEY; const ENDPOINT address.tencentcloudapi.com; function sha256Hex(str) { return crypto.createHash(sha256).update(str, utf8).digest(hex); } function hmac(key, msg) { return crypto.createHmac(sha256, key).update(msg, utf8).digest(); } app.post(/api/parse-address, async (req, res) { const text (req.body.text || ).trim(); // 参数校验空文本和超长文本直接拒绝省一次接口调用 if (!text || text.length 200) { return res.status(400).json({ code: 400, msg: 地址长度不合法 }); } const payload JSON.stringify({ Text: text }); const timestamp Math.floor(Date.now() / 1000); const date new Date(timestamp * 1000).toISOString().slice(0, 10); const hashedPayload sha256Hex(payload); const canonical [ POST, /, , content-type:application/json; charsetutf-8, host:${ENDPOINT}, x-tc-action:parseaddress, , content-type;host;x-tc-action, hashedPayload ].join(\n); const scope ${date}/address/tc3_request; const stringToSign [ TC3-HMAC-SHA256, timestamp, scope, sha256Hex(canonical) ].join(\n); const kDate hmac(TC3 SECRET_KEY, date); const kService hmac(kDate, address); const kSigning hmac(kService, tc3_request); const signature crypto.createHmac(sha256, kSigning) .update(stringToSign, utf8).digest(hex); const authorization TC3-HMAC-SHA256 Credential${SECRET_ID}/${scope}, SignedHeaderscontent-type;host;x-tc-action, Signature${signature}; try { const { data } await axios.post(https://${ENDPOINT}, payload, { headers: { Authorization: authorization, Content-Type: application/json; charsetutf-8, Host: ENDPOINT, X-TC-Action: ParseAddress, X-TC-Timestamp: String(timestamp), X-TC-Version: 2021-01-01 }, timeout: 5000 }); // 只把前端需要的字段透出去不要把整个响应原样返回 const r data.Response || {}; res.json({ code: 0, data: { province: r.Province || , city: r.City || , district: r.District || , street: r.Street || , detail: r.Detail || , confidence: r.Confidence || 0 } }); } catch (e) { // 超时和限流要区分对待前端提示不一样 const isTimeout e.code ECONNABORTED; res.status(500).json({ code: isTimeout ? 5001 : 5002, msg: isTimeout ? 解析超时请重试 : 解析服务异常 }); } }); app.listen(3000, () console.log(proxy on 3000));逻辑说明先做长度校验避免无效调用浪费配额签名逻辑和 Python 版一致只是换了语言返回时只透出六个字段避免把云厂商的原始响应结构暴露给前端也方便以后换服务商时前端不用改。参数上timeout设 5 秒地址解析一般 200 毫秒内返回超过 5 秒基本是网络问题重试比等待更划算。3.2 小程序端的调用与回填小程序端要做三件事防抖、加载态、失败兜底。用户粘贴地址后不要每输入一个字就调一次等他停止输入 500 毫秒再调。// pages/address/index.js —— 小程序端调用与回填 Page({ data: { form: { province: , city: , district: , detail: }, parsing: false, needConfirm: false }, onAddressInput(e) { const text e.detail.value; // 防抖清掉上一次定时器500ms 内不再触发 clearTimeout(this._timer); this._timer setTimeout(() this.parseAddress(text), 500); }, async parseAddress(text) { if (!text || text.length 6) return; // 太短不调省配额 this.setData({ parsing: true }); try { const res await wx.request({ url: https://your-domain.com/api/parse-address, method: POST, data: { text }, timeout: 6000 }); const body res.data; if (body.code ! 0) { wx.showToast({ title: body.msg, icon: none }); return; } const d body.data; this.setData({ form: { province: d.province, city: d.city, district: d.district, detail: [d.street, d.detail].filter(Boolean).join() }, // 置信度低于 0.9 时提示用户核对 needConfirm: d.confidence 0.9 }); } catch (err) { wx.showToast({ title: 解析失败请手动填写, icon: none }); } finally { this.setData({ parsing: false }); } } });逻辑说明_timer挂在页面实例上而不是 data 里避免频繁 setData 引起渲染抖动。text.length 6这个阈值是经验值太短的文本解析出来基本都是错的不如不调。needConfirm控制一个提示条让用户核对省市区这是防止错地址的最后一道防线。3.3 省市区选择器与解析结果的协同解析结果回填后用户可能还要改。这时候省市区三级联动选择器要能接住解析结果也要能在用户手动改省之后清空市区。常见做法是用一个region数组存[province, city, district]解析成功就 setData 这个数组用户手动改就更新数组并清空下级。这里有个细节解析接口返回的区县名可能带「区」「县」「市」而你的选择器数据源里可能存的是不带后缀的名字。回填前要做一次归一化匹配匹配不上就只填到市一级让用户自己选。不要强行把「余杭区」塞进一个只认「余杭」的选择器否则会出现选中态错乱。4. 避坑与排查地址解析最容易翻车的五个地方4.1 现象接口返回 200 但字段全空原因通常是Text里混入了不可见字符比如从某些 App 复制地址时带进来的零宽空格或者全角空格。模型看到的是乱码自然解析不出来。解决在服务端入口做一次清洗text.replace(/[\u200B-\u200D\uFEFF]/g, ).replace(/\s/g, )把零宽字符和连续空白都处理掉再送进接口。这个清洗成本极低但能救回不少「明明地址没问题却解析失败」的工单。4.2 现象直辖市解析出省市重复「北京市朝阳区」有时会返回Province: 北京市、City: 北京市前端如果直接拼成「北京市北京市朝阳区」就闹笑话了。解决在映射层加判断当province city时展示和拼接只保留一个。同理district和city同名的情况也要处理。这个逻辑放在服务端做前端只管拿干净数据。4.3 现象置信度不返回或恒为 0不同接口版本的返回字段名可能不一样有的叫Confidence有的嵌在子对象里。如果你按文档写死了取值路径换版本就翻车。解决取值时做多层兜底r.Confidence ?? r.confidence ?? 0并且在日志里记录原始响应方便排查。更重要的是不要把置信度当成绝对真理它只是一个参考最终还是要靠用户确认。4.4 现象小程序请求被域名白名单拦截小程序要求所有请求域名在后台配置开发时用 localhost 能通上线就报「不在以下 request 合法域名列表中」。解决提前把中转服务的域名配到小程序后台的 request 合法域名里并且必须是 HTTPS。开发阶段可以在开发者工具里勾选「不校验合法域名」但上线前一定要配好否则整个功能直接不可用。4.5 现象并发一高就超时或限流地址解析接口有 QPS 限制用户集中下单时可能触发限流表现为大量超时。解决服务端加一层简单的内存缓存key 用地址文本的哈希相同地址短时间内重复解析直接返回缓存。再加一个队列或信号量控制并发超过阈值就让前端稍后重试。缓存过期时间设 10 分钟就够地址不会变得那么快。5. 把解析准确率再往上抬一截的进阶做法前面讲的是跑通这一章讲怎么让它更准。地址解析的准确率不是靠调接口参数调出来的而是靠前后端的配合。第一个技巧是「解析加校验」双通道。解析接口给出省市区后不要直接信拿省市区去你的行政区划表里查一次查不到就降级为只填 detail。这个校验表可以是一份静态 JSON也可以是你数据库里的地区表。多这一步能把「解析出一个不存在的区」这类错误挡在入库之前。第二个技巧是手机号和姓名的分离。地址文本里经常混着「张三 13800138000」解析接口主要处理地址部分姓名和手机号要自己用正则先抽出来。手机号用1[3-9]\d{9}匹配姓名用「地址关键词之前的中文串」粗略提取抽完再把剩余部分送给解析接口。这样解析接口拿到的文本更干净准确率会明显提升。第三个技巧是建立「解析失败样本库」。每次置信度低于 0.6 或者用户手动修改了省市区就把原始文本和修改后的结果记下来。积累几百条之后你会发现失败模式高度集中要么是省略太狠要么是生僻地名。针对这些模式可以在送解析前做一次预处理比如把「余杭」补成「余杭区」把「深」补成「深圳」。这个样本库不需要多复杂一个数据库表加一个定时任务就够了。验证方法上我一般会准备一份 50 条左右的测试集覆盖直辖市、省略省市、含手机号、含姓名、生僻区县、超长地址这几类每次改动解析逻辑就跑一遍看准确率有没有下降。这份测试集比任何文档都管用它是你判断「这次改动值不值得上」的唯一依据。最后说个我自己的习惯任何涉及地址的功能上线前一定要用真实用户可能粘贴的脏数据跑一遍而不是用自己手写的干净地址。我踩过最深的坑就是测试全过、上线就崩因为测试数据太干净了。地址解析这件事脏数据才是常态干净数据才是意外。希望帮到你。本文还有配套的精品资源点击获取