Python pprint 完全指南:结构化输出与调试
如果你调试过那种层层嵌套的订单配置、爬虫抓回来的大字典、或者对接第三方API返回的复杂JSON你一定经历过这种崩溃一个print()下去几千个字符全部挤在一行里缩进没有、换行没有键值关系全靠肉眼硬认稍微深一点的层级就直接看花眼。这个场景我太熟了。以前排查一个配置合并的问题print()出来的结构长到终端自动换行从第80列开始毫无规律地断行我愣是对着一个屏幕找了十分钟才定位到是哪个键的值写错了。后来换成pprint同样的数据层级清清楚楚键值对齐问题一眼就暴露了。pprint是 Python 标准库里被严重低估的一个模块。全称是 pretty printer直译是“漂亮打印机”但它干的活远不止“打印得好看”这么简单——它本质上是一个针对复杂数据结构的结构化输出引擎递归地遍历对象、根据宽度和缩进阈值重新排版、把嵌套关系可视化。这篇东西我打算把这套工具从头到尾讲透包含核心参数、四个常用接口、自定义对象扩展以及我在实际项目里踩过的坑和总结出来的调试心得。不管你是在写脚本、做接口联调、维护配置系统还是单纯想把日志打得清楚点这篇文章应该都能让你少走不少弯路。1. 先搞清楚pprint到底解决了什么问题1.1print()的三个“原罪”Python 内置的print()函数遇到简单字符串是没问题的但一旦碰上嵌套的数据结构它的表现基本可以用“灾难”来形容。我总结了三个最典型的痛点第一单行挤成一团。一个三层的字典print()会把它渲染成一行极长的文本。终端宽度有限的于是内容在第80列、第120列这些地方被硬生生截断断点完全没有逻辑。你根本看不出来一个列表在哪里结束、一个字典从哪里开始。第二嵌套层级看不出归属。print()不会根据嵌套深度做缩进即使你手动在字符串里拼\n和空格也只是在展示层做文章数据的真实结构并没有被还原出来。嵌套深度一超过两层代码里的缩进和输出里的缩进就对不上了读起来极其痛苦。第三键值顺序容易误导。Python 3.7 之后字典保持插入顺序但如果你拿到的是从外部解析进来的数据比如 JSONprint()输出时只会按照“数据原来的存储顺序”来展示而这个顺序未必是逻辑上的顺序。你要是靠肉眼去比对字典内容很容易被这种顺序迷惑。pprint则是完全不同的思路它不再把数据结构当成一个“字符串”来处理而是把它当成一棵树来遍历逐层展开每层按照你设定的缩进规则排版。1.2 “结构化输出”是什么意思很多教程把pprint说成“美化打印”这个描述其实掩盖了它的本质。它做的不是文字的修饰而是对数据结构的重新表达。它内部会递归地访问你传入的容器对象判断每一个元素是字典、列表、元组还是集合然后根据当前的缩进层级、剩余宽度、是否启用 compact 模式等因素决定元素是继续放在同一行还是折行显示。这本质上是一个排版引擎类似 IDE 里的“格式化代码”功能只不过它针对的不是代码而是任意 Python 对象。把握住这个定位很重要因为它解释了很多“奇怪”的输出行为。比如你设置width40一个很长的字符串在 40 列放不下时pprint不会帮你把字符串从中间切断——因为它不能破坏字符串的完整性只能让整个字符串独占一行甚至溢出。再比如depth参数它控制的不是字符长度而是递归深度超过深度的部分直接显示为...。理解了“递归排版”这个本质后面的参数和坑就都好解释了。1.3 什么场景下最值得用它结合实际经验我认为这几类场景是pprint的主场接口联调后端返回的 JSON 结构复杂用pprint.pprint(response.json())一打字段层级一目了然定位缺字段、类型不对的问题非常快。配置解析读取 YAML、TOML 或者嵌套的 ini 配置后直接pprint出来检查合并结果比逐行打印日志高效得多。爬虫开发抓下来的页面解析成字典或者对象数组后结构化输出能让你快速判断哪些字段是空的、哪些节点被意外嵌套了。日志记录用pformat()把格式化结果拼进日志消息比print()更专业——这个后面会专门写。一句话总结只要你需要“观察”一个数据结构而不是“传输”它pprint就是比print()更合适的选择。2. 五个核心参数决定输出的三种“长相”pprint的核心类是pprint.PrettyPrinter而常用的pprint()函数本质上就是创建了一个默认参数的PrettyPrinter实例。可以这么说理解了PrettyPrinter的构造函数你就理解了整个模块。pprint.PrettyPrinter(indent1, width80, depthNone, streamNone, *, compactFalse, sort_dictsTrue, underscore_numbersFalse)第一次看这个签名大多数人只记住了indent和width。但实际上每个参数都会影响输出的形态而且有些参数之间存在相互制约。我逐个拆开讲。2.1 width 与 compact折行逻辑的两把尺子width默认值是 80单位是“字符宽度”。它代表的是pprint允许的单行最大宽度。当一个容器的内容在当前行放不下时pprint就会把它拆成多行展示。看到这里你可能会想那我把width设大一点比如 200是不是所有内容就会尽量挤在一行对确实如此但这里有个微妙的联动关系当compactFalse默认值时pprint倾向于“每个元素单独占一行”来保证清晰当compactTrue时pprint在宽度允许的范围内会尽可能把多个元素放在同一行以减少总行数。我举一个实际对比的例子数据是同一个import pprint data { name: 订单服务, port: 8080, tags: [core, payment, high-availability], deps: [auth, user, inventory], }默认参数打印出来是{deps: [auth, user, inventory], name: 订单服务, port: 8080, tags: [core, payment, high-availability]}注意几个细节最外层的每个键值对都在单独一行因为默认不是 compact 模式字典的键被排序了这个sort_dicts后面讲第一行{ deps后面直接跟了值是因为第一行还有足够空间不需要换行。如果设置compactTruepprint.pprint(data, compactTrue)输出会变成{deps: [auth, user, inventory], name: 订单服务, port: 8080, tags: [core, payment, high-availability]}咦好像没区别关键在于当compactTrue时列表内部的多个元素会尽可能挤在一行。上面这个数据里列表本来就因为width限制放不下所以折行了可如果列表很短区别就出来了short_data {items: [1, 2, 3], name: test} pprint.pprint(short_data, width40, compactFalse) # 输出 # {items: [1, # 2, # 3], # name: test} pprint.pprint(short_data, width40, compactTrue) # 输出 # {items: [1, 2, 3], # name: test}看到区别了compactFalse时列表元素即使能放下也会一个一个自然折行compactTrue时列表内部会选择“挤一挤”放在一行。所以我的经验是如果视觉上想少翻几页开compactTrue如果希望结构尽可能松散、方便逐行比对保持默认 False。2.2 indent别小看这个起始缩进indent默认值是 1它的作用范围是当前容器开始换行后新行相对于容器起始位置的额外缩进量。很多人把它理解成“每一层的缩进宽度”这个说法不够准确因为它只影响“额外缩进”基础的层级缩进是由pprint自己管理的。看个例子最直观nested {a: {b: {c: 1}}} pprint.pprint(nested, indent4)输出{ a: { b: { c: 1}}}注意{后面跟了 4 个空格才开始写a。而内层b、c的缩进是在这个基础上的。这个参数对“层级感”的影响非常明显。如果你的数据嵌套程度很深调大indent会让层级边界更清晰但也要适可而止太大会导致大量行宽被浪费提前触发折行。我在实际项目里通常保持默认indent1因为追求的是紧凑和可读性的平衡。只有在打印某些嵌套极深、内部结构相近的数据比如 AST 树时才会把indent调到 2 或 3。2.3 depth防止“深水炸弹”depth应该是最被低估的参数。它的作用是限制递归展开的深度。默认是None表示无限制但当你面对一个层级非常深、甚至可能包含循环引用的数据结构时无脑全量展开不仅输出爆炸还可能导致程序性能问题。deep {a: {b: {c: {d: {e: 1}}}}} pprint.pprint(deep, depth2)输出{a: {b: {...}}}超过depth的部分会被替换成...不会继续递归展开。这在处理高维数组、嵌套类对象、或者防御性地打印不可控的外部数据时特别有用。我自己写调试工具函数时往往会加一个max_depth参数默认给 3防止某些沙雕数据把日志直接刷爆。另外depth还有一个副作用它能在一定程度上保护你免受“自引用”结构的无限递归影响。比如一个列表把自己 append 了进去depth可以帮助截断输出。2.4 sort_dicts排序开关暗藏一个坑从 Python 3.8 开始pprint新增了sort_dicts参数默认True。也就是说默认情况下pprint输出的字典键会被排序而不是按照插入顺序。这个默认行为对调试来说经常是好事你可以快速比较两个字典的结构差异不用管它们内部键的顺序有多不一致。但也有人会困惑“为什么我定义的顺序到了pprint这里就不一样了”原因就是sort_dicts。如果你希望保持字典的原始插入顺序必须显式设置sort_dictsFalse。这里藏着一个小坑pprint.pprint()作为函数调用时sort_dicts需要通过关键字参数来传如果你习惯了位置传参很容易把stream的位置挤掉比如写成pprint(data, False)——这会把False当作stream参数然后直接报错。# 正确写法 pprint.pprint(order_data, sort_dictsFalse) # 错误写法False 被当成 streamTypeError pprint.pprint(order_data, False)当你在处理有序字典比如OrderedDict时我会建议主动设置sort_dictsFalse否则它里面的语义顺序会被“按键排序”破坏影响你对状态的判断。3. 接口四件套pprint、pformat、pp与saferepr使用pprint模块时你会发现它有多个看起来差不多的接口。我刚开始用的时候也搞混过后来才总结清楚这其实是四种不同用途的“打开方式”。3.1pprint()最常见的快速打印pprint.pprint(obj, streamNone, indent1, width80, depthNone, *, compactFalse, sort_dictsTrue)它的特征是直接向sys.stdout或者你指定的stream输出格式化结果没有返回值。这是日常调试最顺手的一个。你随便在交互式环境里pprint.pprint(data)结果直接打到屏幕上。有一个值得注意的细节pprint()在输出结束后不会添加额外的空行但如果你的数据以换行结尾它可能会保留。3.2pformat()把格式化结果“握在手里”s pprint.pformat(obj, indent1, width80, depthNone, *, compactFalse, sort_dictsTrue)pformat()和pprint()的区别只有一个它返回格式化后的字符串而不是直接打印。这个差别在实战中太重要了。我来讲一个场景你现在在写一个复杂的服务需要把某一次请求的入参数组打印到日志里。如果用pprint()它会把内容直接打到控制台这时候你想往日志文件里写一份、想加上时间戳、想放到异常信息里就全做不了了。但如果用pformat()import pprint import logging logger logging.getLogger(order) request_data {user_id: 12345, items: [{sku: A100, qty: 2}, {sku: B200, qty: 1}]} logger.debug(request payload: %s, pprint.pformat(request_data))这样日志里既有完整的结构层次又不会受print()的缓冲策略影响。可以说凡是需要“把格式化结果当字符串用”的场景都应该选择pformat()。3.3pp()Python 3.12 加入的快捷方式Python 3.12 里新增了一个高层函数pprint.pp()。它的签名是pprint.pp(object, *args, sort_dictsFalse, **kwargs)和pprint.pprint()的主要区别在于sort_dicts默认值被改成了False。这是社区反馈后的一个修正因为很多人在用pprint的时候本身就想保留字典原本的顺序结果每次都要手动传sort_dictsFalse非常烦人。所以如果你用的是 Python 3.12而且你想要保留原始顺序直接用pp()就行pprint.pp(my_dict)这个函数在功能上等价于pprint(..., sort_dictsFalse)但更简洁。如果你还在用较老的 Python那只能自己手动传参数了。3.4saferepr()与子类化高级玩家的扩展点pprint.saferepr()是一个相对少用的函数它生成一个“安全”的 repr 字符串保证不会因为对象存在递归引用而抛RecursionError。当你处理可能包含自引用的对象时直接调用默认repr()有风险而saferepr()会把递归部分处理成...。更强大的扩展方式是子类化PrettyPrinter并重写format()方法。举个例子假设你有一个自定义的配置对象默认打印出来全是内存地址你想让它在pprint里展现出内部字段这时候你可以这样做import pprint from dataclasses import dataclass, asdict from decimal import Decimal dataclass class PriceItem: sku: str price: Decimal qty: int class PrettyConfigPrinter(pprint.PrettyPrinter): def format(self, object, context, maxlevels, level): if isinstance(object, PriceItem): # 返回三元组文本表示、是否可读、是否递归 return (fPriceItem(sku{object.sku!r}, price{object.price}, qty{object.qty}), True, False) if isinstance(object, Decimal): return (fDecimal({object}), True, False) return super().format(object, context, maxlevels, level)这会让你打印包含PriceItem和Decimal的复杂结构时不再是一堆xxx object at 0x...或者裸数字而是可读性极高的字段级展示。这算是我实际工作中一个非常有用的定制技巧。4. 实战场景一API 响应与嵌套配置的排查理论讲得再多不实战都是空的。这一节我用实际工作中遇到过的场景来演示pprint怎么用。4.1 模拟一个真实的三层嵌套 JSON假设你正在对接一个支付回调接口返回的 JSON 长这样我简化了但保留了多层嵌套callback_payload { event: payment.success, order_id: 20240815001, amount: 168.50, currency: CNY, customer: { user_id: U12345, vip_level: 3, contacts: [ {type: mobile, value: 138****8000, primary: True}, {type: email, value: u12345example.com, primary: False}, ], }, items: [ {sku: A100, name: 无线耳机, price: 99.0, tags: [digital, hot]}, {sku: B200, name: 充电器, price: 69.5, tags: [digital, accessory]}, ], }现在你要排查一个问题这个回调里的items[0].tags到底是不是两个元素如果用默认print()输出的长字符串会让人崩溃。但用pprint.pprint()import pprint pprint.pprint(callback_payload, width60)输出会变成{amount: 168.5, currency: CNY, customer: {contacts: [{primary: True, type: mobile, value: 138****8000}, {primary: False, type: email, value: u12345example.com}], user_id: U12345, vip_level: 3}, event: payment.success, items: [{name: 无线耳机, price: 99.0, sku: A100, tags: [digital, hot]}, {name: 充电器, price: 69.5, sku: B200, tags: [digital, accessory]}], order_id: 20240815001}这才是“结构化输出”应有的样子。你能看到顶层键按字母排序amount、currency、customer...这里用了默认的sort_dictsTrue。在接口返回的场景我会更关心“数据里有没有”而不是“顺序如何”所以默认排序能让我快速定位键。每一层的缩进都非常明确contacts下面的字典嵌套关系一眼可见。由于我设置了width60深层嵌套的行在放不下时自然折行而不是硬挤出屏幕。4.2 对width折行的几个常见误解很多人一看到width60就以为“所有行都必须 60 列以内”这是不对的。pprint不会强行切断单个不可分割的元素。比如上面value: 138****8000如果特别长超出了 60它不会把字符串从中间断开而是仍然整行输出顶多导致这一行超过宽度限制。这个逻辑类似 Web 排版里的word-wrap和overflow-wrap容器宽度只是参考遇到长单词长字符串、长数字就允许撑破边界。理解这一点你看到“为什么我的 width 设置了还是超出”时就不会疑惑了。4.3 处理集合、冻结集合等无序类型pprint打印set的时候也有自己的一套逻辑。集合是无序的但pprint会尽量保证输出的确定性。比如pprint.pprint({apple, banana, orange}, width20)它可能会把集合元素拆成多行每行几个而且多次打印顺序可能不同因为集合本身没有顺序。这里我不建议你把pprint的输出结果用于任何“对集合内容做断言”的自动化场景它只适合肉眼观察。如果需要稳定输出可以先转成sorted(set)不过要注意元素类型必须可比较否则会TypeError。这种场景其实比较少见提醒一下就好。5. 实战场景二自定义对象与 dataclass 的输出优化默认情况下pprint遇到一个不是内置容器类型的对象时它调用的是对象的__repr__()。如果这个对象没有实现好__repr__输出就是module.ClassName object at 0x...这基本等于没输出。这个坑在 Debug 业务对象时非常常见。5.1 先看一个反面教材假设你定义了这样的类class Order: def __init__(self, order_id, amount, items): self.order_id order_id self.amount amount self.items items order Order(20240815001, 168.5, [{sku: A100, qty: 1}]) pprint.pprint(order)输出是类似这样的一行__main__.Order object at 0x7f8a1c1b3a90你什么都看不到。对象里字段的值、列表的内容全被遮住了。很多人到这里就开始抱怨“pprint没用”其实不是pprint的问题而是这个类没有提供可被利用的表示形式。5.2 用自己的__repr__修复最简单的解决方案是在自定义类里实现__repr__方法。我不想引战但__repr__的核心目的就是“给开发者看”所以把它做得可读是有价值的。class Order: def __init__(self, order_id, amount, items): self.order_id order_id self.amount amount self.items items def __repr__(self): return (fOrder(order_id{self.order_id!r}, amount{self.amount!r}, fitems{self.items!r}))这样pprint.pprint(order)就会把你看到的Order对象变成一个可读的字符串行夹在复杂容器里也能看清楚了。不过要注意pprint对自定义对象的进一步嵌套展示仍然有限。它不会帮你把Order.items里的列表信息单独再拆一层排版它只会把这个字符串当作一整段文本输出。如果你希望自定义对象也享受“容器级”的逐层结构输出那就需要回到第 3.4 节里提到的方式——子类化PrettyPrinter重写format()方法。5.3 用format()方法实现字段级展开用format()控制输出的好处是你可以把对象的内部字段“伪装”成pprint能理解的容器结构让它继续递归处理。例如class OrderPrinter(pprint.PrettyPrinter): def format(self, object, context, maxlevels, level): if isinstance(object, Order): # 手动拼一个字典让 pprint 当作普通嵌套结构继续递归排版 return ({ order_id: object.order_id, amount: object.amount, items: object.items, }, True, False) return super().format(object, context, maxlevels, level)不对这里需要注意format()返回的元组中第一个元素必须是一个字符串而不是容器对象。上面我差点写错了。正确的做法是生成一个“格式良好的字符串”并把is_readable字段设为True。我实际用的版本是这样的class OrderPrinter(pprint.PrettyPrinter): def format(self, object, context, maxlevels, level): if isinstance(object, Order): text pprint.pformat( { order_id: object.order_id, amount: object.amount, items: object.items, }, indentself._indent_per_level, widthself._width, depthmaxlevels, compactself._compact, ) return (text, True, False) return super().format(object, context, maxlevels, level)这个方案的精髓在于你在自定义对象的format()里调用pformat()再给它一次“重新排版”的机会生成的字符串本身就是格式化好的多行文本然后外层PrettyPrinter会把它原样吸收进去。输出效果就很接近原生容器了。不过老实说这个技巧更偏“救火”用起来也有点绕。大多数情况下我更建议大家直接给业务对象写一个清晰的__repr__然后在打印时用dataclasses.asdict()或者vars()把它们转成字典再打印这样更简单也更稳妥。5.4 与logging模块的结合一个很自然的延伸如果你在日志里记录结构化数据用pformat()几乎是最好的选择。比如logger.info(order created, detail:\n%s, pprint.pformat(order_ctx, width120, sort_dictsFalse))width120是因为日志文件一般可以接受比终端更宽的行sort_dictsFalse是因为你希望日志里保留上下文原本的字段顺序方便对照代码。在这个场景里稍微注意一下如果日志级别是INFO而你的数据结构很大每次即便不输出也会执行pformat()影响性能。最好判断一下logger.isEnabledFor(logging.DEBUG)if logger.isEnabledFor(logging.DEBUG): logger.debug(request: %s, pprint.pformat(payload, depth5))这个小优化在处理超大请求体时能省不少时间。6. 它和json.dumps到底谁更香在讨论 Python 结构化输出时总有人拿json.dumps(indent2)和pprint做对比。这两个工具确实有一部分重叠但核心定位差别很大。6.1 两者核心差异维度pprintjson.dumps数据格式任意 Python 对象含 set、Decimal、dataclass、自定义对象必须是 JSON 可序列化的类型输出目的给人看的调试/展示数据交换、存储、序列化键顺序默认按排序可关闭保持字典插入顺序中文处理直接输出默认ensure_asciiTrue中文转义成\uXXXX递归处理支持深度截断、自引用保护自引用会抛异常循环引用可处理有限深度不支持直接ValueError说人话就是json.dumps的第一目标是“把对象变成 JSON 字符串”而不是“把对象变得好看”。只要数据里有Decimal、有datetime、有自定义对象json.dumps就直接罢工或者要你写一堆default处理函数。而pprint对这些类型宽容得多——它不关心你能不能序列化只关心你能不能看清。6.2 中文和特殊字符的差距我实打实遇到过这个问题。某次联调我用json.dumps(data, ensure_asciiFalse, indent2)打印一个包含中文的配置倒是正常显示中文了但是输出里引号都是双引号键值对之间还有空格看起来像 JSON 格式但又不完全是可读性最好的展示。而pprint遇到中文就直接原样输出不需要任何额外参数。举个例子data {名称: 订单服务, 描述: 处理订单创建与支付回调} pprint.pprint(data) # 输出{名称: 订单服务, 描述: 处理订单创建与支付回调}如果你的项目需要做数据交换、存文件、给前端选json.dumps没错但如果只是为了让自己快速看清一个结构选pprint更省事。6.3 一个综合建议我自己的习惯是要落盘、要传输用json.dumps要观察、要调试、要打日志用pprint。尤其是面对嵌套很深的字典时pprint的递归排版能力比json.dumps(indent2)更加直观因为它能结合depth、compact等参数按需控制展开程度而json.dumps只能全量展开或者容忍巨大的输出。如果你非要用json.dumps调试复杂对象也不是不行配合一个defaultstr的方法可以把Decimal、datetime转成字符串但这样会丢失这些对象的原始类型信息打印出来就不太准确了。相比之下pprint对类型的保真度更高。7. 常见问题与排查技巧实录最后把我在实际使用中遇到的、以及在给同事答疑时被反复问到的问题整理成一份速查清单。7.1 为什么输出的字典键顺序总是“乱”的很多人第一次用pprint会困惑“我定义的时候明明是 A、B、C输出怎么变成了 C、A、B”原因就是sort_dictsTrue默认开启。这个设计初衷是让结构化内容的比较变得更容易。如果你想保持插入顺序设置sort_dictsFalse或者直接用 Python 3.12 的pp()即可。7.2 为什么设置了width40行还是超出了这个我在前面讲过pprint不会把一个不可分割的元素长字符串、长数字强行折断。它宁愿让这一行超出width也不会把元素的内部结构打乱。这种现象在打印长文本、URL、Base64 编码时特别常见不用惊讶这是设计使然。7.3 为什么depth2还是输出了很多内容要理解depth的粒度。它数的不是“层数”而是“递归层级”。容器内的元素属于第 1 层如果元素本身是容器它的内部字段属于第 2 层。超过depth的层会显示...。所以depth2对于一个 10 层的嵌套来说会在很浅的地方就截断输出自然就简练了。如果你发现输出还是很多那可能是因为顶层元素太多——depth限制的是深度不是广度。7.4 为什么自定义对象打印出来是一堆... object at 0x...因为pprint在没有更多信息时只能回退到__repr__()。解决方案有三种在类里实现可读的__repr__用dataclasses.asdict()转换成字典后再打印或者子类化PrettyPrinter重写format()。按成本排序我一般先试第一种不行就第二种真的需要保留复杂嵌套结构时再上第三种。7.5 打印出来的数据太大、太慢怎么办如果你的对象是一棵几百层深的树或者一个包含数万个元素的大列表直接全量pprint可能会卡顿甚至刷屏。这时候优先设置depth限制递归深度其次设置width较小值让折行更早发生也能变相减少单行长度。如果还嫌慢可以考虑只打印关键层级比如先打印顶层键然后手工提取你关心的分支再打印。我用过的一个技巧是写一个递归裁剪函数把不需要的分支塞成...再交给pprint输出效果很好。7.6 写文件时中文乱码如果你用streamopen(out.txt, w)输出记得指定编码为utf-8否则在 Windows 默认编码下中文会乱码。更推荐的做法是直接用pformat()得到字符串然后自己用open(out.txt, w, encodingutf-8)写入对编码控制更清晰。还有一个小习惯如果你是在日志文件里看结构化内容日志系统通常已经配置了编码你只需要关注pformat()产生的内容即可。实际用下来我的整体感受是pprint属于那种“用起来简单、用好了不简单”的工具。它的核心逻辑并不复杂核心就是递归 宽度控制 缩进管理但真正理解那几个参数如何配合才能在各种复杂数据结构面前游刃有余。这里我分享一个自己长期使用的习惯在项目的调试工具模块里封装一个dump()函数固定参数为width100, sort_dictsFalse, depth6然后内部用pformat()接上logger。这样既避免了重复写参数又保证了所有结构化输出的风格统一遇到需要快速确认数据结构的场合一行代码就能搞出清爽的层次来。这个思路你们也可以试试看。