数据解析
- 分类
(1)正则
(2)bs4
(3)xpath,通用性强,是学习的重点。 - 原理:
解析的局部的文本内容都会在标签之间或者标签对应的属性中进行存储。因此分两步进行解析,第一步进行指定标签的定位,第二步对标签或者标签对应的属性中存储的数据进行提取(解析)。 - 聚焦爬虫
编码流程:指定url–》发起请求–》获取响应数据–》数据解析–》持久化存储
正则表达式
Python 正则表达式
正则表达式有普通字符和元字符组成。普通字符包含大小写字母,数字,在匹配普通字符的时候直接输入字符本身进行匹配。元字符使正则表达式具有处理能力。所谓元字符就是指那些在正则表达式中具有特殊意义的专用字符,可以用来规定其前导字符(即位于元字符前面的字符)在目标对象中的出现模式。
在线正则表达式测试工具
- 字符组
字符用[]括起来,在[]中出现的内容会被匹配,例如:[abc]匹配a或b或c;[a-z]匹配a到z所有字母;[0-9]匹配所有阿拉伯数字。 - 常用简单元字符
- 量词
- 分组
在正则中使用()进行分组。
实例:匹配身份证号(老身份证号15位,新身份证号18位,考虑新身份证结尾为X的情况)
^[1-9]\d{14}(\d{2}[0-9X])?$
^([1-9]\d{16}[0-9X]|[1-9]\d{14})$
-
惰性匹配和贪婪匹配
在量词中的?, *, +, {} 都数据贪婪匹配,就是尽可能多的匹配到结果。
在使用.*后如果加了? 则是尽可能的少匹配,表示惰性匹配。 -
正则练习
import re
# 提取出python
key = "javapythonc++php"
re.findall('python', key)[0]
# out : 'python',返回的是字符串中匹配的第一个‘python’
# 提取出标签内容
key = "<html><h1>hello world<h1></html>"
re.findall('<h1>(.*)<h1>', key)[0]
# out : 'hello world'
# 提取数字
string = '今年是2020年'
re.findall('\d+', string)
# out : ['2020']
# 提取http://和https://,?作用于前一个字符
key = 'http://www.baidu.com and https://cn.bing.com'
re.findall('https?://', key)
# out : ['http://', 'https://']
# 提取出hello
key = 'lalala<hTml>hello</HtMl>hahah'
re.findall('<[Hh][Tt][mM][lL]>(.*)</[Hh][Tt][mM][lL]', key)
# out : ['hello']
# 提取出h开头.结尾的字符串
key = 'world@hello.edu.com'
re.findall('h.*?\.', key)
# out : ['hello.']
# 匹配sas和saas
key = 'saas and sas and saaas'
re.findall('sa{1,2}s', key)
# out : ['saas', 'sas']
re模块
re模块是python提供的一套关于处理正则表达式的模块,核心功能有四个:
(1)findall查找所有,返回list。
(2)search会进行匹配,如果匹配到了第一个结果,就会返回这个结果,如果匹配不上返回None。
# re.search的结果不能直接输出,re.search()方法返回的是一个match对象
# 需要使用.group()才能返回匹配的内容
res = re.search("\d+", "我今年17岁,读高中3年级。")
print(res.group())
(3)match只能从字符串的开头进行匹配。
result = re.match("\d+", "我今年17岁,读高中3年级。")
print(result)
# out : None
result = re.match("\d+", "17岁,读高中3年级。")
print(result)
# out : <re.Match object; span=(0, 2), match='17'>
result = re.match("\d+", "17岁,读高中3年级。")
print(result.group())
# out : 17
(4)finditer和findall相似,区别是返回的是迭代器。
# 返回的是一个iterator类型数据,即一个迭代器,需要使用for循环访问内容
it = re.finditer("\d+", "我今年17岁,读高中3年级。")
for item in it:
print(item)
- 其他操作
- 爬虫必会重点
(1)由于正则表达式中经常出现“\n, \d, \w…”,当要匹配字符串“\n”时就容易产生误读。
为了避免这类问题出现,可以在字符串前面写上r来直接把字符串中的内容全部当成普通字符来处理。
print("你好,\n我叫XXX")
# out :
# 你好,
# 我叫XXX
print(r"你好,\n我叫XXX")
# out :
# 你好,\n我叫XXX
(2)()括起来的内容是你最终想要的结果;(?P正则) 把正则匹配到的内容直接放在name组里面,后面取数据的时候直接group(name)
# 匹配今天吃的什么
import re
obj = re.compile(r"今天吃了(?P<suger>\d+)颗糖,喝了(?P<tea>\d+)杯茶")
result = obj.finditer("昨天我吃了3颗糖,喝了1杯牛奶。今天吃了2颗糖,喝了3杯茶。明天我要吃1颗糖,喝8杯水。")
for item in result:
# print(item.group("suger"))
# print(item.group("tea"))
# out : 2 3
print(item.groupdict()) # 简化写法 out : {'suger': '2', 'tea': '3'}
实例:爬取糗事百科中热图板块下所有图片
获取一张图片
import requests
if __name__=="__main__":
url = "https://pic.qiushibaike.com/system/pictures/12360/123603022/medium/QSL96X7ELIK0LHSN.jpg"
img_data = requests.get(url=url).content
# 图片对应的是二进制数据,response.content返回的就是二进制形式数据
# text(返回字符串类型数据) json(返回对象类型数据) content(返回二进制类型数据)
with open('./qiutu.jpg', 'wb') as fp:
fp.write(img_data)
获取所有图片
前期步骤:
(1)打开糗事百科–》点击热图板块–》打开开发者工具–》点击Elements,定位到一张图片。
(2)观察图片源码标签为img,需要提取出其中src属性值,再对拿到的属性值批量请求得到二进制数据,再批量存储,最终得到所有图片。
(3)分析页面,发现img在a标签中,观察层级发现,属性值为“col1 old-style-col1”的div下的每个子div都是一个图片。因此需要正则对每一个子div的img标签进行提取。
继续观察,发现当鼠标移动到class值为“thumb”的div时,图片被选中,确定图片地址在该div中
import requests
import re
import os
import time
if __name__=="__main__":
# 创建文件夹用于存放图片
if not os.path.exists('./qiutuLibs'):
os.mkdir('./qiutuLibs')
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = "https://www.qiushibaike.com/imgrank/"
# 使用通用爬虫对url对应的一整张页面进行爬取
page_text = requests.get(url=url, headers=headers).text
# 使用聚焦爬虫将页面中所有的图片进行提取
ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?></div>'
img_src_list = re.findall(ex, page_text, re.S) # re.S多行匹配,re.M单行匹配
# print(img_src_list)
for src in img_src_list:
# 拼接出完整图片地址
src = 'https:' + src
# 请求得到图片二进制数据
img_data = requests.get(url=src, headers=headers).content
# 生成图片名称
img_name = src.split('/')[-1]
# 图片存储路径
imgPath = './qiutuLibs/'+img_name
with open(imgPath, 'wb') as fp:
fp.write(img_data)
print(img_name, '下载成功!')
time.sleep(2)
至此完成了第一张页面图片的下载
(4)下载多张页面的图片
点击第二、三、…页,观察该页面的url,发现其规律,并且第一页可以通过“page/1/”访问
下面爬取前两页图片
import requests
import re
import os
import time
if __name__=="__main__":
# 创建文件夹用于存放图片
if not os.path.exists('./qiutuLibs'):
os.mkdir('./qiutuLibs')
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = "https://www.qiushibaike.com/imgrank/page/%d/"
for pageNum in range(1,3): # 爬取前两页图片
# 对应页面的url
new_url = format(url%pageNum)
# =============================================================================
# 或者将前面的代码改为:
# url = "https://www.qiushibaike.com/imgrank/page/{}/"
# 这里变为:
# new_url = url.format(pageNum)
# =============================================================================
# 使用通用爬虫对url对应的一整张页面进行爬取
page_text = requests.get(url=new_url, headers=headers).text
# 使用聚焦爬虫将页面中所有的图片进行提取
ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?></div>'
img_src_list = re.findall(ex, page_text, re.S) # re.S多行匹配,re.M单行匹配
# print(img_src_list)
for src in img_src_list:
# 拼接出完整图片地址
src = 'https:' + src
# 请求得到图片二进制数据
img_data = requests.get(url=src, headers=headers).content
# 生成图片名称
img_name = src.split('/')[-1]
# 图片存储路径
imgPath = './qiutuLibs/'+img_name
with open(imgPath, 'wb') as fp:
fp.write(img_data)
print(img_name, '下载成功!')
time.sleep(2)
bs4
bs4只能应用于Python中。
- bs4数据解析原理:
(1)实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中。
(2)通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取。 - 环境安装:
pip install bs4
pip install lxml
- 如何实例化BeautifulSoup对象
from bs4 import BeautifulSoup
对象的实例化:
(1)将本地的html文档中的数据加载到该对象中
from bs4 import BeautifulSoup
if __name__ == "__main__":
#将本地的html文档中的数据加载到该对象中
fp = open('./test.html', 'r', encoding='utf-8')
soup = BeautifulSoup(fp, 'lxml')
print(soup)
fp.close()
(2)将互联网上获取的页面源码加载到该对象中
page_text = response.text
soup = BeautifulSoup(page_text, 'lxml')
下图为示例网页:
-
提供的用于数据解析的方法和属性:
(1)soup.tagName:返回的是文档中第一次出现的tagName对应的标签。
(2)soup.find():
①find(‘tagName’):等同于soup.tagName
②属性定位:soup.find(‘div’, class_/id/attr=‘song’)【返回标签为div,‘class_’、‘id’、‘attr’等属性为指定值的内容】
③soup.find_all(‘tagName’):返回符合要求的所有标签(列表)
(3)select:
①select(‘某种选择器(id,class,标签…选择器)’),返回的是一个列表
②层级选择器:
想要提取以下内容
print(soup.select(’.tang > ul > li > a’)[0])
“>”(大于号)表示的是一个层级,“ ”(空格)表示多个层级
print(soup.select(’.tang > ul a’)[0]) -
获取标签之间额度文本数据:
soup.a.text()/string()/get_text():获取a标签中文本内容
①text()/get_text():可以获取某一个标签中所有的文本内容【一个字符串】
②string:只可以获取该标签下面直系的文本内容【当没有直系内容时,返回空】 -
获取标签中的属性值
soup.a[‘href’]【获取a标签下属性值为‘href’的内容】
bs4实例:爬取三国演义小说所有的章节标题和章节内容
(1)实例化BeautifulSoup对象,将页面源码数据加载到该对象中,并解析标题和详情页url
查看网页源码结构,发现a标签内为需要的数据,a标签在li标签内
soup = BeautifulSoup(page_text, 'lxml')
li_list = soup.select('.book-mulu > ul > li')
(2)获取章节标题和详情页url
title = li.a.string
detail_url = 'https://www.shicimingju.com' + li.a['href']
# 由于获取的url不完整,所以需要进行拼接
(3)对详情页发起请求,解析出章节内容
# 获取详情页源码数据
detail_page_text = requests.get(url=detail_url, headers=headers).text
查看详情页源码结构,可以发现只需解析div标签下class值为“chapter_content”的项
# 解析出详情页相关的章节内容
detail_soup = BeautifulSoup(detail_page_text, 'lxml')
div_tag = detail_soup.find('div', class_='chapter_content') # 由于class是Pyhton的关键词,所以需要加“_”
content = div_tag.text
全部代码
import requests
from bs4 import BeautifulSoup
import time
if __name__=="__main__":
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = 'https://www.shicimingju.com/book/sanguoyanyi.html'
page_text = requests.get(url=url, headers=headers).text
# 在首页中解析出章节的标题和详情页的url
# 实例化BeautifulSoup对象
soup = BeautifulSoup(page_text, 'lxml')
# 解析章节标题和详情页url
li_list = soup.select('.book-mulu > ul > li')
fp = open('./sanguo.txt', 'w', encoding='utf-8')
for li in li_list:
title = li.a.string
detail_url = 'https://www.shicimingju.com' + li.a['href']
detail_page_text = requests.get(url=detail_url, headers=headers).text
detail_soup = BeautifulSoup(detail_page_text, 'lxml')
div_tag = detail_soup.find('div', class_='chapter_content')
content = div_tag.text
fp.write(title+':'+content+'\n')
print(title, '爬取完成!')
time.sleep(2)
fp.close()
xpath
最常用且最便捷高效的一种解析方式,通用性强(除python外,还能应用在其他编程语言中)。在实际工作应用中建议首选xpath解析。
- xpath解析原理:
(1)实例化一个etree的对象,且需要将被解析的页面源码数据加载到该对象中。
(2)调用etree对象中的xpath方法结合着xpath表达式实现标签的定位和内容的捕获。 - 环境安装:
pip install lxml - 如何实例化etree对象
from lxml import etree
(1)将本地的html文档中的源码数据加载到etree对象中:
etree.parse(filePath)
(2)将互联网上获取的源码数据加载到对象中:
etree.HTML('page_text')
- xpath表达式
下图为示例网页:
(1)从本地实例化etree对象,将被解析的源码加载到该对象中:
from lxml import etree
if __name__ == "__main__":
tree = etree.parse('test.html')
(2)层级获取指定标签内容
“/”表示从根节点(根目录)开始遍历,返回的r是一个element类型对象,该对象中存储了title标签中对应的内容
① /:表示的是从根节点开始定位,表示的是一个层级。(相当于bs4中的“>”)
② //:表示的是多个层级(相当于bs4中的“ ”);还可以表示从任意位置开始定位。
r = tree.xpath('/html/head/title')
r = tree.xpath('/html//div')
r = tree.xpath('//div') # 找到源码中所有div标签
# 以上三行代码返回内容相同
(3)属性定位:
tag[@attrName=“attrValue”]
# 定位到class属性为“song”的数据
r = tree.xpath('//div[@class="song"]')
(4)索引定位(从1开始):
获取“苏轼”所对应的p标签
r = tree.xpath('//div[@class="song"]/p[3]')
# 这里的索引并非从0开始,而是从1开始
(5)取文本:
① /text():获取的是标签中直系的文本内容
② //text():标签中非直系的文本内容(所有的文本内容)
目标文本
r = tree.xpath('//div[@class="tang"]//li[5]/a/text()')[0]
# /text() 用于获取标签中的文本数据,由于获取的是一个列表数据,所以只需用[0]取出即可
r = tree.xpath('//li[5]/text()')
# 返回为空列表,由于“杜牧”并没有直系的存储在li标签中,而是下li标签直系的子标签中
# 如果想要直接获取子标签中内容,使用下面的方法
r = tree.xpath('//li[5]//text()')
(6)取属性:
/@attrName
目标:取得img标签下,src的属性值
r = tree.xpath('//div[@class="song"]/img/@src')
xpath实例:爬取58二手房中的房源信息
(1)实例化etree对象,查看源码数据,发现需要的内容在class属性值为“house-list-weap”的ul标签下的li标签中
页面源码:
tree = etree.HTML(page_text)
li_list = tree.xpath('//ul[@class="house-list-wrap"]/li')
(2)为了得到房源数据和url,需要拿到li标签下的a标签中的数据
for li in li_list:
title = li.xpath('./div[2]/h2/a/text()')[0] # 这里“./”表示前面li.xpath的li(单个“/”放在前面表示整个源码下的指定目录,在这里是div[2]目录)
全部代码
import requests
from lxml import etree
import time
if __name__=="__main__":
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = 'https://bj.58.com/ershoufang/'
page_text = requests.get(url=url, headers=headers).text
# 数据解析
tree = etree.HTML(page_text)
li_list = tree.xpath('//ul[@class="house-list-wrap"]/li')
fp = open('58.txt', 'w', encoding='utf-8')
for li in li_list:
title = li.xpath('./div[2]/h2/a/text()')[0]
fp.write(title + '\n')
time.sleep(2)
fp.close()
xpath实例:4K图片解析下载
(1)实例化etree对象,解析src属性值,alt属性值
网页源码:
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@class="slist"]/ul/li')
(2)解析得到a标签中图片地址和名称
for li in li_list:
img_src = 'http://pic.netbian.com' + li.xpath('./a/img/@src')[0]
img_name = li.xpath('./a/img/@alt')[0] + '.jpg'
img_name = img_name.encode('iso-8859-1').decode('gbk')
(补)解决乱码问题:
# 方法一:
img_name.encode('iso-8859-1').decode('gbk)
# 通用处理中文乱码的解决方案,针对特定位置编码
# 方法二:
# 手动设定响应数据的编码格式,全体编码
response = requests.get(url=url, headers=headers)
response.encoding = 'utf-8'
# 方法三:
response.encoding = response.apparent_encoding # 解决乱码问题
(3)持久化存储:
img_data = requests.get(url=img_src, headers=headers).content
img_path = 'picLibs/' + img_name
with open(img_path, 'wb') as fp:
fp.write(img_data)
全部代码:
import requests
from lxml import etree
import time
import os
if __name__=="__main__":
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = 'http://pic.netbian.com/4kdongwu/'
page_text = requests.get(url=url, headers=headers).text
tree = etree.HTML(page_text)
li_list = tree.xpath('//div[@class="slist"]/ul/li')
if not os.path.exists('./picLibs'):
os.mkdir('./picLibs')
for li in li_list:
img_src = 'http://pic.netbian.com' + li.xpath('./a/img/@src')[0]
img_name = li.xpath('./a/img/@alt')[0] + '.jpg'
img_name = img_name.encode('iso-8859-1').decode('gbk')
img_data = requests.get(url=img_src, headers=headers).content
img_path = 'picLibs/' + img_name
with open(img_path, 'wb') as fp:
fp.write(img_data)
time.sleep(1)
xpath实例:全国城市名称爬取
目标网页源码:
全部代码:
import requests
from lxml import etree
if __name__=="__main__":
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = 'https://www.aqistudy.cn/historydata/'
page_text = requests.get(url=url, headers=headers).text
tree =etree.HTML(page_text)
host_li_list = tree.xpath('//div[@class="bottom"]/ul/li')
all_city_names = []
# 解析热门城市的名称
for li in host_li_list:
host_city_name = li.xpath('./a/text()')[0]
all_city_names.append(host_city_name)
city_names_list = tree.xpath('//div[@class="bottom"]/ul/div[2]/li')
# 解析全部城市的名称
for li in city_names_list:
city_name = li.xpath('./a/text()')[0]
all_city_names.append(city_name)
print(all_city_names, len(all_city_names))
一次性取得热门城市和全部城市的名称,全部代码:
import requests
from lxml import etree
if __name__=="__main__":
headers = {
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)'
}
url = 'https://www.aqistudy.cn/historydata/'
page_text = requests.get(url=url, headers=headers).text
tree =etree.HTML(page_text)
# 解析到热门城市和全部城市对应的a标签
# 热门城市a标签的层级关系://div[@class="bottom"]/ul/li/a
# 全部城市a标签的层级关系://div[@class="bottom"]/ul/div[2]/li/a
a_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
all_city_names = []
for a in a_list:
city_names = a.xpath('./text()')[0]
all_city_names.append(city_names)
print(all_city_names, len(all_city_names))