摘要:
在写爬虫的时候,为了效率我们通常会选择解析网页api来获取数据,但是有时候解析方式比较困难(很多网站会对请求数据和返回数据加密),或者我们纯粹是为了快速实现爬虫,可使用浏览器自动化操作——selenium或pyppeteer。
思路:
对于爬取网站,一般有两种思路:
- 分析 Ajax 请求,通过模拟请求requests得到真实的数据,该情况受限于网站加密(可通过分析js加密解密函数来破解)
- 使用 selenium(或pyppeteer) 模拟浏览器进行动态渲染,从而获取网站返回的html内容,再通过Beautiful Soup4解析获得想要的数据。以下我们将详细讲解这种方法
区别:
selenium和pyppeteer都是模拟浏览器进行渲染,它们的区别如下:
- 环境配置:selenium使用起来是不太方便的,要安装浏览器、下载对应的驱动,而且各个工具的版本还要匹配,大规模部署时就比较麻烦;pyppeteer提供自动化下载chromium浏览器(支持浏览器比较单一),省去了 driver 配置的环节
- 语法结构:pyppeteer基于异步编程思想(使用asyncio构建),所以在使用的时候需要用到 async/await 结构。selenium是同步编程,则没有这些要求。
- 性能方面:pyppeteer基于协程,性能上会比selenium更高。
一、selenium 简介
selenium 就是一个用于 Web 应用程序的测试工具
根据官方文档所说,selenium 最大的优点就是它可以直接运行在浏览器上,模拟用户的真实行为
但同时这也是它最大的缺点,由于需要模拟真实的渲染过程,所以导致它的运行速度变慢
安装和基础语法参考:Selenium:强烈推荐!内含最详细的介绍[安装,基本使用]
设置js加载等待时间参考:Selenium的三种等待,强制等待、隐式等待、显式等待
二、Pyppeteer简介
pyppeteer是puppeteer的Python版本,而puppeteer是什么呢?puppeteer是Google基于Node.js开发的一个工具,它可以使我们通过JavaScript来控制Chrome浏览器执行一些操作,拥有丰富的API,功能非常强大,因此也可以用于网络爬虫。pyppeteer是一位日本的程序员根据Puppeteer开发的非官方Python版本。
2.1、安装模块
pip install pyppeteer
# 使用时导入
import pyppeteer
2.2、等待机制和浏览器实例
page.waitForXPath:等待 xPath 对应的元素出现,返回对应的 ElementHandle 实例
page.waitForSelector :等待选择器对应的元素出现,返回对应的 ElementHandle 实例
启动器
- pyppeteer.launcher.launch() 启动 Chrome 进程并返回浏览器实例
参数:
参数类型解释ignoreHTTPSErrorsbool是否忽略 HTTPS 错误。默认为
False
ignoreDefaultArgsList [str]不要使用 pyppeteer 的默认参数。这是危险的选择;小心使用headlessbool无头模式下运行浏览器。默认为
True
除非
appMode
或
devtools
选项
True
executablePathstr运行 Chromium 或 Chrome 可执行文件的路径,而不是默认捆绑的 ChromiumslowMoint或float按指定的毫秒数减慢 pyppeteer 操作。argsList [str]传递给浏览器进程的附加参数(标志)。dumpiobool是否管道浏览器进程 stdout 和 stderr 进入
process.stdout
和
process.stderr
。默认为 False。userDataDirstr用户数据目录的路径envdict指定浏览器可见的环境变量。默认与 python 进程相同。devtoolsbool为每个选项卡自动打开 DevTools 面板。如果是此选项
True
,
headless
则将设置该选项
False
。logLevelint或str用于打印日志的日志级别。默认值与根记录器相同。autoClosebool脚本完成时自动关闭浏览器进程。默认为
True
。loopasyncio.AbstractEventLoop事件循环(实验)。
移除Chrome正受到自动测试软件的控制,可直接绕过浏览器window.navigator.webdriver检测
# 添加ignoreDefaultArgs=["--enable-automation"] 参数 from pyppeteer import launch browser = await launch(headless=False, ignoreDefaultArgs=["--enable-automation"])
浏览器的console运行如下代码,同正常打开浏览器一样都为undefined,如果不设置就为true
2.3、常用的页面操作
执行js
page.evaluate ( pageFunction [, …args] ) ,返回 pageFunction 执行的结果,pageFunction 表示要在页面执行的函数或表达式, args 表示传入给 pageFunction 的参数
课外内容:
scrollTo和scrollBy这两个JS API也是用来控制元素或者窗体的滚动距离的。
scrollTo()
表示滚到到指定的位置,而
scrollBy()
表示相对当前的位置滚动多少距离。
scrollTo和scrollBy两个JS API的优点有两个:
- 调用统一 scrollLeft/scrollTop这两个属性只能作为元素上,在window对象上没有效果。而pageXOffset/pageYOffset只能作用于window对象上,在元素上没有效果。而scrollTo和scrollBy不仅可以作用于window对象上,还可以作用于元素上。实现的调用的统一。
- 平滑支持 scrollLeft/scrollTop和pageXOffset/pageYOffset控制滚动定位,想要定位平滑,只能借助于CSS scroll-behavior属性,JS这块设置无力。但是scrollTo和scrollBy在比较方便,直接有API参数支持。
代码:
scroll_top = 100
await page.evaluate(f'document.getElementsByClassName("mp-layout-content-container")[0].scrollBy(0, {scroll_top})')
元素操作
ElementHandle 表示页内的DOM元素,你可以通过 page.querySelector() 方法创建。DOM 元素具有和 page 相同的某些方法:J()、JJ()、Jeval()、JJeval()、screenshot()、type()、click()、tap()。此外,还有一些好用的方法:
(1) 获取元素边界框坐标:boundingBox(),返回元素的边界框(相对于主框架)=> x 坐标、 y 坐标、width、height
(2) 元素是否可见:isIntersectingViewport()
(3) 上传文件:uploadFile(*filpaths)
(4) ElementHandle 类 转 Frame类:contentFrame(),如果句柄未引用iframe,则返回None。
(5) 聚焦该元素:focus()
(6) 与鼠标相关:hover () ,将鼠标悬停到元素上面
(7) 与键盘相关:press (key[, options]),按键,key 表示按键的名称,option可配置:
text (string) - 如果指定,则使用此文本生成输入事件 delay (number) - keydown 和 keyup 之间等待的时间。默认是 0
鼠标事件
Mouse 类在相对于视口左上角的主框架 CSS 像素中运行。
(1) page.mouse.down([options]) 按下鼠标,options 可配置:
button(str) 按下了哪个键,可选值为 [ left, right, middle ], 默认是 left, 表示鼠标左键 clickCount(int) 按下的次数,单击,双击或者其他次数
(2) page.mouse.up([options]) 松开鼠标,options 同上
(3) page.mouse.move(x, y, [options]) 移动鼠标到指定位置,options.steps 表示移动的步长
(4) page.mouse.click(x, y, [options]) 鼠标点击指定的位置,其实是 mouse.move 和 mouse.down 或 mouse.up 的快捷操作
键盘事件
Keyboard 提供一个接口来管理虚拟键盘. 高级接口为 keyboard.type, 其接收原始字符, 然后在你的页面上生成对应的 keydown, keypress/input, 和 keyup 事件。
为了更精细的控制(虚拟键盘), 你可以使用 keyboard.down, keyboard.up 和 keyboard.sendCharacter 来手动触发事件, 就好像这些事件是由真实的键盘生成的。
键盘的几个API如下:
keyboard.down(key[, options]) 触发 keydown 事件 keyboard.press(key[, options]) 按下某个键,key 表示键的名称,比如‘ArrowLeft’ 向左键; keyboard.sendCharacter(char) 输入一个字符 keyboard.type(text, options) 输入一个字符串 keyboard.up(key) 触发 keyup 事件
详细的键名映射可以看源码:
Lib\site-packages\pyppeteer\us_keyboard_layout.py
内嵌框架
可以通过 Page.frames、ElementHandle.contentFrame 方法获取,同时具有和 page一样的多个方法;
**其它:
childFrames 获取子框架,返回列表 parentFrame 返回父框架 content() 返回框架的 html 内容 url 获取 url name 获取 name title() 获取 title
更多内容可参考:Pyppeteer库之四:Pyppeteer的页面操作
2.4、使用思路和案例
无论是使用Selenium还是Pyppeteer原理都是模拟浏览器进行加载js渲染页面,所以我们最后要拿到经过渲染后的网页源代码,再结合Beautiful Soup进行html标签元素解析提取
在Pyppeteer中,它操作的是一个类似Chrome的Chromium浏览器,Chromium是相当于Chrome的开发版,是完全开源的,Chrome的所有新功能都会先在Chromium上实现,稳定后才会移植到Chrome上,因此Chromium会包含很多新功能。Pyppeteer就是依赖于Chromium来运行的,当我们第一次运行Pyppeteer的时候,如果Chromium没有安装,那么程序会自动帮我们安装和配置,省去了环境配置这一步。
下面我们详细了解一下Pyppeteer的使用思路。
- aunch 方法新建一个Browser对象,赋值给browser变量,这一步就相当于启动了浏览器
- 然后browser调用newPage方法相当于新建一个选项卡,并且返回一个Page对象,这一步还是一个空白的页面,并未访问任何页面
- 然后Page调用goto方法,就相当于访问此页面
- Page对象调用waitForXpath方法,那么页面就会等待选择器所对应的节点信息加载出来,如果加载出来就立即返回,否则就会持续等待直到超时。这里就比selenium的等待元素加载完毕要清晰的多了。
- 页面加载完成后再调用content方法,获取渲染出来的页面源代码
- 通过BeautifulSoup解析源代码,提取需要的数据
例子:
# -*- coding: utf-8 -*-
"""
@Time : 2023/1/5 11:22 AM
@File :web_to_excel.py
"""
import datetime
import os.path
import time
import requests
from bs4 import BeautifulSoup
import xlrd
from xlutils.copy import copy
import asyncio
from pyppeteer import launch
import lxml
async def main(cookies_str):
html_source = await collect_data(cookies_str)
table_data = await parse_html(html_source)
await write_excel_data(table_data)
async def collect_data(cookies_str):
cookies = []
for i in cookies_str.split(';'):
print(i)
tmp = i.split('=', 1)
cookie = {"name": tmp[0].strip(), "value": tmp[1].strip()}
cookies.append(cookie)
conf_dict = {
'autoClose': True,
'headless': False,
'dumpio': True,
'ignoreDefaultArgs': ["--enable-automation"] # 移除Chrome正受到自动测试软件的控制
}
browser = await launch(conf_dict)
page = (await browser.pages())[0]
# 是否启用JS,enabled设为False,则无渲染效果
await page.goto('需要爬取的网站')
# print('current cookies', page.cookies())
# 刷新网页
await page.setCookie(*cookies)
await page.reload()
# await asyncio.sleep(10)
await page.waitForSelector('.mp-table') # 等待节点出现
html_source = await page.content()
# print(type(html_source), html_source)
return html_source
async def parse_html(html_source):
soup = BeautifulSoup(html_source, 'lxml')
div_table_html = soup.find_all('div', attrs={'class': 'slate-card'})[1] # 找到第二个表格
title_tr = div_table_html.find('tr', attrs={'class': 'mp-table-row-sticky'}) # 标题的标签
title_td = title_tr.find_all('td')
title_text = []
for t_td in title_td:
cell_span = t_td.find_all('span', attrs={'data-slate-string': 'true'}) # 标题的文本
cell_str = '\n'.join([c_span.text for c_span in cell_span])
title_text.append(cell_str)
print(111, len(title_text), title_text) # 标题
content_text = [] # 内容:是个二维列表
for other_tr in title_tr.next_siblings:
other_td = other_tr.find_all('td')
row_text = []
for o_td in other_td:
cell_span = o_td.find_all('span', attrs={'data-slate-string': 'true'}) # 内容的文本
cell_str = '\n'.join([c_span.text for c_span in cell_span])
row_text.append(cell_str)
print(112, len(row_text), row_text) # 内容
content_text.append(row_text)
# content_span = title_tr.next_siblings.find_all('span', attrs={'data-slate-string': 'true'}) #除标题外的其他行
# content_text = [span.text for span in content_span]
content_text.insert(0, title_text)
print(222, content_text)
return content_text
async def write_excel_data(data, save_path="/Users/Desktop/变更操作单"):
# data: 二维数组,表示插入excel的数据
# save_path: 工作簿的路径
# formatting_info=True:保留Excel的原格式
workbook = xlrd.open_workbook('/Users/Desktop/变更操作单/变更操作单模板.xls', formatting_info=True)
new_workbook = copy(workbook) # 将xlrd对象拷贝转化为xlwt对象
print(workbook.sheets())
# 写入表格信息
# 第一次建立工作簿时候调用
write_sheet = new_workbook.get_sheet(0)
index = len(data) # 获取需要写入数据的行数
# workbook = xlwt.Workbook() # 新建一个工作簿
for i in range(0, index):
for j in range(0, len(data[i])):
write_sheet.write(i, j, data[i][j]) # 像表格中写入数据(对应的行和列)
# now_date_str = time.strftime("%Y%m%d", time.localtime())
now_date_str = '20230129'
save_path = os.path.join(save_path, "{}变更操作单.xls".format(now_date_str))
new_workbook.save(save_path) # 保存工作簿
if __name__ == '__main__':
# 需手动更新cookie
cookies_str = '页面中获取'
asyncio.get_event_loop().run_until_complete(main(cookies_str))
要实现用户自动登录,可获取浏览器cookie
参考:获取cookies(pyppeteer)
还有另一种方法实现自动登录验证,即启动浏览器时,传入userDataDir参数即可:
conf_dict = { 'userDataDir': "存放浏览记录的文件夹", #完成第一次手动输入验证,后续就不用再验证了 'autoClose': False, 'headless': False, 'dumpio': True, 'ignoreDefaultArgs': ["--enable-automation"] } browser = await launch(conf_dict)
三、BeautifulSoup简介
Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过转换器实现惯用的文档导航、查找、修改文档的方式。Beautiful Soup 3 目前已经停止开发,官网推荐在现在的项目中使用Beautiful Soup 4, 移植到BS4。
3.1、安装模块
# 安装 Beautiful Soup
pip install beautifulsoup4
# 安装解析器
pip install lxml
3.2、解析器
下表列出了主要的解析器,以及它们的优缺点,官网推荐使用lxml作为解析器,因为效率更高。 在Python2.7.3之前的版本和Python3中3.2.2之前的版本,必须安装lxml或html5lib, 因为那些Python版本的标准库中内置的HTML解析方法不够稳定。
解析器使用方法优势劣势Python标准库BeautifulSoup(markup, "html.parser")
Python的内置标准库
执行速度适中
文档容错能力强
Python 2.7.3 or 3.2.2)前 的版本中文档容错能力差
lxml HTML 解析器BeautifulSoup(markup, "lxml")速度快
文档容错能力强
需要安装C语言库
lxml XML 解析器
BeautifulSoup(markup, ["lxml", "xml"])
BeautifulSoup(markup, "xml")
速度快
唯一支持XML的解析器
需要安装C语言库
html5libBeautifulSoup(markup, "html5lib")最好的容错性
以浏览器的方式解析文档
生成HTML5格式的文档
速度慢
不依赖外部扩展
3.3、Beautiful Soup的使用
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
# 容错处理,文档的容错能力指的是在html代码不完整的情况下,使用该模块可以识别该错误。
# 使用BeautifulSoup解析上述代码,能够得到一个 BeautifulSoup 的对象,并能按照标准的缩进格式的结构输出
from bs4 import BeautifulSoup
soup=BeautifulSoup(html_doc,'lxml') #具有容错功能
res=soup.prettify() #处理好缩进,结构化显
print(res)
3.4、查找元素
1、遍历文档树
# 遍历文档树:即直接通过标签名字选择,特点是选择速度快,但如果存在多个相同的标签则只返回第一个
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p id="my p" class="title"><b id="bbb" class="boldest">The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
# 获取BeautifulSoup对象
soup=BeautifulSoup(html_doc,'lxml')
print(soup.p) # 存在多个相同的标签则只返回第一个
print(soup.a) # 存在多个相同的标签则只返回第一个
# 1. 获取标签的名称
print(soup.p.name)
# 2. 获取标签的属性
print(soup.p.attrs)
# 3. 获取标签的内容
print(soup.p.string) # p下的文本只有一个时,取到,否则为None
print(soup.p.strings) # 拿到一个生成器对象, 取到p下所有的文本内容,可以转换为list
print(soup.p.text) # 取到p下所有的文本内容
for line in soup.stripped_strings: # 去掉空白
print(line)
# 4. 嵌套选择
print(soup.head.title.string)
print(soup.body.a.string)
# 5. 子节点、子孙节点
print(soup.p.contents) # p下所有子节点
print(soup.p.children) # 得到一个迭代器,包含p下所有子节点
for i,child in enumerate(soup.p.children):
print(i,child)
print(soup.p.descendants) # 获取子孙节点,p下所有的标签都会选择出来
for i,child in enumerate(soup.p.descendants):
print(i,child)
# 6. 父节点、祖先节点
print(soup.a.parent) # 获取a标签的父节点
print(soup.a.parents) # 找到a标签所有的祖先节点,父亲的父亲,父亲的父亲的父亲...
# 7. 兄弟节点
print(soup.a.next_sibling) # 下一个兄弟
print(soup.a.previous_sibling) # 上一个兄弟
print(list(soup.a.next_siblings)) # 下面的兄弟们=>生成器对象
print(soup.a.previous_siblings) # 上面的兄弟们=>生成器对象
2、搜索文档树
(1)五种过滤器
字符串、正则表达式、列表、True、方法
# 过滤器结合find() 和 find_all()方法使用查找元素
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p id="my p" class="title"><b id="bbb" class="boldest">The Dormouse's story</b>
</p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup=BeautifulSoup(html_doc,'lxml')
# 1.字符串
print(soup.find_all('b'))
# 2.、正则表达式
# 利用re.compile()使用正则
import re
print(soup.find_all(re.compile('^b'))) # 找出b开头的标签,结果有body和b标签
# 3.列表:
# 如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回.下面代码找到文档中所有<a>标签和<b>标签:
print(soup.find_all(['a','b']))
# 4.True
# 可以匹配任何值,下面代码查找到所有的tag,但是不会返回字符串节点
print(soup.find_all(True))
for tag in soup.find_all(True):
print(tag.name)
# 5.方法
# 如果没有合适过滤器,那么还可以定义一个方法,方法只接受一个元素参数 ,如果这个方法返回 True 表示当前元素匹配并且被找到,如果不是则反回 False
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
print(soup.find_all(has_class_but_no_id))
# 匿名函数
print(soup.find_all(lambda tag: True if tag.has_attr("class") and tag.has_attr("id") else False))
(2)find_all( name , attrs , recursive , text , **kwargs )
# 1、name: 搜索name参数的值可以使任一类型的 过滤器 ,字符窜,正则表达式,列表,方法或是 True .
print(soup.find_all(name=re.compile('^t')))
# 2、keyword: key=value的形式,value可以是过滤器:字符串 , 正则表达式 , 列表, True .
print(soup.find_all(id=re.compile('my')))
print(soup.find_all(href=re.compile('lacie'),id=re.compile('d'))) #注意类要用class_
print(soup.find_all(id=True)) # 查找有id属性的标签
# 有些tag属性在搜索不能使用,比如HTML5中的 data-* 属性:
data_soup = BeautifulSoup('<div data-foo="value">foo!</div>','lxml')
# data_soup.find_all(data-foo="value") #报错:SyntaxError: keyword can't be an expression
# 但是可以通过 find_all() 方法的 attrs 参数定义一个字典参数来搜索包含特殊属性的tag:
print(data_soup.find_all(attrs={"data-foo": "value"}))
# [<div data-foo="value">foo!</div>]
# 3、按照类名查找,注意关键字是class_,class_=value,value可以是五种选择器之一
print(soup.find_all('a',class_='sister')) #查找类为sister的a标签
print(soup.find_all('a',class_='sister ssss')) #查找类为sister和sss的a标签,顺序错误也匹配不成功
print(soup.find_all(class_=re.compile('^sis'))) #查找类为sister的所有标签
# 4、attrs
print(soup.find_all('p',attrs={'class':'story'}))
# 5、text: 值可以是:字符,列表,True,正则
print(soup.find_all(text='Elsie'))
print(soup.find_all('a',text='Elsie'))
# 6、limit参数:如果文档树很大那么搜索会很慢.如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量.效果与SQL中的limit关键字类似,当搜索到的结果数量达到 limit 的限制时,就停止搜索返回结果
print(soup.find_all('a',limit=2))
# 7、recursive:调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数 recursive=False
print(soup.html.find_all('a'))
print(soup.html.find_all('a',recursive=False))
(3)find( name , attrs , recursive , text , **kwargs )
唯一的区别是 find_all() 方法的返回结果是值包含一个元素的列表,而 find() 方法直接返回结果.
find_all() 方法没有找到目标是返回空列表, find() 方法找不到目标时,返回 None .
print(soup.find("nosuchtag"))
# None
soup.head.title 是 tag的名字 方法的简写.这个简写的原理就是多次调用当前tag的 find() 方法
(4)CSS选择器(select('.class'))
#该模块提供了select方法来支持css,详见官网:https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html#id37
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title">
<b>The Dormouse's story</b>
Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">
<span>Elsie</span>
</a>
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
<div class='panel-1'>
<ul class='list' id='list-1'>
<li class='element'>Foo</li>
<li class='element'>Bar</li>
<li class='element'>Jay</li>
</ul>
<ul class='list list-small' id='list-2'>
<li class='element'><h1 class='yyyy'>Foo</h1></li>
<li class='element xxx'>Bar</li>
<li class='element'>Jay</li>
</ul>
</div>
and they lived at the bottom of a well.
</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup=BeautifulSoup(html_doc,'lxml')
# 1. CSS选择器
# select 返回的是一个列表
print(soup.p.select('.sister'))
print(soup.select('.sister span'))
print(soup.select('#link1'))
print(soup.select('#link1 span'))
print(soup.select('#list-2 .element.xxx'))
print(soup.select('#list-2')[0].select('.element')) # 可以一直select,但其实没必要,一条select就可以了
# 2. 获取属性
print(soup.select('#list-2 h1')[0].attrs)
# 3. 获取内容
print(soup.select('#list-2 h1')[0].get_text())
版权归原作者 神秘的doge 所有, 如有侵权,请联系我们删除。