前 言
在做 AI 应用开发时,我们经常会拿到各种 “看起来是对象,实际是字符串” 的数据:
新手容易踩坑:直接eval()、直接split()暴力切割,要么有安全漏洞,要么解析不稳定。下面两个案例由浅入深分析。
案例一:JSON 格式字符串转 List
输入字符串:
raw_str ='["qwen-turbo","qwen-plus","deepseek"]'
这个字符串是标准 JSON 数组字符串。
❌ 错误写法 1:直接 split 分割
# 不推荐!如果字符串内部包含逗号、引号直接崩掉
res = raw_str.strip('[]').split(',')
问题:一旦元素内部出现逗号、转义引号,分割直接出错,鲁棒性极差。
❌ 错误写法 2:eval 执行
res = eval(raw_str)
虽然这里可以跑通,eval 极度危险:如果字符串来源是用户输入,传入恶意代码,会直接执行任意系统命令,生产环境严禁使用。
✅ 标准方案:使用json.loads()
import json
raw_str ='["qwen-turbo","qwen-plus","deepseek"]'
model_list: list = json.loads(raw_str)
print(model_list)
# ['qwen-turbo', 'qwen-plus', 'deepseek']
print(type(model_list)) # <class 'list'>
💡知识点:
- json.loads():安全的 JSON 字符串反序列化,只解析 JSON 数据,不会执行代码;
- json.dumps():对象转为 JSON 字符串。
边界提醒:JSON 字符串必须是双引号,单引号的字符串 json 库会报错。如果遇到单引号 JSON,需要特殊处理,不要直接丢给 eval。
案例二:字符串化的 LangChain Document 对象,提取 page
问题背景: 当你执行str([Document(…), Document(…)]),就会得到题目中的这一大段字符串。 注意:这不是 JSON!这是 Python 对象打印输出的文本表示。
"[Document(metadata={'pk': 12, 'page': 2}, page_content='xxx'), Document(…)]"
我们的目标:提取每一个 Document 里面metadata["page"]页码,并且拼接page_content文本。
❌ 为什么不能用 json.loads?
⚠️ 不推荐方案:eval
# 仅本地调试可以看效果,生产绝对禁止!
doc_str = """[Document(metadata={'pk': 12, 'page': 2}, page_content='xxx'), …]"""
docs = eval(doc_str)
- 优点:一行代码直接还原List[Document]对象;
- 致命风险:如果字符串来自外部输入,攻击者可以注入恶意 Python 代码,服务器直接沦陷。
适用场景:仅限本地调试、自己生成的日志字符串;接口、用户输入绝对禁用 eval。
✅ 方案 A:安全解析 —— pyparsing /ast.literal_eval(推荐)
ast.literal_eval 是安全版本的 eval,只解析 Python 字面量:列表、字典、对象调用形式,不会执行函数和恶意代码。 但是注意:ast.literal_eval可以解析Dict、List,但是不能直接实例化 Document 类,遇到Document(…)会报错。
所以分两条路线:
方案 A‑1:正则提取(只拿数据,不需要还原 Document 对象,最稳妥)
不需要导入 langchain,不实例化对象,正则把page数字、page_content内容提取出来。
import re
doc_str = """[Document(metadata={'pk': 12, 'page': 2}, page_content='2. 费⽤报销:…'), Document(metadata={'pk': 27, 'page': 2}, page_content='2. 费⽤报销:…'), Document(metadata={'pk': 14, 'page': 3}, page_content='3. 最后警告:…')]"""
# 正则匹配 metadata={'xxx','page': N},提取page数字
page_pattern = re.compile(r"metadata=\\{.*?'page':\\s*(\\d+)")
# 提取page_content的文本
content_pattern = re.compile(r"page_content='(.*?)'\\)", re.S)
pages = page_pattern.findall(doc_str)
contents = content_pattern.findall(doc_str)
# pages是字符串数字,转int
page_nums = [int(p) for p in pages]
full_text = "\\n".join(contents)
print("提取到页码列表:", page_nums)
print("拼接后的全部文档文本:\\n", full_text)
- 优点:零依赖、安全,不怕恶意输入,不需要导入 Document 类;
- 缺点:字符串格式如果发生变化(空格、换行)需要微调正则;适合日志、缓存字符串解析场景。
方案 A‑2:受控 eval /ast.literal_eval + Document 类(仅限可信内部数据源)
⚠️ 前提:字符串来源是程序自己打印输出的日志,不是用户输入。
from langchain_core.documents import Document
import ast
raw_s = """[Document(metadata={'pk': 12, 'page': 2}, page_content='demo'),Document(metadata={'pk': 14, 'page':3}, page_content='demo2')]"""
# ast.literal_eval 无法直接处理Document(…),所以eval在这里,仅限可信数据源
docs = eval(raw_s)
page_list = [doc.metadata["page"] for doc in docs]
all_text = "\\n".join([doc.page_content for doc in docs])
print(page_list)
print(all_text)
💡重要提醒:
ast.literal_eval不支持类实例化表达式Document(),所以这种打印出来的对象字符串,ast.literal_eval会抛异常,这种场景下 ast 不能替代 eval。 记住分界线:只要文本来自外部用户输入,坚决不能 eval。
✅ 最佳工程实践:不要把str(list[Document])当做持久化存储
很多人踩坑根源:把str(docs)直接存数据库、缓存。这是错误做法! LangChain 原生提供序列化方法:
from langchain_core.documents import Document
docs: list[Document] = […]
# 对象转可序列化字典列表
dict_list = [doc.model_dump() for doc in docs]
# 保存为json字符串
import json
json_str = json.dumps(dict_list, ensure_ascii=False)
# 还原回来
loaded_dicts = json.loads(json_str)
restore_docs = [Document(**item) for item in loaded_dicts]
✅ 存的时候转标准 JSON,读取的时候再重建 Document 对象,从根源避免这种诡异的 “对象打印字符串”。
这才是生产环境标准做法,不要把对象 print 出来的文本拿来解析。
核心总结对照表
| 标准 JSON 数组字符串 | '["a","b"]' | json.loads() | eval、split 粗暴分割 |
| Python 打印输出对象字符串 [Document(…)] | str([Document()]) | 1. 优先使用 model_dump 做标准 JSON 序列化;2. 本地调试可信数据用 eval;3. 外部来源使用正则提取 | 用户输入下使用 eval |
关键安全原则
拓展思考
1️⃣ 什么时候用ast.literal_eval? 当你拿到的字符串是单引号的 python 字面量,比如"{'name':'test'}",不是标准 JSON,可以使用ast.literal_eval安全解析字典,它可以处理单引号字典,且不会执行代码。
import ast
d = ast.literal_eval("{'page': 2}")
2️⃣ RAG 开发中很多坑都来自序列化:向量库返回 Document、LLM 输出字符串、缓存数据,一定要做好边界处理。
完整可运行代码汇总
# ========== 第一题 标准JSON字符串转list ==========
import json
raw_str ='["qwen-turbo","qwen-plus","deepseek"]'
model_list = json.loads(raw_str)
print("第一题结果:", model_list)
# ========== 第二题 字符串化Document,正则提取page和内容 ==========
import re
doc_str = """[Document(metadata={'pk': 12, 'page': 2}, page_content='2. 费⽤报销: 业务活动中产⽣的费⽤,须凭真 实、合规的发票,并写明事由,经审批后⽅可报销。严禁虚报、\\\\n谎报费⽤。\\\\n第五章 奖惩制度 \\\\n第13条 奖励\\\\n对于符合下列条件的员⼯,公司将视情况给予通报表扬、奖⾦、晋升等奖励:\\\\n1. 超额完成销售指标,业绩突出者。\\\\n2. 提出合理化建议,经采纳后为公司带来显著效益者。\\\\n3. 在维护公司利益和声誉⽅⾯有重 ⼤贡献者。'), Document(metadata={'pk': 27, 'page': 2}, page_content='2. 费⽤报销: 业务活动中产⽣的费⽤,须凭真实、合规的发票,并写明事由,经审批后⽅可报销。严禁虚报、\\\\n谎报费⽤。\\\\n第五章 奖惩制度 \\\\n第13条 奖励\\\\n对于符合下列条件的员⼯,公司将视情况给予通报表扬、奖⾦、晋升等奖励:\\\\n1. 超额完成销售指标,业绩突出者。\\\\n2. 提出合理化建议,经采纳后为公司带来显著效益者。\\\\n3. 在维护公司利益和 声誉⽅⾯有重⼤贡献者。'), Document(metadata={'pk': 14, 'page': 3}, page_content='3. 最后警告: 情节严重,留司察看。\\\\n4. ⽴即解雇: 对于严重违纪者,公司有权单⽅⾯⽆条件解除劳动合同,⽆需⽀付经济补偿⾦。严重违纪包括但\\\\n不限于:\\\\n贪污、受贿、泄露核⼼商业秘密。\\\\n虚报业绩、伪造销售记录或报销凭证。\\\\n⻓期旷⼯(连续三天或⽉累计五天)。\\\\n严重失职,给公司造成重⼤经济损失或声誉损害。\\\\n违反国家法律法规,被依法追究刑事责任。\\\\n第六章 附则 \\\\n第15条 守则的修订与解释')]"""
page_pattern = re.compile(r"metadata=\\{.*?'page':\\s*(\\d+)")
content_pattern = re.compile(r"page_content='(.*?)'\\)", re.S)
page_nums = [int(x) for x in page_pattern.findall(doc_str)]
content_list = content_pattern.findall(doc_str)
concat_text = "\\n".join(content_list)
print("\\n第二题提取页码:", page_nums)
print("\\n拼接后的文档内容:")
print(concat_text)
# ========== 生产环境正确存储Document示范 ==========
from langchain_core.documents import Document
# 模拟原始document对象
docs = [
Document(metadata={"page":1}, page_content="文档1"),
Document(metadata={"page":2}, page_content="文档2"),
]
# 对象转可序列化字典
dump_data = [d.model_dump() for d in docs]
json_text = json.dumps(dump_data, ensure_ascii=False)
print("\\n✅生产环境序列化结果:", json_text)
# 恢复
recover_docs = [Document(**item) for item in json.loads(json_text)]
print("✅恢复后的对象:", recover_docs)
网硕互联帮助中心





评论前必须登录!
注册