🎯 学习目标
- 建立文本预处理的完整流程意识
- 会去除 HTML 标签、URL、@提及、表情符号、特殊符号
- 掌握大小写、全角/半角、Unicode NFKC、繁简等归一化操作
- 会做中英文句子分割
- 掌握英文 NLTK 分词、停用词、词干提取、词形还原
- 掌握中文停用词过滤、去重与长度过滤,写出完整 pipeline 函数
1为什么预处理决定 NLP 上限
NLP 项目中,数据清洗往往占用 60%~80% 的时间。模型再先进,喂进去的是噪声也学不到规律。一条典型的原始评论可能长这样:
"<p>这个手机<a href=...>真的</a>很Nice!!!👍👍 购买链接 https://x.co/abc
@客服小二 价格¥2999(全角数字),发货超快~~~ "
其中对机器无用甚至有害的成分包括:HTML 标签、URL、@提及、emoji、重复标点、全角字符、多余空白……预处理就是把它变成干净文本:
"这个手机真的很nice 价格2999,发货超快"
(再经分词)→ ["这个", "手机", "真的", "很", "nice", "价格", "2999", "发货", "超快"]
2标准预处理流水线
原始文本
├─ 1. 基础清洗:去 HTML/URL/@/#话题#/emoji/特殊符号/多余空白
├─ 2. 归一化:大小写、全角→半角、繁→简、Unicode 规范化、数字统一
├─ 3. 句子分割(sentence segmentation)
├─ 4. 分词(tokenization)………… 中文第3章详讲
├─ 5. 停用词过滤、(英文)词干化/词形还原
├─ 6. 过滤:去重、去空文本、长度过滤
└─ 干净 token 列表 → 进入向量化/建模
清洗没有万能模板:做情感分析时"!!""太差"这类情绪信号要保留;做实体识别时标点和大小写可能是特征;训练大模型时反而倾向少清洗、保留真实分布。先明确任务,再决定删什么。
3基础清洗:正则实战
import re
from bs4 import BeautifulSoup
raw = '''<p class="x">华为Mate60真的太香了!!!👍</p>
<a href="https://xx.com">点击购买</a> @华为主页 #数码# 联系扣扣123456
客服电话400-800-1234,价格¥2999。'''
# ① 去 HTML 标签:优先用 BeautifulSoup(能顺带取纯文本)
soup = BeautifulSoup(raw, "lxml")
text = soup.get_text(separator=" ")
# 正则兜底去标签(简单场景)
text = re.sub(r"<[^>]+>", "", text)
# ② 去 URL
text = re.sub(r"https?://\S+|www\.\S+", "", text)
# ③ 去 @提及 和 #话题#(话题内容可保留,只去 # 号)
text = re.sub(r"@[\w\-_]+", "", text)
text = re.sub(r"#([^#]+)#", r"\1", text)
# ④ 去 emoji(常见 Unicode emoji 范围)
emoji_pattern = re.compile(
"[" "\U0001F600-\U0001F64F" "\U0001F300-\U0001F5FF"
"\U0001F680-\U0001F6FF" "\U0001F1E0-\U0001F1FF" "]+",
flags=re.UNICODE)
text = emoji_pattern.sub("", text)
# ⑤ 合并重复标点、去除多余空白
text = re.sub(r"([,。!?,.!?])\1+", r"\1", text)
text = re.sub(r"\s+", " ", text).strip()
print(text)
# 华为Mate60真的太香了! 点击购买 数码 联系扣扣123456
# 客服电话400-800-1234,价格¥2999。
只保留 / 删除指定字符
# 中文文本:只保留中英文、数字、常见中文标点
text_zh = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、;:""''()《》\s]", "", text)
# 去掉所有数字(分类任务中数字往往无意义);也可归一化为占位符 <NUM>
text_nonum = re.sub(r"\d+", "<NUM>", text)
# "联系扣扣<NUM> 客服电话<NUM>,价格¥<NUM>"
把数字、邮箱、手机号、URL 替换成 <NUM>、<URL>、<TEL> 等占位符而不是直接删除,既能降噪声又保留"这里曾有某类信息"的结构,在分类、NER 中往往效果更好。
4文本归一化
大小写(英文必须)
"NLP is Great".lower() # 'nlp is great'
# 让 "Apple"、"apple"、"APPLE" 成为同一个特征
# 例外:专名识别、真实文本生成任务可保留大小写
全角转半角
def full_to_half(text):
"""全角字符(ABC123)转半角(ABC123)。"""
result = []
for ch in text:
code = ord(ch)
if code == 0x3000: # 全角空格
code = 0x20
elif 0xFF01 <= code <= 0xFF5E: # 其他全角字符
code -= 0xFEE0
result.append(chr(code))
return "".join(result)
print(full_to_half("价格¥2999")) # 价格¥2999
Unicode 规范化(NFKC)
import unicodedata
# 视觉相同但码点不同的字符统一:① 罗马数字、连体字、上下标等
unicodedata.normalize("NFKC", "①②③") # '123'
unicodedata.normalize("NFKC", "Hello") # 'Hello'
# 组合字符(é = e + 重音符)归一,避免同一词出现多种码点
unicodedata.normalize("NFC", "café")
繁体转简体
pip install opencc-python-reimplemented
from opencc import OpenCC
cc = OpenCC("t2s") # traditional → simplified
print(cc.convert("臺灣繁體字很好學")) # 台湾繁体字很好学
# 反向用 s2t;其他配置:s2tw、tw2sp 等
5句子分割
# 中文:按句末标点切分(保留分句可用 finditer)
def split_sentences_zh(text):
parts = re.split(r"(?<=[。!?;\n])", text) # 在标点【之后】切,保留标点
return [s.strip() for s in parts if s.strip()]
print(split_sentences_zh("你好!我学习Python。你呢?"))
# ['你好!', '我学习Python。', '你呢?']
# 英文:NLTK 的 sent_tokenize 能处理 Mr. Smith 这种缩写陷阱
import nltk
# nltk.download("punkt_tab")
para = "Hello! I am Mr. Smith. What's up?"
print(nltk.sent_tokenize(para))
# ['Hello!', 'I am Mr. Smith.', "What's up?"]
6英文处理四件套(NLTK)
① 分词
from nltk.tokenize import word_tokenize
# nltk.download("punkt_tab")
print(word_tokenize("Don't stop. Can't wait!"))
# ['Do', "n't", 'stop', '.', 'Ca', "n't", 'wait', '!']
# 能正确处理缩写 n't、标点分离
# 简单场景也可以用正则
from nltk.tokenize import regexp_tokenize
regexp_tokenize("I love NLP 3000", pattern=r"\w+")
# ['I','love','NLP','3000']
② 停用词过滤
from nltk.corpus import stopwords
# nltk.download("stopwords")
en_stop = set(stopwords.words("english")) # 务必转 set,查询 O(1)
text = "this is a very good movie and i love it"
tokens = [w for w in word_tokenize(text.lower())
if w.isalpha() and w not in en_stop]
print(tokens)
# ['good', 'movie', 'love'] the/is/a/and/i/it 全被过滤
③ 词干提取 Stemming(粗粒度、快)
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
for w in ["running", "runs", "ran", "studies", "studying", "cars"]:
print(w, "→", stemmer.stem(w))
# running → run ; runs → run ; studies → studi ; cars → car
# 规则砍后缀,结果有时不是真词(studi),但特征归并目的达到了
④ 词形还原 Lemmatization(细粒度、需词性)
from nltk.stem import WordNetLemmatizer
# nltk.download("wordnet")
lemm = WordNetLemmatizer()
print(lemm.lemmatize("studies", pos="n")) # study
print(lemm.lemmatize("was", pos="v")) # be
print(lemm.lemmatize("better", pos="a")) # good(能识别不规则变化)
# 比 stemming 更准确(输出真实词条),但稍慢,最好传词性
| Stemming 词干提取 | Lemmatization 词形还原 | |
|---|---|---|
| 原理 | 规则砍后缀 | 词典 + 词性还原到原形 |
| 速度 | 快 | 稍慢 |
| 结果 | 可能不是词(studi) | 一定是真实词条 |
| 适用 | 搜索/分类粗粒度归并 | 要求准确的语义处理 |
7中文停用词
中文停用词:的、了、是、在、也、就、很、一个、我们……它们高频但信息量低。常用停用词表:
- 哈工大停用词表(约 1000+ 词,最全最常用)
- 百度停用词表、四川大学机器智能实验室停用词库
- GitHub 搜索 "stopwords_hit.txt" 即可获得
def load_stopwords(path="stopwords_hit.txt"):
try:
with open(path, encoding="utf-8") as f:
return set(line.strip() for line in f if line.strip())
except FileNotFoundError:
# 文件不存在时用内置最小集合兜底,保证程序可跑
return set("的 了 是 在 和 也 就 都 很 我 你 他 她 它 们 这 那 一个 一些 "
"吧 呢 啊 吗 哦 嗯 被 把 让 给 对 从 向 于 以".split())
stopwords_zh = load_stopwords()
print(len(stopwords_zh))
# 结合 jieba 分词后的过滤(jieba 第3章细讲)
import jieba
raw = "这个手机真的是太好用了,我非常喜欢它的外观"
tokens = [w for w in jieba.lcut(raw)
if w.strip() and w not in stopwords_zh]
print(tokens)
# ['手机', '真的', '太', '好用', '非常', '喜欢', '外观'](停用表不同结果略有差异)
停用词表不是越长越好:情感分析中"不、没、别、无"是否定词,绝不能删("不好"删掉"不"就成"好"了);NER、问答、机器翻译通常不删停用词。按任务定制词表。
8去重与过滤
docs = ["重复文本", "重复文本", "", " ", "短", "正常长度的评论内容"]
# 去空、去空白、长度过滤(分词后 token 数或原始长度)
min_len, max_len = 5, 500
clean = []
seen = set()
for d in docs:
d = d.strip()
if not d or not (min_len <= len(d) <= max_len):
continue
if d in seen: # 精确去重
continue
seen.add(d)
clean.append(d)
# 近似重复(水军刷屏、模板文案)可用 SimHash / MinHash,NLP 进阶内容
print(clean)
9完整可复用清洗器
import re, unicodedata
from bs4 import BeautifulSoup
class TextCleaner:
def __init__(self, keep_punct=True, normalize_number=False):
self.keep_punct = keep_punct
self.normalize_number = normalize_number
self.emoji_re = re.compile(
"[" "\U0001F600-\U0001F64F" "\U0001F300-\U0001F5FF"
"\U0001F680-\U0001F6FF" "\U0001F1E0-\U0001F1FF" "]")
def clean(self, text):
text = BeautifulSoup(text, "lxml").get_text(" ") # HTML → 纯文本
text = unicodedata.normalize("NFKC", text) # Unicode 归一
text = re.sub(r"https?://\S+|www\.\S+", " <URL> ", text)
text = re.sub(r"1[3-9]\d{9}", " <TEL> ", text)
text = re.sub(r"[\w.+-]+@[\w-]+\.[\w.]+", " <EMAIL> ", text)
text = re.sub(r"@[\w\-_]+", " ", text)
text = self.emoji_re.sub(" ", text)
if self.normalize_number:
text = re.sub(r"\d+", "<NUM>", text)
if not self.keep_punct:
text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s<>]", " ", text)
text = re.sub(r"\s+", " ", text).strip()
return text
cleaner = TextCleaner(normalize_number=True)
demo = "<b>加我微信</b>> https://t.cn/x 电话13800000000 😊哈哈!!!"
print(cleaner.clean(demo))
# 加我微信 <URL> 电话<TEL> 哈哈!!!
10本章小结与练习
📌 关键结论
- 清洗流水线:去噪(HTML/URL/emoji)→ 归一化 → 分句 → 分词 → 停用词 → 过滤。
- 敏感信息用占位符
<URL>/<TEL>/<NUM>替换,比硬删更保留结构。 - 英文:lower + word_tokenize + stopwords + stem/lemmatize;中文:归一化 + jieba + 中文停用词表。
- 清洗策略服从任务:情感分析保留否定词,NER/翻译保留停用词与大小写。
练习1:为什么做情感分析时不能把"不、没、别"放进停用词表?
这些否定词直接决定情感极性:"不好吃""没兴趣""别买"都是负面,删掉否定词后变成"好吃/兴趣/买",语义甚至极性反转,会严重误导模型。情感分析要保留否定词,必要时还需专门处理否定转移(如把"不+形容词"合并为一个特征)。
练习2:将全角字符串"QQ邮箱:123@qq.com"清洗为半角小写文本。
unicodedata.normalize("NFKC", s) 或自定义全角转半角函数得到 'QQ邮箱:123@qq.com',再对需要的部分 .lower()(中文冒号可按需替换为英文)。NFKC 能一次性处理全角字母数字与常见符号。
练习3:用 NLTK 对英文句子 "The cats were running quickly!" 完成:分词、小写、去停用词、词形还原。
word_tokenize 得 ['The','cats','were','running','quickly','!'];lower 并过滤停用词 the/were 与标点后 ['cats','running','quickly'];WordNetLemmatizer 对 running 传 pos='v' 得 'run',cats 得 'cat',最终 ['cat','run','quickly']。