Python爬虫开发:从入门到实战(微课版)
上QQ阅读APP看书,第一时间看更新

3.1 正则表达式

正则表达式(Regular Expression)是一段字符串,它可以表示一段有规律的信息。Python自带一个正则表达式模块,通过这个模块可以查找、提取、替换一段有规律的信息。

在一万个人里面找一个人很困难,但是在一万个人里面找一个非常“有特点”的人却很容易。假设有一个人,皮肤是绿色的,身高三米,那么即使这个人混在一万人中,其他人也能一眼找到他。这个“寻找”的过程,在正则表达式中叫作“匹配”。

在程序开发中,要让计算机程序从一大段文本中找到需要的内容,就可以使用正则表达式来实现。

使用正则表达式有如下步骤。

(1)寻找规律。

(2)使用正则符号表示规律。

(3)提取信息。

举一个例子,有下面一段话:

    今天天气不错,我正在读一本爬虫开发的书。password:88886666:password,我刚刚不小心把我的密码写了出来。你能看到我
    的密码吗?
    我发现我们都喜欢使用同样的密码,昨天我不小心看到了小红的密码password:11112222:password,难道她的其他密码也是这
    一个?
    在她的电脑中,我发现了她的银行卡密码果然是password:33334444:password,于是我提醒她注意密码安全。

在这一段文字中一共出现了3个密码。这3个密码很有规律,它们都是password:数字:password这种格式的。那么,如果能把符合password:数字:password这种格式的内容里面的“数字”提取出来,就可以直接得到密码了。这就需要使用正则表达式来完成这个工作的第一步,发现规律。

3.1.1 正则表达式的基本符号

1.点号“.”

V3-1 基本符号的意义

一个点号可以代替除了换行符以外的任何一个字符,包括但不限于英文字母、数字、汉字、英文标点符号和中文标点符号。例如,有如下几个不同的字符串:

                      kingname
                      kinabcme
                      kin123me
                      kin我是谁me
                      kin嗨你好me
                      kin"m"me

这些字符串的前3个字符都是“kin”,后两个字符都是“me”,只有中间的3个字符不同。如果使用点号来表示,那么全部都可以变成kin...me的形式,中间有多少个字就用多少个点。

2.星号“*”

一个星号可以表示它前面的一个子表达式(普通字符、另一个或几个正则表达式符号)0次到无限次。

例如,有如下几个不同的字符串:

    如果快乐你就笑哈
    如果快乐你就笑哈哈
    如果快乐你就笑哈哈哈哈
    如果快乐你就笑哈哈哈哈哈哈哈哈哈

这些字符串里面,“哈”字重复出现,所以如果用星号来表示,那么就可以全部变成:

    如果快乐你就笑哈*

由于星号可以表示它前面的字符0次,所以即使写成“如果快乐你就笑”,没有“哈”字,也是满足这个正则表达式的。

既然星号可以表示它前面的字符,那么如果它前面的字符是一个点号呢?例如下面这个正则表达式:

    如.*哈

它表示在“如”和“哈”中间出现“任意多个除了换行符以外的任意字符”。这句话看起来有点绕,用下面几个字符串来说明,它们全部都可以用上面的这个正则表达式来表示:

    如哈
    如果快乐哈
    如果快乐你就笑哈
    如果你知道1+1=2那么请计算地球的半径哈
    如aklsdjfjaf哈

3.问号“? ”

问号表示它前面的子表达式0次或者1次。注意,这里的问号是英文问号。

例如下面这两个不同的字符串:

    笑起来。
    笑起来哈。

在汉字“来”和中文句号之间有0个或者1个“哈”字,都可以使用下面这个正则表达式来表示:

    笑起来哈?。

问号最大的用处是与点号和星号配合起来使用,构成“.*? ”。通过正则表达式来提取信息的时候,用到最多的也是这个组合。

下面的所有字符串:

    如哈
    如果快乐哈
    如果快乐你就笑哈
    如果你知道1+1=2那么请计算地球的半径哈
    如aklsdjfjaf哈

