
(专栏:Python 从真零基础到纯文本 LLM 全栈实战・第 8 篇 | 字数:10000 字 | 零基础友好 | LLM 场景深度绑定 | 代码可运行)
你有没有过这样的经历?
这些无关内容、格式符号、特殊字符就是 LLM 语料中的 “噪声”,它们会严重影响 LLM 的训练效果和推理质量。而Python 正则表达式(Regex)就是清洗 LLM 噪声语料的 “手术刀”—— 它能帮你:
本文将从LLM 噪声语料的真实场景出发,系统讲解 Python 正则表达式的核心技术,并结合电商评论清洗、用户输入预处理、LLM 生成结果过滤等实战需求给出代码示例。
正则表达式是一种用于匹配字符串的模式,它由普通字符(如字母、数字)和特殊字符(如*、.、[]等)组成,用于从字符串中查找、替换或提取符合模式的内容。
字符 | 含义 | 示例 | ||
|---|---|---|---|---|
. | 匹配任意单个字符(除换行符外) | a.b匹配aab、a1b、a@b等 | ||
^ | 匹配字符串的开头 | ^abc匹配abcdef,不匹配xabc | ||
$ | 匹配字符串的结尾 | abc$匹配xyzabc,不匹配abcd | ||
* | 匹配前面的字符 0 次或多次 | a*b匹配b、ab、aab、aaab等 | ||
+ | 匹配前面的字符 1 次或多次 | a+b匹配ab、aab、aaab等,不匹配b | ||
? | 匹配前面的字符 0 次或 1 次 | a?b匹配b、ab,不匹配aab | ||
[] | 匹配字符集中的任意一个字符 | [a-z]匹配任意小写字母 | ||
[^] | 匹配不在字符集中的任意一个字符 | [^0-9]匹配任意非数字字符 | ||
() | 分组,提取匹配的内容 | (a+b)匹配aab并将其分组 | ||
` | ` | 或,匹配左边或右边的模式 | `a | b匹配a或b` |
rePython 通过re模块支持正则表达式,以下是常用的方法:
re.match():从字符串开头匹配re.search():在整个字符串中匹配第一个符合模式的内容re.findall():在整个字符串中匹配所有符合模式的内容,返回列表re.sub():替换字符串中符合模式的内容re.split():根据模式分割字符串re.match():从开头匹配import re
# 匹配以"Hello"开头的字符串
pattern = r"^Hello"
result = re.match(pattern, "Hello LLM!")
print(result) # 输出:<re.Match object; span=(0, 5), match='Hello'>
print(result.group()) # 输出:Hello
result2 = re.match(pattern, "Hi LLM!")
print(result2) # 输出:Nonere.search():匹配整个字符串import re
# 匹配字符串中的"LLM"
pattern = r"LLM"
result = re.search(pattern, "Hello LLM! Welcome to LLM world!")
print(result) # 输出:<re.Match object; span=(6, 9), match='LLM'>
print(result.group()) # 输出:LLMre.findall():匹配所有符合模式的内容import re
# 匹配字符串中的所有数字
pattern = r"\d+" # \d匹配数字,+匹配1次或多次
result = re.findall(pattern, "苹果15手机壳29.9元,华为Mate60手机壳39.9元")
print(result) # 输出:['15', '29', '9', '60', '39', '9']re.sub():替换符合模式的内容import re
# 替换字符串中的"LLM"为"大语言模型"
pattern = r"LLM"
result = re.sub(pattern, "大语言模型", "Hello LLM! Welcome to LLM world!")
print(result) # 输出:Hello 大语言模型! Welcome to 大语言模型 world!re.split():分割字符串import re
# 根据数字分割字符串
pattern = r"\d+"
result = re.split(pattern, "苹果15手机壳29.9元")
print(result) # 输出:['苹果', '手机壳', '.', '元']如果要匹配特殊字符本身,需要使用\转义。
import re
# 匹配字符串中的"."
pattern = r"\." # 转义"."
result = re.findall(pattern, "29.9元")
print(result) # 输出:['.']
# 匹配字符串中的"^"
pattern = r"\^"
result = re.findall(pattern, "Hello ^LLM!")
print(result) # 输出:['^']Python 正则表达式提供了预定义字符类,用于匹配常见的字符类型:
字符类 | 含义 |
|---|---|
\d | 匹配任意数字(0-9) |
\D | 匹配任意非数字 |
\w | 匹配任意字母、数字或下划线 |
\W | 匹配任意非字母、数字或下划线 |
\s | 匹配任意空白字符(空格、制表符、换行符等) |
\S | 匹配任意非空白字符 |
import re
# 匹配字符串中的所有空白字符
pattern = r"\s+"
result = re.sub(pattern, " ", "Hello \n\t LLM!")
print(result) # 输出:Hello LLM!
# 匹配字符串中的所有字母和数字
pattern = r"\w+"
result = re.findall(pattern, "Hello @#$ LLM! 2024")
print(result) # 输出:['Hello', 'LLM', '2024']可以使用 **{m,n}** 指定匹配的重复次数:
{m}:匹配 m 次{m,}:匹配至少 m 次{,n}:匹配最多 n 次{m,n}:匹配 m 到 n 次import re
# 匹配3位数字
pattern = r"\d{3}"
result = re.findall(pattern, "123 4567 89")
print(result) # 输出:['123', '456']
# 匹配2到4位数字
pattern = r"\d{2,4}"
result = re.findall(pattern, "12 345 6789 0")
print(result) # 输出:['12', '345', '6789']使用 **()** 分组,提取匹配的内容。
import re
# 提取邮箱地址的用户名和域名
pattern = r"(\w+)@(\w+\.\w+)"
result = re.match(pattern, "test@example.com")
print(result.group()) # 输出:test@example.com
print(result.group(1)) # 输出:test
print(result.group(2)) # 输出:example.com
# 提取所有邮箱的用户名
pattern = r"(\w+)@\w+\.\w+"
result = re.findall(pattern, "test1@example.com test2@gmail.com")
print(result) # 输出:['test1', 'test2']在 LLM 开发中,噪声语料主要包括以下几类:
★★★★★ 苹果15手机壳质量很好!\n
【中评】物流有点慢\n
★★★ 材质一般\n
【好评】支持无线充电\n
此用户没有填写评价内容\nimport re
# 原始语料
raw_corpus = [
"★★★★★ 苹果15手机壳质量很好!\n",
"【中评】物流有点慢\n",
"★★★ 材质一般\n",
"【好评】支持无线充电\n",
"此用户没有填写评价内容\n"
]
# 清洗函数
def clean_comment(comment):
# 去除星级评分
comment = re.sub(r"★+", "", comment)
# 去除评论标签
comment = re.sub(r"【.*?】", "", comment)
# 去除空白字符
comment = re.sub(r"\s+", "", comment)
# 去除冗余评价内容
if comment == "此用户没有填写评价内容":
return None
return comment
# 批量清洗
clean_corpus = [clean_comment(comment) for comment in raw_corpus if clean_comment(comment)]
print("清洗前语料:")
for comment in raw_corpus:
print(comment, end="")
print("\n清洗后语料:")
for comment in clean_corpus:
print(comment)清洗前语料:
★★★★★ 苹果15手机壳质量很好!
【中评】物流有点慢
★★★ 材质一般
【好评】支持无线充电
此用户没有填写评价内容
清洗后语料:
苹果15手机壳质量很好!
物流有点慢
材质一般
支持无线充电"@AI助手 你好!我想知道<<苹果15手机壳>>的发货时间???\t\nimport re
# 原始用户输入
raw_input = "@AI助手 你好!我想知道<<苹果15手机壳>>的发货时间???\t\n"
# 预处理函数
def preprocess_user_input(input_text):
# 去除@提到的用户
input_text = re.sub(r"@.*?\s", "", input_text)
# 去除<<>>等格式符号
input_text = re.sub(r"<<|>>", "", input_text)
# 去除多余的标点符号
input_text = re.sub(r"[?!]{2,}", "?", input_text)
# 去除空白字符
input_text = re.sub(r"\s+", " ", input_text).strip()
return input_text
# 预处理结果
processed_input = preprocess_user_input(raw_input)
print(f"原始输入:{repr(raw_input)}")
print(f"预处理后:{processed_input}")原始输入:'@AI助手 你好!我想知道<<苹果15手机壳>>的发货时间???\t\n'
预处理后:你好!我想知道苹果15手机壳的发货时间?"---LLM生成结果---\n
苹果15手机壳的发货时间是24小时内。\n
### 更多信息:\n
1. 支持顺丰快递\n
2. 偏远地区3-5天到达\n
---"import re
# 原始生成结果
raw_output = "---LLM生成结果---\n苹果15手机壳的发货时间是24小时内。\n### 更多信息:\n1. 支持顺丰快递\n2. 偏远地区3-5天到达\n---"
# 过滤函数
def filter_llm_output(output):
# 去除---分隔符
output = re.sub(r"---.*?---", "", output, flags=re.DOTALL) # re.DOTALL匹配换行符
# 去除###标题
output = re.sub(r"###.*?\n", "", output)
# 去除多余的换行符
output = re.sub(r"\n+", "\n", output).strip()
return output
# 过滤结果
filtered_output = filter_llm_output(raw_output)
print(f"原始输出:{repr(raw_output)}")
print(f"过滤后:{filtered_output}")原始输出:'---LLM生成结果---\n苹果15手机壳的发货时间是24小时内。\n### 更多信息:\n1. 支持顺丰快递\n2. 偏远地区3-5天到达\n---'
过滤后:苹果15手机壳的发货时间是24小时内。
1. 支持顺丰快递
2. 偏远地区3-5天到达苹果15手机壳(防摔)
华为Mate60手机壳_磨砂
小米14手机壳-轻薄import re
# 原始语料
raw_corpus = [
"苹果15手机壳(防摔)",
"华为Mate60手机壳_磨砂",
"小米14手机壳-轻薄"
]
# 统一函数
def unify_format(corpus):
return [re.sub(r"[()_-]", " ", text) for text in corpus]
# 统一结果
unified_corpus = unify_format(raw_corpus)
print(f"原始语料:{raw_corpus}")
print(f"统一后:{unified_corpus}")原始语料:['苹果15手机壳(防摔)', '华为Mate60手机壳_磨砂', '小米14手机壳-轻薄']
统一后:['苹果15手机壳 防摔 ', '华为Mate60手机壳 磨砂', '小米14手机壳 轻薄']开发一个批量 LLM 语料清洗系统,实现以下功能:
llm_corpus_cleaner/
├── cleaner.py # 清洗核心模块
├── main.py # 主程序cleaner.pyimport re
import json
import csv
class LLMCorpusCleaner:
def __init__(self, rules=None):
# 默认清洗规则
self.default_rules = [
# 去除星级评分
(r"★+", ""),
# 去除评论标签
(r"【.*?】", ""),
# 去除@提到的用户
(r"@.*?\s", ""),
# 去除格式符号
(r"---+|###+|<<|>>", ""),
# 去除多余的标点符号
(r"[?!]{2,}", "?"),
(r"[!!]{2,}", "!"),
# 去除空白字符
(r"\s+", " ")
]
# 自定义清洗规则
self.custom_rules = rules if rules else []
# 合并所有规则
self.all_rules = self.default_rules + self.custom_rules
def clean_text(self, text):
"""清洗单条文本"""
if not isinstance(text, str):
return None
cleaned = text
for pattern, replacement in self.all_rules:
cleaned = re.sub(pattern, replacement, cleaned, flags=re.IGNORECASE)
# 去除首尾空格
cleaned = cleaned.strip()
# 去除空字符串
return cleaned if cleaned else None
def clean_corpus(self, corpus):
"""清洗批量语料"""
return [self.clean_text(text) for text in corpus if self.clean_text(text)]
def read_corpus(self, file_path):
"""读取不同格式的语料文件"""
if file_path.endswith(".txt"):
with open(file_path, "r", encoding="utf-8") as file:
return file.readlines()
elif file_path.endswith(".json"):
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
return [item["content"] for item in data]
elif file_path.endswith(".csv"):
with open(file_path, "r", encoding="utf-8") as file:
reader = csv.DictReader(file)
return [row["content"] for row in reader]
else:
raise ValueError("不支持的文件格式")
def save_corpus(self, corpus, file_path):
"""保存清洗后的语料到文件"""
if file_path.endswith(".txt"):
with open(file_path, "w", encoding="utf-8") as file:
for text in corpus:
file.write(text + "\n")
elif file_path.endswith(".json"):
data = [{"content": text} for text in corpus]
with open(file_path, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=2)
elif file_path.endswith(".csv"):
with open(file_path, "w", encoding="utf-8", newline="") as file:
fieldnames = ["content"]
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
for text in corpus:
writer.writerow({"content": text})
else:
raise ValueError("不支持的文件格式")main.pyfrom cleaner import LLMCorpusCleaner
# 初始化清洗器
cleaner = LLMCorpusCleaner()
# 读取原始语料
raw_corpus = cleaner.read_corpus("raw_corpus.txt")
print(f"原始语料数量:{len(raw_corpus)}")
# 清洗语料
clean_corpus = cleaner.clean_corpus(raw_corpus)
print(f"清洗后语料数量:{len(clean_corpus)}")
# 保存清洗后的语料
cleaner.save_corpus(clean_corpus, "clean_corpus.json")
print("清洗后的语料已保存到clean_corpus.json")
# 打印部分清洗结果
print("\n清洗结果示例:")
for i in range(min(5, len(clean_corpus))):
print(f"{i+1}. {clean_corpus[i]}")原始语料数量:1000
清洗后语料数量:850
清洗后的语料已保存到clean_corpus.json
清洗结果示例:
1. 苹果15手机壳质量很好!
2. 物流有点慢
3. 材质一般
4. 支持无线充电
5. 外观很好看对于频繁使用的正则表达式,可以使用re.compile()预编译,提高匹配效率。
import re
# 预编译正则表达式
pattern = re.compile(r"\d+")
# 多次使用
result1 = pattern.findall("苹果15手机壳")
result2 = pattern.findall("华为Mate60手机壳")默认情况下,正则表达式是贪婪匹配的,会尽可能匹配最长的字符串。可以使用?将其转换为非贪婪匹配。
import re
# 贪婪匹配(默认)
text = "<div>Hello</div><div>LLM</div>"
pattern = r"<div>.*</div>"
result = re.findall(pattern, text)
print(result) # 输出:['<div>Hello</div><div>LLM</div>']
# 非贪婪匹配(加?)
pattern = r"<div>.*?</div>"
result = re.findall(pattern, text)
print(result) # 输出:['<div>Hello</div>', '<div>LLM</div>']在 Python 中,使用原始字符串(r"")定义正则表达式,可以避免转义字符的问题。
# 原始字符串(推荐)
pattern = r"\d+\.\d+"
# 非原始字符串(需要转义)
pattern = "\\d+\\.\\d+"对于大文件处理,可以结合正则表达式和生成器,避免将整个文件加载到内存。
import re
# 预编译正则表达式
pattern = re.compile(r"[\w]+@[\w]+\.[\w]+")
# 使用生成器处理大文件
def extract_emails(file_path):
with open(file_path, "r", encoding="utf-8") as file:
for line in file:
emails = pattern.findall(line)
if emails:
for email in emails:
yield email
# 使用示例
for email in extract_emails("large_corpus.txt"):
print(email)问题:正则表达式没有匹配到预期的内容。解决:
re.DEBUG参数查看匹配过程re.DOTALL或re.IGNORECASE等标志问题:正则表达式匹配速度很慢。解决:
问题:使用了错误的转义字符。解决:
问题:匹配到的内容比预期的长。解决:在重复次数后面加?,使用非贪婪匹配。
正则表达式功能 | LLM 开发场景 |
|---|---|
匹配格式噪声 | 去除星级评分、评论标签、格式符号等 |
匹配空白噪声 | 去除换行符、制表符、多余空格等 |
匹配特殊字符噪声 | 去除 @#¥%& 等特殊字符 |
匹配冗余内容噪声 | 去除 “此用户没有填写评价内容” 等冗余内容 |
替换内容 | 统一语料格式、替换分隔符等 |
提取内容 | 从噪声语料中提取有效信息 |
Python 正则表达式是LLM 语料清洗的核心技术,掌握它能帮你快速、精准地清洗语料,提高 LLM 的训练效果和推理质量。在实际开发中,要注意:
下一篇我们将学习《Python装饰器:LLM API的安全与可观测性增强》,讲解如何使用异步 IO 提高 LLM 批量推理的性能。