collection.aggregate() 返回的嵌套结构需类型判断后递归扁平化:遇 dict 按键展开、遇 list 遍历并带下标路径,用 .get() 防 KeyError,ObjectId 转 str,路径用点号分隔,同名字段加前缀防覆盖。

collection.aggregate() 返回的嵌套结构,不能直接当字典用——常见报错 TypeError: string indices must be integers 就是试图用字符串 key 去索引一个 list。扁平化不是“展开展开再展开”,而是得按类型分支、带路径记录、跳过不可处理值。
遇到 list 和 dict 混杂时,必须动态判断类型再取值
聚合结果里常出现 [{"name": "Alice"}, {"name": "Bob"}] 或 {"user": {"profile": {"age": 30}}},甚至更深层混搭。直接写 doc["user"]["profile"]["age"] 在某一层是 list 时就崩。
- 每次访问前先用
isinstance(doc, dict)或isinstance(doc, list)判断类型 - 遇到
list,要遍历每个元素再递归;遇到dict,才继续用 key 取值 - 别假设某个字段一定存在——
KeyError得兜底,比如用doc.get("field", None)
同一字段名在多层重复出现,扁平化后必须加路径前缀
比如 {"user": {"name": "Alice"}, "manager": {"name": "Bob"}},若直接 flatten 成 {"name": "Bob"},上层 user.name 就被覆盖了。
- 递归函数里传一个
path参数,初始为"",每进一层子字段就拼上f"{path}.{key}" - 遇到数组时,路径可带下标,如
"users.0.name",避免同名冲突 - 路径分隔符统一用点号(
.),和 Pandas / JSONPath 习惯一致,后续也方便转 DataFrame
ObjectId、None、缺失键这些值不处理就会中断递归
PyMongo 默认把 _id 字段转成 ObjectId 对象,而它不可 JSON 序列化;空值或字段根本不存在,也会让 doc[key] 报错。
立即学习“Python免费学习笔记(深入)”;
-
ObjectId提前转成字符串:str(obj_id),别等最后 dump 时报TypeError -
None值建议保留(不是跳过),否则统计时会漏样本;但要确保下游能识别None - 用
.get()替代方括号取值,例如doc.get("tags", []),避免因字段缺失中断整个文档处理
用原生递归函数比依赖第三方库更可控
不需要装 flatten-dict 或 jsonpath-ng,一个不到 20 行的函数就能覆盖绝大多数聚合输出场景。
def flatten_doc(doc, path="", result=None):
if result is None:
result = {}
if isinstance(doc, dict):
for k, v in doc.items():
new_path = f"{path}.{k}" if path else k
flatten_doc(v, new_path, result)
elif isinstance(doc, list):
for i, item in enumerate(doc):
new_path = f"{path}.{i}" if path else str(i)
flatten_doc(item, new_path, result)
else:
# str, int, float, bool, None, ObjectId 等叶子节点
if isinstance(doc, ObjectId):
result[path] = str(doc)
else:
result[path] = doc
return result这个函数没做深度限制,但实际聚合结果一般不会超过 10 层;真遇到超深嵌套,加个 depth 参数控制递归上限即可。
真正容易被忽略的是:聚合阶段顺序直接影响扁平化难度。$unwind 放太晚,会导致 list 嵌在 dict 里难处理;$project 提前把关键字段提上来,比全量递归更省事。别总想着“一股脑 flatten 完事”,该在聚合里做的结构整理,就在 pipeline 里做。


















