有了前面几节课的准备,我们这一次终于可以真刀真枪的干一场大的了,但是呢,在进行实战之前,我们还要讲讲正则表达式的实用方法和扩展语法,然后再来实战,大家多把持一会啊。
我们先来翻一下文档:
首先,我们要举的例子是讲得最多的 search() 方法,search() 方法 既有模块级别的,就是直接调用 re.search() 来实现,另外,编译后的正则表达式模式对象也同样拥有 search() 方法,我问问大家,它们之间有区别吗?
如果你的回答仅仅是 模块级别的search() 方法比模式级别的search() 方法要多一个正则表达式的参数,那你肯定没有去翻文档。
re.
search
(pattern, string, flags=0)Scan through string looking for the first location where the regular expression pattern produces a match, and return a corresponding match object. Return
None
if no position in the string matches the pattern; note that this is different from finding a zero-length match at some point in the string.
这是模块级别的 search() 方法,大家注意它的参数,它有一个 flags 参数, flags 参数就我们上节课讲得 编译标志 位,作为一个模块级别的,它没办法复印,它直接在这里使用它的标志位就可以了。
pattern 是正则表达式的模式
string 是要搜索的字符串
我们再来看一下 如果是编译后的模式对象,它的 search() 方法又有哪些参数:
regex.
search
(string[, pos[, endpos]])Scan through string looking for the first location where this regular expression produces a match, and return a corresponding match object. Return
None
if no position in the string matches the pattern; note that this is different from finding a zero-length match at some point in the string.The optional second parameter pos gives an index in the string where the search is to start; it defaults to
0
. This is not completely equivalent to slicing the string; the'^'
pattern character matches at the real beginning of the string and at positions just after a newline, but not necessarily at the index where the search is to start.The optional parameter endpos limits how far the string will be searched; it will be as if the string is endpos characters long, so only the characters from pos to
endpos - 1
will be searched for a match. If endpos is less than pos, no match will be found; otherwise, if rx is a compiled regular expression object,rx.search(string, 0, 50)
is equivalent torx.search(string[:50], 0)
.
前面的 pattern, 模式对象的参数,就不需要了。
string 第一个参数就是待搜索的字符串
后面有两个可选参数是我们模块级别的 search() 方法没有的,它分别代表需要搜索的起始位置(pos)和结束位置(endpos)
你就可以像 rx.search(string, 0, 50) 或者 rx.search(string[:50], 0) 这样子去匹配它的搜索位置了。
还有一点可能被忽略的就是,search() 方法并不会立刻返回你所需要的字符串,取而代之,它是返回一个匹配对象。我们来举个例子:
-
>>> import re
-
>>> result = re.search(r" (\w+) (\w+)", "I love Python.com")
-
>>> result
-
<_sre.SRE_Match object; span=(1, 13), match=' love Python'>
我们看到,这个 result 是一个匹配对象( match object.),而不是一个字符串。它这个匹配对象有一些方法,你使用这些方法才能够获得你所需要的匹配的字符串:
例如:group()方法:
-
>>> result.group()
-
' love Python'
我们就把匹配的内容打印出来了。首先是 一个空格,然后是 \w+ ,就是任何字符,这里就是love,然后又是一个空格,然后又是 \w+,这里就是Python。
说到这个 group()方法,值的一提的是,如果正则表达式中存在着 子组,子组会将匹配的内容进行 捕获,通过这个 group()方法 中设置序号,可以提取到对应的 子组(序号从1开始) 捕获的字符串。例如:
-
>>> result.group(1)
-
'love'
-
>>> result.group(2)
-
'Python'
除了 group()方法 之外,它还有 start()方法 、end()方法、 span() 方法,分别返回它匹配的开始位置、结束位置、范围。
match.
start
([group])
match.
end
([group])Return the indices of the start and end of the substring matched by group; group defaults to zero (meaning the whole matched substring). Return
-1
if group exists but did not contribute to the match. For a match object m, and a group g that did contribute to the match, the substring matched by group g (equivalent tom.group(g)
) ism.string[m.start(g):m.end(g)]Note that
m.start(group)
will equalm.end(group)
if group matched a null string. For example, afterm = re.search('b(c?)', 'cba')
,m.start(0)
is 1,m.end(0)
is 2,m.start(1)
andm.end(1)
are both 2, andm.start(2)
raises an IndexError exception.An example that will remove remove_this from email addresses:
>>> email = "tony@tiremove_thisger.net" >>> m = re.search("remove_this", email) >>> email[:m.start()] + email[m.end():] 'tony@tiger.net'
match.
span
([group])For a match m, return the 2-tuple
(m.start(group), m.end(group))
. Note that if group did not contribute to the match, this is(-1, -1)
. group defaults to zero, the entire match.
-
>>> result.start()
-
1
-
>>> result.end()
-
13
-
>>> result.span()
-
(1, 13)
接下来讲讲 findall() 方法:
re.
findall
(pattern, string, flags=0)Return all non-overlapping matches of pattern in string, as a list of strings. The string is scanned left-to-right, and matches are returned in the order found. If one or more groups are present in the pattern, return a list of groups; this will be a list of tuples if the pattern has more than one group. Empty matches are included in the result unless they touch the beginning of another match.
有人可能会觉得,findall() 方法很容易,不就是找到所有匹配的内容,然后把它们组织成列表的形式返回吗。
没错,这是在正则表达式里没有子组的情况下所做的事,如果正则表达式里包含了子组,那么,findall() 会变得很聪明。
我们来举个例子吧,上贴吧爬图:
例如我们想下载这个页面的所有图片:贴吧404
我们先来踩点,看到图片格式的标签:
我们就来直接写代码啦:
首先,我们写下下面的代码,来爬取 图片地址:
-
import re
-
p = r'<img class="BDE_Image" src="[^"]+\.jpg"'
-
imglist = re.findall(p, html)
-
for each in imglist:
-
print(each)
打印的结果为:
-
============== RESTART: C:\Users\XiangyangDai\Desktop\tieba.py ==============
-
<img class="BDE_Image" src="https://imgsa.baidu.com/forum/w%3D580/sign=65ac7c3d9e0a304e5222a0f2e1c9a7c3/4056053b5bb5c9ea8d7d0bdadc39b6003bf3b34e.jpg"
-
<img class="BDE_Image" src="https://imgsa.baidu.com/forum/w%3D580/sign=d887aa03394e251fe2f7e4f09787c9c2/77f65db5c9ea15ceaf60e830bf003af33b87b24e.jpg"
-
<img class="BDE_Image" src="https://imgsa.baidu.com/forum/w%3D580/sign=0db90d472c1f95caa6f592bef9167fc5/2f78cfea15ce36d34f8a8b0933f33a87e850b14e.jpg"
-
<img class="BDE_Image" src="https://imgsa.baidu.com/forum/w%3D580/sign=abfd18169ccad1c8d0bbfc2f4f3f67c4/bd2713ce36d3d5392db307fa3387e950342ab04e.jpg"
很显然,这不是我们需要的地址,我们需要的只是后面的部分。我们接下来要解决的问题就是如何将里面的地址提取出来,不少人听到这里,可能就已经开始动手了。但是,别急,我这里有更好的方法。
只需要把图片地址用小括号括起来,即将:
p = r'<img class="BDE_Image" src="[^"]+\.jpg"' 改为 p = r'<img class="BDE_Image" src="([^"]+\.jpg)"',
大家再来看一下运行后的结果:
-
============== RESTART: C:\Users\XiangyangDai\Desktop\tieba.py ==============
-
https://imgsa.baidu.com/forum/w%3D580/sign=65ac7c3d9e0a304e5222a0f2e1c9a7c3/4056053b5bb5c9ea8d7d0bdadc39b6003bf3b34e.jpg
-
https://imgsa.baidu.com/forum/w%3D580/sign=d887aa03394e251fe2f7e4f09787c9c2/77f65db5c9ea15ceaf60e830bf003af33b87b24e.jpg
-
https://imgsa.baidu.com/forum/w%3D580/sign=0db90d472c1f95caa6f592bef9167fc5/2f78cfea15ce36d34f8a8b0933f33a87e850b14e.jpg
-
https://imgsa.baidu.com/forum/w%3D580/sign=abfd18169ccad1c8d0bbfc2f4f3f67c4/bd2713ce36d3d5392db307fa3387e950342ab04e.jpg
是不是很兴奋,是不是很惊讶,先别急,我先把代码敲完,再给大家讲解。
-
import urllib.request
-
import re
-
def open_url(url):
-
req = urllib.request.Request(url)
-
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36')
-
response = urllib.request.urlopen(url)
-
html = response.read()
-
return html
-
def get_img(url):
-
html = open_url(url).decode('utf-8')
-
p = r'<img class="BDE_Image" src="([^"]+\.jpg)"'
-
imglist = re.findall(p, html)
-
'''
-
for each in imglist:
-
print(each)
-
'''
-
for each in imglist:
-
filename = each.split('/')[-1]
-
urllib.request.urlretrieve(each, filename, None)
-
if __name__ == '__main__':
-
url = "https://tieba.baidu.com/p/4863860271"
-
get_img(url)
运行结果,就是很多美眉图片出现在桌面了(前提是这个程序在桌面运行,图片自动下载到程序所在文件夹。)
接下来就来解决大家的困惑了:为什么加个小括号会如此方便呢?
这是因为在 findall() 方法中,如果给出的正则表达式是包含着子组的话,那么就会把子组的内容单独给返回回来。然而,如果存在多个子组,那么它还会将匹配的内容组合成元组的形式再返回。
我们还是举个例子:
因为有时候 findall() 如果使用的不好,很多同学就会感觉很疑惑,很迷茫……
拿前面匹配 ip 地址的正则表达式来讲解,我们使用 findall() 来尝试自动从https://www.xicidaili.com/wt/获取 ip 地址:
初代码如下:
-
import urllib.request
-
import re
-
def open_url(url):
-
req = urllib.request.Request(url)
-
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36')
-
reponse = urllib.request.urlopen(req)
-
html = reponse.read()
-
return html
-
def get_ip(url):
-
html = open_url(url).decode('utf-8')
-
p = r'(([0,1]?\d?\d|2[0-4]\d|25[0-5])\.){3}([0,1]?\d?\d|2[0-4]\d|25[0-5])'
-
iplist = re.findall(p, html)
-
for each in iplist:
-
print(each)
-
if __name__ == "__main__":
-
url = "https://www.xicidaili.com/wt/"
-
get_ip(url)
运行结果如下:
-
============== RESTART: C:\Users\XiangyangDai\Desktop\getIP.py ==============
-
('180.', '180', '122')
-
('248.', '248', '79')
-
('129.', '129', '198')
-
('217.', '217', '7')
-
('40.', '40', '35')
-
('128.', '128', '21')
-
('118.', '118', '106')
-
('101.', '101', '46')
-
('3.', '3', '4')
得到的结果让我们很迷茫,为什么会这样呢?这明显不是我们想要的结果,这是因为我们在正则表达式里面使用了 3 个子组,所以,findall() 会自作聪明的把我们的结果做了分类,然后用 元组的形式返回给我们。
那有没有解决的方法呢?
要解决这个问题,我们可以让子组不捕获内容。
我们查看 -> Python3 正则表达式特殊符号及用法(详细列表),寻求扩展语法。
让子组不捕获内容,扩展语法 就是非捕获组:
所以我们的初代码修改如下:
-
import urllib.request
-
import re
-
def open_url(url):
-
req = urllib.request.Request(url)
-
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36')
-
reponse = urllib.request.urlopen(req)
-
html = reponse.read()
-
return html
-
def get_ip(url):
-
html = open_url(url).decode('utf-8')
-
p = r'(?:(?:[0,1]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[0,1]?\d?\d|2[0-4]\d|25[0-5])'
-
iplist = re.findall(p, html)
-
for each in iplist:
-
print(each)
-
if __name__ == "__main__":
-
url = "https://www.xicidaili.com/wt/"
-
get_ip(url)
运行得到的结果也是我们想要的 ip 地址了,如下:
-
============== RESTART: C:\Users\XiangyangDai\Desktop\getIP.py ==============
-
183.47.40.35
-
61.135.217.7
-
221.214.180.122
-
101.76.248.79
-
182.88.129.198
-
175.165.128.21
-
42.48.118.106
-
60.216.101.46
-
219.245.3.4
-
117.85.221.45
接下来我们又回到文档:
另外还有一些使用的方法,例如:
finditer() ,是将结果返回一个迭代器,方便以迭代方式获取数据。
sub() ,是实现替换的操作。
在Python3 正则表达式特殊符号及用法(详细列表)中也还有一些特殊的语法,例如:
(?=...):前向肯定断言。
(?!...):前向否定断言。
(?<=...):后向肯定断言。
(?<!...):后向肯定断言。
这些都是非常有用的,但是呢,这些内容有点多了,如果说全部都讲正则表达式的话,那我们就是喧宾夺主了,我们主要讲的是 网络爬虫 哦。