4. 文本数据清洗(上):去除HTML标签、特殊字符与大小写转换

文本数据清洗,说白了就是把那些乱七八糟的「噪音」从文本里揪出来扔掉。我做了这么多年数据工程,处理过的文本数据少说也有几十TB,最深的体会就是:干净的数据长得都差不多,脏的数据各有各的脏法。

今天咱们先搞定四个最基础的清洗操作。别看它们简单,实战中80%的文本脏数据都能靠这几招解决。

本章核心知识点一览:

  • 用 BeautifulSoup 精准剥离 HTML 标签
  • 正则表达式清理特殊字符与标点
  • 大小写统一转换的策略选择
  • 空白字符的「斩草除根」
文本数据清洗(上)知识框架 文本清洗四步法 ① 去除HTML标签 ② 去除特殊字符 ③ 大小写转换 ④ 去除空白字符 BeautifulSoup.get_text() re.sub(r'[^\w\s]', '') str.lower() / str.upper() str.strip() / re.sub(r'\s+', ' ') 💡 实战建议:按 ①→②→③→④ 的顺序执行,效果最佳 先剥离标签 → 再清理符号 → 统一大小写 → 最后整理空白

4.1 去除HTML标签:BeautifulSoup 的正确打开方式

从网页爬下来的数据,十有八九都带着一堆 HTML 标签。<div>、<p>、<a href="...">…… 这些标签对机器解析来说是结构,但对文本分析来说就是噪音。

我见过不少新手直接用正则表达式去匹配标签,结果要么漏掉嵌套标签,要么把标签里的属性值也切坏了。其实 Python 生态里已经有现成的轮子——BeautifulSoup,专门干这个的。

我的习惯:只要涉及 HTML 解析,我第一反应就是上 BeautifulSoup。它底层用的是 lxml 或 html.parser,对标签结构的理解比正则表达式靠谱得多。

基本用法很简单:

from bs4 import BeautifulSoup

html_text = "<p>Hello, <b>World</b>!</p>"
soup = BeautifulSoup(html_text, 'html.parser')
clean_text = soup.get_text()
print(clean_text)  # 输出: Hello, World!

你看,get_text() 方法直接把所有标签剥干净,只留下纯文本。不过这里有个坑——默认会把所有标签里的文本拼在一起,中间不留空格。

比如:

html_text = "<li>苹果</li><li>香蕉</li><li>橘子</li>"
soup = BeautifulSoup(html_text, 'html.parser')
print(soup.get_text())  # 输出: 苹果香蕉橘子

嗯,这显然不是我们想要的。解决办法是加个分隔符参数:

print(soup.get_text(separator=' '))  # 输出: 苹果 香蕉 橘子

我曾经踩过的坑:有一次处理一个新闻网站的 RSS 数据,直接用 get_text() 没加 separator,结果所有段落连在一起,分词模型直接崩了。后来排查了半天才发现是这里的问题。所以我的建议是——不管用不用得上,先加上 separator=' ',养成习惯。

另外,如果你只想提取某个特定标签里的内容,可以用 find() 或 find_all() 先定位:

soup = BeautifulSoup(html_doc, 'html.parser')
# 只提取所有 <p> 标签里的文本
paragraphs = [p.get_text(strip=True) for p in soup.find_all('p')]

strip=True 这个参数也很实用,它会自动去掉每个标签文本前后的空白字符。

4.2 去除特殊字符与标点:正则表达式是王道

HTML 标签去掉了,但文本里可能还残留着各种「妖魔鬼怪」——@、#、$、%、^、&…… 还有各种全角半角标点。这些符号对自然语言处理来说基本都是噪音。

我个人习惯用 Python 的 re 模块来处理。正则表达式虽然看起来像「天书」,但常用的就那么几个模式。

最通用的做法是保留字母、数字和空格,其他全干掉:

import re

text = "Hello, World! @2024 #数据清洗 $100%"
clean_text = re.sub(r'[^\w\s]', '', text)
print(clean_text)  # 输出: Hello World 2024 数据清洗 100

这里 \w 匹配字母、数字和下划线,\s 匹配空白字符。[^\w\s] 的意思就是「既不是字母数字也不是空白的字符」,统统替换成空字符串。

但有时候你想保留某些符号,比如句号或连字符。那就把要保留的符号加到排除列表里:

# 保留句号、逗号和连字符
clean_text = re.sub(r'[^\w\s\.\,\-]', '', text)

一个小技巧:中文文本里经常混着全角标点,比如中文逗号「,」、中文句号「。」。你可以先用 str.translate() 把全角转半角,再用正则清理。这样一步到位。

全角转半角的代码片段:

def fullwidth_to_halfwidth(text):
    result = []
    for char in text:
        code = ord(char)
        if 0xFF01 <= code <= 0xFF5E:  # 全角字符范围
            result.append(chr(code - 0xFEE0))
        elif code == 0x3000:  # 全角空格
            result.append(' ')
        else:
            result.append(char)
    return ''.join(result)

