本教程所有源码下载链接:https://share.weiyun.com/5xmFeUO 密码:fzwh6g
正则表达式,又称规则表达式,英文Regular Expression,常简写为regex、regexp或者RE;它通常被用来快速检索、替换那些符合某个正则表达式的文本。
正则表达式的优势,决定了我们需要学习它:
但是对于新手而言,掌握它的使用方法又是比较困难。
re库是一个Python内置的用于进行一系列正则表达式操作的库。使用它,我们可以方便的使用正则表达式对字符串进行操作。它可以将一个正则表达式字符串编译为一个正则表达式特征,从而表达具有相同特征的字符串。
例如:我们有这样一组字符串:HI
、HII
、HIII
、HIIII
、……、HIIIIIII...
,那么,就可以用正则表达式HI+
来表达这一组无穷字符串。
这些操作符是组成正则表达式的基本单元,因此,我们需要熟悉它们:
操作符 | 含义 | 例子 | ||
---|---|---|---|---|
. | 表示任意的单个字符 | |||
[] | 对单个字符设定取值范围,字符集 | [a-z]表示a到z单个字符 | ||
[^] | 对单个字符设定排除范围,非字符集 | [^xyz]表示非x非y非z的单个字符 | ||
* | 前一个字符,出现0次或者无限次扩展 | xyz*表示xy,xyz,xyzz,xyzzz,… | ||
+ | 前一个字符,出现1次或无限次扩展 | xyz+表示xyz,xyzz,xyzzz,… | ||
? | 前一个字符,出现0次或者1次扩展 | xyz?表示xy,xyz | ||
` | ` | 左右表达式任意一个字符串 | `abc | xyz`表示abc,xyz |
{m} | 重复前一个字符m次 | xy{2}z表示xyyz | ||
{m,n} | 重复前一个字符m到n次,前后包含 | xy{2,3}z表示xyyz,xyyyz | ||
^ | 匹配开头,匹配字符串的开头 | ^xyz表示xyz在一个字符串的开头 | ||
$ | 匹配结尾,匹配字符串的结尾 | xyz$表示xyz在一个字符串的结尾 | ||
() | 分组标记,里面只能使用\ | 操作符 | `(abc | xyz)`表示abc,xyz |
\d | 匹配任意一个0-9的数字 | 相当于[0-9] | ||
\w | 非特殊字符并且非标点符号 | 相当于[a-zA-Z0-9] |
^
这个符号叫做异或符。
看几个小例子:
正则表达式 | 表示的字符串 | |||
---|---|---|---|---|
f(r\ | ri\ | rie\ | rien)?d | fd,frd,frid,fried,friend |
boy+ | boy,boyy,boyyy,boyyy,… … | |||
frien{:4}d | fried,friend,friennd,friennnd,friennnnd |
这里总结了一些常用的正则表达式,既能够达到练手的目的,也能够方便日后直接使用。需要注意的是,这些常用正则表达式不一定精确,有时只在特定的业务背景下,能够得到自己想要的结果。
正则表达式 | 含义 | |||
---|---|---|---|---|
^[A-Za-z]+$ | 26个字母组成的字符串 | |||
^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ | 匹配Email地址 | |||
[1-9]\d{5} | 中国邮政编码 | |||
` ^([1-9]\d{5}[12]\d{3}(0[1-9] | 1[012])(0[1-9] | [12][0-9] | 3[01])\d{3}[0-9xX])$` | 匹配身份证 |
\d+.\d+.\d+.\d+ | 匹配ip地址,提取时有用 | |||
^[1-9]\d*$ | 匹配正整数 | |||
` ^[1-9]\d* | 0$` | 匹配非负整数(正整数 + 0) | ||
^[A-Z]+$ | 匹配由26个英文字母的大写组成的字符串 | |||
^[a-z]+$ | 匹配由26个英文字母的小写组成的字符串 | |||
^[A-Za-z0-9]+$ | 匹配由数字和26个英文字母组成的字符串 | |||
^w+$ | 匹配由数字、26个英文字母或者下划线组成的字符串 | |||
[\u4e00-\u9fa5] | 匹配中文字符 |
re库是Python的标准库,主要用于字符串的匹配。
使用时,导入re即可:
import re
\
当做普通字符,不具有转义功能。
re库采用raw string类型表示正则表达式,格式为:
r'[1-9]\d{5}'
或者r"[1-9]\d{5}"
。
\
理解为转义字符,因此写起来比较繁琐。
例如,上面的邮政编码正则表达式,就必须写为:'[1-9]\\d{5}'
,第1个斜杠为转义字符标识,将第2个字符转义为普通的斜杠,从而表示\d
。
因此,我们推荐,当正则表达式中包含转义字符\
的时候,使用raw string类型表示。
函数 | 含义 |
---|---|
re.findall() | 返回列表类型,返回匹配正则表达式的全部子字符串 |
re.match() | 返回match对象,从字符串的开始位置起,匹配正则表达式 |
re.search() | 返回match对象,在字符串中搜索和正则表达式相匹配的第一个位置 |
re.sub() | 在字符串中替换掉所有匹配正则表达式的子字符串,返回替换后的字符串 |
re.finditer() | 在字符串中搜索匹配正则表达式的子字符串,返回迭代类型,其中元素是match对象 |
re.split() | 将字符串按照正则表达式进行匹配,将字符串匹配正则表达式的部分割开并返回一个列表 |
下面,我们对这些函数进行详细解释以及在ipython
中测试使用:
re.search(pattern,string,flags=0)
[A-Z]
可以匹配小写字符 |
| re.M(re.MULTILINE) | ^
操作符能够将给定字符串的每行当做匹配开始,例如,字符串是一篇文章由多个段落组成,那么可以匹配每一行,并且从每一行的开始匹配 |
| re.S(re.DOTALL) | .
操作符能够匹配所有字符,默认匹配除换行外的所有字符,如果设置了,将可以匹配所有字符包括换行 |
例子,匹配字符串中的邮政编码,示例字符串为北京海淀 100036
:
In [10]: match = re.search(r'[1-9]\d{5}', '北京海淀 100036') In [11]: if match: ...: print(match.group(0)) ...: 100036
re.match(pattern,string,flags=0)
re.search()
的一致 例子,匹配字符串中的邮政编码,示例字符串为北京海淀 100036
:
In [12]: match = re.match(r'[1-9]\d{5}', '北京海淀 100036') In [13]: match.group(0) --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-13-4d972d6c40f1> in <module>() ----> 1 match.group(0) AttributeError: 'NoneType' object has no attribute 'group' 报错了。也就是说这个match
是空的。因为,re.match()
是从起始位置开始匹配,所以没有匹配到数据。验证了re.match()
是从开始位置进行匹配。
In [14]: match = re.match(r'[1-9]\d{5}', '100036 北京海淀') In [15]: match.group(0) Out[15]: '100036'
re.findall(pattern,string,flags=0)
re.search()
的一致 例子,字符串为北京海淀100036 郑州高新450001
:
In [16]: ls = re.findall(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001') In [17]: ls Out[17]: ['100036', '450001']
re.split(pattern,string,maxsplit=0,flags=0)
:
例子1,测试字符串为北京海淀100036 郑州高新450001
:
In [18]: ls = re.split(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001') In [19]: ls Out[19]: ['北京海淀', ' 郑州高新', ''] 例子2,增加参数maxsplit=1
:
In [20]: ls = re.split(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001', maxsplit=1) In [21]: ls Out[21]: ['北京海淀', ' 郑州高新450001']
例子3,是否保留匹配项的用法,给正则表达式加上小括号()
,则保留匹配项,反之不保留:
In [22]: ls = re.split(r'([1-9]\d{5})', '北京海淀100036 郑州高新450001')
In [23]: ls
Out[23]: ['北京海淀', '100036', ' 郑州高新', '450001', '']
re.findall(pattern,string,flags=0)
:
re.search()
的一致 例子,测试字符串为北京海淀100036 郑州高新450001
:
In [24]: for m in re.finditer(r'[1-9]\d{5}', '北京海淀100036 郑州高新450001'): ...: print(m.group(0)) ...: 100036 450001
re.sub(pattern,repl,string,count=0,flag=0)
:
例子,字符串为北京海淀100036 郑州高新450001
:
In [29]: re.sub(r'[1-9]\d{5}',repl=":邮编",string="北京海淀100036 郑州高新450001") Out[29]: '北京海淀:邮编 郑州高新:邮编' # 将count=1传入 In [30]: re.sub(r'[1-9]\d{5}',repl=":邮编",string="北京海淀100036 郑州高新450001",count=1) Out[30]: '北京海淀:邮编 郑州高新450001'
上面的讲解中,类似re.findall()
的用法,叫做函数式用法
;我们也可以使用面向对象的思想,来调用这些方法:
In [31]: regex = re.compile(r'[1-9]\d{5}')
In [32]: regex.sub(repl=":邮编",string="北京海淀100036 郑州高新450001")
Out[32]: '北京海淀:邮编 郑州高新:邮编'
优点:当我们需要多次使用同一个正则表达式规则时,特别方便,可以重复使用pattern
这个对象来调用不同的方法达到目的。可以提高匹配速度。
regex = re.compile(pattern, flags=0)
:
该函数根据包含的正则表达式的字符串创建模式对象,将正则表达式的字符串形式编译成正则表达式对象。
注意1:没有经过编译的正则表达式字符串仅仅是一种表达形式,只有经过编译的正则表达式字符串才能形成一个正则表达式对象,它表示了一组符合规则的字符串。
注意2:经过编译后得到的正则表达式对象,可以调用的方法和re
调用的函数一致,请注意,由于正则表达式已经被编译为模式对象,因此,通过模式对象regex
调用相应方法的时候,方法的参数pattern
不再需要提供。
我们在前面的例子中,曾经用到一个对象match
,这个对象的类型是SRE_Match
:
In [14]: match = re.match(r'[1-9]\d{5}', '100036 北京海淀')
In [15]: match.group(0)
Out[15]: '100036'
... ... ... ...
In [33]: type(match)
Out[33]: _sre.SRE_Match
Match对象的常用属性有4个:
属性 | 含义 |
---|---|
.re | 匹配时使用的pattern对象(正则表达式) |
.string | 待匹配的文本 |
.pos | 搜索文本的开始位置 |
.endpos | 搜索文本的结束位置 |
我们在ipython
中使用一下:
In [34]: match.re
Out[34]: re.compile(r'[1-9]\d{5}', re.UNICODE)
In [35]: match.string
Out[35]: '100036 北京海淀'
In [36]: match.pos
Out[36]: 0
In [37]: match.endpos
Out[37]: 11
Match对象常用的方法:
方法 | 含义 |
---|---|
.group(0) | 获得匹配后的字符串 |
.start() | 匹配字符串在原始字符串的开始位置 |
.end() | 匹配字符串在原始字符串的结束位置 |
.span() | 返回(.start(),end()) |
我们在ipython
中使用一下:
# 匹配的结构存储在这里
In [38]: match.group(0)
Out[38]: '100036'
In [39]: match.start()
Out[39]: 0
In [40]: match.end()
Out[40]: 6
In [41]: match.span()
Out[41]: (0, 6)
首先看一个例子:
In [49]: match = re.search(r'one.*n','oneanbncndnen')
In [50]: match.group(0)
Out[50]: 'oneanbncndnen'
g.*n
这个正则表达式可以匹配多个字符串,例如,gitopen、gin、等,但是,结果却返回最长的那个匹配字符串gitaabbccddopen
。
re库默认采用贪婪匹配,即输出匹配最长的子字符串。
但是有的时候,我们需要输出最短的那个子字符串,这时候,需要使用正则表达式的最小匹配:
In [51]: match = re.search(r'one.*?n','oneanbncndnen')
In [52]: match.group(0)
Out[52]: 'onean'
re库的最小匹配,需要对以下几个操作符进行扩展:
操作符 | 含义 |
---|---|
*? | 前一个字符0次或者无限次扩展,最小匹配 |
+? | 前一个字符1次或者无限次扩展,最小匹配 |
?? | 前一个字符0次或者1次扩展,最小匹配 |
{m,n}? | 扩展前一个字符m到n次,含n次,最小匹配 |
如果re库的操作符匹配不同长度的字符串的话,都可以在操作符后面加上?
进行最小匹配。
问题0:按照销量进行爬取,这样可以排除页面中广告
得到的连接如下:
https://search.jd.com/Search?keyword=书包&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&page=1&s=1&click=0
从浏览器开发者工具中可以得到这些GET请求的参数,如下:
{
"keyword": "书包",
"enc": utf-8",
"qrst": 1",
"rt": 1",
"stop": 1",
"vt": 2",
"psort": 3",
"stock": 1",
"page": 1",
"s": 1",
"click: 0",
}
这样,我们得到了用requests
发送GET说必须的连接和参数,连接为https:// search.jd.com/Search
,参数为上面的这个字典。其中,需要我们控制的几个参数为:keyword
、s
、page
问题1:请求链接的构造,page竟然是奇数?!
在京东搜索商品以后,我们会来搜索页面,这时观察页面的url不难发现一个规律,拼接页面url的时候的page参数,需要传入的数字为奇数。经过计算发现,总共100页搜索结果,如果我们想获取全部信息,就必须从把page的值赋值为200以内的奇数,即1,3,5,7,9,...,199
。
如:
第1页 page=1
https://search.jd.com/Search?keyword=书包&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&page=1&s=1&click=0
第2页 page=3
https://search.jd.com/Search?keyword=书包&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&page=3&s=61&click=0
问题2:编写页面请求,得到的页面中只有30个商品信息?!
当我们在编写程序的时候,用requests
库请求回来的html文本内容中,属性为class='gl-item'
的li
标签只有30个。什么?刚才不是数过了吗?一共60个呀。为什么是30个呢?
打开浏览器的开发者工具栏,将页面从上往下慢慢拖动,并且观察Network
中的网络请求,突然,有一个神秘的请求出现了,它的连接为https://search.jd.com/s_new.php?keyword=%E4%B9%A6%E5%8C%85&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&page=2&s=31&scrolling=y&log_id=1529934788.49163&tpl=3_M&show_items=4208541,12596174640,2804865,877258,4593360,3598223,5308846,11031192090,4028698,5471930,6511832,765798,3796277,6530586,311349,1015774,10552271564,6044018,5025051,10552555536,28941008721,1106925,10285134151,4120808,10492711739,4044524,25595327933,6401561,5181576,2437187963
,这是什么东西?
然后查看这个链接的Response
,搜索gl-item
,我们发现,竟然也有30个!那么。。。刚才的30加上现在的30,不就是60个商品?仔细观察这个链接,我们发现了一个某东做的小手脚!快看,连接中的page=2
!So,~~~~,似乎明白了什么。
原来,我们在问题1中得到的搜索页面一共有100页,实际上有200页,奇数页就是我们直接看到的搜索结果页面,一共请求到30个商品信息,而偶数页,则是当用户拖动滚动条的时候,看完了30个,就会自动后台请求另外30个商品,这后来请求的30个,就是偶数页的信息,并且动态的添加到页面上去。
请求的基本链接为https://search.jd.com/s_new.php
,请求的基本参数我们提取为字典,其中需要控制的参数为:keyword
、page
、s
、log_id
、show_items
。
log_id
和show_items
必须从奇数页请求结果中提取,show_items
是用一个列表转化成的字符串,其中数字是,奇数页中每个带有class='gl-item'
属性的div,它的data-pid
属性的值。
{
'keyword': '书包',
'enc': 'utf-8',
'qrst': '1',
'rt': '1',
'stop': '1',
'vt': '2',
'psort': '3',
'stock': '1',
'page': '2',
's': '31',
'scrolling': 'y',
'log_id': '1529759067.93124',
'tpl': '3_M',
'show_items': '2241345,4153317,10285134151,10503046182,13180766655,6117013,2330770,5168648,10285341597,26194798282,5471930,2804865,28561551177,1585517611,1025981997,6174172,11693982705,10540540570,5386778,11096315457,20682615258,23533395828,5248730,27306328004,3929123,5218868,12422531859,4208541,5634094,2330752'
}
问题3:讲了这么多我们该如何获取到60个商品信息呢?
循环遍历,然后判断页码的奇偶性,根据奇偶性发送不同连接不同请求参数的请求,得到不同的结果进行内容解析。
整个程序的源码如下:
import requests
import re
from bs4 import BeautifulSoup
import time
import json
def get_html(url, params=None):
headers = {
'Referer': 'https://search.jd.com',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0'
}
try:
r = requests.get(url=url, timeout=30, headers=headers, params=params)
r.raise_for_status()
r.encoding = r.apparent_encoding
print("### 正在请求的链接为:{}".format(r.url))
return r.text
except Exception as e:
print(str(e))
def set_even_params(html, even_params):
soup = BeautifulSoup(html, "lxml")
li_list = soup.find_all(class_="gl-item")
show_items = []
for li in li_list:
show_items.append(li.attrs['data-pid'])
log_id = re.compile(r"log_id:'\d+.\d+'").search(html).group(0).replace('log_id:', '')
even_params['show_items'] = ",".join(show_items)
even_params['log_id'] = log_id
def parse_page(html):
soup = BeautifulSoup(html, "lxml")
li_list = soup.find_all(class_='gl-item')
products = []
for gl_item in li_list:
product = {}
product['slogan'] = gl_item.find(class_='p-img').a.attrs['title']
product['url'] = "https:" + gl_item.find(class_='p-img').a.attrs['href']
product['price'] = gl_item.find(class_='p-price').strong.i.string
pin_gou = gl_item.find(class_='price-pingou')
if pin_gou is None:
product['pin_price'] = ""
else:
product['pin_price'] = re.compile(r'\d+.\d+').search(str(pin_gou)).group(0)
product['name'] = gl_item.find(class_='p-name').a.em.get_text()
product['comment'] = gl_item.find(class_='p-commit').strong.a.string
a_shop = gl_item.find(class_='p-shop').find('a')
if a_shop is None:
product['shop_name'] = ""
product['shop_url'] = ""
else:
product['shop_name'] = a_shop.attrs['title']
product['shop_url'] = "https:" + a_shop.attrs['href']
products.append(product)
return products
def main():
# 搜索关键词
goods = '书包'
# 爬取深度,最大100页
depth = 10
# 结果列表
products = []
# 每页商品的起始数字
s = 1
odd_params = {
'keyword': goods,
'enc': 'utf-8',
'wq': goods,
'page': '1',
'psort': '3',
's': '1'
}
odd_url = 'https://search.jd.com/Search'
even_params = {
'keyword': goods,
'enc': 'utf-8',
'qrst': '1',
'rt': '1',
'stop': '1',
'vt': '2',
'psort': '3',
'stock': '1',
'page': '2',
's': '31',
'scrolling': 'y',
'log_id': '1529759067.93124',
'tpl': '3_M',
'show_items': '2241345,4153317,10285134151,10503046182,13180766655,6117013,2330770,5168648,10285341597,26194798282,5471930,2804865,28561551177,1585517611,1025981997,6174172,11693982705,10540540570,5386778,11096315457,20682615258,23533395828,5248730,27306328004,3929123,5218868,12422531859,4208541,5634094,2330752'
}
even_url = 'https://search.jd.com/s_new.php'
for num in range(1, 2 * depth + 1):
# 奇数
if num % 2 != 0:
page_id = num
odd_params['page'] = page_id
odd_params['s'] = str(s)
html = get_html(url=odd_url, params=odd_params)
set_even_params(html, even_params)
products.append(parse_page(html))
# 偶数
else:
page_id = num
even_params['page'] = page_id
even_params['s'] = str(s)
html = get_html(url=even_url, params=even_params)
products.append(parse_page(html))
# 每次奇偶数页面迭代,都是30个商品
s += 30
# 将结果保存到文件中,以json格式
localtime = "-".join(time.asctime(time.localtime(time.time())).split(' '))
with open("jd_" + localtime + '.json', 'w') as filename:
filename.write(json.dumps(products, ensure_ascii=False))
print("### 爬取完毕")
if __name__ == '__main__':
main()
欣慰帮到你 一杯热咖啡