1. 从一份京东手机列表 XML 说起:Python xml 转 json 到底难在哪
如果你手上有一份爬虫产出的 XML,想把它塞进数据管道、丢给前端或者写进 MongoDB,第一反应基本都是「转成 JSON 不就行了」。真动手你会发现,Python 里 xml 转 json 的坑不在「转」这个动作,而在三件事:属性往哪放、文本节点和子节点同名怎么合并、以及转完之后结构对不对。
我拿一份典型的电商列表 XML 举例,结构大概长这样:
<products> <product id="1001" sku="P-1001"> <name>手机A</name> <price currency="CNY">2999.00</price> <tags> <tag>5G</tag> <tag>旗舰</tag> </tags> </product> <product id="1002" sku="P-1002"> <name>手机B</name> <price currency="CNY">1999.00</price> <tags> <tag>性价比</tag> </tags> </product> </products>这份 XML 里有几个典型特征:product有属性id和sku,price既有属性currency又有文本2999.00,tags下面有多个同名tag。用现成库一把梭,常见结果是属性丢了、多个tag只剩最后一个、或者price变成一个奇怪的嵌套对象。
所以这篇不讲「装个库调个函数」就完事,而是把整条链路走通:用xml.etree.ElementTree解析、按字段映射规则转成 dict、json.dumps输出、再用 TaoToken 统一 Key 调接口做一次结构校验。适合正在写数据管道、爬虫后处理、或者做 XML 接口对接的同学。全文代码可直接复制运行,Python 3.8+ 即可。
先说清楚一个前提:XML 和 JSON 不是一一对应的。XML 有属性、有文本、有混合内容、有命名空间,JSON 只有对象、数组、字符串、数字、布尔、null。所谓「转换」本质是你自己定一套映射约定,然后按约定把树拍平。约定不统一,下游就会天天报字段缺失。
我试过直接上xmltodict,小文件很爽,但遇到属性、重复标签、混合文本就开始加各种参数,最后还是得自己写一层。所以下面这套脚本的思路是:解析用标准库,映射规则用配置,输出用json.dumps,校验交给接口。这样每一步都可控、可测、可回滚。
2. TaoToken 前置准备:统一 Key 打通解析与校验链路
转换脚本本身不需要联网,但「校验」这一步需要。为什么要在转换链路里加校验?因为 XML 转 JSON 最容易出的问题不是报错,而是静默错误:字段名拼错、数组退化成对象、数字变成字符串,脚本照样跑完,等你写进数据库才发现下游全乱。这时候用一个模型接口对转换结果做一次结构体检,比人肉盯 JSON 靠谱得多。
TaoToken 在这里的角色是「统一 Key 的模型调用入口」。你不需要为不同模型分别维护 Key,一个 Key 就能在解析、校验、甚至后续的字段语义归一化里复用。对数据管道来说,少一套凭证管理就少一类事故。
前置动作只有三步,都很轻:
第一步,拿到 Key。访问 API Keys 管理页创建:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite创建后复制那串sk-开头的 Key,先存到环境变量,别硬编码进脚本:
export TAOTOKEN_API_KEY="sk-你的key"第二步,确认 Base URL。所有请求走同一个入口:
https://taotoken.net/api注意这个地址不带任何查询参数,是纯 API 根路径。你在代码里拼/v1/chat/completions这类路径时,基于它来拼。
第三步,选一个用于校验的 Model ID。校验任务对模型要求不高,选一个响应快、稳定的即可。Model ID 要写全,比如gpt-4o-mini这种完整标识,不要只写gpt。三件套凑齐就是:Base URL + Key + Model ID,后面所有调用都靠这三个。
如果你更习惯在对话界面里先手动验证一下 Key 是否可用,可以打开模型对话页试一句:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite能正常返回,说明 Key 和网络都没问题,再回到脚本里调。这一步看着多余,但能帮你把「Key 错」和「代码错」两类问题提前分开,排障时省很多时间。
对于长期跑数据管道的场景,如果你后面还要接 Agent 或者做批量编码任务,可以了解下 Coding Plan,它把额度管理做得更集中:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite但就本篇的 xml 转 json 校验来说,一个普通 Key 足够了。别一上来就上重装备,先把链路跑通。
3. 可复制配置:ElementTree 解析 + 字段映射 + json.dumps 输出
这一节是全文核心,给你一套能直接跑的脚本。我把它拆成三块:解析器、映射配置、输出。先看完整代码,再逐段解释。
# xml2json_pipeline.py # Python 3.8+ import json import os import xml.etree.ElementTree as ET # ---------- 字段映射配置 ---------- # 约定: # "@attr" 表示取 XML 属性 # "#text" 表示取文本节点 # 其余 key 表示取同名子节点 FIELD_MAP = { "product": { "id": "@id", "sku": "@sku", "name": "name", "price_value": "price.#text", "price_currency": "price.@currency", "tags": "tags.tag[]", # [] 表示强制为数组 } } def _get_by_path(elem, path): """按 'a.b.@c' / 'a.b.#text' / 'a.b[]' 取值""" force_list = path.endswith("[]") if force_list: path = path[:-2] parts = path.split(".") cur = elem for i, part in enumerate(parts): if part.startswith("@"): return cur.get(part[1:]) if part == "#text": return (cur.text or "").strip() # 普通子节点 nxt = cur.find(part) if nxt is None: return [] if force_list else None cur = nxt if force_list: # 收集所有同名兄弟节点 parent = elem for part in parts[:-1]: parent = parent.find(part) items = parent.findall(parts[-1]) return [(it.text or "").strip() for it in items] return (cur.text or "").strip() if cur.text else None def xml_to_dict(xml_path, record_tag="product"): tree = ET.parse(xml_path) root = tree.getroot() mapping = FIELD_MAP[record_tag] records = [] for node in root.findall(record_tag): item = {} for out_key, path in mapping.items(): item[out_key] = _get_by_path(node, path) records.append(item) return {record_tag + "s": records} def main(): src = "products.xml" dst = "products.json" data = xml_to_dict(src) with open(dst, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"written {dst}, records={len(data['products'])}") if __name__ == "__main__": main()配套的products.xml就用第 1 节那份。运行:
python xml2json_pipeline.py输出products.json:
{ "products": [ { "id": "1001", "sku": "P-1001", "name": "手机A", "price_value": "2999.00", "price_currency": "CNY", "tags": ["5G", "旗舰"] }, { "id": "1002", "sku": "P-1002", "name": "手机B", "price_value": "1999.00", "price_currency": "CNY", "tags": ["性价比"] } ] }几个设计点值得说清楚。第一,属性用@前缀,文本用#text,这是为了在配置里一眼区分来源,避免price这种既有属性又有文本的节点产生歧义。第二,[]后缀强制数组,解决「只有一个 tag 时退化成字符串」的经典问题——下游按数组处理就不会崩。第三,映射配置和解析逻辑分离,换一份 XML 只改FIELD_MAP,不动函数。
如果你更偏好 TOML 管理映射(比如字段多到几十个),可以外置成配置文件:
# field_map.toml [product] id = "@id" sku = "@sku" name = "name" price_value = "price.#text" price_currency = "price.@currency" tags = "tags.tag[]"然后用tomllib(Python 3.11+)读取:
import tomllib with open("field_map.toml", "rb") as f: FIELD_MAP = tomllib.load(f)这样非开发同学也能改字段映射,不用碰 Python。踩过的坑是:TOML 里[]会被当成数组语法,所以tags = "tags.tag[]"必须加引号,否则解析报错。
4. 验证请求:用统一 Key 调接口做 JSON 结构校验
脚本跑完只是「转出来了」,不代表「转对了」。这一步用 TaoToken 的接口对 JSON 做结构校验。思路很简单:把转换结果和一份期望的 schema 描述一起发给模型,让它判断字段是否齐全、类型是否一致、有没有静默错误。
先写校验函数:
import json import os import urllib.request BASE_URL = "https://taotoken.net/api" API_KEY = os.environ["TAOTOKEN_API_KEY"] MODEL_ID = "gpt-4o-mini" def validate_json(json_path, expected_desc): with open(json_path, "r", encoding="utf-8") as f: payload = json.load(f) prompt = ( "你是数据校验助手。下面是一份 JSON 和它的期望结构描述。\n" "请只回答:字段是否齐全、类型是否一致、有无明显静默错误。\n" "期望结构:\n" + expected_desc + "\n" "实际 JSON:\n" + json.dumps(payload, ensure_ascii=False) ) body = { "model": MODEL_ID, "messages": [{"role": "user", "content": prompt}], "temperature": 0, } req = urllib.request.Request( BASE_URL + "/v1/chat/completions", data=json.dumps(body).encode("utf-8"), headers={ "Content-Type": "application/json", "Authorization": "Bearer " + API_KEY, }, method="POST", ) with urllib.request.urlopen(req, timeout=60) as resp: result = json.loads(resp.read().decode("utf-8")) return result["choices"][0]["message"]["content"] if __name__ == "__main__": desc = ( "products 为数组,每项含 id(str)、sku(str)、name(str)、" "price_value(str)、price_currency(str)、tags(str数组)。" ) print(validate_json("products.json", desc))运行:
python validate_json.py正常返回类似:
字段齐全,类型一致。tags 均为数组,price_value 与 price_currency 分离正确,未发现静默错误。这里有几个细节。temperature设 0,让校验结果稳定可复现。Authorization用Bearer前缀,这是标准写法。超时设 60 秒,避免网络抖动直接挂掉。返回里取choices[0].message.content,这是 OpenAI 兼容格式的固定路径。
如果你在对话界面里手动验证,可以把同样的 prompt 贴进去,效果一致。区别只是脚本能进 CI,手动适合一次性排查。
校验通过后,这份 JSON 就可以放心写进 MongoDB 或者丢给下游了。整个链路是:XML 文件 → ElementTree 解析 → 映射配置 → json.dumps → 接口校验 → 入库。每一步都有明确输入输出,出问题能快速定位到是哪一段。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
转换和校验跑起来后,报错基本集中在几类。我按真实遇到的顺序列一下,对照着查。
401 Unauthorized。最常见。原因通常是 Key 没读到、Key 写错、或者Authorization头拼错。先确认环境变量:
echo $TAOTOKEN_API_KEY如果为空,说明export没生效或者换了终端。如果 Key 正确,检查请求头是不是写成了Authorization: sk-xxx,少了Bearer前缀就会 401。还有一种情况是 Key 前后带了空格或换行,复制时容易带上,用strip()处理一下。
local proxy failed / connection refused。这类报错说明请求根本没发出去,卡在本地网络层。先确认 Base URL 拼对了,是https://taotoken.net/api,不要多写或少写路径。然后确认本机没有奇怪的网络配置拦截请求。如果是在容器里跑,检查容器网络是否能出网。这类问题跟 Key 无关,别去反复重建 Key。
reading 'choices' / KeyError: 'choices'。说明请求发出去了,但返回体不是预期的对话格式。常见原因是把错误响应当成了成功响应。先打印完整返回:
print(json.dumps(result, ensure_ascii=False, indent=2))如果里面是{"error": {...}},那就是请求参数有问题,比如 Model ID 写错、messages格式不对。确认 Model ID 是完整标识,messages是数组且每项有role和content。修好参数后choices自然就有了。
OAuth / 认证方式不匹配。如果你之前用的是别的认证体系,可能残留了 OAuth 相关的配置或环境变量,导致请求头被覆盖。检查一下有没有全局的HTTP_PROXY、OPENAI_API_KEY之类的变量干扰。最干净的做法是在脚本里显式指定 Key 和 Base URL,不依赖隐式配置。
字段静默错误。这类不报错,但结果不对。典型表现:tags只有一个元素时变成字符串、price_value变成null、属性丢失。排查方法是拿一份「边界样本」测:只有一个 tag 的记录、没有属性的记录、空文本的记录。如果tags退化成字符串,检查映射里有没有加[]。如果属性丢失,检查路径有没有写@前缀。
编码问题。XML 里有中文时,如果读取没指定encoding="utf-8",可能乱码。ET.parse一般能自动识别声明,但json.dump记得加ensure_ascii=False,否则中文会变成\uXXXX,虽然不影响解析,但可读性差。
把这几类对照一遍,基本能覆盖 90% 的报错。剩下的多半是 XML 本身结构跟映射配置不匹配,打印一下root的标签树就能看出来。
6. 把校验接进管道:统一 Key 的长期用法
链路跑通之后,真正省事的是把它变成可重复的动作。我的做法是把校验封装成一个函数,在数据管道的每个转换节点后调用一次,Key 从环境变量读,Base URL 和 Model ID 写成常量。这样无论是本地调试还是 CI 跑批,行为一致。
如果你后面要接更多模型做字段归一化、语义去重,统一 Key 的价值会更明显:不用为每个模型单独配凭证,换模型只改MODEL_ID一个常量。需要看完整接口说明的话,接入文档在这里:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite控制台可以看调用量和额度:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite一个实用技巧:校验 prompt 里把「期望结构」写成固定模板,别每次手写。可以存成schema.txt,脚本读取后拼接。这样字段变了只改模板,校验逻辑不动。另一个技巧是给校验结果加个简单的关键词判断,比如返回里包含「字段齐全」就认为通过,否则打日志告警,方便接进监控。
最后提醒一句:模型校验是辅助,不是替代。核心字段的类型和必填约束,最好还是在代码里用显式断言兜底,模型负责发现你没想到的边界情况。两者结合,XML 转 JSON 这条链路才算真正稳。