都可以用下面这个正则表达式来表示:

    如.*?哈

那么“.*”和“.*? ”有什么区别呢?在学习了Python的正则表达式以后,将通过实际的例子来进行解答。

4.反斜杠“\”

反斜杠在正则表达式里面不能单独使用,甚至在整个Python里都不能单独使用。反斜杠需要和其他的字符配合使用来把特殊符号变成普通符号,把普通符号变成特殊符号。

在正则表达式里面,很多符号都是有特殊意义的,例如问号、星号、大括号、中括号和小括号。那么如果要匹配的内容里面本身就有这些符号怎么办呢?如何告诉正则表达式现在只想把问号当作普通的问号来使用呢?

有如下一段字符串:

    我的密码是*12345*不包括最外层星号。

如何通过正则表达式来表示呢?如果写成:

    我的密码是*.**不包括最外层星号。

此时就会出问题,因为星号本身在正则表达式里面是有特殊意义的,不能直接用星号来匹配星号。这个时候反斜杠就要登场了。反斜杠放在星号的前面,写成“\*”可以把星号变成普通的字符,不再具有正则表达式的意义。因此,正则表达式可以写成:

    我的密码是\*.*\*不包括最外层星号。

反斜杠不仅可以把特殊符号变成普通符号,还可以把普通符号变成特殊符号。例如“n”只是一个普通的字母,但是“\n”代表换行符。在Python开发中,经常遇到的转义字符,如表3-1所示。

表3-1 常见的转义字符

在使用了反斜杠以后,反斜杠和它后面的一个字符构成一个整体,因此应该将“\n”看成一个字符,而不是两个字符。

V3-2 提取数字

5.数字“\d”

正则表达式里面使用“\d”来表示一位数字。为什么要用字母d呢?因为d是英文“digital(数字)”的首字母。

再次强调一下,“\d”虽然是由反斜杠和字母d构成的,但是要把“\d”看成一个正则表达式符号整体。

如果要提取两个数字,可以使用\d\d;如果要提取3个数字,可以使用\d\d\d。但是如果不知道这个数有多少位怎么办呢?就需要用*号来表示一个任意位数的数字。

下面一段字符串:

    是123455677,请记住它。
    是1,请记住它。
    是66666,请记住它。

全部都可以使用下面这个正则表达式来表示:

    是\d*,请记住它。

V3-3 括号的使用

6.小括号“()”

小括号可以把括号里面的内容提取出来。

前面讲到的符号仅仅能让正则表达式“表示”一串字符串。但是如果要从一段字符串中“提取”出一部分的内容应该怎么办呢?这个时候就需要使用小括号了。

有如下一个字符串:

                      我的密码是:12345abcde你帮我记住。

可以看出,这里的密码左边有一个英文冒号,右边有一个汉字“你”。当构造一个正则表达式:.*?你时,得到的结果将会是:

    :12345abcde你

然而,冒号和汉字“你”并不是密码的一部分,如果只想要“12345abcde”,就需要使用括号:

    :(.*? )你

得到的结果就是:

    12345abcde

3.1.2 在Python中使用正则表达式

Python已经自带了一个功能非常强大的正则表达式模块。使用这个模块可以非常方便地通过正则表达式来从一大段文字中提取有规律的信息。

V3-4 findall的使用

V3-5 提取文本

Python的正则表达式模块名字为“re”,也就是“regular expression”的首字母缩写。在Python中需要首先导入这个模块再进行使用。导入的语句为:

                                            import re

1.findall

Python的正则表达式模块包含一个findall方法,它能够以列表的形式返回所有满足要求的字符串。

findall的函数原型为:

    re.findall(pattern, string, flags=0)

pattern表示正则表达式,string表示原来的字符串,flags表示一些特殊功能的标志。

findall的结果是一个列表,包含了所有的匹配到的结果。如果没有匹配到结果,就会返回空列表,如图3-1所示。

图3-1 findall返回的内容