你想想看,如果不清除这些特殊字符,后面做词频统计的时候,「hello」和「hello!」会被当成两个不同的词,结果就偏了。

4.3 大小写转换:统一才是硬道理

英文文本里,「Python」、「python」、「PYTHON」在语义上是一回事,但在计算机眼里它们是三个不同的字符串。所以,统一大小写是文本清洗里必不可少的一步。

Python 自带的方法就够用:

方法 作用 示例
str.lower() 全部转小写 "Hello" → "hello"
str.upper() 全部转大写 "Hello" → "HELLO"
str.capitalize() 首字母大写,其余小写 "hello world" → "Hello world"
str.title() 每个单词首字母大写 "hello world" → "Hello World"

我处理英文语料时,90% 的情况都用 lower()。为什么?因为小写是最「安全」的归一化方式,不会丢失信息,而且后续做词嵌入或 TF-IDF 时,小写形式更容易匹配。

注意:有些专有名词转小写后会丢失含义。比如「US」(美国)转成「us」(我们),意思完全变了。如果你处理的文本里专有名词很多,建议先做实体识别,把实体保护起来再转小写。

中文文本一般不需要大小写转换,但如果你处理的是中英混合文本,建议只对英文部分做转换。可以用正则把英文部分摘出来处理:

import re

def normalize_case(text):
    # 只对英文字母部分转小写
    return re.sub(r'[a-zA-Z]+', lambda m: m.group(0).lower(), text)

4.4 去除空白字符:别小看这些「隐形杀手」

空白字符包括空格、制表符 \t、换行符 \n、回车符 \r 等。它们肉眼看不见,但经常在数据里捣乱。

最常见的场景:从 CSV 或 Excel 里读出来的字段,前后可能带着空格。比如 " Python " 和 "Python",你看着差不多,但 == 比较返回 False。

Python 自带的 strip() 方法可以去掉字符串首尾的空白:

text = "   Hello, World!   \n"
print(text.strip())  # 输出: "Hello, World!"

但有时候空白字符藏在字符串中间,比如 "Hello World"(多个连续空格)。这时候 strip() 就无能为力了,得用正则:

text = "Hello    World   Python"
# 把连续多个空白字符替换成单个空格
clean_text = re.sub(r'\s+', ' ', text)
print(clean_text)  # 输出: "Hello World Python"

我的实战经验:处理从 PDF 或 OCR 识别出来的文本时,经常遇到各种奇怪的空白字符,比如不换行空格(\xa0)、零宽空格(\u200b)。这些用 \s 匹配不到,得单独处理:

text = text.replace('\xa0', ' ').replace('\u200b', '')

如果你想把所有空白字符全部删掉(包括单词之间的空格),可以用:

text = "Hello World Python"
no_space = re.sub(r'\s+', '', text)
print(no_space)  # 输出: "HelloWorldPython"

但说实话,我很少这么干。除非是做某些特殊的字符级模型,否则保留单词之间的空格对后续分词更友好。

4.5 组合起来:一个完整的清洗函数

好了,四个步骤都讲完了。咱们把它们串成一个完整的清洗函数:

import re
from bs4 import BeautifulSoup

def clean_text_pipeline(text, remove_html=True, remove_punct=True,
                        to_lower=True, strip_whitespace=True):
    """
    文本清洗流水线
    """
    # 1. 去除HTML标签
    if remove_html:
        soup = BeautifulSoup(text, 'html.parser')
        text = soup.get_text(separator=' ')
    
    # 2. 去除特殊字符与标点
    if remove_punct:
        text = re.sub(r'[^\w\s]', '', text)
    
    # 3. 大小写转换
    if to_lower:
        text = text.lower()
    
    # 4. 去除多余空白
    if strip_whitespace:
        text = re.sub(r'\s+', ' ', text).strip()
    
    return text

# 使用示例
raw_text = "<h1>Hello, World!</h1><p>Python  &  数据清洗</p>"
cleaned = clean_text_pipeline(raw_text)
print(cleaned)  # 输出: "hello world python 数据清洗"

关键点总结:

  • 清洗顺序很重要:先标签 → 再符号 → 大小写 → 最后空白
  • BeautifulSoup 的 get_text(separator=' ') 比正则更可靠
  • 正则清理符号时,想清楚要保留什么,而不是一味删除
  • 大小写转换用 lower() 最通用,但要注意保护专有名词
  • 空白字符不止空格,还有 \t、\n、\xa0 等「隐形杀手」

这四步看起来简单,但我在实际项目中见过太多因为「没洗干净」导致模型效果差一大截的案例。说白了,数据清洗的功夫都在细节里。你多花十分钟把数据洗干净,后面建模就能省下十个小时的调参时间。