在 python 开发中,我们经常会遇到看起来像 list,但实际是 str 的数据。本文通过两道实战题目,带你掌握两种不同场景下的解析方法。
一、引言
你是否遇到过这样的情况:
data = '["qwen-turbo","qwen-plus","deepseek"]' print(type(data)) # <class 'str'> ???
明明看起来是个列表,为什么类型是字符串?
更头疼的是,有时候字符串里还包含自定义对象,比如 langchain 的 document:
data = "[document(metadata={'pk': 12, 'page': 2}, page_content='...')]"
这两种情况,解析方法完全不同!本文通过两道题,带你彻底搞懂。
二、第一题:标准格式字符串转列表
2.1 题目
raw_str = '["qwen-turbo","qwen-plus","deepseek"]' # 将其转换为 list
2.2 分析
这是一个标准的 json 格式字符串,内容是字符串数组。
2.3 解决方案
方法一:json.loads()(推荐)
import json raw_str = '["qwen-turbo","qwen-plus","deepseek"]' result = json.loads(raw_str) print(result) # ['qwen-turbo', 'qwen-plus', 'deepseek'] print(type(result)) # <class 'list'>
优点:速度快,标准库支持,json 格式通用性强
方法二:ast.literal_eval()
import ast raw_str = '["qwen-turbo","qwen-plus","deepseek"]' result = ast.literal_eval(raw_str) print(result) # ['qwen-turbo', 'qwen-plus', 'deepseek'] print(type(result)) # <class 'list'>
优点:安全性高,只解析字面量,不会执行恶意代码
方法三:eval()(不推荐)
raw_str = '["qwen-turbo","qwen-plus","deepseek"]' result = eval(raw_str) print(result) # ['qwen-turbo', 'qwen-plus', 'deepseek']
缺点:有安全风险,会执行任意 python 代码,生产环境慎用!
2.4 三种方法对比
| 方法 | 速度 | 安全性 | 适用场景 |
|---|---|---|---|
json.loads() | ⭐⭐⭐ 最快 | ⭐⭐ 安全 | json 格式字符串 |
ast.literal_eval() | ⭐⭐ 一般 | ⭐⭐⭐ 最安全 | python 字面量格式 |
eval() | ⭐⭐ 一般 | ⭐ 危险 | 不推荐使用 |
三、第二题:对象格式字符串提取字段
3.1 题目
raw_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. 最后警告:情节严重,留司察看...')]"""
# 问题:如何获取里面的 page 属性?
3.2 分析
这个字符串包含 langchain 的 document 对象,它不是标准 json,而是 python 的 repr() 输出格式。
为什么 json.loads() 不行?
import json json.loads(raw_str) # ❌ 报错!json.decoder.jsondecodeerror
原因:
- 字符串中使用了单引号
',json 要求双引号" - 包含
document(...)这种自定义对象,不是 json 支持的数据类型
为什么 ast.literal_eval() 也不行?
import ast ast.literal_eval(raw_str) # ❌ 报错!valueerror: malformed node
原因:ast.literal_eval() 只能解析 python 字面量(字符串、数字、列表、字典等),不能解析函数调用 document(...)。
3.3 解决方案:正则表达式
import re
from collections import defaultdict
raw_str = "[document(metadata={'pk': 12, 'page': 2}, page_content='...'), ...]"
# 方法1:只提取 page
pages = re.findall(r"'page':\s*(\d+)", raw_str)
pages = [int(p) for p in pages]
print(pages) # [2, 2, 3]
# 方法2:提取 page + page_content
docs = re.findall(
r"document\(metadata=\{.*?'page':\s*(\d+).*?page_content='(.*?)'\)",
raw_str,
re.dotall
)
# 按页码分组
page_contents = defaultdict(list)
for page, content in docs:
page_contents[int(page)].append(content)
# 按页码排序输出
for page in sorted(page_contents.keys()):
combined = "\n".join(page_contents[page])
print(f"=== 第 {page} 页 ===")
print(combined)
print()
3.4 正则表达式解析
r"'page':\s*(\d+)"
| 部分 | 含义 |
|---|---|
'page': | 匹配字面量 'page': |
\s* | 匹配 0 或多个空白字符 |
(\d+) | 捕获 1 或多个数字(这是我们想要的) |
r"document\(metadata=\{.*?'page':\s*(\d+).*?page_content='(.*?)'\)"
| 部分 | 含义 |
|---|---|
document\( | 匹配 document( |
metadata=\{ | 匹配 metadata={ |
.*? | 非贪婪匹配任意字符 |
'page':\s*(\d+) | 捕获 page 的值 |
page_content='(.*?)' | 捕获 page_content 的内容 |
\) | 匹配 ) |
四、完整代码
4.1 第一题完整代码
import json
import ast
def parse_json_list(raw_str: str) -> list:
"""解析 json 格式的字符串列表"""
return json.loads(raw_str)
def parse_python_list(raw_str: str) -> list:
"""解析 python 字面量格式的字符串列表"""
return ast.literal_eval(raw_str)
# 测试
raw_str = '["qwen-turbo","qwen-plus","deepseek"]'
result1 = parse_json_list(raw_str)
result2 = parse_python_list(raw_str)
print(f"json.loads: {result1}")
print(f"ast.literal_eval: {result2}")
4.2 第二题完整代码
import re
from collections import defaultdict
def extract_pages(raw_str: str) -> list:
"""从 document 字符串中提取所有 page"""
return [int(p) for p in re.findall(r"'page':\s*(\d+)", raw_str)]
def extract_docs_by_page(raw_str: str) -> dict:
"""从 document 字符串中提取 page 和 page_content,按页分组"""
docs = re.findall(
r"document\(metadata=\{.*?'page':\s*(\d+).*?page_content='(.*?)'\)",
raw_str,
re.dotall
)
page_contents = defaultdict(list)
for page, content in docs:
page_contents[int(page)].append(content)
return dict(page_contents)
def combine_by_page(raw_str: str) -> str:
"""按页码拼接所有内容"""
page_contents = extract_docs_by_page(raw_str)
result = []
for page in sorted(page_contents.keys()):
combined = "\n".join(page_contents[page])
result.append(f"=== 第 {page} 页 ===\n{combined}")
return "\n\n".join(result)
# 测试
raw_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. 最后警告...')]"""
print("提取的页码:", extract_pages(raw_str))
print("\n按页拼接结果:")
print(combine_by_page(raw_str))
五、对比总结
| 对比项 | 第一题(纯数据) | 第二题(对象格式) |
|---|---|---|
| 字符串格式 | json 标准格式 | python repr 格式 |
| 数据类型 | 基本类型(字符串、数字) | 自定义对象(document) |
| 引号类型 | 双引号 " | 单引号 ' |
| 能否用 json.loads() | ✅ 能 | ❌ 不能 |
| 能否用 ast.literal_eval() | ✅ 能 | ❌ 不能 |
| 能否用 eval() | ✅ 能 | ⚠️ 需要 import document |
| 推荐方案 | json.loads() | re.findall() |
选型建议
遇到字符串形式的"列表"?
│
├─ 是标准 json 格式? → json.loads()
│
├─ 是 python 基本字面量? → ast.literal_eval()
│
└─ 包含自定义对象? → 正则表达式提取
六、常见问题
q1:为什么不推荐 eval()?
eval() 会执行任意 python 代码,存在安全风险:
# 恶意输入
malicious = "__import__('os').system('rm -rf /')"
eval(malicious) # 💀 危险!
q2:正则表达式太难了,有更简单的方法吗?
如果字符串格式固定,也可以用字符串分割:
# 简单粗暴的方法
pages = []
for part in raw_str.split("'page': "):
if part and part[0].isdigit():
pages.append(int(part.split(",")[0]))
但正则更通用、更可靠。
q3:langchain 的 document 对象为什么不是标准 json?
因为 document 是 python 类,它的 repr() 输出是 python 语法,不是 json。这是 python 对象序列化的常见问题。
七、总结
- 标准格式用标准工具:json 字符串用
json.loads() - 复杂格式用正则:包含自定义对象的字符串,正则是最佳选择
- 安全第一:避免使用
eval(),除非你能完全信任输入源
掌握这两种技巧,以后再遇到"看起来像 list 的字符串",就不会懵了!
到此这篇关于python中两种“伪列表“字符串的解析技巧详解的文章就介绍到这了,更多相关python字符串解析内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论