当需要提取某些内容的时候,使用小括号将这些内容括起来,这样才不会得到不相干的信息。如果包含多个“(.*? )”怎么返回呢?如图3-2所示,返回的仍然是一个列表,但是列表里面的元素变为了元组,元组里面的第1个元素是账号,第2个元素为密码。

图3-2 多个括号内的内容会以元组形式返回

请注意代码中的冒号和逗号,图3-1代码中为中文冒号和中文逗号;图3-2代码中为英文冒号和英文逗号。在实际使用正则表达式的过程中,中英文标点符号混淆常常会导致各种问题。特别是冒号、逗号和引号,虽然中英文看起来非常相似,但实际上中文冒号和英文冒号是不一样的,中文逗号和英文逗号也是不一样的。在某些字体里面,这种差异甚至无法察觉,因此在涉及正则表达式中的标点符号时,最好直接复制粘贴,而不要手动输入。

函数原型中有一个flags参数。这个参数是可以省略的。当不省略的时候,具有一些辅助功能,例如忽略大小写、忽略换行符等。这里以忽略换行符为例来进行说明,如图3-3所示。

图3-3 使用re.S作为flag来忽略换行符

在爬虫的开发过程中非常容易出现这样的情况,要匹配的内容存在换行符“\n”。要忽略换行符,就需要使用到“re.S”这个flag。虽然说匹配到的结果中出现了“\n”这个符号,不过总比什么都得不到强。内容里面的换行符在后期清洗数据的时候把它替换掉即可。

V3-6 search的使用

2.search

search()的用法和findall()的用法一样,但是search()只会返回第1个满足要求的字符串。一旦找到符合要求的内容,它就会停止查找。对于从超级大的文本里面只找第1个数据特别有用,可以大大提高程序的运行效率。

search()的函数原型为:

                      re.search(pattern, string, flags=0)

对于结果,如果匹配成功,则是一个正则表达式的对象;如果没有匹配到任何数据,就是None。如果需要得到匹配到的结果,则需要通过.group()这个方法来获取里面的值,如图3-4所示。

图3-4 使用.group()来获取search()方法找到的结果

只有在.group()里面的参数为1的时候,才会把正则表达式里面的括号中的结果打印出来。

.group()的参数最大不能超过正则表达式里面括号的个数。参数为1表示读取第1个括号中的内容,参数为2表示读取第2个括号中的内容,以此类推,如图3-5所示。

图3-5 .group()的参数意义

V3-7 (.*)和(.*? )的区别

3.“.*”和“.*? ”的区别

在爬虫开发中,.*?这3个符号大多数情况下一起使用。

点号表示任意非换行符的字符,星号表示匹配它前面的字符0次或者任意多次。所以“.*”表示匹配一串任意长度的字符串任意次。这个时候必须在“.*”的前后加其他的符号来限定范围,否则得到的结果就是原来的整个字符串。

如果在“.*”的后面加一个问号,变成“.*? ”,那么可以得到什么样的结果呢?问号表示匹配它前面的符号0次或者1次。于是.*?的意思就是匹配一个能满足要求的最短字符串。

这样说起来还是非常抽象,下面通过一个实际的例子来进行说明。请看下面这一段话:

    我的微博密码是:1234567, QQ密码是:33445566, 银行卡密码是:888888, Github密码是:999abc999,帮我记住它们

这段话有一个显著的规律,即密码是:xxxxxx, ”,也就是在“密码是”这3个汉字的后面跟一个中文的冒号,冒号后面是密码,密码后面是中文的逗号。

如果想把这4个密码提取出来,可以构造以下两个正则表达式:

    密码是:(.*),
    密码是:(.*? ),

配合Python的findall方法,得到结果如图3-6图所示。

图3-6 使用“.*”和“.*? ”返回的结果

使用“(.*)”得到的是只有一个元素的列表,里面是一个很长的字符串。

使用第2个正则表达式“(.*? )”,得到的结果是包含4个元素的列表,每个元素直接对应原来文本中的每个密码。

