
1. 再聊游戏测试里的JSON这次当成一门硬功夫来练我做了几年游戏测试最深的体会是这行当早就不是“点点点”就能混下去的了。尤其是接触到接口测试、配置校验、日志分析这些活儿之后你会发现有个东西几乎无处不在——JSON。客户端发个请求是JSON服务器返回个奖励是JSON策划配的表导出来是JSON连自动化测试的测试数据也常常是JSON。可以说不懂JSON游戏测试的很多活儿你都干不踏实。有人可能觉得JSON不就是个数据格式嘛有什么好学的说实话这种想法我也曾经有过直到有一次因为少了一个字段的处理导致线上活动发放奖励时大面积出错我才彻底明白JSON数据处理这门基本功看起来简单里面的坑是真不少。这不仅关乎代码能不能跑通更关乎数据在各个环节之间流转的时候会不会变形、丢失、搞错类型甚至直接干崩整个服务。这篇内容主要围绕游戏测试工程师在Python方向上的JSON数据处理展开。我会把JSON的基本概念、Python内置的json模块、游戏测试里常见的处理场景、以及我在实际工作中踩过的坑和排查思路全部拆开来讲。不管你是刚入门Python的测试新人还是已经写了一段时间脚本但没系统梳理过JSON处理的同学这篇内容应该都能帮你补上一些盲区。毕竟“8周通关Python”这个目标里JSON数据处理绝对是绕不过去的一关。2. JSON凭什么成为游戏测试的“通用语言”2.1 从一次接口排查说起先讲个具体的场景。有一次我做活动接口的联调测试客户端上报玩家完成了某个任务服务器返回的数据大致长这样{ code: 0, msg: success, data: { player_id: 10086, task_id: 5023, reward: { gold: 500, diamond: 20 }, finish_time: 1710000000 } }这段JSON看起来很简单但你要真正去校验它就涉及不少细节player_id到底是字符串还是数字reward.gold的取值范围是多少finish_time是秒级还是毫秒级时间戳如果服务器把code从0改成200你的测试脚本能不能敏锐发现这就是JSON数据处理在游戏测试中的日常——它不仅是一种数据交换格式更是客户端和服务器之间约定的“契约”。你理解了这份契约才能写出真正有价值的校验逻辑而不是跑一遍、看个“通过”就完事。2.2 JSON的核心结构对象与数组JSON全称是JavaScript Object Notation虽然名字里带着JavaScript但它早已成为跨语言通用的数据交换格式。它的核心结构就两种对象Object和数组Array。对象是用花括号包裹的一组键值对键是字符串值可以是任意合法JSON类型。数组是用方括号包裹的一组有序值。这两种结构可以任意嵌套所以它能表达的数据模型非常灵活。我经常用一个生活化的类比来解释JSON对象就像是一个填写好的“个人档案表”每个字段都是表上的一个栏目JSON数组就像是一排“档案袋”你可以从里面抽出任意一份来看。游戏里的背包数据、排行榜数据、任务列表本质上都是各种“档案袋”套着各种“档案表”。理解了这个基础你就能明白为什么游戏服务器特别钟爱JSON——它足够灵活策划要加个新字段服务器不用改表结构直接在JSON里多塞一个键就行它也足够直观人类能直接读懂调试起来比二进制协议友好太多了。2.3 游戏测试中JSON存在的典型位置根据我这几年的经验游戏测试工程师和JSON打交道的地方主要集中在以下几块接口测试绝大多数游戏的后端接口都采用JSON作为请求和响应的数据格式。登录验证、背包查询、商城购买、任务领取全都是一来一回的JSON报文。配置表校验很多游戏的配置表会导出成JSON格式给客户端用。策划改了数值测试需要校验配置的正确性比如技能ID是否重复、物品价格是否合理、掉落概率加起来是否等于1。日志数据分析游戏运行过程中的行为日志、报错日志很多也以JSON格式记录。测试需要从海量日志中筛选出关键信息定位问题根因。自动化测试数据用Python写接口自动化测试时测试用例的输入数据、期望结果、环境配置通常都用JSON文件来管理。封包协议模拟在弱网测试、异常测试中你可能需要手工构造或篡改JSON报文模拟客户端发一些畸形数据看服务器能不能兜住。这些场景里你光会“看”JSON远远不够必须能在Python里熟练地读写、转换、校验JSON数据。这也是“JSON数据处理”作为独立一课存在的根本原因。3. Python内置json模块先啃下这块硬骨头3.1 json.dumps与json.loads的一来一回Python的json模块是标准库不需要额外安装直接导入就能用。处理JSON就两个最核心的方向一是把Python对象转成JSON字符串叫序列化二是把JSON字符串转成Python对象叫反序列化。对应到代码上就是两个最常用的函数json.dumps()负责把Python的字典、列表等对象转成JSON字符串json.loads()负责把JSON字符串解析成Python的字典、列表等对象。import json player_info { player_id: 10086, level: 30, vip_level: 5, items: [sword, shield, potion] } json_str json.dumps(player_info, ensure_asciiFalse, indent2) print(json_str) parsed_data json.loads(json_str) print(parsed_data[player_id]) print(type(parsed_data))这里有一个非常关键的参数——ensure_asciiFalse。如果你不设置它json.dumps()默认会把所有非ASCII字符转成\uXXXX形式的转义序列。中文会变成一长串\u73a9\u5bb6这种不仅肉眼没法看写进日志文件里也是一团乱麻。我做测试的时候所有落盘的JSON文件几乎都会加这个参数就是为了保证可读性。indent2是格式化参数让输出的JSON字符串带上缩进层级清晰。调试的时候特别好用但如果你要压缩JSON以减少传输体积那就得反过来用separators(,, :)来去除多余空白。另外要注意json.loads()的解析结果——JSON里的true、false、null会分别转换成Python的True、False、None。反过来Python的True、False、None也会被序列化成JSON的true、false、null。这个对应关系一定要记牢不然很容易在数据比较的时候翻车。3.2 类型对应关系一张表说清楚Python和JSON的类型不是一一对应的尤其是元组、集合这类JSON里根本没有的类型直接序列化会报错。我整理了一张在实际工作中最常用的对应表Python类型JSON类型注意事项字典 dict对象 object键必须能转成字符串列表 list数组 array最常用的对应关系元组 tuple数组 array序列化时自动转list字符串 str字符串 string默认转义非ASCII字符整数 int数字 number支持任意精度浮点数 float数字 number注意NaN和Infinity会默认被允许布尔 booltrue/false注意大小写差异Nonenull常被新手忽略这里要特别提醒几个点。第一Python的set集合不能被直接序列化你得先转成列表。第二json.dumps()在遇到float(nan)或float(inf)时默认会输出NaN和Infinity但这两个东西不是标准JSON的一部分有些解析器根本不认所以如果你要把数据发给其他语言的服务端最好提前处理。第三json.loads()可以传一个parse_constant参数来拦截NaN、Infinity这些非常规值但在游戏测试的场景里我更建议你做后置校验确保上游给的数据本身就不带这些东西。3.3 读写文件别用openread再手动解析了很多新手在处理JSON文件的时候会写成这样的代码with open(config.json, r, encodingutf-8) as f: content f.read() data json.loads(content)这样写没错但不够优雅。更直接的做法是用json.load()和json.dump()它们直接对接文件对象with open(config.json, r, encodingutf-8) as f: data json.load(f) data[new_version] 1.2.0 with open(config_modified.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)注意函数名差了一个s——不带s的是跟文件对象打交道带s的是跟字符串打交道。这个细节看起来不起眼但我在带新人的时候发现几乎每个新人都会在某个时刻把json.load和json.loads混着用然后报出一堆奇奇怪怪的错误。还有一点务必谨记读写JSON文件一定要指定encodingutf-8。在Windows环境下如果不指定编码open()默认使用系统编码通常是GBK遇到中文内容极容易报UnicodeDecodeError。这个问题我在Windows上踩过太多次了现在已经养成了习惯凡是操作文本文件必带encodingutf-8。3.4 更安全的选择json.JSONDecoder与众不同的容错能力游戏测试里经常会遇到各种“不太规矩”的JSON比如服务器日志里混了一堆调试信息、配置表导出的时候带了BOM头、或者干脆就是某个同事手欠多打了个逗号。直接用json.loads()去解析这些字符串结果就是抛异常。遇到这种情况json.JSONDecoder()会给你多一些容错空间。它的raw_decode()方法可以在字符串开头解析出一个JSON值剩余部分忽略不管from json import JSONDecoder decoder JSONDecoder() data, end_index decoder.raw_decode({code:0,msg:ok} 后面还有一些垃圾信息) print(data) print(end_index)这个能力在解析日志文件的时候特别有用——一行日志里前缀可能是时间戳、进程ID之类的文本后面紧跟一个JSON对象用raw_decode()可以轻松抽出来不用先做字符串切片。不过要注意raw_decode()同样要求JSON部分本身是合法的逗号、单引号这种错误它一样不接受。对于真正的容错需求可以考虑demjson3之类的第三方库但引入额外依赖之前要想清楚游戏测试的脚本大多数时候跑在受控环境里数据源格式相对固定标准库够用就不要轻易加依赖省得部署的时候出幺蛾子。4. 游戏测试里的高频JSON处理场景实操4.1 配置表校验用Python写一个“找茬”脚本游戏测试里最常见的一个需求就是校验配置表。通常策划会把配置导成JSON格式可能是数组套对象也可能是对象套对象。测试要确保里面没有低级错误。举个具体例子假设有一份道具配置items.json结构是数组每个元素是一个道具对象[ {id: 1001, name: 新手剑, type: weapon, price: 100}, {id: 1002, name: 治疗药水, type: consumable, price: 50}, {id: 1002, name: 重复ID道具, type: unknown, price: -5} ]肉眼一看就知道有问题但配置多了之后肉眼根本看不过来。我通常会写一个校验脚本把规则一步步加上去import json def load_items(path): with open(path, r, encodingutf-8) as f: return json.load(f) def validate_items(items): errors [] seen_ids set() valid_types {weapon, consumable, armor, material} for idx, item in enumerate(items): if not isinstance(item, dict): errors.append(f第{idx}项不是对象格式) continue item_id item.get(id) if item_id in seen_ids: errors.append(f道具ID重复: {item_id}) seen_ids.add(item_id) item_type item.get(type) if item_type not in valid_types: errors.append(f道具ID {item_id} 的类型异常: {item_type}) price item.get(price, 0) if not isinstance(price, (int, float)) or price 0: errors.append(f道具ID {item_id} 的价格异常: {price}) return errors if __name__ __main__: all_items load_items(items.json) result validate_items(all_items) if result: print(发现配置错误) for err in result: print( -, err) else: print(配置校验通过)这个脚本的逻辑并不复杂但它的价值在于你可以把策划在配置里可能犯的所有错误类型都沉淀成规则每次配置更新后跑一遍就能在测试之前先把低级问题挡在门外。实际做的时候规则只会比这个更细——比如掉落概率之和是不是1、技能ID是否在合法枚举范围内、引用的目标配置是否存在等等。4.2 接口报文校验从“能通”到“验得准”接口测试是游戏测试工程师的重头戏。很多人用Python做接口测试只会盯着HTTP状态码和返回里头的code字段觉得只要code是0就万事大吉。但实际上返回的数据结构、字段类型、字段取值范围每一个地方都可能出错。举个例子一个领取每日奖励的接口正常情况下返回{ code: 0, data: { daily_reward: { gold: 100, item_list: [{id: 3001, count: 1}] } } }但有一次我在测试中发现服务器在某种边界条件下返回的data竟然是个空字符串而不是一个对象。用json.loads()解析倒是没问题但后续访问data[daily_reward]就直接报TypeError: string indices must be integers。这种问题在Python脚本里会立刻暴露但如果有人在代码里做了过度防御反而可能让问题悄悄溜过去。所以我写接口校验的时候会重点检查这几个方面顶层字段完整性code、msg、data是否都存在。data的类型期望是对象还是数组能不能为空关键字段的数值范围金币是不是负数数量是不是0时间戳是不是合理区间嵌套结构的深度某些异常情况下服务器会不会只返回部分字段我会把校验逻辑封装成函数每次接口调用后统一执行这样既保证了准确性又不会在业务代码里到处塞校验逻辑。def check_daily_reward_response(resp_data): assert resp_data[code] 0, f业务code错误: {resp_data[code]} data resp_data.get(data) assert isinstance(data, dict), fdata类型错误: {type(data)} reward data.get(daily_reward) assert isinstance(reward, dict), fdaily_reward缺失或类型错误: {reward} gold reward.get(gold, 0) assert isinstance(gold, int) and gold 0, f金币数值异常: {gold}这种写法非常直接出错的时候报错信息一眼就能看出问题在哪。比起用一堆if判断然后打印日志断言式校验在测试脚本里反而更高效。4.3 日志数据提取在千万行记录里捞“案发现场”游戏服务端会滚动输出大量日志里面经常混着JSON格式的关键信息。出问题的时候测试要第一时间从日志里找到对应的记录分析当时玩家的状态、请求参数、服务器返回。有一次线上玩家反馈“充值到账延迟”我处理的第一步就是去日志里捞那个玩家当天的充值记录。日志文件很大直接打开搜索很慢我写了个小脚本按关键词筛选import json def extract_json_from_line(line): try: start line.index({) decoder JSONDecoder() obj, _ decoder.raw_decode(line[start:]) return obj except Exception: return None keywords [player_id10086, recharge, order] with open(server.log, r, encodingutf-8) as f: for line in f: if any(kw in line for kw in keywords): obj extract_json_from_line(line) if obj: print(json.dumps(obj, ensure_asciiFalse, indent2))这个脚本的思路是先用关键词粗筛再用raw_decode()把行内嵌的JSON提取出来最后用json.dumps格式化打印。实际处理海量日志的时候效率、内存占用都是需要考量的点但如果只是定位单个问题这种逐行扫描的方式已经够用了。真遇到GB级别的日志我一般会先把日志文件按时间范围切分或者用grep命令先做一层筛选再丢给Python脚本处理。4.4 数据驱动测试把测试用例从代码里“抽”出去游戏测试的自动化用例如果所有数据都硬编码在代码里维护起来非常痛苦。策划改个数值测试代码就得跟着改一遍还容易引发连锁问题。更好的做法是把测试数据和业务逻辑分离用JSON文件来存放用例数据。[ { case_id: login_success, request: { username: test001, password: 123456 }, expected: { code: 0, has_role: true } }, { case_id: login_wrong_password, request: { username: test001, password: wrong }, expected: { code: 1001 } } ]测试脚本读取这个JSON文件遍历执行每条用例import json def load_cases(path): with open(path, r, encodingutf-8) as f: return json.load(f) def run_case(case): url https://game-api.example.com/login resp client.post(url, jsoncase[request]) resp_data resp.json() for key, value in case[expected].items(): assert resp_data.get(key) value, f{case[case_id]} 断言失败: {key}期望{value}实际{resp_data.get(key)} for case in load_cases(login_cases.json): run_case(case)用这种数据驱动的方式新增一条用例只需要往JSON文件里加一个对象不需要动代码。但这里有个容易踩的坑——JSON文件里不能写Python风格的注释。很多人习惯了Python的#注释往JSON文件里塞# 这里是登录成功用例结果解析直接报错。一种绕过办法是在JSON里约定一个专门放备注的字段比如desc: 登录成功用例测试代码不去校验它就行。5. 常见问题与排查技巧实录5.1 中文变\uXXXX怎么办最典型的表现是json.dumps(data)之后中文全变成了\u73a9\u5bb6这种。这是正常的不是编码错误——JSON规范允许这种转义形式多数解析器也能正确处理。问题是人类阅读排查的时候太费劲了。解决方法就是加ensure_asciiFalse参数。如果你已经生成了带\uXXXX的文件想转回可读格式可以用json.loads()把字符串解析回来再重新dumps一次with open(bad.json, r, encodingutf-8) as f: data json.load(f) with open(good.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)5.2 键值对顺序被打乱Python 3.7之后字典默认保持插入顺序所以json.dumps()输出的JSON会按照你构造字典时的顺序来排列。但如果你是从文件里load进来的然后再修改其中某个字段再dump出去新加的字段会出现在最后不会自动按字母序或某个逻辑顺序排列。如果你希望输出时键的顺序是确定的比如按拼音或按数字ID排需要自己在代码里处理。可以构造一个collections.OrderedDict或者更简单地先取出所有键排个序再组装新字典。不过我个人觉得在游戏测试的场景里键的顺序对语义没有任何影响只要解析方不依赖顺序就行没必要过度纠结。5.3 解析超大JSON文件时内存爆了游戏测试中遇到超大JSON文件并不罕见比如把全服玩家的信息导出成一个JSON轻松几百MB。这时候如果直接用json.load()一次性读进来内存很容易吃紧甚至直接卡死。这种情况下我建议评估一下你的真实需求如果只是要查某几个特定字段可以用流式解析的思路。标准库没有高效的流式JSON解析能力但有一个ijson库可以做到。如果你只是要按大块切分处理也可以先读文件按括号层级切出一个个小的JSON片段再逐个解析。考虑到大多数游戏测试场景里“超大JSON”也就是几百MB的规模我通常的折中方案是——先确认到底需要哪些数据如果只是统计类信息用Python的逐行扫描也能解决如果确实要做精细解析那就上ijson或者干脆让开发侧导出更小的数据子集。5.4 接口返回的是JSON数组但顶层不是对象有些接口的返回值是数组比如获取排行榜直接返回[ {rank: 1, name: playerA, score: 9999}, {rank: 2, name: playerB, score: 8888} ]新手拿到这种数据往往会愣一下因为他习惯了解析成字典之后用键取值。实际上json.loads()解析数组后会得到Python的list你只需要按列表的方式遍历就行data json.loads(response_text) for item in data: print(item[rank], item[name], item[score])这种情况在接口测试里非常常见不是什么异常数据只要心里有数就行。5.5 时间戳到底是不是毫秒游戏服务器返回的时间戳有的用秒级有的用毫秒级十位和十三位的区别。做断言的时候如果拿当前时间直接去比对很容易因为单位不一致导致误报。我的习惯是拿到时间戳先判断位数十位按秒处理十三位除以1000转成秒然后再跟当前时间做差值计算。同时还要考虑服务器时区问题如果游戏是海外发行时间戳直接涉及转换逻辑测试脚本里也要一并处理。5.6 嵌套深了之后取层级数据费劲当JSON嵌套层级特别深的时候比如data[activity][stages][1][rewards][items][0][count]这种写起来费劲而且一旦某个中间层变成空值整行代码直接崩。两种思路一种是老老实实用get()逐层取写个辅助函数另一种是写一个通用函数支持点路径取值比如get_value(data, data.activity.stages[1].rewards.items[0].count)。第二种方式在自动化测试里会清爽不少但实现的时候要注意索引的解析。我建议初学者先掌握第一种把基础打牢等真的写烦了再考虑封装工具函数。6. 一些让JSON处理更顺畅的辅助工具与习惯6.1 VS Code里装个JSON插件做游戏测试代码编辑器里离不开JSON相关插件。VS Code本身对JSON文件的支持不错语法高亮、格式化、折叠都有。但我建议再装两个扩展一个是JSON Tools可以快速对选中的JSON做格式化和压缩另一个是Prettier - Code formatter写代码时顺手把JSON文件也统一格式化。我自己还喜欢用一个叫JSON to TS的扩展不过这是在你需要把JSON转成TypeScript接口定义时才有用游戏测试场景里用得少主要是方便写前端工具脚本。6.2 在线工具和本地开发的小习惯在线JSON格式化、校验工具很多我偶尔会用但涉及敏感数据的JSON我从来不丢到网上。数据安全这根弦什么时候都不能松。自己本地备一个随手能跑的格式化脚本反而更稳妥import json import sys def format_json_file(src, dstNone): with open(src, r, encodingutf-8) as f: data json.load(f) dst dst or src with open(dst, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) if __name__ __main__: format_json_file(sys.argv[1], sys.argv[2] if len(sys.argv) 2 else None)6.3 善用json.tool模块命令行里临时想看一个JSON文件的结构不用打开编辑器直接python -m json.tool input.json output.json这个命令会把input.json格式化后输出到output.json如果原文件本身解析不了它会在终端直接报错。我个人很喜欢这个命令写脚本调试的时候临时格式化一下很方便也不用额外写代码。7. 在游戏测试这个方向上JSON数据处理还得注意什么游戏测试工程师学Python和数据处理跟纯后端开发有个很大的不同——你不需要把每段代码都写得像生产环境那么健壮但一定要能快速定位问题、快速验证假设。JSON数据处理也不例外。我在带人的时候经常说一句话“看JSON别只盯着值要看结构变没变。”接口返回的JSON结构在正常情况下应该保持稳定。如果你发现某一次返回的JSON字段比昨天少了两个或者类型突然变了这往往意味着服务端代码有改动甚至可能是发错了版本。这种结构性变化靠肉眼很难发现所以我建议在自动化测试里加入“结构快照”对比的思路——定期把接口返回的JSON结构存成一份基准文件每次跑用例的时候对比一下结构一旦发现差异立刻报警。另外游戏测试处理JSON时会大量涉及数据一致性的问题。举例来说客户端展示的货币数量、任务进度和服务器返回的数据必须一致服务器返回的奖励配置和策划配置表里的数值必须一致。这些地方都需要你在脚本里做联合校验而不是只断言单个字段的值对不对。有一点必须强调JSON数据本身没有类型约束但它承载的业务语义往往有严格的类型要求。比如player_id如果设计成字符串那不管什么情况下都不能变成整数否则下游系统拼接、索引、缓存可能全部乱套。测试的时候要有意识地造一些“类型边界”的用例比如传一个数字型ID、传一个带小数点的经验值、传一个空字符串作为名称看看服务端能不能正确处理。这些用例不在策划配置里但你写了就能帮团队提前挡掉很多脏数据问题。还有一件事如果是做客户端自动化经常要读取本地存档或配置文件这些文件很多也是JSON格式。这时候要注意不要假设配置文件的键一定存在。游戏版本迭代过程中旧存档可能缺少新版本才有的字段。你的解析代码必须要能容忍这些缺失否则老玩家一更新就崩溃。通常的做法是用get()加默认值或者解析后统一做一次字段补全。8. 折腾几次之后我养成的几个JSON处理习惯说到底JSON数据处理不是一门“学完就完事”的知识而是在反复实践中慢慢沉淀出的一套直觉和习惯。我自己折腾了几年最后养成这么几个习惯第一所有涉及JSON的读写统一走Python的json模块不用正则硬抠。看起来正则也能匹配出某个字段但只要嵌套结构稍微复杂一点正则就会变得极其脆弱。用json.loads()先解析成Python对象再用字典和列表操作去访问逻辑又清晰又稳。第二格式化输出时ensure_asciiFalse和indent2是默认配置。除非明确要压缩体积否则我都会把它们加上。这俩参数看着平平无奇但能让你在调试和排查时省下大量眼睛的力气。第三校验JSON时先校验结构再校验值。我会先写一组函数确保对象、数组、嵌套层级、字段类型都是预期内的然后再去检查数值范围、逻辑关系。顺序反了的话很可能一个键值错误触发一串异常排查起来反而更慢。第四处理完一批JSON数据随手落一份日志或输出文件。游戏测试经常要对着结果复盘——这条数据怎么改的、中间经过了哪些转换、最终结果是什么样。把中间产物留下来出了问题时能倒查不会翻来覆去找不到线索。这些习惯不一定适合所有人但对我来说它们实实在在地减少了排查问题的时间。数据处理这活儿做了十年的老手和刚入门的新手差别未必在语法上更多的就是在这些细碎的习惯里。如果你也在游戏测试这条路上慢慢磨Python建议你现在就打开一个真实的JSON文件——不管是配置表、接口返回还是日志记录——亲手写一遍加载、解析、校验、转换、落盘的完整流程。走完这一圈你对JSON数据处理的体感会比看十篇文章都来得实在。