首页 / 资讯中心 / 文章详情

Python JSONDecodeError 报错解析:从 Expecting ‘,‘ delimiter 到防御性编程

Python JSONDecodeError 报错解析:从 Expecting ‘,‘ delimiter 到防御性编程 ★ FEATURED ARTICLE
1. 从一个让人抓狂的报错说起json.decoder.JSONDecodeError: Expecting , delimiter: line 1 column 1780 (char 17——这个报错信息我敢打赌只要写过 Python 处理 JSON 的人十有八九都见过。它不像FileNotFoundError那样直白也不像KeyError那样容易定位它给你一个行号、一个列号、一个字符偏移量然后就让你自己去猜到底哪里少了个逗号哪里多了个引号我第一次遇到这个报错的时候盯着那个char 17看了半天心想第 17 个字符能有什么问题后来才搞明白char 17指的是从文件开头算起的第 17 个字符位置而line 1 column 1780才是真正出问题的位置。这两个数字经常让人混淆尤其是当 JSON 文件被压缩成一行的时候行号永远是 1列号才是你真正需要关注的东西。这篇文章就是围绕这个报错展开的。我会把 JSON 解析报错的常见原因、排查思路、修复方法、以及如何从根源上避免这类问题全部拆开来讲清楚。不管你是刚接触 Python 的新手还是已经写过不少项目的老手只要你的工作涉及读取 JSON 配置文件、调用 API 返回的 JSON 数据、或者处理别人给你的 JSON 文件这篇文章里的内容你都用得上。核心关键词就一个JSONDecodeError。围绕它我会讲清楚三件事——为什么会出现这个报错、怎么快速定位和修复、以及怎么在代码层面做好防御。这三件事搞定了以后遇到类似的报错你就不用再靠运气去猜了。2. JSONDecodeError 到底在说什么2.1 报错信息的结构拆解先把这个报错信息拆开看。json.decoder.JSONDecodeError是 Python 标准库json模块在解析 JSON 字符串时抛出的异常。它的完整格式通常是这样的json.decoder.JSONDecodeError: 具体原因: line 行号 column 列号 (char 字符偏移量)拿标题里的报错举例具体原因Expecting , delimiter——解析器在某个位置期望看到一个逗号但实际看到的不是逗号。行号line 1——问题出现在第 1 行。列号column 1780——问题出现在第 1780 列。字符偏移量char 17——这个数字在不同 Python 版本中含义略有差异有些版本显示的是从行首算起的偏移有些版本显示的是从文件开头算起的偏移。不要过度依赖这个数字以行号和列号为准。Expecting , delimiter这个原因说明解析器已经成功解析了一个键值对按照 JSON 语法接下来应该是一个逗号表示还有下一个键值对或者一个右花括号表示对象结束。但它遇到的是别的东西所以报错了。2.2 JSON 语法中逗号的角色JSON 的对象结构是这样的{ key1: value1, key2: value2, key3: value3 }每个键值对之间用逗号分隔最后一个键值对后面不能有逗号。这是 JSON 和 JavaScript 对象字面量的一个重要区别——JavaScript 允许尾逗号JSON 不允许。解析器的工作方式是逐个字符扫描。当它解析完value1之后它期望下一个非空白字符是,或者}。如果遇到的是其他字符就会抛出Expecting , delimiter。2.3 为什么列号指向的位置看起来没问题这是最让人困惑的地方。报错说column 1780你打开文件跳到第 1780 个字符发现那里明明有个逗号啊这种情况通常有几个原因原因一列号是从 0 开始算的。JSON 解析器内部使用 0-based 索引所以column 1780实际上指的是第 1781 个字符。差一个位置可能就错过了真正的问题点。原因二问题出在前一个字符。解析器报错的位置是它期望看到逗号但没看到的位置但真正的问题可能是前一个值没有正确结束。比如字符串少了一个闭合引号解析器会把后面的内容都当成字符串的一部分直到遇到下一个引号才结束然后发现后面没有逗号就报错了。原因三不可见字符作祟。从某些系统复制粘贴的 JSON 可能包含不可见字符比如零宽空格、BOM 头、或者非标准的换行符。这些字符在编辑器里看不出来但解析器会遇到它们。2.4 这个报错和其他 JSON 报错的区分JSONDecodeError有很多种具体原因Expecting , delimiter只是其中之一。常见的还有报错原因含义常见场景Expecting , delimiter期望逗号但没找到缺少逗号、字符串未闭合、多余字符Expecting : delimiter期望冒号但没找到键值对之间缺少冒号Expecting property name enclosed in double quotes期望双引号包裹的键名使用了单引号、键名未加引号Expecting value期望一个值值为空、有多余逗号Unterminated string starting at字符串未终止缺少闭合引号Invalid control character无效的控制字符字符串中包含未转义的换行符Extra data多余的数据JSON 后面还有其他内容搞清楚具体是哪种原因排查方向就明确了。Expecting , delimiter的核心排查方向就是找到解析器认为上一个值已经结束的位置检查它后面为什么没有逗号。3. 五大常见原因与逐一定位方法3.1 原因一真的少了一个逗号这是最直接的原因。JSON 文件里两个键值对之间忘记加逗号了。{ name: test version: 1.0 }上面这个 JSON 中test后面缺少逗号解析器解析完name: test之后期望看到逗号或右花括号结果看到了version的开引号于是报错。定位方法根据报错的行号和列号往前找最近的或}或]检查它后面是否紧跟逗号。注意如果 JSON 是压缩成一行的列号会很大这时候建议先用格式化工具把 JSON 展开再定位。修复方法在缺失的位置补上逗号。但要注意如果缺失逗号的位置是最后一个键值对之后那就不是补逗号而是删掉多余的逗号。3.2 原因二字符串内部有未转义的特殊字符JSON 字符串中如果包含双引号、反斜杠、换行符等特殊字符必须转义。如果没转义解析器会提前结束字符串导致后续解析错位。{ path: C:\Users\test\Documents, desc: He said hello to me }上面这个 JSON 有两个问题\U和\D不是合法的转义序列虽然某些解析器会宽容处理hello中的双引号没有转义导致字符串提前结束。定位方法报错位置附近如果有字符串检查字符串内部是否包含未转义的、\、换行符、制表符等。特别注意从 Windows 系统复制的路径反斜杠需要写成\\。修复方法把特殊字符转义。双引号写成\反斜杠写成\\换行符写成\n。如果是用代码生成 JSON用json.dumps()而不是手动拼接字符串。3.3 原因三不可见字符混入从网页复制、从 PDF 提取、或者从某些系统导出的 JSON 文件可能包含不可见字符。最常见的是 BOM 头\ufeff和零宽空格\u200b。BOM 头通常出现在文件开头会导致Expecting value而不是Expecting , delimiter。但零宽空格可能出现在任何位置包括逗号附近导致解析器看不到逗号。定位方法用十六进制编辑器打开文件或者用 Python 读取文件后打印每个字符的 Unicode 码点with open(data.json, r, encodingutf-8) as f: content f.read() for i, ch in enumerate(content): if ord(ch) 127 and not ch.isprintable(): print(f位置 {i}: U{ord(ch):04X})修复方法读取文件后先清洗不可见字符import re with open(data.json, r, encodingutf-8-sig) as f: content f.read() # 移除零宽字符和BOM content re.sub(r[\u200b\u200c\u200d\ufeff], , content) data json.loads(content)注意encodingutf-8-sig会自动处理文件开头的 BOM 头。3.4 原因四JSON 被截断或不完整网络请求返回的 JSON 可能因为超时、缓冲区大小限制等原因被截断。文件写入过程中如果程序崩溃也可能留下不完整的 JSON。{ data: [ {id: 1, name: test1}, {id: 2, name: test2}, {id: 3, name: tes上面这个 JSON 在最后一个字符串中间被截断了。解析器会一直读到文件末尾发现字符串没有闭合报错位置可能在截断点附近。定位方法检查文件大小是否和预期一致检查 JSON 末尾是否完整应该以}或]结尾。如果是网络请求打印response.text的最后 200 个字符看看。修复方法重新获取完整的 JSON。如果是网络请求增加超时时间、检查分页逻辑、确认响应没有被中间层修改。3.5 原因五编码问题导致的字符错位JSON 标准要求使用 UTF-8 编码但实际项目中可能遇到 GBK、Latin-1 等编码的文件。如果用错误的编码读取中文字符会变成乱码乱码中可能包含引号、逗号等特殊字符导致解析错位。定位方法用chardet库检测文件编码import chardet with open(data.json, rb) as f: raw f.read() result chardet.detect(raw) print(result) # {encoding: utf-8, confidence: 0.99}修复方法用正确的编码读取文件with open(data.json, r, encodinggbk) as f: data json.load(f)如果文件编码不确定可以先用二进制模式读取再尝试不同编码解码with open(data.json, rb) as f: raw f.read() for enc in [utf-8, gbk, latin-1]: try: content raw.decode(enc) data json.loads(content) print(f成功使用 {enc} 解码) break except (UnicodeDecodeError, json.JSONDecodeError): continue4. 手把手排查实战从报错到修复4.1 第一步格式化 JSON 让问题可见拿到报错后第一件事不是盯着原始文件看而是把 JSON 格式化。压缩成一行的 JSON 列号动辄几千根本没法定位。格式化之后每个键值对占一行问题一目了然。如果你能打开文件用编辑器的格式化功能VS Code 的Format Document、Sublime 的Pretty JSON插件即可。如果文件太大打不开用 Python 脚本格式化import json with open(data.json, r, encodingutf-8) as f: content f.read() try: data json.loads(content) # 格式化输出 formatted json.dumps(data, indent2, ensure_asciiFalse) with open(data_formatted.json, w, encodingutf-8) as f: f.write(formatted) print(格式化成功) except json.JSONDecodeError as e: print(f解析失败: {e}) print(f错误位置附近的内容:) print(content[max(0, e.pos - 100):e.pos 100])注意e.pos属性它给出了解析器认为出错的位置。打印这个位置前后各 100 个字符通常能直接看到问题。4.2 第二步用二分法缩小范围如果 JSON 文件很大几 MB 甚至几十 MB格式化后还是很难定位可以用二分法。把 JSON 从中间切开分别尝试解析两半看哪一半报错。不断对半切直到定位到具体的问题行。import json def find_error(content, start0, endNone): if end is None: end len(content) if end - start 200: print(f问题区域: {content[start:end]}) return mid (start end) // 2 # 找到mid附近的换行符避免切断字符串 left content.rfind(\n, start, mid) right content.find(\n, mid, end) if left -1: left start if right -1: right end # 尝试解析前半部分 try: json.loads(content[start:left] }) # 前半部分没问题问题在后半部分 find_error(content, left, end) except json.JSONDecodeError: find_error(content, start, left) with open(data.json, r, encodingutf-8) as f: content f.read() find_error(content)这个方法的思路是如果前半部分能解析成功补上闭合符号后说明问题在后半部分否则问题在前半部分。不断缩小范围最终定位到具体位置。4.3 第三步逐字符检查问题区域定位到问题区域后逐字符检查。重点看这几个东西逗号前后是否有异常字符字符串的引号是否配对是否有未转义的特殊字符括号是否配对import json with open(data.json, r, encodingutf-8) as f: content f.read() try: json.loads(content) except json.JSONDecodeError as e: pos e.pos print(f错误位置: {pos}) print(f错误原因: {e.msg}) print(f上下文:) print(repr(content[max(0, pos-50):pos50])) # 逐字符打印问题区域 for i in range(max(0, pos-20), min(len(content), pos20)): ch content[i] print(f{i}: {repr(ch)} (U{ord(ch):04X}))用repr()打印可以显示不可见字符用UXXXX格式打印 Unicode 码点可以识别特殊字符。4.4 第四步修复并验证找到问题后修复它。修复方式取决于具体原因缺少逗号补上逗号多余逗号删掉逗号未转义字符转义它不可见字符删除它编码问题用正确编码读取修复后重新解析验证import json with open(data_fixed.json, r, encodingutf-8) as f: try: data json.load(f) print(解析成功) print(f数据类型: {type(data)}) if isinstance(data, dict): print(f键数量: {len(data)}) elif isinstance(data, list): print(f元素数量: {len(data)}) except json.JSONDecodeError as e: print(f仍然报错: {e})4.5 一个完整的排查脚本把上面的步骤整合成一个脚本以后遇到类似问题直接跑import json import sys def diagnose_json(filepath): print(f诊断文件: {filepath}) # 读取文件 with open(filepath, rb) as f: raw f.read() print(f文件大小: {len(raw)} 字节) # 检测编码 encodings [utf-8, utf-8-sig, gbk, latin-1] content None used_encoding None for enc in encodings: try: content raw.decode(enc) used_encoding enc print(f使用编码: {enc}) break except UnicodeDecodeError: continue if content is None: print(无法解码文件) return # 检查不可见字符 invisible [] for i, ch in enumerate(content): if ord(ch) 127 and not ch.isprintable(): invisible.append((i, ch)) if invisible: print(f发现 {len(invisible)} 个不可见字符:) for pos, ch in invisible[:10]: print(f 位置 {pos}: U{ord(ch):04X}) # 尝试解析 try: data json.loads(content) print(JSON 解析成功) return data except json.JSONDecodeError as e: print(f解析失败: {e.msg}) print(f行号: {e.lineno}, 列号: {e.colno}, 位置: {e.pos}) # 打印问题区域 start max(0, e.pos - 100) end min(len(content), e.pos 100) print(f\n问题区域 (位置 {start} 到 {end}):) print(content[start:end]) # 逐字符分析 print(f\n问题位置附近字符:) for i in range(max(0, e.pos - 10), min(len(content), e.pos 10)): marker -- 错误位置 if i e.pos else print(f {i}: {repr(content[i])}{marker}) return None if __name__ __main__: diagnose_json(sys.argv[1] if len(sys.argv) 1 else data.json)这个脚本会自动检测编码、查找不可见字符、定位错误位置、打印问题区域基本覆盖了大部分排查场景。5. 从根源上避免 JSON 解析报错5.1 生成 JSON 时用 json.dumps 而不是手动拼接手动拼接 JSON 字符串是万恶之源。字符串里的引号、反斜杠、换行符都需要转义手动处理很容易出错。# 错误做法手动拼接 data { name: test, path: C:\\Users\\test, desc: He said \hello\ } json_str {name: data[name] , path: data[path] } # 这样拼接出来的 JSON 很可能有问题 # 正确做法用 json.dumps json_str json.dumps(data, ensure_asciiFalse)json.dumps()会自动处理所有转义保证生成的 JSON 合法。ensure_asciiFalse让中文正常显示而不是转成\uXXXX形式。5.2 读取 JSON 时做好异常处理不要假设 JSON 文件一定合法。网络请求可能返回 HTML 错误页文件可能被截断用户可能上传了错误格式的文件。做好异常处理给出有意义的错误信息。import json def load_json_safe(filepath): try: with open(filepath, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: print(f文件不存在: {filepath}) return None except json.JSONDecodeError as e: print(fJSON 格式错误: {e.msg}) print(f位置: 第 {e.lineno} 行, 第 {e.colno} 列) # 打印问题区域帮助定位 with open(filepath, r, encodingutf-8) as f: content f.read() start max(0, e.pos - 50) end min(len(content), e.pos 50) print(f问题区域: ...{content[start:end]}...) return None except UnicodeDecodeError: print(f文件编码错误请确认文件编码) return None5.3 网络请求返回的 JSON 要先检查再解析调用 API 时返回的不一定是 JSON。可能是 HTML 错误页、可能是空响应、可能是被截断的数据。先检查再解析。import requests import json def fetch_json(url): response requests.get(url, timeout10) # 检查状态码 if response.status_code ! 200: print(f请求失败: {response.status_code}) return None # 检查 Content-Type content_type response.headers.get(Content-Type, ) if application/json not in content_type: print(f返回的不是 JSON: {content_type}) print(f响应内容前200字符: {response.text[:200]}) return None # 检查响应内容是否为空 if not response.text.strip(): print(响应内容为空) return None # 尝试解析 try: return response.json() except json.JSONDecodeError as e: print(fJSON 解析失败: {e.msg}) print(f响应内容前500字符: {response.text[:500]}) return None5.4 大文件用流式解析如果 JSON 文件很大几百 MB 以上用json.load()一次性加载可能会内存不足。这时候可以用ijson库做流式解析。import ijson def parse_large_json(filepath): with open(filepath, rb) as f: # 逐个解析数组元素 parser ijson.items(f, item) for item in parser: # 处理每个元素 process_item(item)ijson不会一次性加载整个文件而是逐个解析元素内存占用小。但它的 API 和标准json模块不同需要适应。5.5 用 JSON Schema 做校验如果 JSON 有固定的结构可以用jsonschema库做校验。这样不仅检查语法还检查数据结构是否符合预期。import json from jsonschema import validate, ValidationError schema { type: object, properties: { name: {type: string}, version: {type: string}, dependencies: { type: object, additionalProperties: {type: string} } }, required: [name, version] } with open(package.json, r, encodingutf-8) as f: data json.load(f) try: validate(instancedata, schemaschema) print(校验通过) except ValidationError as e: print(f校验失败: {e.message}) print(f失败路径: {e.json_path})6. 常见问题速查与避坑经验6.1 常见问题速查表报错信息最可能的原因快速修复Expecting , delimiter缺少逗号、字符串未闭合、不可见字符检查报错位置前一个值的结尾Expecting : delimiter键值对之间缺少冒号检查键名后面是否有冒号Expecting property name enclosed in double quotes键名用了单引号或没加引号键名必须用双引号Expecting value值为空、有多余逗号、BOM头检查值是否缺失文件开头是否有BOMUnterminated string starting at字符串缺少闭合引号检查字符串引号是否配对Invalid control character字符串中有未转义的换行符换行符写成\nExtra dataJSON 后面还有其他内容检查是否有多个 JSON 对象拼接Invalid \escape无效的转义序列检查反斜杠转义是否正确6.2 避坑经验一不要用记事本编辑 JSONWindows 记事本会在文件开头添加 BOM 头导致json.load()报错。用 VS Code、Sublime、Notepad 等编辑器保存时选择 UTF-8 无 BOM 格式。如果已经用了记事本读取时用encodingutf-8-sig可以自动跳过 BOM 头with open(data.json, r, encodingutf-8-sig) as f: data json.load(f)6.3 避坑经验二注意 JSON 和 Python 字典的区别JSON 的true、false、null在 Python 中对应True、False、None。反过来Python 的True、False、None不是合法的 JSON 值。# 错误Python 的 True 不是合法 JSON json_str {flag: True} # 解析报错 # 正确JSON 用 true json_str {flag: true} # 解析成功 # 用 json.dumps 会自动转换 data {flag: True} json_str json.dumps(data) # {flag: true}6.4 避坑经验三JSON 不支持注释JSON 标准不支持注释。有些工具如 VS Code 的 settings.json支持注释但那是扩展语法标准 JSON 解析器会报错。如果需要在 JSON 中加注释有两个方案用json5库它支持注释和尾逗号把注释写成特殊的键如_comment: 这是注释import json5 with open(config.json5, r, encodingutf-8) as f: data json5.load(f)6.5 避坑经验四注意数字精度问题JSON 中的数字如果超出浮点数精度解析后可能丢失精度。比如大整数 ID、高精度小数。import json from decimal import Decimal # 默认解析大整数可能丢失精度 data json.loads({id: 12345678901234567890}) print(data[id]) # 12345678901234567890Python 3 的 int 是任意精度没问题 # 但浮点数会有精度问题 data json.loads({price: 0.1}) print(data[price]) # 0.1 # 用 Decimal 保持精度 data json.loads({price: 0.1}, parse_floatDecimal) print(data[price]) # Decimal(0.1)6.6 避坑经验五处理循环引用Python 对象如果有循环引用json.dumps()会报ValueError: Circular reference detected。这时候需要自定义序列化逻辑。import json class Node: def __init__(self, name): self.name name self.parent None self.children [] # 创建循环引用 root Node(root) child Node(child) root.children.append(child) child.parent root # 直接 dumps 会报错 try: json.dumps(root.__dict__) except ValueError as e: print(f错误: {e}) # 解决方案自定义序列化跳过 parent 引用 def node_to_dict(node): return { name: node.name, children: [node_to_dict(c) for c in node.children] } json_str json.dumps(node_to_dict(root), ensure_asciiFalse) print(json_str)6.7 避坑经验六用在线工具辅助排查如果本地排查困难可以把 JSON 粘贴到在线 JSON 校验工具如 JSONLint中它们会给出更直观的错误位置提示。但注意不要上传敏感数据。对于本地排查VS Code 的 JSON 插件会在编辑器中直接标红错误位置比看报错信息直观得多。建议把 JSON 文件用 VS Code 打开它会自动校验并提示问题。7. 几个实用的 JSON 处理技巧7.1 用 json.tool 快速格式化Python 自带的json.tool模块可以快速格式化 JSONpython -m json.tool data.json如果 JSON 有错误它会报错并指出位置。如果 JSON 合法它会输出格式化后的内容。可以重定向到文件python -m json.tool data.json data_formatted.json7.2 用 jq 命令行工具处理 JSONjq是一个强大的命令行 JSON 处理工具适合在终端快速查看和过滤 JSON 数据。# 格式化输出 cat data.json | jq . # 提取特定字段 cat data.json | jq .name # 过滤数组 cat data.json | jq .items[] | select(.price 100) # 统计数组长度 cat data.json | jq .items | lengthjq的语法需要学习但一旦掌握处理 JSON 的效率会大幅提升。7.3 用 Python 的 json.JSONEncoder 自定义序列化如果对象包含datetime、Decimal等 JSON 不支持的类型可以自定义JSONEncoderimport json from datetime import datetime from decimal import Decimal class CustomEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, Decimal): return float(obj) return super().default(obj) data { time: datetime.now(), price: Decimal(19.99) } json_str json.dumps(data, clsCustomEncoder, ensure_asciiFalse) print(json_str)7.4 用 json.loads 的 object_hook 做后处理json.loads()的object_hook参数可以在解析每个对象时做后处理import json from datetime import datetime def datetime_hook(d): for key, value in d.items(): if isinstance(value, str): try: d[key] datetime.fromisoformat(value) except ValueError: pass return d json_str {time: 2024-01-15T10:30:00, name: test} data json.loads(json_str, object_hookdatetime_hook) print(data[time]) # 2024-01-15 10:30:00 print(type(data[time])) # class datetime.datetime7.5 处理 JSON 中的 NaN 和 InfinityJSON 标准不支持NaN和Infinity但 Python 的json模块默认允许它们。如果和其他系统交互可能会出问题。import json # 默认允许 NaN data json.loads({value: NaN}) print(data) # {value: nan} # 禁止 NaN遇到就报错 try: data json.loads({value: NaN}, parse_constantlambda x: (_ for _ in ()).throw(ValueError(fInvalid: {x}))) except ValueError as e: print(f错误: {e}) # 生成 JSON 时禁止 NaN try: json.dumps({value: float(nan)}, allow_nanFalse) except ValueError as e: print(f错误: {e})8. 我踩过的几个坑第一个坑是用json.load()读取网络请求的响应。json.load()接收的是文件对象json.loads()接收的是字符串。requests库的response.json()内部用的是json.loads()。如果混用了会报AttributeError而不是JSONDecodeError排查方向就偏了。第二个坑是忽略了 JSON 文件中的注释。有些配置文件比如某些工具的配置允许注释但标准 JSON 解析器不认。我一开始以为是格式错误排查了半天才发现是注释的问题。后来改用json5库解决了。第三个坑是在 Windows 上用默认编码读取文件。Windows 的默认编码是 GBK如果 JSON 文件是 UTF-8 编码且包含中文用默认编码读取会乱码进而导致 JSON 解析报错。解决方案是显式指定encodingutf-8。第四个坑是用字符串拼接生成 JSON。早期写代码时为了省事直接拼接字符串生成 JSON结果遇到用户输入包含引号、反斜杠时就出问题。后来全部改用json.dumps()再也没出过这类问题。第五个坑是没有处理 JSON 解析失败的情况。程序读取配置文件时直接json.load()如果配置文件被用户改坏了程序直接崩溃。后来加了异常处理解析失败时给出友好的错误提示并回退到默认配置。9. 一个完整的 JSON 处理工具类把上面讲的内容整合成一个工具类以后项目中直接复用import json import re from pathlib import Path from typing import Any, Optional, Union class JsonHelper: JSON 处理工具类封装常见操作和异常处理 staticmethod def load(filepath: Union[str, Path], encoding: str utf-8) - Optional[Any]: 安全加载 JSON 文件 filepath Path(filepath) if not filepath.exists(): print(f文件不存在: {filepath}) return None # 尝试多种编码 encodings [encoding, utf-8-sig, gbk, latin-1] content None for enc in encodings: try: with open(filepath, r, encodingenc) as f: content f.read() break except UnicodeDecodeError: continue if content is None: print(f无法解码文件: {filepath}) return None # 清洗不可见字符 content re.sub(r[\u200b\u200c\u200d\ufeff], , content) try: return json.loads(content) except json.JSONDecodeError as e: print(fJSON 解析失败: {e.msg}) print(f位置: 第 {e.lineno} 行, 第 {e.colno} 列) start max(0, e.pos - 50) end min(len(content), e.pos 50) print(f问题区域: ...{content[start:end]}...) return None staticmethod def save(data: Any, filepath: Union[str, Path], indent: int 2) - bool: 安全保存 JSON 文件 filepath Path(filepath) try: with open(filepath, w, encodingutf-8) as f: json.dump(data, f, indentindent, ensure_asciiFalse) return True except (TypeError, ValueError) as e: print(f序列化失败: {e}) return False staticmethod def dumps(data: Any, indent: int None) - Optional[str]: 安全序列化为 JSON 字符串 try: return json.dumps(data, indentindent, ensure_asciiFalse) except (TypeError, ValueError) as e: print(f序列化失败: {e}) return None staticmethod def loads(content: str) - Optional[Any]: 安全解析 JSON 字符串 if not content or not content.strip(): print(内容为空) return None # 清洗不可见字符 content re.sub(r[\u200b\u200c\u200d\ufeff], , content) try: return json.loads(content) except json.JSONDecodeError as e: print(fJSON 解析失败: {e.msg}) print(f位置: 第 {e.lineno} 行, 第 {e.colno} 列) start max(0, e.pos - 50) end min(len(content), e.pos 50) print(f问题区域: ...{content[start:end]}...) return None这个工具类封装了编码检测、不可见字符清洗、异常处理、错误定位等功能基本覆盖了日常 JSON 处理的场景。用的时候直接JsonHelper.load(config.json)解析失败会打印详细的错误信息方便排查。10. 最后再分享几个小技巧技巧一用json.dumps的sort_keys参数让输出稳定。如果 JSON 用于对比或版本控制加上sort_keysTrue让键按字母顺序排列避免因为键顺序不同导致 diff 混乱。技巧二用json.dumps的separators参数控制输出格式。默认情况下json.dumps会在逗号和冒号后面加空格。如果追求紧凑输出用separators(,, :)去掉空格。技巧三用json.load的parse_int和parse_float参数控制数字解析。默认情况下整数解析为int浮点数解析为float。如果需要Decimal保持精度用parse_floatDecimal。技巧四用json.dumps的default参数处理不支持的类型。如果对象包含datetime等类型用defaultstr可以快速转换但会丢失类型信息。更好的做法是自定义JSONEncoder。技巧五用json.loads的strict参数控制严格模式。默认strictTrue字符串中不允许控制字符。如果 JSON 中有未转义的控制字符可以设strictFalse宽容处理但不推荐最好还是修复源数据。技巧六用json.detect_encoding检测 JSON 字节流的编码。这个函数在json模块中可以检测字节流的编码适合处理来源不明的 JSON 数据。import json raw b{name: \xe4\xb8\xad\xe6\x96\x87} encoding json.detect_encoding(raw) print(encoding) # utf-8 data json.loads(raw.decode(encoding)) print(data) # {name: 中文}这些技巧看起来零散但实际项目中组合使用能解决大部分 JSON 处理的问题。核心思路就一条不要假设 JSON 一定合法做好防御性编程。解析前检查、解析时捕获异常、解析后校验结构三步走下来JSON 相关的报错基本就绝迹了。
阅读完成 · 觉得有帮助?
咨询建站