云计算百科
云计算领域专业知识百科平台

Python:字符串伪装列表、字符串序列化对象解析踩坑实战

前 言

在做 AI 应用开发时,我们经常会拿到各种 “看起来是对象,实际是字符串” 的数据:

  • 配置、接口返回:'["qwen-turbo","qwen-plus","deepseek"]',JSON 格式的字符串列表;
  • 日志打印 / 缓存字符串:把List[Document]直接转字符串,得到一长串"[Document(metadata={…}, page_content='…'), …]"。
  • 新手容易踩坑:直接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?

  • 里面是Document(…),这是 Python 类实例,JSON 没有 Document 类型;
  • 字典内部使用单引号{'pk':12},JSON 规范强制双引号; 直接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(…)会报错。

    所以分两条路线:

  • 如果你手上有 Document 类(已经导入 langchain 的 Document),可以结合 ast 做有限解析;
  • 如果只有纯字符串文本,没有 Document 类,我们做正则提取,只拿我们关心的page和page_content。
  • 方案 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

    关键安全原则

  • 但凡字符串来自用户、接口请求、外部网络,永远不要调用eval();
  • 尽量不要依赖str(对象)的输出做解析,打印输出是给人看的,不是用来程序解析;
  • 对象持久化,优先转为标准 JSON 结构,避免后面各种奇葩解析难题。
  • 拓展思考

    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)

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Python:字符串伪装列表、字符串序列化对象解析踩坑实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!