2、正则表达式入门:正则表达式的基本语法,元字符与转义,Python re模块的使用,常用正则模式。
好,咱们进入正题。正则表达式,说白了就是一套用来描述文本模式的规则。很多同学一看到正则就头大,觉得像天书。我刚开始学的时候也这样,满屏的符号看得眼花。但后来我发现,只要抓住几个核心概念,这东西其实挺顺手的。
你想想看,做NLP最常干的事是什么?清洗数据。而清洗数据最离不开的就是正则。我敢说,你项目中80%的文本处理需求,用正则都能搞定。剩下的20%,要么是需求太奇葩,要么是你正则没写对。
2.1 正则表达式的基本语法
正则表达式由普通字符和元字符组成。普通字符就是字母、数字这些,匹配自己。元字符才是真正的灵魂。
举个例子,abc 这个正则,就匹配字符串中的 "abc"。但如果你写成 a.c,这里的点号 . 就是元字符,它匹配任意一个字符。所以 a.c 能匹配 "abc"、"aac"、"a1c" 等等。
我个人习惯把正则拆成三部分来看:
- 字面量:就是普通字符,匹配自身
- 元字符:有特殊含义的符号
- 量词:控制前面字符出现的次数
嗯,这里要注意,正则默认是区分大小写的。如果你要忽略大小写,得加个标志位,后面会讲。
2.2 元字符与转义
元字符是正则的基石。我把最常用的几个列出来,你记牢了,基本就能应付大部分场景。
| 元字符 | 含义 | 示例 |
|---|---|---|
. |
匹配任意单个字符(除换行符) | a.b 匹配 "aab", "a1b" |
^ |
匹配字符串开头 | ^abc 匹配以 "abc" 开头的字符串 |
$ |
匹配字符串结尾 | abc$ 匹配以 "abc" 结尾的字符串 |
* |
匹配前一个字符0次或多次 | ab*c 匹配 "ac", "abc", "abbc" |
+ |
匹配前一个字符1次或多次 | ab+c 匹配 "abc", "abbc",不匹配 "ac" |
? |
匹配前一个字符0次或1次 | ab?c 匹配 "ac", "abc" |
[] |
字符集,匹配括号内任意一个字符 | [abc] 匹配 "a", "b", "c" |
() |
分组,将多个字符当作一个整体 | (ab)+ 匹配 "ab", "abab" |
| |
或操作 | a|b 匹配 "a" 或 "b" |
\ |
转义字符 | \. 匹配字面量点号 |
说到转义,我得提一嘴。我在项目中遇到过好几次,有人想匹配一个点号,直接写了个 .,结果匹配了一大堆东西。为什么?因为点号是元字符啊!正确的写法是 \.。记住,想匹配元字符本身,前面加个反斜杠就行。
.,结果把 "192.168.1.1" 匹配成了 "192 168 1 1" 四个独立的部分。后来才意识到,点号没转义。所以,凡是遇到 .、*、+ 这些符号要匹配字面量时,一定记得加反斜杠。
2.3 Python re模块的使用
Python 的 re 模块是处理正则的标准库。我常用的就四个函数:match、search、findall、sub。说白了,一个负责开头匹配,一个负责全文搜索,一个负责找所有,一个负责替换。
先看个最简单的例子:
import re
text = "我的手机号是13812345678,请记一下。"
# 用 search 查找手机号
pattern = r'1[3-9]\d{9}'
result = re.search(pattern, text)
if result:
print(result.group()) # 输出:13812345678
这里我用了原始字符串 r''。为什么?因为正则里反斜杠太多了,不用原始字符串的话,你得写两个反斜杠才能表示一个。比如 \d 在普通字符串里得写成 \\d,多麻烦。所以,我建议你写正则时一律用 r''。
再来看 findall,这个在数据清洗中特别常用:
import re
text = "邮箱1: alice@example.com, 邮箱2: bob@test.org"
pattern = r'\w+@\w+\.\w+'
emails = re.findall(pattern, text)
print(emails) # 输出:['alice@example.com', 'bob@test.org']
你想想看,如果让你手动从几百行日志里提取邮箱,那得累死。但用正则,一行代码就搞定了。
最后说下 sub,替换函数。我在清洗用户评论时经常用:
import re
text = "这个商品真TM垃圾,操蛋的客服!"
# 替换脏话
pattern = r'TM|操蛋|垃圾'
clean_text = re.sub(pattern, '***', text)
print(clean_text) # 输出:这个商品真***,***的客服!
re.compile 可以预编译正则模式。如果你要多次使用同一个正则,先编译再调用,性能会好很多。我在处理百万级数据时,编译后的速度能快30%左右。
2.4 常用正则模式
下面这些模式,是我在实际项目中反复用到的。你直接拿去用就行,不用每次都自己写。
| 用途 | 正则模式 | 说明 |
|---|---|---|
| 匹配手机号 | 1[3-9]\d{9} |
11位数字,以1开头,第二位3-9 |
| 匹配邮箱 | \w+@\w+\.\w+ |
简单的邮箱格式 |
| 匹配URL | https?://[^\s]+ |
http或https开头的链接 |
| 匹配IP地址 | \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} |
IPv4地址,注意没做范围校验 |
| 匹配中文 | [\u4e00-\u9fa5]+ |
匹配中文字符 |
| 匹配空白行 | ^\s*$ |
空行或只包含空白字符的行 |
| 匹配日期 | \d{4}-\d{2}-\d{2} |
如 2024-01-15 |
这里有个坑,我必须要说。IP地址的正则 \d{1,3} 虽然能匹配 "999",但这不是合法的IP。为什么?因为IP地址每段最大是255。如果你要做严格的校验,得用更复杂的模式。不过,在数据清洗阶段,我们通常只做初步提取,后续再验证。
再分享一个我常用的组合拳:
import re
def clean_text(text):
# 1. 去除HTML标签
text = re.sub(r'<[^>]+>', '', text)
# 2. 去除URL
text = re.sub(r'https?://[^\s]+', '', text)
# 3. 去除特殊符号,保留中文、英文、数字和空格
text = re.sub(r'[^\w\u4e00-\u9fa5\s]', '', text)
# 4. 合并多个空格
text = re.sub(r'\s+', ' ', text)
return text.strip()
sample = "这是一段<b>测试</b>文本,详情请访问 https://example.com !@#¥"
print(clean_text(sample))
# 输出:这是一段测试文本,详情请访问
你看,几行正则就把文本清洗得干干净净。这就是正则的魅力。
好了,正则入门就讲这么多。下一节我们会深入讨论更高级的模式,比如贪婪与非贪婪匹配、前瞻与后顾断言。这些东西在复杂文本处理中特别有用。到时候我会结合我踩过的坑,给你讲透。