字符串里扒数据:从“假 list“到“一堆 Document“的两种正确姿势
字符串里扒数据:从"假 list"到"一堆 Document"的两种正确姿势
Python 数据处理实战笔记。明明看着像列表、像对象,用起来却只是个字符串?两道题教你安全、规范地把它变回真正的数据。
我们常遇到这种场景:日志、接口返回、LangChain 的Document对象被print或str()变成了一长串看起来像 Python 对象、但本质还是字符串的文本。直接当对象用会报错,硬着头皮正则又容易翻车。下面两题给一套可复用的解法。
第一题:把"像 list"的字符串,变成真正的 list
输入就是一个字符串,内容是["qwen-turbo","qwen-plus","deepseek"]。
❌ 错误直觉:直接当 list 用
它现在类型是str,做for x in raw_str会一个字一个字地遍历,而不是按元素遍历。
✅ 正确做法一:json.loads(最推荐)
只要字符串是标准 JSON 格式(双引号、合法的数组/对象),用json.loads最稳:
importjson raw_str='["qwen-turbo","qwen-plus","deepseek"]'result=json.loads(raw_str)# result = ['qwen-turbo', 'qwen-plus', 'deepseek'],类型已经是 list✅ 正确做法二:ast.literal_eval(更宽容)
字符串是Python 字面量风格(比如用了单引号、或含True/None这类 JSON 不支持的写法)时,用ast.literal_eval。它只解析字面量,不执行代码,比eval安全得多:
importast raw_str='["qwen-turbo","qwen-plus","deepseek"]'result=ast.literal_eval(raw_str)# 同样是真正的 list,且能识别单引号、True/False/None 等⚠️为什么不要用 eval?
eval(raw_str)确实能用,但它会执行字符串里的任意代码。一旦这段字符串来自外部(接口、文件、用户),就等于敞开了一道任意代码执行后门。能用json.loads/ast.literal_eval就绝不用eval。
✅结论:标准 JSON →
json.loads;Python 风格字面量 →ast.literal_eval;两者都能把"假 list"变成真 list。
第二题:从一堆 Document(…) 里,提取每个 page 并拼接内容
这次不是普通列表,而是一整串Document(metadata={...}, page_content='...')被包在[ ]里。我们要拿到每个 Document 的page,再把page_content按页拼接成长文。
核心思路:把它当成"Python 表达式"来解析,而不是当文本硬切
整段字符串其实是一个合法的 Python 表达式:一个列表,里面是多个对Document(...)的"调用"。我们用ast.parse(..., mode='eval')解析它的语法树,再逐层取出每个调用里的关键字参数,最后用ast.literal_eval把metadata和page_content还原成真实数据。
importast raw_docs="""[Document(metadata={'pk': 12, 'page': 2}, page_content='...'), Document(metadata={'pk': 27, 'page': 2}, page_content='...'), Document(metadata={'pk': 14, 'page': 3}, page_content='...')]"""defparse_documents(raw):tree=ast.parse(raw,mode="eval")# 解析成语法树docs=[]ifisinstance(tree.body,ast.List):foreltintree.body.elts:# 遍历列表里的每个 Document(...)ifisinstance(elt,ast.Call):d={}forkwinelt.keywords:# metadata= / page_content=d[kw.arg]=ast.literal_eval(kw.value)docs.append(d)returndocs docs=parse_documents(raw_docs)拿到数据后做什么?
# 1) 提取每个 Document 的 pagepages=[d["metadata"]["page"]fordindocs]# pages = [2, 2, 3]# 2) 按 page 排序,拼接 page_content 还原成完整文档docs_sorted=sorted(docs,key=lambdad:d["metadata"]["page"])full_text="\n".join(d["page_content"]fordindocs_sorted)# 3) 去重:本例中第 2 页出现了两次(pk=12 和 pk=27),按 page 去重避免重复seen,unique=set(),[]fordindocs_sorted:p=d["metadata"]["page"]ifpnotinseen:seen.add(p);unique.append(d)full_text_dedup="\n".join(d["page_content"]fordinunique)💡为什么要"按 page 排序 + 去重"?真实场景里,一个 PDF 可能被切成了多块、多份召回结果里同一页会重复出现。先按
page排序保证正文顺序正确,再按page去重,拼接出的才是干净、连续、不重复的全文。
备选:正则(适合结构极其规整的简单场景)
如果你只想快速拿到 page 数字,正则一行就能解决:
importre pages=[int(x)forxinre.findall(r"'page':\s*(\d+)",raw_docs)]# [2, 2, 3]⚠️正则的坑:一旦
page_content里出现单引号、转义字符或嵌套结构,正则很容易提前截断或匹配错位。所以——只要想拿完整内容,优先用上面的ast方案;正则只适合"只取几个简单数字"的临时查看。
两题的共通心法
| 场景 | 本质 | 推荐工具 | 千万别用 |
|---|---|---|---|
| 像 list 的字符串 | JSON / Python 字面量 | json.loads或ast.literal_eval | eval |
| 一堆 Document(…) | Python 表达式(列表+调用) | ast.parse+ast.literal_eval | 盲目正则 /eval |
✅ 一句话记住:看到"像代码的字符串",先别急着切,先用
ast把它当代码解析——安全、准确、还能顺带拿到嵌套结构里的任何字段(page、pk、content 随便取)。