数据解析分类:
-
正则表达式
-
bs4
-
xpath(重点,最通用,xpath不止适用于python语言编写的爬虫)
数据解析原理概述
前端都知道,文字通常存在div、li等标签里,图片、视频等存于标签的src属性里,所以我们只需要定位到标签或src即可。
聚焦爬虫编码流程
-
指定url
-
发起请求
-
获取响应数据
-
数据解析
-
持久化存储
基于正则表达式的数据解析
实战之保存图片
import re
import requests
if __name__ == '__main__':
url = 'https://i0.hdslb.com/bfs/face/d7c4d7a191af8218450b2462657f5ffc15c05652.jpg@240w_240h_1c_1s.webp'
# text返回字符串形式的响应数据,content返回二进制形式的响应数据,而图片正是二进制格式,json()则适用content-type: json
# 试过了,用text,写入txt文件是一堆方框代表的乱码
img = requests.get(url=url).content
# wb:以二进制格式打开一个文件,只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
with open('./qiutu.webp', 'wb') as file: # 'wb'指write byte,即 写入二进制文件,写入时会覆盖文件,可用于下载和写入图片、视频、压缩包等二进制文件
file.write(img)
如何查阅python官方文档
其实没必要,pycharm给你看的就是官方文档给你看的,并没有多详细
实战之基于通用式爬虫爬取整张网页,基于此用正则表达式数据分析出url,最后下载保存图片
import re
import requests
import os
if __name__ == '__main__':
if not os.path.exists('./img'):
os.mkdir('./img')
url = 'https://anzhiy.cn/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
}
text = requests.get(url=url, headers=headers).text
pattern = r"""<div class="post_cover right">.*?<img.*?data-lazy-src="(.*?)!cover"""
pic_list = re.findall(pattern, text, re.S)
for i in range(len(pic_list)):
img_name = pic_list[i].split('/')[-1]
with open('./img/' + img_name, 'wb') as file:
img = requests.get(url=pic_list[i], headers=headers).content
file.write(img)
file.close()
print(img_name + "download successfully!")
print("脚本over!")
基于Bs4的数据解析
前言:基于正则的数据解析,由于正则规则通用于各语言,所以它适用于其他语言的爬虫;而bs4是python独有的,所以基于bs4的数据解析只适用于python
bs4数据解析的原理
-
实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中
-
通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取
-
初衷是:要是能把js的选择器语法搬到python中就好了
环境安装
-
pip install bs4
-
pip install lxml (一种数据解析库/器,辅助bs4和xpath)
流程
-
实例化BeautifulSoup对象
-
from bs4 import BeautifulSoup
-
将本地的html文档中的数据或网上爬到的页面源码加载到该对象中去
file = open('./target.html', 'r', encoding='utf-8') soup = BeautifulSoup(file, 'lxml') # 参数二是固定的,指定了BeautifulSoup使用lxml数据解析器解析参数一代表的文档 # 这步是借用了类的构造函数初始化实例 或 text = response.text soup = BeautifulSoup(text, 'lxml')
-
-
调用BeautifulSoup api
选中标签
比较方便的是下面的api不止soup可用,api返回的结果亦可用,如soup.find(div).find('ul')
-
print(soup)是载入的整个html文档
-
soup.标签名如soup.a即可表示第一个a标签,含其子节点,就是js的document.getElementByTagName()
-
soup.find()
-
soup.find('标签名')如soup.find(div)等同于soup.div
-
soup.find(‘标签名’, class_='类名'),等同于document.getElementsByTagName('标签名')[0, 1, ..., n].getElementByClassName('类名'),之所以class_而非class是因为class是python关键字.。所得结果亦含子标签,只匹配第一个符合条件的标签
-
soup.find_all('标签名')返回符合要求的所有标签,结果以list形式返回
soup.find_all(‘标签名’, class_='类名')
-
事实上我们不仅可以用class筛选标签,看代码
soup.find_all(‘a’, attrs={‘class’: ‘bets-name’})
暂不细究。
-
-
soup.select(选择器如id、class、标签...选择器),结果以list形式返回,标签亦夹带子标签。如soup.select('div')、 soup.select('.className')、soup.select('#idName'),从他兄弟soup.select_one()可见前者相当于document.querySelectorAll(),后者相当于document.querySelector()。
值得注意的是soup.select()还混用了css的选择器,如soup.select("#idName > ul > .className")或soup.select("#idName > ul .className"),即soup.select()可以使用层级选择器
获取标签的文本数据或属性
记obj = soup.select('div')[0]
-
获取标签之间的文本数据
obj.text或obj.string或obj.get_text(),只用记get_text()即可
区别:text/get_ text():可以获取某一个标签中所有的文本内容,即使是其子标签的内容(非直系);string: 只可以获取该标签下面直系的文本内容
例如对于<div>abc<a>123</a></div>来说,obj.text或obj.get_text()是abc123,对于obj.string理应是abc,其实输出None,BeautifulSoup的官方文档解释道:.string方法在tag包含多个子节点时,tag无法确定.string方法应该调用哪个子节点的内容,所以输出None。总结:弃用.string
-
获取标签的属性值
obj['href']、obj['src']
-
实战之批量爬取三国演义
import requests
import os
from bs4 import BeautifulSoup
if __name__ == '__main__':
if not os.path.exists('./sanguo'):
os.mkdir('./sanguo')
url = 'https://www.shicimingju.com/book/sanguoyanyi.html'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
}
response = requests.get(url=url, headers=headers)
# 可以F12检查<meta charset='字符集是啥'>,然后灵活变换下行的值
response.encoding = 'utf-8' # 如果不加上,下面打印出来的是第丕啕·宴桕啕豪数丕绕乕 敩黕巾蕱镕馕竕å
sanguoyanyi_text = response.text
soup = BeautifulSoup(sanguoyanyi_text, 'lxml')
li_list = soup.select('.book-mulu > ul > li')
href_list = []
title_list = []
for li in li_list:
href_list.append('https://www.shicimingju.com' + li.a['href'])
title_list.append(li.find('a').text)
print(title_list[0]) # 打印:第一回·宴桃园豪杰三结义 斩黄巾英雄首立功
for i in range(len(href_list)):
with open('./sanguo/' + title_list[i] + '.txt', 'w', encoding='utf-8') as file:
sub_response = requests.get(url=href_list[i], headers=headers)
sub_response.encoding = 'utf-8'
sub_text = sub_response.text
sub_soup = BeautifulSoup(sub_text, 'lxml')
file.write(sub_soup.select('.bookmark-list .chapter_content')[0].text)
file.close()
print("download第" + str(i) + "个 successfully!")
基于Xpath的聚焦爬虫
xpath解析是三种方案里最方便、最通用的方案,即亦可以应用到其他语言中。
xpath解析原理
-
实例化一个etree的对象,且需要将被解析的页面源码数据加载到该对象中。
-
调用etree对象中的xpath方法结合着xpath表达式实现标签的定位和内容的捕获。
环境的安装
pip install lxml
如何实例化一个etree对象
from lxml import etree
-
将本地的html文档中的源码数据加载到etree对象(寓意element tree,文档树)中:tree= etree. parse(filePath)
其实文件输入流即tree = etree.parse(open('bili.html', 'r+', encoding='utf-8'))也是OK的,记得选用可读且不清空文本的模式。
-
可以将从互联网上获取的源码数据加载到该对象中tree= etree.HTML(page_text)
注意,我遇到一个没指定编码格式导致etree看到的是乱码带来报错说是非法的标签名:lxml.etree.XMLSyntaxError: StartTag: invalid element name, line 1, column 2,解决:
parser = etree.HTMLParser(encoding="utf-8") tree = etree.parse('./temp/二手房.html', parser=parser)
-
xpath只需要掌握这一个函数,重点在xpath表达式,tree.xpath(‘xpath表达式’)
xpath表达式
xpath通过层级关系构建表达式并且只能通过层级关系构建表达式。例如获取<title></title>节点,tree.xpath('html/head/title'),但是html来自根节点,要前缀/,即tree.xpath('/html/head/title')
实战发现tree = etree.pase(filePath)中filePath的文件的html标签要严丝合缝地闭合:
反例:
<img src="">
<meta charset="UTF-8">
link rel="shortcut icon" href="logo.ico">
正例:
<img src="" />
<meta charset="UTF-8" />
link rel="shortcut icon" href="logo.ico" />
不然会报错:lxml.etree.XMLSyntaxError: Opening and ending tag mismatch: meta line 6 and head, line 8, column 8,好消息是报错会告诉你在第几line没闭合。
实现定位的xpath表达式语法
from lxml import etree
if __name__ == '__main__':
tree = etree.parse('bili.html')
r = tree.xpath('/html/head/title')
print(r) # [<Element title at 0x18632903880>]
# 分析:列表,返回了标题对象地地址组成的list
亲子选择器/和后代选择器//
对于<body>
<div>1<div>4</div></div>
<div>2</div>
<div>3</div>
</body>来说print(tree.xpath('/html/body/div')) # [<Element div at 0x26518df3640>, <Element div at 0x26518df3880>, <Element div at 0x26518df3980>]
print(tree.xpath('/html/div')) # []
可见这种方式有层层剥皮的韵味,外皮没剥就不能剥内皮,得从根皮开始剥。换个前端人易理解的话,这里的 父标签/子标签 相当于BeautfulSoup.select()的>或者说css选择器的亲子选择器>;当/出现在xpath表达式的最左侧表示文档根标签如tree.xpath('/html/head');当然也就有 祖先标签//后代标签 等价于css的后代选择器 祖先标签空格后代标签:
print(tree.xpath('/html//div')) # [<Element div at 0x1fc8e8135c0>, <Element div at 0x1fc8e813500>, <Element div at 0x1fc8e813800>, <Element div at 0x1fc8e813900>]
如果//作用于xpath表达式的最左侧,如obj.xpath('//div')则表示可以从tree的任意位置去寻找div标签,请注意,我说明的很准确,即使是甲div含着乙ul,乙含着丙li,然后丙li.xpath('//div')都能选中甲,正确的做法是丙li.xpath('.//div'),这样只会在丙里查找div
print(tree.xpath('///div')) # [<Element div at 0x1fc8e8135c0>, <Element div at 0x1fc8e813500>, <Element div at 0x1fc8e813800>, <Element div at 0x1fc8e813900>]
通过属性值定位元素
对于<body>
<div class="demo">1<div>4</div></div>
<div data-index="0">2</div>
<div>3</div>
</body>来说也可以通过属性键值对选中标签,但目前这是唯一一处不用.#的语法:
tree,xpath('//标签名[@属性名="属性值"]') # 标签除了div等,还可以是通配符*,代表任意标签
print(tree.xpath('//div[@data-index="0"]')) # [<Element div at 0x15aa5be3740>]
print(tree.xpath('//div[@class="demo"]')) # [<Element div at 0x15aa5be3780>]
索引定位
对于<body>
<div><li>1</li>
<li>2</li>
<li>3</li></div>
</body>来说print(tree.xpath('//div/li[3]')) # [<Element li at 0x28957816340>]
很无语索引是从第1个开始计数的
obj.xpath()的obj可以是整个网页文档树tree,也可以是某个标签
li = etree.parse('1.html').xpath('/html/body/ul/li')[0]
li.xpath('./div') # 此时可以.开头,不可以/开头,.代表当前节点li,就像上行最左的/代表根节点'1.html'
li.xpath('.//div')
而li.xpath('//div') 依旧是从html根开始匹配
定位后如何取文本
/text() | 取标签中直系的文本内容,返回list |
---|---|
//text() | 取标签中直系和非直系的文本内容(所有的文本内容),返回list |
对于<ul><li>1</li></ul>来说
print(tree.xpath('//ul/text()')) # []
print(tree.xpath('//ul//text()')) # ['1']
print(tree.xpath('//ul/li/text()')) # ['1']
对于
<ul> <!-- 记为A -->
<li>1<!-- 记为B --></li><!-- 记为C -->
<li>2<!-- 记为D --></li><!-- 记为E -->
</ul>来说
print(tree.xpath('//ul/text()')) # ['\n ', '\n ', '\n '] 即[A, B, C]
print(tree.xpath('//ul//text()')) # ['\n ', '1', '\n ', '2', '\n ']即[A、B、C、D、E]
print(tree.xpath('//ul/li/text()')) # ['1', '2']即[B、D]
请注意,/text()和//text()返回list,这是为什么呢?产生多个结果有两种因:
-
像上例的第一个print(),就有三个值,三个值源于值在heml中被其他子标签分割
-
tree.xpath('/html/body/ul/li')的选出的li结果可能是多个,例如三个ul,每个ul有2个li,那么就选除了6个li,所以tree.xpath('/html/body/ul/li/text()')的list至少有6项。
定位后如何取属性
选中元素后接/@属性名即可,例如tree.xpath('//img/@src')
xpath表达式之 A表达式 | B表达式
对于<ul><li>1</li></ul>
<ol><li>2</li></ol>来说与其
print(tree.xpath(r'//ul/li/text()')) # ['1']
print(tree.xpath(r'//ol/li/text()')) # ['2'] 不如
print(tree.xpath(r'//ul/li/text() | //ol/li/text()')) # ['1', '2']
哦,对了,浏览器选中元素,右键,不止能复制节点,还能复制节点的xpath。
不是很推荐,这是浏览器给的xpath: '/html/body/div[6]/div[2]/div[2]/div[1]/div[7]/div[2]/ul/li[1]/a'。看得出来它是严格的索引定位,当有的网页段落多,有的网页段落少,本网页的索引序号可能不适用另一个网页了。
指定编码方案总结
-
response = requests.get(url=url, headers=headers) response.encoding = 'utf-8 # 这样会把抓到的整个文档都指定编码,如果你想保留原样,只针对某一块内容指定编码,请看方案二。这是从被解析的文本的角度设置编码方案 但是这样子还是需要爬虫工程师手动去看网页的<meta charset='啥'>,不如 response.encoding = response.apparent_encoding 因为print(response.apparent_encoding)会得到原网页的原始编码方案如GB2312
-
page_text= = requests.get(url=url, headers=headers).text img_name = page_text.xpath('//img/@title')[0] img_name.encode('iso-8859-1').decode('gbk') # 只截取了图片名字做编码处理,iso-8859-1和gbk适用于中文被乱码了
-
page_text= = requests.get(url=url, headers=headers).text file = open('1.txt', 'w', encoding='utf-8') file.write(page_text)
-
page_text= = requests.get(url=url, headers=headers).text parser = etree.HTMLParser(encoding='utf-8') tree = etree.HTML(page_text, parser=parser) # 有时page_text是乱码,导致etree认不出方框乱码是不是标签,可以从解析器的角度设置编码方案
bs4好还是xpath好?
xpath好。首先xpath不仅适用python语言,也适用于其他语言编写的爬虫;其次xpath只需要学会一个函数,bs4有四五个;最后,xpath自由度更高,我曾经做过一个案例,xpath比bs4少写个for in 循环。
import requests
from lxml import etree
if __name__ == '__main__':
url = 'http://www.aqistudy.cn/historydata/'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0'
}
page_text = requests.get(url=url, headers=headers).text
parser = etree.HTMLParser(encoding='utf-8')
tree = etree.HTML(page_text, parser=parser)
a_list = tree.xpath(r'//div[@class="bottom"]/ul[@class="unstyled"]/li/a/text()')
print(a_list)
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://www.ink0.cn/index.php/2023/03/05/03%e8%81%9a%e7%84%a6%e5%bc%8f%e7%88%ac%e8%99%ab/
共有 0 条评论