摘要
开发中经常遇到"看起来像列表/对象的字符串",怎么转成真正的Python对象?本文从三个场景展开:标准JSON字符串转列表、类列表字符串的安全解析、以及RAG项目中Document对象的元数据提取。读完即用。
一、场景一:标准JSON字符串转列表
这是最简单的情况。字符串本身就是合法的JSON数组。
python
import json raw_str = '["qwen-turbo","qwen-plus","deepseek"]' result = json.loads(raw_str) print(result) print(type(result))
输出:
text
['qwen-turbo', 'qwen-plus', 'deepseek'] <class 'list'>
json.loads()一步到位,干净利落。
也可以用ast.literal_eval:
python
import ast raw_str = '["qwen-turbo","qwen-plus","deepseek"]' result = ast.literal_eval(raw_str)
ast.literal_eval更安全,只能解析合法的Python字面量,不会执行恶意代码。不过JSON格式用json.loads更标准。
二、场景二:看起来像列表但实际是对象的字符串
开发中常遇到这种数据——打印出来是[Document(...), Document(...)],看着像列表,实际是一个字符串,不能直接用json.loads解析。
比如RAG检索返回的Document列表被转成字符串打印到日志里:
python
raw_str = '[Document(metadata={"pk": 12, "page": 2}, page_content="内容1"), Document(metadata={"pk": 27, "page": 2}, page_content="内容2")]'目标:提取每个Document的page属性。
解法:正则提取。
python
import re raw_str = '你的长字符串...' # 提取所有page值 pages = re.findall(r"'page':\s*(\d+)", raw_str) pages = [int(p) for p in pages] print(pages)
输出:
text
[2, 2, 3]
如果想同时提取page和page_content:
python
import re doc_pattern = r"Document\(metadata=\{([^}]+)\}, page_content='([^']*)'" matches = re.findall(doc_pattern, raw_str) for metadata, content in matches: page_match = re.search(r"'page':\s*(\d+)", metadata) if page_match: page = int(page_match.group(1)) print(f"page: {page}, content预览: {content[:50]}...")关键点:这种字符串的本质是对象的repr()输出,不是标准序列化格式。正则是最直接的处理方式。
三、场景三:更复杂的Document字符串解析
如果字符串中page_content包含换行符、引号等特殊字符,情况会复杂一些。
比如:
python
raw_str = '[Document(metadata={"page": 1}, page_content="包含\'引号\'和\\n换行的内容")]'简单正则可能匹配失败。这时可以分两步:
先按
Document(分割再用括号匹配提取内容
python
import re def extract_docs_from_str(raw_str): # 按Document(分割 parts = raw_str.split('Document(')[1:] results = [] for part in parts: # 找到metadata部分 meta_match = re.search(r"metadata=\{([^}]+)\}", part) if not meta_match: continue metadata_str = meta_match.group(1) page_match = re.search(r"'page':\s*(\d+)", metadata_str) page = int(page_match.group(1)) if page_match else None # 找到page_content部分(从第一个引号到最后一个引号) content_match = re.search(r"page_content='(.*)'\)?\s*$", part, re.DOTALL) if not content_match: content_match = re.search(r'page_content="(.*)"\)?\s*$', part, re.DOTALL) content = content_match.group(1) if content_match else "" results.append({"page": page, "content": content}) return results注意:对于嵌套引号和转义字符,正则的健壮性有限。如果数据来源可控,建议在数据源头用json.dumps()或pickle序列化,不要用repr()打印后再解析。
四、三种方案的对比
| 场景 | 数据格式 | 推荐方案 |
|---|---|---|
| JSON数组 | ["a","b"] | json.loads() |
| Python字面量 | [1,2,3]或{"a":1} | ast.literal_eval() |
| 对象repr输出 | [Document(...)] | 正则提取 |
五、踩坑提醒
1. 永远不要用eval()
python
# ❌ 危险:可能执行恶意代码 result = eval(raw_str)
eval()会执行字符串中的任意代码。如果数据来自外部输入,攻击者可以注入__import__('os').system('rm -rf /')之类的代码。
2. 区分字符串来源
来自
print()打印的对象 → 用正则来自
json.dumps()→ 用json.loads()来自
str(list)或repr()→ 用正则或ast.literal_eval()(如果格式合法)
3. 单引号和双引号要统一
json.loads()要求双引号。如果字符串是单引号包裹的JSON,先用replace("'", '"')转换(但要注意内容里的引号不会误转)。
六、总结
| 方法 | 适用场景 | 安全性 |
|---|---|---|
json.loads() | 标准JSON | 安全 |
ast.literal_eval() | Python字面量 | 安全 |
| 正则提取 | 对象repr输出 | 安全 |
eval() | 不推荐任何场景 | 危险 |
记住一句话:能用json.loads就用json.loads,不能用就上正则,永远别用eval。