正则表达式对数据处理而言非常重要。数据科学家的一部分使命是操作大量数据。有时候,这些数据中会包含大量文本语料。比如,假如我们需要搞清楚「特朗普文件 [注意,可能是敏感词]」中谁给谁发送过邮件,那么我们就要筛查 1150 万份文档!我们可以采用人工方式,亲自阅读每一封电子邮件,但我们也可以利用 Python 的力量。毕竟,代码存在的意义就是自动执行任务。
即便如此,从头开始写一个脚本也需要大量时间和精力。这就是正则表达式的用武之地。正则表达式(regular expression)也被称为 RE、regex 和 regular pattern,这是一种让我们能快速筛查和分析文本的紧凑型语言。
正则表达式始于 1956 年——Stephen Cole Kleene 创造了它并将其用于描述人类神经系统的 McCulloch-Pitts 模型。到了 60 年代,Ken Thompson 将这种标记方法添加到了一个类似 Windows 记事本的文本编辑器中,自那以后,正则表达式不断发展壮大。
正则表达式的一大关键特征是其经济实用的脚本。你甚至可以将其看作是代码中的捷径。没有它,我们就要码更多代码才能实现相同的功能。
现在,我们来看看正则表达式的能力。
我们将使用来自 Kaggle 的 Fraudulent Email Corpus(欺诈电子邮件语料库)。其中包含 1998 年到 2007 年之间发送的数千封钓鱼邮件。这些邮件读起来很有意思。我们首先将使用单封邮件学习基本的正则表达式命令,然后我们会对整个语料库进行处理。
语料库地址:https://www.kaggle.com/rtatman/fraudulent-email-corpus
01 介绍 Python 的正则表达式模块
首先,准备数据集:打开那个文本文件,将其设置成「只读」,然后读取它。我们也为其分配了一个变量 fh,表示文件句柄(file handle)。
fh = open(r"test_emails.txt", "r").read()
注意我们直接在目录路径之前使用了 r。这项技术会将一个字符串转换成一个原始字符串,这有助于避免由某些机器阅读字符的方式所导致的冲突,比如 Windows 中目录路径中的反斜杠。
你可能注意到了我们目前没有使用整个语料库。我们只是人工地取了该语料库中前面几封邮件,然后将其做成了一个测试文件。这样做的目的是在本教程中输出显示测试结果时,就不用每次都显示数千行结果了。这能免除很多烦恼。你自己练习的时候使用完整语料库或我们的测试文件都不会有问题。
现在,假设我们想知道这些电子邮件的发件人。我们可以试试只用原始的 Python 来实现:
for line in fh.split("\n"):
if "From:" in line:
print(line)
也可以使用正则表达式:
import re
for line in re.findall("From:.*", fh):
print(line)
我们来解读一下这段代码。我们首先导入了 Python 的 re 模块。然后我们写了操作代码。在这个简单的示例中,这段代码只比原始 Python 少一行。但是,随着任务的增加,正则表达式可以让你的脚本继续保持简单经济。
re.findall() 返回字符串中满足其模式的所有实例的列表。这是 Python 内置的 re 模块中最常用的函数之一。分解看看。该函数的形式是 re.findall(pattern, string),有两个参数。其中,pattern 表示我们希望寻找的子字符串,string 表示我们要在其中查找的主字符串。主字符串可以包含很多行。
.* 是字符串模式的简写。我们马上就会详细解释。现在只需知道它们的作用是匹配 From: 字段中的名称和电子邮箱地址。
在我们继续深入之前,我们先了解一些常见的正则表达式模式。
02 常见的正则表达式模式
我们在上面的 re.findall() 中使用的模式中包含一个完全拼写出来的字符串 From:。这在我们知道我们所要寻找的东西是什么时非常有用,可以确定到实际的字母以及大小写。如果我们不知道我们所想要的字符串的确切格式,我们将难以为继。幸运的是,正则表达式有解决这类情况的基本模式。我们看看本教程中会使用的一些模式:
\w 匹配字母数字字符,即 a-z、A-Z 和 0-9,也会匹配下划线 _ 和连接号 –
\d 匹配数字,即 0-9
\s 匹配空白字符,包括制表符、换行符、回车符和空格符
\S 匹配非空白字符
. 匹配除换行符 \n 之外的任意字符
有了这些正则表达式模式,你就能在我们继续解释代码时很快理解。
03 使用正则表达式模式
我们现在可以解释上面 re.findall("From:.*", text) 一行中的 .* 了。首先来看 .
for line in re.findall("From:.", fh):
print(line)
通过在 From: 后面添加一个 .,我们是要寻找 From: 之后另外的一个字符。因为 . 是查找除 \n 之外的任意字符,所以这会得到我们看不到的空格。我们可以多加一些点来验证这个情况
for line in re.findall("From:...........", fh):
print(line)
看起来加点就能让我们得到这一行的其余内容了。但这很单调乏味,而且我们不知道需要加多少个点。这就是星号 * 发挥作用的地方。
* 匹配 0 个或更多个其左侧的模式的实例。也就是说它会查找重复的模式。当我们查找重复模式时,我们说我们的搜索是「贪婪匹配」。如果我们没有查找重复模式,我们可以说我们的搜索是「非贪婪匹配」或「懒惰匹配」。
04 让我们使用 * 构建一个 . 的贪婪搜索
for line in re.findall("From:.*", fh):
print(line)
因为 * 匹配 0 个或多个其左侧模式的实例且 . 在其左侧,所以我们可以获取 From: 字段中的所有字符,直到该行结束。这样就用美丽而简洁的代码输出显示了一整行。
我们甚至可以更进一步只取出其中的名称。
match = re.findall("From:.*", fh)
for line in match:
print(re.findall("\".*\"", line))
这里,我们先使用之前的做法通过 re.findall() 得到了包含 From:.* 模式的行的列表。接下来,我们遍历这个列表。在这一次训练中,我们都再执行一次 re.findall()。这一次,该函数先从匹配第一个引号开始。
注意我们在第一个引号后使用了一个反斜杠。这个反斜杠是一个用于给其它特殊字符转义的特殊字符。比如说,当我们想将引号用作字符串本身而不是特殊字符时,我们可以像 \" 这样使用反斜杠对其转义。如果我们不使用反斜杠转义上述模式,它就会变成 "".*"",Python 解释器就会将其看作是两个空字符串之间的一个句号和一个星号。这会出错并使该脚本中断。因此,我们这里必须使用反斜杠给引号转义。
在第一个引号匹配后,.* 会获取这一行中下一个引号前的所有字符。当然,该模式中的下一个引号也经过了转义。这让我们可以得到引号之中的名称。每个名称都输出显示在方括号中,因为 re.findall 以列表形式返回匹配结果。
05 如果我们想得到电子邮箱地址呢?
match = re.findall("From:.*", fh)
for line in match:
print(re.findall("\w\S*@.*\w", line))
看起来很简单,是不是?只是模式不一样而已。让我们详细看看。
这是我们匹配电子邮箱地址前半部分的方式:
for line in match:
print(re.findall("\w\S*@", line))
电子邮箱地址中总会包含一个 @ 符号,所以我们从它开始入手。电子邮箱地址中 @ 符号前面的部分可能包含字母数字字符,这意味着需要 \w。但是,由于某些电子邮箱地址包含句号或连接号,所以这还不够。我们增加了 \S 来查找非空白字符。但 \w\S 只能得到两个字符,所以增加 * 来重复查找。所以 @ 符号之前部分的模式是 \w\S*@。接下来看 @ 符号之后的部分。
for line in match:
print(re.findall("@.*", line))
域名通常包含字母数字字符、句号,有时候还会有连接号。这很简单,一个 . 就行。为了实现贪婪搜索,我们使用 * 来延展。这让我们可以匹配直到该行结束的任意字符。
简单看看这些行,我们可以发现每个电子邮箱地址都被放在一对尖括号 <> 之中。我们的模式 .* 会将右尖括号 > 包含进来。我们再调整一下:
for line in match:
print(re.findall("@.*\w", line))
电子邮箱地址是以字母数字字符结尾的,所以我们用 \w 作为这一模式的结尾。因此,@ 符号之后的部分是 .*\w,也就是说我们想要的模式是一组以字母数字字符结尾的任意类型的字符。这样就排除了 >。因此,完整的电子邮箱地址模式就为 \w\S*@.*\w
看起来有些麻烦。实际上正则表达式确实需要花些时间才能熟练,但一旦你掌握了,在写分析字符串的代码时就会快很多。接下来,我们会介绍一些常见的 re 函数,这些函数在重新组织这个语料库时会很有用。
06 常见的正则表达式函数
re.findall() 毫无疑问非常有用,re 模块还提供了一些同样方便的函数,其中包括:
re.search()
re.split()
re.sub()
我们先逐一介绍一下这些函数,然后再将它们用来整理笨重难读的语料库。
re.search()
re.findall() 匹配的是一个模式在一个字符串中的所有实例然后以列表的形式返回它们,而 re.search() 匹配的是一个模式在一个字符串中的第一个实例,然后以 re 匹配对象的形式返回它。
match = re.search("From:.*", fh)
print(type(match))
print(type(match.group()))
print(match)
print(match.group())
与 re.findall() 类似,re.search() 也有两个参数。第一个参数是所要匹配的模式,第二个是要在其中查找的字符串。这里为了简洁我们已经分配了 match 变量的结果。
因为 re.search() 返回的是一个 re 匹配对象,所以我们不能直接通过 print 展示其中的名称和电子邮箱地址。我们必须首先为其应用 group() 函数。我们已经在上面的代码中将它们输出显示了出来。如我们所见,group() 函数的作用是将匹配对象转换成字符串。
我们还能看到 print(match) 会显示字符串以及除字符串本身之外的属性,而 print(match.group()) 只会显示字符串。
re.split()
假设我们需要一种获取电子邮箱地址域名的快速方式。我们可以用 3 个正则表达式操作来完成。如下:
address = re.findall("From:.*", fh)
for item in address:
for line in re.findall("\w\S*@.*\w", item):
username, domain_name = re.split("@", line)
print("{}, {}".format(username, domain_name))
第一行我们很熟悉。我们返回一个字符串列表并为其分配一个变量,其中每个字符串都包含了 From: 字段的内容。接下来我们遍历整个列表,寻找电子邮箱地址。与此同时,我们遍历这些电子邮箱地址并使用 re 模块的 split() 函数以 @ 符号为分割符将每个电子邮件一分为二。最后,我们将其显示出来。
re.sub()
re.sub() 是另一个很好用的 re 函数。顾名思义,它的功能是替换一个字符串的一部分。举个例子:
sender = re.search("From:.*", fh)
address = sender.group()
email = re.sub("From", "Email", address)
print(address)
print(email)
其中第一行和第二行的任务我们之前已经见过。第三行我们在 address 上应用 re.sub(); address 是电子邮件标头中的完整的 From: 字段。
re.sub() 有三个参数。第一个是所要替换的子字符串,第二个是用来替换前者的字符串,第三个是主字符串本身。
来源:网络大数据
大数据周刊
邮箱:tougao@bigdatamag.cn
电话:010-57524293
众论大数据 引领大时代
长按二维码关注