作者:CoderPig正则表达式(Regular Expression),一个根据「特定语法结构编写的字符串」,常用于字符串的检索、替换及匹配验证。有啥用?来个简单的例子感受下:从下述字符串中提取手机号码水电费了开的斯洛13712345678伐克圣诞节要怎么提取?遍历字符串一个个字符判断?很麻烦吧,如果用正则的话,只需下述一串字符串:(0|86|17951)?(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])[0-9]{8}随手打开一个在线正则测试工具,复制粘贴:
https://tool.oschina.net/regex/
点击测试匹配后,可以看到手机号码已被提取出来了,强大如斯,看不懂上述表达式?没关系,本节就来系统地讲解下Python中正则的详细用法。re模块Python中内置re模块用来处理正则表达式,上述在线测试的例子可以写成Python代码:import re
上述用到的search()函数是re模块提供的正则匹配函数,一一介绍下re模块提供的几个常用函数~
reg_string = '水电费了开的斯洛13712345678伐克圣诞节'
reg = '(0|86|17951)?(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])[0-9]{8}'
result = re.search(reg, reg_string)print(result)# 输出:#
// 0x1、常用函数 //
----------------①:匹配re.match(pattern, string, flags=0)尝试从字符串的开头进行匹配,匹配成功返回匹配对象,否则返回None;re.search(pattern, string, flags=0)扫描整个字符串,返回第一个匹配对象,否则返回None;----------------②:检索与替换re.findall(pattern, string, flags=0)扫描整个字符串,匹配所有能匹配的对象,以列表形式返回;re.finditer(pattern, string, flags=0)参数同findall,匹配所有能匹配的对象,但是是以迭代器形式返回;re.sub(pattern, repl, string, count=0, flags=0)将匹配的字符串替换为其他字符串,count为替换的最大次数,默认为0,替换所有。re.split(pattern, string, maxsplit=0, flags=0)将匹配的字符串进行分割,返回列表,maxsplit为分割的最大次数,默认为0,分割所有。----------------③:编译Pattern对象对于多次用的正则表达式,可调用compile()函数将正则表达式编译成Pattern对象,调用时直接Pattern对象.xxx即可,以此提高复用性。----------------④:flags修饰符在调用这些常用函数时,可传入flags参数(标志位修饰符),来控制匹配模式,有下表这些可供选择,如果想同时选择多个使用运算符"|"进行连接,比如:re.I|re.M。修饰符 | 描述 |
re.I | IGNORECASE → 忽略大小写 |
re.M | MULTILINE → 多行匹配,影响^和$ |
re.S | DOTALL → 使.匹配包括换行在内的所有字符 |
re.X | VERBOSE → 忽略空白和注释,并允许使用'#'来引导一个注释 |
re.U | UNICODE → 根据Unicode字符集解析字符,影响\w、\W、\b和\B |
re.L | LOCALE → 做本地化识别(locale-aware)匹配 |
// 0x1、加在正则字符串前的'r' //
用于告知编译器这个string是raw string(原始字符串),不要转义反斜杠,比如r'\n'是两个字符:反斜杠+n,而不是换行!// 0x2、字符规则 //
字符 | 作用 |
. | 匹配任意一个字符(除\n外) |
[...] | 匹配[]中列举的字符 |
[^...] | 匹配不在[]中列举的字符 |
\d | 匹配数字,0-9 |
\D | 匹配非数字 |
\s | 匹配空白,即空白与tab缩进 |
\S | 匹配非空白 |
\w | 匹配字母数字或下划线,a-z,A-Z,0-9,_ |
\W | 匹配非字母数字或下划线 |
- | 匹配范围,如a-f |
// 0x3、数量规则 //
Tips:前三个做了优化,速度会更快,尽量使用它们~ ?♂️字符 | 作用 |
* | 前面的字符出现0次或无限次,即可有可无 |
+ | 前面的字符出现1次或无限次,即最少一次 |
? | 前面的字符出现0次或一次,即要么不出现,要么只出现一次 |
{m} | 前面的字符出现m次 |
{m,} | 前面的字符至少出现m次 |
{m,n} | 前面的字符出现m到n次 |
// 0x4、边界规则 //
字符 | 作用 |
^ | 行的开始 |
$ | 行的结束 |
\b | 单词边界,即单词和空格间的位置,如'er\b'可匹配'never'中的'er',但不能匹配'verb'中的'er' |
\B | 匹配非单词边界 |
\A | 匹配字符串开头 |
\Z | 匹配字符串结尾,如果有换行,只匹配到换行前的结束字符串 |
\z | 匹配字符串结尾,如果有换行,会连换行符也匹配 |
// 0x5、分组 //
有时我们需要的可能是匹配字符串中的一部分内容,可以进行分组,使用括号()包裹,比如:从匹配的字符串中提取出区号和本地号码 → ^(\d{3})-(\d{3,8})$,具体规则如下表所示:字符 | 作用 |
l | 匹配左右任意一个表达式 |
(re) | 匹配括号内的表达式,也表示一个组 |
(?:re) | 同上,但不表示一个组 |
(?=re)→前向肯定断言,仅当子表达式在此位置的右侧匹配时才继续匹配,如:18(?=88) 与88结尾的18实例匹配。
(?!re)→前向否定断言,仅当子表达式不在此位置的右侧匹配时才继续匹配,如:(?!88) 与不以88结尾的实例匹配,所以与1888不匹配。
(?<&#61;re)→后向肯定断言&#xff0c;仅当子表达式在此位置的左侧匹配时才继续匹配&#xff0c;如&#xff1a;(?&#61;18)88 与 跟在18后的88实例匹配。
(?→后向否定断言&#xff0c;仅当子表达式不在此位置的左侧匹配时才继续匹配&#xff0c;如&#xff1a;(?
reg_pattern &#61; re.match(r&#39;^(\d{4})-(\d{3,8})$&#39;, &#39;0756-1234567&#39;)print(reg_pattern.group())print(reg_pattern.group(0))print(reg_pattern.group(1))print(reg_pattern.group(2))# 输出&#xff1a;0756-12345670756-123456707561234567
除此之外&#xff0c;还有四个常用函数&#xff1a;
groups()&#xff1a;从group(1)开始往后的所有值&#xff0c;返回一个元组
start()&#xff1a;返回匹配的开始位置
end()&#xff1a;返回匹配的结束位置
span()&#xff1a;返回一个元组&#xff0c;表示匹配位置(开始&#xff0c;结束)
// 0x6、贪婪与非贪婪 //
正则匹配默认是贪婪匹配&#xff0c;即匹配尽可能多的字符&#xff0c;如&#xff1a;re.match(r&#39;^(\d&#43;)(0*)$&#39;,&#39;12345000&#39;).groups()# 原意是向得到(&#39;12345&#39;, &#39;000&#39;)这样的结果的&#xff0c;但输出的却是&#xff1a;
(&#39;12345000&#39;, &#39;&#39;)# 由于贪婪匹配&#xff0c;直接把后面的0券给匹配了&#xff0c;结果0*只能匹配空字符串了&#xff0c;# 若果想尽可能少的匹配&#xff0c;可在\d&#43;后加上一个问号?&#xff0c;采用非贪婪匹配
re.match(r&#39;^(\d&#43;?)(0*)$&#39;,&#39;12345000&#39;).groups()# 输出结果&#xff1a;
(&#39;12345&#39;, &#39;000&#39;)
// 0x7、转义字符 //
当遇到用于正则匹配模式的特殊字符时&#xff0c;在前面加反斜线转义一下即可&#xff0c;比如\.
&#xff0c;\(
&#xff0c;\)
等。正则练习// 0x1、简单验证手机号码格式 //
# ① 开头可能是&#xff1a;0(长途)、86(天朝国际区号)、17951(国际电话)中的# 一个或者一个都没有
(0|86|17951)?# ② 接着1xx&#xff0c;有13x&#xff0c;14x&#xff0c;15x&#xff0c;17x&#xff0c;18x&#xff0c;后面这个x的取值范围是不一样的# 13x&#xff1a;0123456789# 14x&#xff1a;579# 15x&#xff1a;012356789# 17x&#xff1a;01678# 18x&#xff1a;0123456789
(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])# ③ 剩下的8个数字
[0-9]{8}# 将匹配字符串拼接到一起&#xff1a;
phone_ number_regex &#61; re.compile("^(0|86|17951)?(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])[0-9]{8}$")# Tips&#xff1a;0-9也可以直接用\d匹配
// 0x2、身份证号码验证 //
# 身份证分两代&#xff1a;# 一代&#xff1a;15位号码组成 → xxxxxx yy mm dd pp s# 二代&#xff1a;18个号码组成 → xxxxxx yyyy mm dd ppp s# 把这两种情况分开&#xff0c;先推算18位# 前6位 → 地址编码(省市县)&#xff0c;第1位1-9&#xff0c;其他五位0-9
[1-9]\d{5}# ② 7-10位 → 年&#xff0c;范围1800-2099
(18|19|20)\d{2})# ③ 11-12位 → 月&#xff0c;1-9月需补零0&#xff0c;10&#xff0c;11&#xff0c;12
(0[1-9]|10|11|12)# ④ 13-14位 → 日&#xff0c;13位可能为0、1、2&#xff0c;16位0-9&#xff0c;还需补上10、20、30、31
([012][1-9]|10|20|30|31)# ⑤ 15-17位 → 顺序码&#xff0c;对同年同月同日出生的人编订的顺序号&#xff0c;奇数分给男的&#xff0c;偶数分给女的&#xff1b;
\d{3}# ⑥ 18位 → 校验码&#xff0c;0-9或x和X
[0-9Xx]# 将字符串拼接下得出18位身份证号码的过滤正则字符串&#xff1a;
[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{3}[0-9Xx]# 18位的推算出来了&#xff0c;15也不在话下了
[1-9]\d{5}\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{2}[0-9Xx]# 最后用|连接符将两个正则表达式拼接&#xff1a;
id_card_regex &#61; re.compile(r"^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{3}[0-9Xx]|[1-9]\d{5}\d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)\d{2}[0-9Xx]$")
// 0x3、实用正则收集 //
# 摘自&#xff1a;https://juejin.cn/post/6844904182835757064&#xff0c;更多可自行打开源链接查阅# 网址(url,支持端口和"?&#43;参数"和"#&#43;参数)
实战&#xff1a;爬取城市编码列表
((ht|f)tps?://)?[\w-]&#43;(\.[\w-]&#43;)&#43;([\w.,&#64;?^&#61;%&:/~&#43;#-]*[\w&#64;?^&#61;%&/~&#43;#-])?# 24小时制时间(HH:mm:ss)
(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d# 12小时制时间(hh:mm:ss)
(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d# date(日期)
\d{4}(-)(1[0-2]|0?\d)\1([0-2]\d|\d|30|31)# email(邮箱)
(([^<>()\[\]\\.,;:\s&#64;"]&#43;(\.[^<>()\[\]\\.,;:\s&#64;"]&#43;)*)|(".&#43;"))&#64;((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]&#43;\.)&#43;[a-zA-Z]{2,}))# 中文/汉字
(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])&#43;# 匹配连续重复的字符
(.)\1&#43;
// 0x1、爬取分析 //
城市编码在很多地方用到&#xff0c;比如查询某个地区的天气&#xff1a;http://www.weather.com.cn/weather1dn/101280601.shtml
后面跟着的101280601就是深圳的城市编码&#xff0c;网上搜到大部分城市编码大全大多比较旧不全&#xff0c;中国天气网既然提供查询接口&#xff0c;那么应该是有完整的最新的城市列表&#xff0c;当然不会直接暴露&#xff0c;需要用间接的方法获取到。打开国内天气预报文字版地址&#xff1a;http://www.weather.com.cn/textFC/hb.shtml#
看到下图所示列表&#xff1a;点击北京&#xff0c;跳转至&#xff1a;http://www.weather.com.cn/textFC/beijing.shtml
再点击表里的北京&#xff0c;跳转至&#xff1a;http://www.weather.com.cn/weather/101010100.shtml
这里的101010100就是北京的城市编码&#xff0c;打开海淀&#xff0c;城市编码为101010200&#xff0c;所以我们要做就是&#xff1a;① 获取所有省的跳转地址列表&#xff1b;
② 循环遍历每个跳转地址&#xff0c;提取后面的城市编码保存起来&#xff1b;
// 0x2、完整代码实现 //
# -*- coding: utf-8 -*-# !/usr/bin/env python"""------------------------------------------------- File : city_code_spider.py Author : CoderPig date : 2020-12-02 00:03 Desc : 城市编码爬取-------------------------------------------------"""import requests as rimport re
运行后&#xff0c;控制台陆续打印出爬取的url&#xff1a;打开本地生成的city_code.txt文件&#xff0c;可以看到城市编码信息已保存到本地
origin_url &#61; &#39;http://www.weather.com.cn/textFC/hb.shtml&#39;# 提取省跳转城市的正则
province_pattern &#61; re.compile(r&#39;)
province_set &#61; set() # 用set的原因是去重# 非省排除列表
province_remove_list &#61; [&#39;hb&#39;, &#39;db&#39;, &#39;hd&#39;, &#39;hz&#39;, &#39;hn&#39;, &#39;xb&#39;, &#39;xn&#39;, &#39;gat&#39;]# 省地址
province_base_url &#61; &#39;http://www.weather.com.cn/textFC/{}.shtml&#39;# 提取城市编码及城市名的正则
city_pattern &#61; re.compile(r&#39;(101\d{6}).shtml.*?>(.*?))
city_dict &#61; dict() # 用dict的原因同样是去重
headers &#61; {&#39;Host&#39;: &#39;www.weather.com.cn&#39;,&#39;Referer&#39;: &#39;http://www.weather.com.cn/textFC/db.shtml&#39;,&#39;User-Agent&#39;: &#39;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) &#39;&#39;Chrome/83.0.4103.97 Safari/537.36 &#39;
}# 获取省def extract_province():
resp &#61; r.get(origin_url, headers&#61;headers)print("爬取&#xff1a;", resp.url)if resp is not None:
resp_text &#61; resp.text
province_list &#61; province_pattern.findall(resp_text)for province in province_list:if province not in province_remove_list:
province_set.add(province)# 提取市及城市编码def extract_city(extract):
resp &#61; r.get(province_base_url.format(extract), headers&#61;headers)print("爬取&#xff1a;", resp.url)if resp is not None:
resp.encoding &#61; &#39;utf-8&#39; # 需设置编码&#xff0c;否则中文会乱码
resp_text &#61; resp.text
city_list &#61; city_pattern.findall(resp_text)for city in city_list:if city[1] !&#61; &#39;详情&#39;:
city_dict[city[0]] &#61; city[1]# 写入文件def write_to_file(content, file_path):with open(file_path, &#39;w&#43;&#39;, encoding&#61;&#39;utf-8&#39;) as f:
f.write(content)if __name__ &#61;&#61; &#39;__main__&#39;:
extract_province() for province in province_set:
extract_city(province)# 根据编码顺序排序
asc_city_dict &#61; {}for c in sorted(city_dict):
asc_city_dict[c] &#61; city_dict[c]
write_content &#61; &#39;&#39;for (key, value) in asc_city_dict.items():
write_content &#43;&#61; &#39;{}: {}\n&#39;.format(key, value)
write_to_file(write_content, "city_code.txt")
- EOF -