前言:
做 Python 开发,尤其是做 RAG、做数据处理的时候,你肯定遇到过这种场景:
手里拿着一个字符串,长得跟 Python 的列表、对象一模一样,打印出来看着就是个 list、就是个 Document 对象,但就是不能直接用——遍历报错、取属性报错、下标访问也报错。
新手第一反应就是上eval(),结果功能是实现了,但留下了天大的安全漏洞;
稍微懂点的用json.loads(),又各种报错:单引号不行、自定义类不行、括号格式不对…
折腾半天,要么功能实现了但不安全,要么安全了但实现不了。
今天我就把这个问题彻底讲透:从最简单的 JSON 列表字符串,到 LangChain 里的 Document 自定义类字符串,从最基础的用法到生产级最佳实践,再到所有你会踩的坑,全给你讲明白。
看完这篇,以后再遇到「长得像对象的字符串」,你再也不会瞎折腾了。
先看第一题:JSON格式的列表字符串,怎么转成真正的 list?
题目很简单:
raw_str='["qwen-turbo","qwen-plus","deepseek"]'把这个字符串转成 Python 的 list。
1. 正确做法:用 json.loads
这是最标准、最安全、最通用的做法,没有之一。
importjson raw_str='["qwen-turbo","qwen-plus","deepseek"]model_list=json.loads(raw_str)print(type(model_list))# <class 'list'>print(model_list)# ['qwen-turbo', 'qwen-plus', 'deepseek']就这么简单,一行代码搞定。
2. 为什么绝对不要用 eval?
很多新手图省事,直接写:
model_list=eval(raw_str)功能确实能实现,但我劝你永远不要这么写。
为什么?因为eval()会执行字符串里的任何Python代码,如果这个字符串是用户输入的、或者来自不可信的来源,那就是天大的安全漏洞。
举个例子,如果别人传过来的字符串是这样的:
raw_str="__import__('os').system('rm -rf /')"你用 eval 一执行,你服务器的文件就全没了,哭都来不及。
划重点:
只要是处理不可信来源的字符串,永远不要用 eval,这是底线。
安全这根弦,什么时候都不能松。
3. 常见坑:单引号字符串,json.loads 报错怎么办?
很多人会遇到这种情况:字符串里是单引号,不是双引号,比如:
raw_str="['qwen-turbo', 'qwen-plus', 'deepseek']"这时候用json.loads()会直接报错,因为 JSON 标准要求字符串必须用双引号,单引号是不合法的。
这时候有两种解决方法:
方法一:替换单引号为双引号(简单场景可用)
importjson raw_str="['qwen-turbo', 'qwen-plus', 'deepseek']"model_list=json.loads(raw_str.replace("'",'"'))适合简单的列表、字典,里面没有嵌套的引号,不会替换错。
如果字符串内容本身就包含引号,就别用这个方法,容易把内容里的引号也替换了,导致格式错。
方法二:用 ast.literal_eval(推荐,安全)
ast.literal_eval是 Python 标准库提供的,专门用来解析 Python 字面量的字符串,比 eval 安全得多,它只会解析字面量(列表、字典、字符串、数字这些),不会执行任何代码,绝对安全。
importast raw_str="['qwen-turbo', 'qwen-plus', 'deepseek']"model_list=ast.literal_eval(raw_str)print(type(model_list))# <class 'list'>这个方法既安全,又能兼容 Python 风格的单引号字符串,是处理内置类型字符串的首选。
大佬经验:
处理列表、字典、数字、字符串这些 Python 内置类型的字符串,优先用ast.literal_eval,安全又好用,比 json.loads 兼容性好,比 eval 安全一万倍。
重点:第二题,自定义类的 repr 字符串,怎么拿属性?
这道题才是真正的高频痛点,尤其是做 LangChain、做 RAG 的同学,90% 都遇到过:
你打印检索结果,输出是这样的:
"[Document(metadata={'pk': 12, 'page': 2}, page_content='...'), Document(metadata={'pk': 27, 'page': 2}, page_content='...')]"看着就是个 Document 对象的列表,但它是个字符串,你想拿里面的page属性,怎么都拿不到,急死人。
先搞懂本质:这不是 JSON,是对象的 repr 字符串
首先你要明白:这个字符串是 Python 对象调用str()或者repr()生成的,是给人看的,不是给程序解析的。
它不是标准 JSON 格式,也不是标准的序列化格式,只是一个可读的字符串表示,本来就不是用来反序列化的。
所以你用json.loads()肯定不行,因为里面有Document(...)这种语法,JSON 根本不认识。
下面我给你讲四种方案,从最不推荐到最佳实践,挨个讲,你根据自己的场景选。
方案一:用 eval(最不推荐,绝对不要用在生产)
最简单粗暴的方法,直接 eval:
fromlangchain_core.documentsimportDocument raw_str="[Document(metadata={'pk': 12, 'page': 2}, page_content='...')]"doc_list=eval(raw_str)print(doc_list[0].metadata["page"])# 2功能是实现了,但还是那个问题:不安全。
如果字符串里有恶意代码,eval 会直接执行,生产环境绝对不能这么写,自己本地调试图省事可以用,上线绝对不行。
方案二:ast.literal_eval(也不行,解析不了自定义类)
很多人会说,那我用安全的ast.literal_eval行不行?
不行。
因为ast.literal_eval只能解析 Python 的内置字面量类型:列表、字典、字符串、数字、元组、集合、布尔值、None 这些。Document是自定义类,它根本不认识,会直接报错。
importast raw_str="[Document(metadata={'pk': 12, 'page': 2}, page_content='...')]"ast.literal_eval(raw_str)# 报错:ValueError: malformed node or string on line 1: <ast.Call object at 0x10xxx>所以这个方案对自定义类无效,pass。
方案三:正则提取(推荐,只需要个别字段的时候用)
如果你的需求很简单:不需要把整个 Document 对象还原出来,只是想拿里面的某几个字段,比如page、pk这些。
那最简单、最高效、最安全的方法,就是用正则直接提取。
根本不需要把整个字符串解析成对象,你要什么就提什么,简单粗暴,还不会有安全问题。
比如我们要提取所有的page属性:
importre raw_str="[Document(metadata={'pk': 12, 'page': 2}, page_content='...'), Document(metadata={'pk': 27, 'page': 2}, page_content='...'), Document(metadata={'pk': 14, 'page': 3}, page_content='...')]"# 正则匹配 'page': 数字page_pattern=r"'page':\s*(\d+)"pages=re.findall(page_pattern,raw_str)# 转成整数pages=[int(p)forpinpages]print(pages)# [2, 2, 3]就这么几行代码,直接拿到所有 page 值,又快又安全。
如果要提取 pk 和 page 两个字段,也很简单:
pattern=r"'pk':\s*(\d+),\s*'page':\s*(\d+)"matches=re.findall(pattern,raw_str)result=[{"pk":int(pk),"page":int(page)}forpk,pageinmatches]print(result)# [{'pk': 12, 'page': 2}, {'pk': 27, 'page': 2}, {'pk': 14, 'page': 3}]大佬经验:
很多人一遇到这种问题,就想着怎么把整个对象解析出来,其实完全没必要。
你只需要一两个字段,用正则直接提就行,代码量少、性能高、还安全,是性价比最高的方案。
不要为了「优雅」而搞复杂的方案,能解决问题的就是好方案。
方案四:从根源解决(最佳实践,生产级方案)
上面的方案都是「亡羊补牢」,真正的最佳实践,是从一开始就不要把对象转成 repr 字符串去存储或传输。
你之所以会遇到这个问题,本质上是因为你用了错误的序列化方式:把对象用str()转成了字符串存起来了,这本来就不是用来干这个的。
正确的做法是:用标准的序列化方式,存的时候就存成可反序列化的格式,用的时候直接还原,根本不用搞什么字符串解析。
方法1:给自定义类加 to_dict / from_dict 方法,用JSON序列化
这是最通用的方法,任何自定义类都可以这么做:
fromlangchain_core.documentsimportDocumentimportjson# 存的时候:转成字典,再转JSON字符串defdoc_to_json(doc_list):returnjson.dumps([doc.dict()fordocindoc_list],ensure_ascii=False)# 取的时候:JSON转字典,再转成Document对象defjson_to_doc(json_str):doc_dicts=json.loads(json_str)return[Document(**d)fordindoc_dicts]LangChain 的 Document 本身就自带dict()方法,可以直接转成字典,非常方便。
存的时候存 JSON 字符串,用的时候直接转回来,标准、安全、跨语言都能用。
方法2:用 pickle 序列化(Python专属,可信环境可用)
如果只是 Python 内部用,不需要跨语言,也可以用 pickle 序列化:
importpickle# 序列化:对象转字节doc_bytes=pickle.dumps(doc_list)# 反序列化:字节转对象doc_list=pickle.loads(doc_bytes)优点是几乎所有 Python 对象都能序列化,不用自己写转换方法;
缺点是不安全,pickle 也能执行代码,所以只能用在完全可信的环境,不可信来源的 pickle 数据绝对不要加载。
划重点:
生产环境优先用 JSON 序列化,通用、安全、好调试;
只有内部可信环境、JSON 搞不定的复杂对象,才考虑 pickle。
RAG 场景的实战建议
做 RAG 的同学,这一条一定要记住:
永远不要把检索到的 Document 列表转成字符串存到数据库或者缓存里。
很多人图省事,直接str(documents)存进去,等要拿出来用的时候,就傻了,解析都没法解析。
正确的做法:
- 如果只需要存文本和元数据:转成字典列表,存 JSON 格式,用的时候再转成 Document
- 如果要完整保留对象:用 pickle 序列化,存二进制,或者用专门的对象存储
- 调试的时候打印看看没问题,但绝对不要把 repr 字符串当持久化格式
我见过太多人犯这个错,一开始图省事,存了 str 字符串,后面要加功能、要取元数据的时候,折腾半天解析不出来,最后还是得改存储格式,返工成本极高。
从一开始就用对序列化方式,后面省超多事。
避坑清单:这些坑90%的人都踩过
1. 永远不要用 eval 处理不可信字符串
这是铁律,没有任何商量的余地。
不管是用户输入的、接口传过来的、数据库读出来的,只要是不可信来源的字符串,绝对不能用 eval。
安全无小事,别等出事了才后悔。
2. 内置类型字符串优先用 ast.literal_eval
处理列表、字典、数字这些内置类型的字符串,用ast.literal_eval,比 json.loads 兼容性好,比 eval 安全,是首选。
3. 只需要个别字段,用正则就够了
别一上来就想着怎么把整个对象解析出来,你只需要一两个字段,用正则提取就行,简单高效,还没安全问题。
不要为了所谓的「优雅」搞复杂方案,能解决问题的就是最好的方案。
4. 自定义对象不要靠解析 repr 还原,从源头做好序列化
repr 是给人看的,不是给程序解析的,不要指望靠解析 repr 来还原对象。
从一开始就用 JSON 或者 pickle 做序列化,从根源上解决问题,比什么都强。
5. 搞清楚数据格式,别用错工具
JSON 是 JSON,Python repr 是 Python repr,是两回事,别拿着 json.loads 去解析 repr 字符串,那肯定报错。
先搞清楚你手里的字符串是什么格式,再选对应的工具,事半功倍。
给新手的 4 条实战心法
1. 解决问题选最简单的方案,不要过度设计
你只需要拿个 page 字段,就用正则,没必要把整个 Document 都解析出来。
简单的方案代码少、bug 少、维护成本低,比什么都强。
不要为了炫技搞复杂的东西,能解决问题的就是好方案。
2. 安全永远是第一位的,eval 能不用就不用
很多新手觉得「我这就是个小项目,没人会攻击我」,这种想法最危险。
安全漏洞都是出其不意的,养成良好的习惯,从一开始就写安全的代码,比什么都重要。
3. 序列化要从源头做好,不要亡羊补牢
不要等把对象转成字符串了,才想着怎么解析回来。
从设计存储结构的时候,就选好正确的序列化方式,后面所有问题都不会出现。
上游做好了,下游就不用擦屁股。
4. 搞懂本质,而不是记答案
遇到问题不要只抄代码,要搞懂背后的本质:
为什么 json.loads 不行?因为不是 JSON 格式;
为什么 eval 不安全?因为会执行任意代码;
为什么正则可以?因为你只需要提取固定模式的内容。
搞懂了本质,以后遇到类似问题,你自己就能想出解决方案,而不是每次都要搜。
最后
字符串转对象这个问题,说难不难,说简单也不简单,新手很容易踩坑。
但只要你搞懂了本质,选对了方法,其实就是一两行代码的事。
记住:简单、安全、高效,永远是写代码的第一原则。
如果文章对你有帮助,欢迎点赞收藏,有问题评论区交流。