举一个例子,10个人肩并肩并排站着,使用“(.*)”取到了第1个人左手到第10个人右手之间的所有东西,而使用“(.*? )”取到的是“每个人”的左手和右手之间的东西。

一句话总结如下。

①“.*”:贪婪模式,获取最长的满足条件的字符串。

②“.*? ”:非贪婪模式,获取最短的能满足条件的字符串。

3.1.3 正则表达式提取技巧

1.不需要compile

网上很多人的文章中,正则表达式使用re.compile()这个方法,导致代码变成下面这样:

    import re
    example_text = ’我是kingname, 我的微博账号是:kingname, 密码是:12345678, QQ账号是:99999, 密
    码是:890abcd, 银行卡账号是:000001, 密码是:654321, Github账号是:99999@qq.com, 密码
    是:7777love8888, 请记住他们。'
    new_pattern=re.compile(’账号是:(.*? ), 密码是:(.*? ), ', re.S)
    user_pass = re.findall(new_pattern, example_text)
    print(user_pass)

V3-8 正则表达式提取技巧

这种写法虽然结果正确,但纯粹是画蛇添足,是对Python的正则表达式模块没有理解透彻的体现,是从其他啰嗦的编程语言中带来的坏习惯。如果阅读Python的正则表达式模块的源代码,就可以看出re.compile()是完全没有必要的。

对比re.compile()和re.findall()在源代码中的写法,如图3-7所示的两个方框。

图3-7 Python正则表达式模块中的re.findall()和re.compile()

使用re.compile()的时候,程序内部调用的是_compile()方法;当使用re.finall()的时候,在模块内部自动先调用了_compile()方法,再调用findall()方法。re.findall()自带re.compile()的功能,所以没有必要使用re.compile()。

Python 3中正则表达式模块的源代码的入口文件为re.py。这个文件里面的注释就是学习Python正则表达式模块非常好的文档,它包含了正则表达式各种符号的简单说明和这个模块内部各个方法的使用,如图3-8所示。

图3-8 Python re.py文件自带的文档

re.py在Python 3安装文件夹下面的Lib文件夹中。

使用Windows的读者可以在Python安装文件夹下面的Lib文件夹里面找到re.py,例如:

    C:\Python3.6\Lib\re.py

使用Mac OS的读者,可以在类似于如下路径的地方找到re.py:

    /usr/local/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/re.py

使用Linux的读者,可以在类似于如下路径的地方找到re.py:

    /usr/lib/python3.6/re.py

2.先抓大再抓小

一些无效内容和有效内容可能具有相同的规则。这种情况下很容易把有效内容和无效内容混在一起,如下面这段文字:

    有效用户:
    姓名:张三
    姓名:李四
    姓名:王五
    无效用户:
    姓名:不知名的小虾米
    姓名:隐身的张大侠

有效用户和无效用户的名字前面都以“姓名: ”开头,如果使用“姓名: (.*? )\n”来进行匹配,就会把有效信息和无效信息混在一起,难以区分,如图3-9所示。

图3-9 使用“姓名: (.*? )\n”导致有效内容和无效内容混在一起

要解决这个问题,就需要使用先抓大再抓小的技巧。先把有效用户这个整体匹配出来,再从有效用户里面匹配出人名,代码和运行效果如图3-10所示。先抓大再抓小的思想会贯穿整个爬虫开发过程,一定要重点掌握。

图3-10 代码和运行效果

3.括号内和括号外

在上面的例子中,括号和“.*? ”都是一起使用的,因此可能会有读者认为括号内只能有这3种字符,不能有其他普通的字符。但实际上,括号内也可以有其他字符,对匹配结果的影响如图3-11所示。

图3-11 括号里有无其他字符对匹配结果的影响

如果括号里面有其他普通字符,那么这些普通字符就会出现在获取的结果里面。举一个例子,如果说“左手和右手之间”,一般指的是躯干这一部分。但如果说“左手和右手之间,包括左手和右手”,那么就是指的整个人。而把普通的字符放在括号里面,就表示结果中需要包含它们。