python学习笔记之爬虫(二) re、bs4、xpath解析 丨【生长吧!Python】
## 第二章 ###
import csv
''' 1、数据解析概述 '''
'''
三种解析方式:
1、re解析
2、bs4解析
3、xpath解析
这三种方式可以混合使用,完全以结果做向导,只要能拿到你想要的数据,
用什么方式并不重要,当你掌握了这些之后,再考虑性能问题。
'''
''' 2、正则表达式 '''
'''
工具链接:
开源中国 https://www.oschina.net/
实用在线工具 https://tool.oschina.net/
在线正则表达式测试 https://tool.oschina.net/regex
Regular Expression,正则表达式,一种使用表达式的方式对字符串进行匹配的语法规则。
正则的优点:速度快,效率高,准确性高
正则的缺点:新手上手难度有点高。
正则的语法:使用元字符进行排列组合用来匹配字符串
元字符:具有固定含义的特殊符号
常用元字符:
. 匹配除换行符以外的任意字符
\w 匹配字母、数字、下划线
\s 匹配任意的空白符
\d 匹配数字
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配字符串的开始
$ 匹配字符串的结尾
\W 匹配非字母、数字、下划线
\D 匹配非数字
\S 匹配非空白符
a|b 匹配字符a或字符b
() 匹配括号内的表达式,也表示一个组
[...] 匹配字符组中的字符
[^...] 匹配除了字符组中字符的所有字符
量词:控制前面的元字符出现的次数
* 重复0次或更多次
+ 重复1次或更多次
? 重复0次或1次
{n} 重复n次
{n,} 重复n到更多次
{n,m} 重复n到m次
贪婪匹配和惰性匹配:
.* 贪婪匹配,尽可能多的匹配
.*? 惰性匹配,尽可能少的匹配
这两个要着重说一下,因为我们写爬虫用的最多的就是这个惰性匹配
先看案例:
str: 玩儿吃鸡游戏,晚上一起上游戏,干嘛呢?打游戏啊
reg: 玩儿.*?游戏
结果:玩儿吃鸡游戏
reg: 玩儿.*游戏
结果:玩儿吃鸡游戏,晚上一起上游戏,干嘛呢?打游戏
str: <div>胡辣汤</div>
reg: <.*>
结果:<div>胡辣汤</div>
reg: <.*?>
结果:<div>
</div>
reg: <div>.*?</div>
结果:<div>胡辣汤</div>
所以我们能发现这样一个规律:
.* 表示尽可能多的匹配。
.*? 表示尽可能少的匹配,
'''
''' 3、re模块 '''
'''
import re
# re模块中我们只需要记住这么几个方法就足够使用了。
# 1、findall():匹配字符串中所有符合正则表达式的内容,返回list列表
lst = re.findall(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
print(lst) # ['10086', '10010']
# 2、finditer: 匹配字符串中所有的内容,返回iter迭代器,从iter中拿数据需要.group()
it = re.finditer(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
for i in it:
# print(i) # <re.Match object; span=(8, 13), match='10086'>
print(i.group()) # 10086 10010
# 3、search(): 找到一个结果就返回,返回的结果是match对象,匹配不上就返回None,拿数据需要.group()
s = re.search(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
print(s) # <re.Match object; span=(8, 13), match='10086'>
print(s.group()) # 10086
# 4、match(): 只能从头开始匹配,返回的结果是match对象
s1 = re.search(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
print(s1) # <re.Match object; span=(8, 13), match='10086'>
print(s1.group()) # 10086
# 5、compile(): 预加载正则表达式,方便后面使用
obj = re.compile(r'\d+')
ret = obj.finditer('我的电话号码是:10086,我女朋友的电话号码是:10010')
for it in ret:
print(it.group()) # 10086 10010
# 6、正则中的内容如何单独提取?
# 单独获取到正则中的具体内容可以给分组起名字
s = """
<div class='type1'><span id='10000'>中国电信</span></div>
<div class='type2'><span id='10086'>中国移动</span></div>
<div class='type3'><span id='10010'>中国联通</span></div>
"""
pattern = re.compile(r"class='(?P<class>.*?)'><span id='(?P<id>.*?)'>(?P<name>.*?)</span>", re.S)
# re.S 使 . 匹配包括换行在内的所有字符
result = pattern.finditer(s)
for it in result:
print(it.group('class'))
print(it.group('id'))
print(it.group('name'))
''' 执行结果
type1
10000
中国电信
type2
10086
中国移动
type3
10010
中国联通
'''
''' 4、re案例:豆瓣top250电影排行 '''
import requests
import re
import csv
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3756.400 QQBrowser/10.5.4043.400'
}
for i in range(0, 101, 25):
url = 'https://movie.douban.com/top250?start=%s' % i
# 使用requests拿到页面源代码
with requests.get(url, headers=headers) as resp: # 省略resp.close()语句
content = resp.text
# 通过re模块提取有效信息
pattern = re.compile(r'.*?<li>.*?<span class="title">(?P<name>.*?)</span>'
r'.*?<br>(?P<year>.*?) '
r'.*?property="v:average">(?P<score>.*?)</span>'
r'.*?<span>(?P<num>.*?)人评价</span>'
r'.*?<span class="inq">(?P<quote>.*?)</span>', re.S) # # re.S 使 . 匹配包括换行在内的所有字符
result = pattern.finditer(content)
# print(result) # <callable_iterator object at 0x000002BEBDA44160>
# Windows默认编码是gbk,需手动设置编码方式为utf-8,否则文件内容为乱码
# Python3使用csv模块csv.writer().writerow()保存csv文件,会产生空行,newline=''设置为空
with open('data.csv', mode='a', encoding='utf-8', newline='') as f:
# print(f) # <_io.TextIOWrapper name='data.csv' mode='w' encoding='cp936'>
csvwriter = csv.writer(f)
for it in result:
# print(it.group('name'))
# print(it.group('year').strip())
# print(it.group('score'))
# print(it.group('num'))
# Match.groupdict(default=None):
# 返回一个字典,包含了所有的 命名 子组。key就是组名,value就是内容。
# default 参数用于不参与匹配的组合;默认为 None。
dct = it.groupdict()
dct['year'] = dct['year'].strip()
# 将内容保存到data.csv
csvwriter.writerow(dct.values())
# 将内容保存到data.txt
# with open('data.txt', 'a', encoding='utf-8') as wstream:
# wstream.write(str(dct) + '\n')
else:
print(f'{url} Done!')
''' 5、bs4解析 '''
'''
1)bs4解析案例:北京新发地菜价
安装bs4模块:
pip install bs4
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple bs4
'''
import requests
from bs4 import BeautifulSoup
# 拿到页面源代码
for i in range(1, 6):
url = 'http://www.xinfadi.com.cn/marketanalysis/0/list/%s.shtml' % i
resp = requests.get(url)
f = open('菜价.csv', 'a', encoding='utf-8', newline='')
csvwriter = csv.writer(f)
# 使用bs4进行解析,拿到数据
# 1、解析数据,把页面源代码交给BeautifulSoup进行处理,生成bs对象
page = BeautifulSoup(resp.text, 'html.parser') # 指定html解析器
# print(type(page)) # <class 'bs4.BeautifulSoup'>
# 报出警告信息如下:,添加'html.parser'后即可消除警告信息
# D:/Project/Pycharm/Project3 Spider/demo2.py:261: GuessedAtParserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("html.parser"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.
# The code that caused this warning is on line 261 of the file D:/Project/Pycharm/Project3 Spider/demo2.py. To get rid of this warning, pass the additional argument 'features="html.parser"' to the BeautifulSoup constructor.
# page = BeautifulSoup(resp.text)
# 2、从bs对象中查找数据。
# 两种方式:
# find(标签, 属性=值)
# find_all(标签, 属性=值)
# table = page.find('table', class_="hq_table") # class是python关键字,为防止报错。加下划线用以区分
table = page.find('table', attrs={'class': 'hq_table'}) # 和上一行一个意思,可以避免同名关键字
# print(type(table)) # <class 'bs4.element.Tag'>
# 拿到所有数据行
trs = table.find_all('tr')[1:]
for tr in trs: # 遍历每一行
tds = tr.find_all('td') # 拿到每行中的所有td
name = tds[0].text # .text表示拿到被标签标记的内容
min_price = tds[1].text # 最低价
avg_price = tds[2].text # 平均价
max_price = tds[3].text # 最高价
spec = tds[4].text # 规格
unit = tds[5].text # 单位
release_date = tds[6].text # 发布日期
# print(name, min_price, avg_price, max_price, spec, unit, release_date)
# 3、将数据写入文件
csvwriter.writerow([name, min_price, avg_price, max_price, spec, unit, release_date])
else:
f.close()
resp.close()
print(f'{url} Done!')
'''
2)bs4解析案例:优美图库图片
'''
# 1、拿到主页面的源代码,然后提取子页面的链接地址,href
# 2、通过href拿到子页面的内容,从子页面中找到图片的下载地址 imgg -> src
# 3、下载图片
import requests
from bs4 import BeautifulSoup
from time import sleep
import os
# 创建文件夹用以保存下载的图片
path_dir = os.path.join(os.path.dirname(__file__), 'img')
if not os.path.exists(path_dir):
os.mkdir(path_dir)
url = 'https://www.umei.net/bizhitupian/weimeibizhi/'
resp = requests.get(url)
resp.encoding = 'utf-8' # 处理乱码
# 把源代码交给bs
main_page = BeautifulSoup(resp.text, 'html.parser')
alist = main_page.find('div', class_='TypeList').find_all('a') # 缩小查找范围到div标签
# 遍历获取a标签href属性的值
for a in alist:
href = 'https://www.umei.net/' + a.get('href')
# 拿到子页面源代码
child_page_resp = requests.get(href)
child_page_resp.encoding = 'utf-8'
child_page_text = child_page_resp.text
# 从子页面中拿到图片的下载路径
child_page = BeautifulSoup(child_page_text, 'html.parser')
p = child_page.find('p', align='center') # 缩小查找范围到p标签
img = p.find('img')
# 获取img标签src属性的值
src = img.get('src')
# 下载图片到本地,直接请求地址即可下载
img_resp = requests.get(src)
# print(img_resp.content) # 这里拿到的是图片字节
# rfind()切片获取文件名
# img_name = src[src.rfind('/') + 1:]
## 或 split()切片获取文件名
img_name = src.split('/')[-1]
# 判断图片是否存在,不存在则下载图片
if os.path.exists('img/' + img_name):
print(img_name, '已存在此图片!')
else:
# 注意此处有坑!!!
# 不能是with open('img/img_name', 'wb'),否则会有错误结果。
with open('img/' + img_name, 'wb') as wstream:
wstream.write(img_resp.content) # 图片内容写入文件
print(f'{img_name} Done!!!')
sleep(1)
提示:
当你下载图片、视频、音频时,若Pycharm会特别卡顿,是因为下载内容是,Pycharm会逐个标记已下载的内容并建立索引,此过程会感觉明显的卡顿,可用如下方法解决卡顿问题:
右键文件夹,选择“Mark Directory as”,点击“Excluded”即可。
如下图所示:
Xpath解析
''' 6、Xpath解析 '''
''' Xpath入门1
<book>
<id>1</id>
<name>野花遍地香</name>
<price>1.23</price>
<author>
<nick>周大强</nick>
<nick>周芷若</nick>
</author
</book>
在以上html中,
1、book, id, name, price...等都被称为节点
2、id, name, price, author都被称为book的子节点
3、book被称为id, name, price, author的父节点
4、id, name, price, author都被称为兄弟节点
安装lxml包
pip install lxml
用法:
1、将要解析的html内容构造出etree对象
2、使用etree对象的xpath()方法配合xpath表达式来完成对数据的提取
'''
from lxml import etree
xml = '''
<book>
<id>1</id>
<name>野花遍地香</name>
<price>1.23</price>
<nick>臭豆腐</nick>
<author>
<nick id='10086'>周大强</nick>
<nick id='10010'>周芷若</nick>
<nick class='joy'>周杰伦</nick>
<nick class='jolin'>蔡依林</nick>
<div>
<nick>热热热1</nick>
</div>
<span>
<nick>热热热2</nick>
<div>
<nick>热热热3</nick>
</div>
</span>
</author>
<partner>
<nick id='ppd'>胖胖东</nick>
<nick id='ppbd'>胖胖不东</nick>
</partner>
</book>
'''
tree = etree.XML(xml)
# 找到book节点
result = tree.xpath('/book') # /表示层级关系,第一个/是根节点
print(result) # [<Element book at 0x18a285ca6c0>]
# 找到book节点下的name节点里的文本内容“野花遍地香”
result2 = tree.xpath('/book/name') # 此时找的是name节点而不是文本内容
print(result2) # [<Element name at 0x21974761cc0>]
result2 = tree.xpath('/book/name/text()') # text()表示提取文本内容
print(result2) # ['野花遍地香']
# 找到author下的nick节点的内容
result3 = tree.xpath('/book/author/nick/text()')
print(result3) # ['周大强', '周芷若', '周杰伦', '蔡依林']
# 找到nick节点里的内容“热热热”
result4 = tree.xpath('/book/author/div/nick/text()')
print(result4) # ['热热热']
# 找到author中所有的nick节点内容
result5 = tree.xpath('/book/author//nick/text()') # //表示后代,所有后代节点nick都将被匹配
print(result5) # ['周大强', '周芷若', '周杰伦', '蔡依林', '热热热1', '热热热2', '热热热3']
# 找到author中“热热热1”和“热热热2”
# 类似于
# /author/div/nick
# /author/span/nick
# 实现方式:
# /author/*/nick,即将div和span都使用通配符 * 号表示
result6 = tree.xpath('/book/author/*/nick/text()') # *表示任意节点,通配符,应该只是author下的所有孙子节点nick
print(result6) # ['热热热1', '热热热2']
# 找到author中“热热热1”和“热热热2”和“热热热3”
result7 = tree.xpath('/book/author//*/nick/text()') # //*表示后代任意子孙节点,所有后代任意子孙节点
print(result7) # ['热热热1', '热热热2', '热热热3']
# 找到book内所有的nick内容
result8 = tree.xpath('/book//nick/text()')
print(result8) # ['臭豆腐', '周大强', '周芷若', '周杰伦', '蔡依林', '热热热1', '热热热2', '热热热3', '胖胖东', '胖胖不东']
''' Xpath入门2
test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<div>
<ul>
<li><a href="http://www.baidu.com">百度</a></li>
<li><a href="http://www.google.com">谷歌</a></li>
<li><a href="http://www.sogou.com">搜狗</a></li>
</ul>
<ol>
<li><a href="feiji">飞机</a></li>
<li><a href="dapao">大炮</a></li>
<li><a href="huoche">火车</a></li>
</ol>
<h1>
<span class="job">品鉴师</span>
<span class="common">胡辣汤</span>
</h1>
</div>
</body>
</html>
'''
from lxml import etree
# 解析一个文件用parse()
tree = etree.parse('test.html')
# result = tree.xpath('/html')
# print(result)
# 找到ul/li/a中的所有文本内容
result = tree.xpath('/html/body/div/ul/li/a/text()')
print(result) # ['百度', '谷歌', '搜狗']
'''
错误描述:
Traceback (most recent call last):
File "D:/Project/Pycharm/Project3 Spider/demo2.py", line 466, in <module>
tree = etree.parse('test.html')
File "src\lxml\etree.pyx", line 3521, in lxml.etree.parse
File "src\lxml\parser.pxi", line 1859, in lxml.etree._parseDocument
File "src\lxml\parser.pxi", line 1885, in lxml.etree._parseDocumentFromURL
File "src\lxml\parser.pxi", line 1789, in lxml.etree._parseDocFromFile
File "src\lxml\parser.pxi", line 1177, in lxml.etree._BaseParser._parseDocFromFile
File "src\lxml\parser.pxi", line 615, in lxml.etree._ParserContext._handleParseResultDoc
File "src\lxml\parser.pxi", line 725, in lxml.etree._handleParseResult
File "src\lxml\parser.pxi", line 654, in lxml.etree._raiseParseError
File "test.html", line 6
lxml.etree.XMLSyntaxError: Opening and ending tag mismatch: meta line 4 and head, line 6, column 8
错误原因:
test.html文件中的开始和结束标签不匹配
即<meta charset="UTF-8">少了结束标签/
解决方法:
在<meta charset="UTF-8"/>中添加结束标签/即可
'''
# 找到ul/li/a中的第一个文本内容
result2 = tree.xpath('/html/body/div/ul/li[1]/a/text()') # []表示取索引,xpath的下标是从1开始的
print(result2) # ['百度']
# 找到href="dapao"的标签的值
result3 = tree.xpath('/html/body/div/ol/li/a[@href="dapao"]/text()') # [@属性=属性值]表示属性的筛选
print(result3) # ['大炮']
# 遍历获取ol/li/a中的所有文本内容
ol_li_list = tree.xpath('/html/body/div/ol/li')
# [<Element li at 0x26a63207cc0>, <Element li at 0x26a63207d40>, <Element li at 0x26a63207d80>]
for li in ol_li_list:
result4 = li.xpath('./a/text()') # ./表示当前所在位置,可理解为相对路径
print(result4)
# ['飞机']
# ['大炮']
# ['火车']
# 获取a标签中属性href的属性值
result5 = li.xpath('./a/@href') # @href表示获取href的属性值
print(result5)
# ['feiji']
# ['dapao']
# ['huoche']
# 找到ul/li/a中所有href的值
result6 = tree.xpath('/html/body/div/ul/li/a/@href')
print(result6)
# ['http://www.baidu.com', 'http://www.google.com', 'http://www.sogou.com']
# 找到所有a标签中href的值
result7 = tree.xpath('/html//a/@href')
print(result7)
# ['http://www.baidu.com', 'http://www.google.com', 'http://www.sogou.com', 'feiji', 'dapao', 'huoche']
# 找到div的孙子标签span中a的href值
result8 = tree.xpath('/html/body/div/*/span/@class')
print(result8)
# ['job', 'common']
# 找到div下的所有任意子孙节点中a的文本内容
result9 = tree.xpath('/html/body/div//*/a/text()')
print(result9)
# ['百度', '谷歌', '搜狗', '飞机', '大炮', '火车']
小技巧:
# 在网页中按F12打开开发者工具,找到相应内容后,
# 右键,选择“copy”,选择“copy XPath”,即可复制该内容的xpath路径。
如下图所示
Xpath案例:抓取猪八戒网信息
''' Xpath案例:抓取猪八戒网信息
为什么找猪八戒网作为参考案例呢?因为它足够复杂!!!
这个网站搞定之后,xpath也基本掌握了。
'''
import requests
from lxml import etree
from time import sleep
url = 'https://wuhan.zbj.com/search/f/?'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3756.400 QQBrowser/10.5.4043.400'
}
params = {
'type': 'new',
'kw': 'saas',
}
# 请求页面获取相应
resp = requests.get(url, headers=headers, params=params)
# 解析相应
html = etree.HTML(resp.text)
# 拿到每一个服务商的div
divs = html.xpath('/html/body/div[6]/div/div/div[2]/div[5]/div[1]/div')
for div in divs: # 每一个服务商信息
# price = div.xpath('./div/div/a/div[2]/div[1]/span[1]/text()')[0].replace('¥', '') # 去除¥符号
# 或
price = div.xpath('./div/div/a/div[2]/div[1]/span[1]/text()')[0].strip('¥') # 去除¥符号
trading_volume = div.xpath('./div/div/a[1]/div[2]/div[1]/span[2]/text()')[0]
# info = div.xpath('./div/div/a/div[2]/div[2]/p/text()')
# 此时的info中有多余的逗号,通过观察网页发现,逗号都可以用SaaS关键词替换
# 现在进行拼接与替换,如下所示
# 注意:这里是将列表拼接,join()语法为:str.join(sequence)
info = 'SaaS'.join(div.xpath('./div/div/a/div[2]/div[2]/p/text()'))
company_name = div.xpath('./div/div/a[2]/div[1]/p/text()')[0]
location = div.xpath('./div/div/a[2]/div[1]/div/span/text()')[0]
# print(price, trading_volume, info, company_name, location)
'''
一个一个地找标签很痛苦,也可以使用另一种方法:
复制你要找的内容的xpath路径,比如这里要找公司名称,//*[@id="utopia_widget_74"]/a[2]/div[1]/p/text()
通过id的属性值,在网页中找到utopia_widget_74所在的位置,
往上找,看看现在/a[2]/div[1]/p/text()距离当前的相对位置有多少个标签,
然后将//*[@id="utopia_widget_74"]修改成当前所在的相对位置,
例如 ./div/div/a[2]/div[1]/p/text()
该路径就是一个完整的xpath路径,即可找到你想要的内容,且不用一个一个地找标签,提高了效率。
例子:
name = div.xpath('./div/div/a[2]/div[1]/p/text()')
print(name) # ['美讯科技-软件开发']
'''
'''
下面的代码只能放在for div in divs:之内,放在外面则 猪八戒网.csv 中没有数据,为什么???
'''
with open('猪八戒网.csv', 'a', encoding='utf-8', newline='') as wstream:
csvwriter = csv.writer(wstream)
csvwriter.writerow([price, trading_volume, info, company_name, location])
print('Done!')
# sleep(0.5)
【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897
- 点赞
- 收藏
- 关注作者
评论(0)