认识正则表达式
为什么要学习正则表达式
因为爬虫需要!!!
一般来说爬虫需要四个主要步骤:
- 明确目标 (要知道你准备在哪个范围或者网站去搜索)
一般情况我们拉取的网页数据庞大并且很混乱,其中很大一部分东西是我们不关心的,因此我们需要将其按要求过滤和匹配出来。
那么对于文本的过滤和指定规则的匹配,最强大的就是正则表达式,可以说正则表达式是Python爬虫世界里必不可少的神兵利器。
什么是正则表达式
正则表达式,又称规则表达式,通常被用来检索、替换那些符合某个模式(规则)的文本。
正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。
对于爬虫来说,有了正则表达式,再从 HTML 里提取想要的信息就非常方便了。
给定一个正则表达式和目标字符串,我们可以达到如下的目的:
- 目标字符串是否符合正则表达式的过滤逻辑("匹配");
- 通过正则表达式,从目标字符串中获取我们想要的特定部分("过滤")。
正则表达式匹配能力非常的强大且实用,同时相对应的匹配规则也非常的丰富,要想学好用好正则表达式光靠看和记是不行的,最好的方法就是想办法去使用它,在使用的过程中强化对匹配规则的理解。
正则表达式示例
前面说了很多的概念,对正则表达式到底是个什么,可能还是比较模糊,下面我们就用实例来带大家看一下正则表达式的用法。
这里需要用到一个工具,地址是: http://tool.oschina.net/regex/,打开页面,输入待匹配的文本,然后选择常用的正则表达式,就可以得出相应的匹配结果了。
比如,这里输入待匹配的文本如下:
晚上好, 欢迎来到Python课堂,电话是: 0731-16899168; email是: ybf@bafu.com; 个人主页: http://www.youbafu.com.
这段字符串中包含了一个电话号码和一个电子邮件,接下来就尝试用正则表达式提取出来,如下图所示:
在网页右侧选择 "匹配 Email 地址",就可以看到下方出现了文本中的 E-mail。如果选择 "匹配网址 URL",就可以看到下方出现了文本中的 URL,是不是有点神奇。
其实,这里就是用到了正则表达式匹配,也就是用一定的规则将特定的文本提取出来。比如,电子邮件开头是一段字符串,然后是一个 @符号,最后是某个域名,这是有特定的组成格式的。另外,对于 URL,开头是协议类型,然后是冒号加双斜线,最后是域名加路径。
对于 URL 来说,可以用下面的正则表达式匹配:
用这个正则表达式去匹配一个字符串,如果这个字符串中包含类似 URL 的文本,那就会被提取出来。
这个正则表达式看上去是乱糟糟的一团,其实不然,这里面都是有特定的语法规则的。比如,a-z 代表匹配任意的小写字母,\s 表示匹配任意的空白字符,* 就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。
写好正则表达式后,就可以拿它去一个长字符串里匹配查找了。不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。对于网页来说,如果想找出网页源代码里有多少 URL,用匹配 URL 的正则表达式去匹配即可。
上面我们列举了几个匹配规则,下表列出了一些较为常用的匹配语法规则。
| |
|---|
| 匹配字母、数字、下划线,等价于[a-zA-Z0-9_] \w可以匹配汉字(python) |
| |
| |
| |
| |
| |
| |
| 匹配字符串结尾的,如果存在换行,只匹配到换行前的结束字符串 |
| |
| |
| |
| |
| |
', content)print(result)print(result.group())print(result.span()) | |
这里我们将中间部分直接省略,全部用.* 来代替,最后加一个结尾字符串就好了。运行结果如下:
<re.Match object; span=(0, 34), match='Hi 1234567 Python一起来学习正则 match Demo'>Hi 1234567 Python一起来学习正则 match Demo(0, 34)
可以看到,group() 方法输出了匹配的全部字符串,也就是说我们写的正则表达式匹配到了目标字符串的全部内容;span() 方法输出 (0, 35) ,这是整个字符串的长度。
因此,我们可以使用.* 简化正则表达式的书写。
贪婪与非贪婪
在使用上面的通用匹配.* 时,需要注意一点,可能有时候匹配到的并不是我们想要的结果。
来看示例:
import recontent = 'Hi 1234567 Python一起来学习正则 match Demo'result = re.match('^Hi.*(\d+).*Demo ,看样子并没有什么问题。我们看下运行结果:<re.Match object; span=(0, 34), match='Hi 1234567 Python一起来学习正则 match Demo'>7我们发现只得到了 7 这个数字,这是为什么呢?这里就涉及一个贪婪匹配与非贪婪匹配的问题了。在贪婪匹配下,.* 会匹配尽可能多的字符。正则表达式中.* 后面是 \d+ ,也就是至少一个数字,并没有指定具体多少个数字,因此,.* 就尽可能匹配多的字符,这里就把 123456 匹配了,给 \d+ 留下一个可满足条件的数字 7,最后得到的内容就只有数字 7 了。但这很明显会给我们带来很大的不便。有时候,匹配结果会莫名其妙少了一部分内容。其实,这里只需要使用非贪婪匹配就好了。非贪婪匹配的写法是.*? ,多了一个 ? ,那么它可以达到怎样的效果?我们再用实例看一下:import recontent = 'Hi 1234567 Python一起来学习正则 match Demo'result = re.match('^Hi.*?(\d+).*Demo', content)print(result.group(1))和上面的例子相仿,我们在字符串中加了换行符,正则表达式还是一样的,用来匹配其中的数字。看一下运行结果:Traceback (most recent call last): File "E:/爬虫/修饰符.py", line 16, in <module> print(result.group(1))AttributeError: 'NoneType' object has no attribute 'group'运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为 None ,而我们又调用了 group() 方法导致 AttributeError 。那么,为什么加了一个换行符,就匹配不到了呢?这是因为 . 匹配的是除换行符之外的任意字符,当遇到换行符时,.*? 就不能匹配了,所以导致匹配失败。这里只需加一个修饰符 re.S ,即可修正这个错误:result = re.match('^Hi.*?(\d+).*?Demore.S使. 匹配包括换行在内的所有字符re.U根据 Unicode 字符集解析字符。这个标志影响 \w 、\W 、\b 和 \Bre.X该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解在网页匹配中,较为常用的有 re.S 和 re.I 。转义匹配我们知道正则表达式定义了许多匹配模式,如. 匹配除换行符以外的任意字符,但是如果目标字符串里面就包含. ,那该怎么办呢?这里就需要用到转义匹配了,示比如下:import recontent = '(渣男教父)www.bpsend.net'result = re.match('\(渣男教父\)www\.bpsend\.net', content)print(result)当遇到用于正则匹配模式的特殊字符时,在前面加反斜线转义一下即可。比如. 就可以用 . 来匹配,运行结果如下:<re.Match object; span=(0, 16), match='(渣男教父)www.bpsend.net'>可以看到,这里成功匹配到了原字符串。这些是写正则表达式常用的几个知识点,熟练掌握它们对后面写正则表达式匹配非常有帮助。search()前面提到过,match() 方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。我们看下面的例子:import recontent = '加点头头 Hi 1234567 Python一起来学习正则 search Demo 接点尾巴'result = re.match('Hi.*?(\d+).*?Demo', content)print(result)这里的字符串以加点头头开头,但是正则表达式以 Hi 开头,整个正则表达式是字符串的一部分,但是这样匹配是失败的。运行结果如下:None因为 match() 方法在使用时需要考虑到开头的内容,这在做匹配时并不方便。它更适合用来检测某个字符串是否符合某个正则表达式的规则。这里就有另外一个方法 search() ,它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果。也就是说,正则表达式可以是字符串的一部分,在匹配时,search() 方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,就返回 None 。我们把上面代码中的 match() 方法修改成 search() 。再看下运行结果:import recontent = '加点头头 Hi 1234567 Python一起来学习正则 search Demo 接点尾巴'result = re.search('Hi.*?(\d+).*?Demo', content)print(result)print(result[1])这时就得到了匹配结果。 上代码:<re.Match object; span=(5, 40), match='Hi 1234567 Python一起来学习正则 search Demo'>1234567因此,为了匹配方便,我们可以尽量使用 search() 方法。下面再用几个实例来看看 search() 方法的用法。首先,这里有一段待匹配的 HTML 文本,接下来写几个正则表达式实例来实现相应信息的提取:文档总结一、核心知识点回顾1. 正则表达式到底是什么?正则表达式就是一套"规则模板",用来在一大段文本里精准找到你想要的东西。打个比方:动筛选器",你告诉它规则,它帮你找爬虫中的位置:获取网页HTML → 正则表达式提取 → 得到目标数据2. 常用匹配符号(必须记住的)符号含义示例匹配内容\d数字\d{3}3个数字,如 123\w字母、数字、下划线\w+python、user_01\s空白字符\s空格、制表符.任意字符(除换行)a.cabc、a@c、a c*0次或多次ab*a、ab、abb+1次或多次ab+ab、abb(至少1个b)?0次或1次ab?a、ab{n}精确n次\d{4}4个数字{n,m}n到m次\d{2,4}2-4个数字^开头^Hello以Hello开头$结尾end$以end结尾[]字符集合[abc]a或b或c[^]排除集合[^abc]除a、b、c外的字符``或`a()分组(ab)+ab、abab记忆口诀:\d = digit(数字)\w = word(单词字符)\s = space(空白)* = 可有可无(0到无穷)+ = 至少一个(1到无穷)? = 有没有都行(0或1)3. re 模块三大方法方法作用匹配位置返回值match()从开头匹配必须从字符串开头Match对象或Nonesearch()全文搜索任意位置(找第一个)Match对象或Nonefindall()全文搜索任意位置(找所有)列表使用建议:验证格式用 match()(比如验证手机号、邮箱)提取内容用 search()(找第一个匹配的)批量提取用 findall()(找所有匹配的)4. 分组提取(重点)import retext = "电话: 0731-12345678"result = re.search(r'(\d{4})-(\d{8})', text)print(result.group(0)) # 完整匹配: 0731-12345678print(result.group(1)) # 第一组: 0731print(result.group(2)) # 第二组: 12345678规则:group(0) 或 group() = 完整匹配结果group(1) = 第一个括号里的内容group(2) = 第二个括号里的内容以此类推...5. 贪婪 vs 非贪婪(必考)贪婪模式: 能匹配多少就匹配多少(尽量多)import retext = "abc123def456ghi"result = re.search(r'a.*d', text)print(result.group()) # abc123d(尽量多匹配)非贪婪模式: 能匹配多少就匹配多少(尽量少)import retext = "abc123def456ghi"result = re.search(r'a.*?d', text)print(result.group()) # abc123d(尽量少匹配)关键区别:.* = 贪婪(吃得多).*? = 非贪婪(吃得少)实战建议:提取HTML标签内容时,一定要用非贪婪.*?否则会把中间所有标签都吞掉!6. 修饰符(常用两个)修饰符作用使用场景re.S让.匹配换行符匹配HTML多行内容re.I忽略大小写匹配URL、邮箱等import rehtml = """<div>第一行第二行</div>"""# 不用 re.S,. 匹配不到换行result1 = re.search(r'<div>.*</div>', html)print(result1) # None# 用了 re.S,. 可以匹配换行result2 = re.search(r'<div>.*</div>', html, re.S)print(result2.group()) # <div>\n第一行\n第二行\n</div>7. 转义匹配如果目标文本里就有正则符号(比如 .、(、)),需要加 \ 转义:import retext = "文件名为: report(2024).pdf"# 错误写法:() 和 . 会被当成正则符号# result = re.search(r'report(2024).pdf', text) # 报错!# 正确写法:转义特殊字符result = re.search(r'report\(2024\)\.pdf', text)print(result.group()) # report(2024).pdf需要转义的字符:.^$*+?{}[]\|()二、常见坑与解决方案坑1:贪婪匹配导致提取内容过多症状:用 .* 匹配HTML,结果把后面一堆不相关的内容也包进来了原因:.* 是贪婪模式,会尽量多匹配解决:改用 .*? 非贪婪模式坑2:match() 匹配失败症状:明明字符串里有目标内容,match() 却返回 None原因:match() 只从开头匹配,开头不对就失败解决:改用 search() 或检查正则开头是否正确坑3:. 匹配不到换行症状:HTML明明有内容,但正则就是匹配不到原因:HTML经常换行,而 . 默认不匹配换行符解决:加 re.S 修饰符坑4:分组索引错误症状:调用 group(1) 时报错原因:正则里没有写 () 分组,或者分组数量不够解决:检查正则里有多少对括号,group(n) 的 n 不能超过括号数量坑5:特殊字符没转义症状:匹配包含 .、() 等符号的文本时结果不对原因:这些符号在正则有特殊含义解决:前面加 \ 转义,比如 \. \( \)三、学习建议理解比死记更重要 —— 不用背所有符号,理解每个符号的含义就行多用在线工具练习 —— 推荐 regex101: build, test, and debug regex ,可以实时看到匹配结果爬虫中优先用 search() —— 比 match() 灵活,比 findall() 简单HTML提取必用非贪婪 —— .*? 是爬虫标配,千万别用 .*re.S 是HTML好搭档 —— 网页源码经常换行,不加 re.S 经常匹配失败分组是提取利器 —— 想提取什么就用 () 括起来,然后 group(n) 获取