现在大多数人在使用模拟浏览器进行数据获取的时候, 用的都是Selenium .以至于现在很多网站已经对它做了很多针对性的反爬(比如检测浏览器的webdriver属性). 而Pyppeteer 作为一个新的工具在绕过这些反爬措施中表现的很好. 本文借鉴了部分'原创: 崔庆才 进击的Coder, 别只用 Selenium,新神器 Pyppeteer 绕过淘宝更简单!' 的内容. 所以很多细节的东西就不说了, 主要记录一下在使用Pyppeteer 时遇到的一些问题和处理方法.
附上Pyppeteer的文档地址: https://miyakogi.github.io/pyppeteer/#
大家可能接触过Puppeteer, Puppeteer 是 Google 基于 Node.js 开发的一个工具,有了它我们可以通过 JavaScript 来控制 Chrome 浏览器的一些操作,当然也可以用作网络爬虫上,其 API 极其完善,功能非常强大。 而 Pyppeteer 又是什么呢?它实际上是 Puppeteer 的 Python 版本的实现,但他不是 Google 开发的,是一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。
首先就是安装问题了,由于 Pyppeteer 采用了 Python 的 async 机制,所以其运行要求的 Python 版本为 3.5 及以上。
安装方式非常简单:
pip3 install pyppeteer
好了,安装完成之后我们命令行下测试下:
>>> import pyppeteer
如果没有报错,那么就证明安装成功了。
python -c 'import pyppeteer; pyppeteer.chromium_downloader.download_chromium()' 直接下载pyppeteer
import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
async def main():
browser = await launch()
page = await browser.newPage()
await page.goto('http://quotes.toscrape.com/js/')
doc = pq(await page.content())
print('Quotes:', doc('.quote').length)
await browser.close()
asyncio.get_event_loop().run_until_complete(main())
那么这里面的过程发生了什么?
实际上,Pyppeteer 整个流程就完成了浏览器的开启、新建页面、页面加载等操作。另外 Pyppeteer 里面进行了异步操作,所以需要配合 async/await 关键词来实现。
首先, launch 方法会新建一个 Browser 对象,然后赋值给 browser,然后调用 newPage 方法相当于浏览器中新建了一个选项卡,同时新建了一个 Page 对象。然后 Page 对象调用了 goto 方法就相当于在浏览器中输入了这个 URL,浏览器跳转到了对应的页面进行加载,加载完成之后再调用 content 方法,返回当前浏览器页面的源代码。然后进一步地,我们用 pyquery 进行同样地解析,就可以得到 JavaScript 渲染的结果了。
另外其他的一些方法如调用 asyncio 的 get_event_loop 等方法的相关操作则属于 Python 异步 async 相关的内容了,大家如果不熟悉可以了解下 Python 的 async/await 的相关知识。
好,通过上面的代码,我们就可以完成 JavaScript 渲染页面的爬取了。
class GetJsEncryptPage():
def __init__(self):
self.loop = asyncio.get_event_loop()
self.log = ICrawlerLog('spider').save
async def main(self, url, ): # 定义main协程函数,
# 以下使用await 可以针对耗时的操作进行挂起
browser = await launch({'headless': True, 'args': ['--no-sandbox', '--disable-infobars',
# '--proxy-server={}'.format(get_ip()),
],}) # 启动pyppeteer 属于内存中实现交互的模拟器
page = await browser.newPage() # 启动个新的浏览器页面标签
await page.setUserAgent("Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36")
cookies = {}
try:
await page.goto(url) # 访问页面
# 始终让window.navigator.webdriver=false
# navigator是windiw对象的一个属性,同时修改plugins,languages,navigator 且让
# await page.setJavaScriptEnabled(enabled=True) # 使用 JS 渲染
await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''') # 以下为插入中间js,将淘宝会为了检测浏览器而调用的js修改其结果。
await page.evaluate('''() =>{ window.navigator.chrome = { runtime: {}, }; }''')
await page.evaluate('''() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }''')
await page.evaluate('''() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }''')
await page.goto(url) # 访问页面
# content = await page.content() # 获取页面内容
await asyncio.sleep(2)
except:
await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''')
await page.evaluate('''() =>{ window.navigator.chrome = { runtime: {}, }; }''')
await page.evaluate('''() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }''')
await page.evaluate('''() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }''')
# await page.evaluate('window.open("{}");'.format(url))
await page.evaluate('window.location="{}";'.format(url))
# await page.goto(url) # 访问登录页面
try:
cookies = await self.get_cookie(page)
except Exception as e:
await browser.close()
finally:
await browser.close()
return cookies
async def get_cookie(self, page):
# res = await page.content()
cookies_list = await page.cookies()
cookies = {}
for cookie in cookies_list:
cookies[cookie.get('name')] = cookie.get('value')
return cookies
def retry_if_result_none(self, result):
return result is None
def input_time_random(self, ):
return random.randint(100, 151)
def work(self, url):
pass
def run(self, url, func):
result = {}
try:
# task = asyncio.wait([])
result = self.loop.run_until_complete(func(url)) # 将协程注册到事件循环,并启动事件循环
except Exception as e:
self.log.info('协程被动结束, chrome关闭')
for task in asyncio.Task.all_tasks():
task.cancel()
self.loop.stop()
self.loop.run_forever()
# self.loop.close()
return result
async def goto(self, page, url):
while True:
try:
await page.goto(url, {'timeout': 0, 'waitUntil': 'networkidle0'})
break
except (pyppeteer.errors.NetworkError, pyppeteer.errors.PageError) as ex:
# 无网络'net::ERR_INTERNET_DISCONNECTED','net::ERR_TUNNEL_CONNECTION_FAILED'
if 'net::' in str(ex):
await asyncio.sleep(10)
else:
raise
修改浏览器属性
async def change_status(self, page):
await page.evaluate('''() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }''')
await page.evaluate('''() =>{ window.navigator.chrome = { runtime: {}, }; }''')
await page.evaluate('''() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }''')
await page.evaluate('''() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }''')
----------------------------------------
执行JS
await page.evaluate('''() =>{ return 'python' } }) }''')
page.on(event, function) ,指定监听事件, 与处理函数
例如: page.on('request', intercept_response)
# 请求处理函数
async def request_check(req):
'''请求过滤'''
if req.resourceType in ['image', 'media', 'eventsource', 'websocket']:
await req.abort()
else:
await req.continue_()
# 响应处理函数
async def intercept_response(res):
resourceType = res.request.resourceType
if resourceType in ['image', 'media']:
resp = await res.text()
print(resp)
# 依据Xpath 进行网页截图
async def get_picture(page, xpath):
# 进行截图
picture = ''
try:
for _ in range(6):
tdContent = await page.xpath(xpath)
clip = await tdContent[0].boundingBox()
picture = base64.b64encode(await page.screenshot({
'path': './dashboard_shot.png', # 图片路径, 不指定就不保存
'clip': clip, # 指定图片位置,大小
# 'encoding': 'base64', # 返回的图片格式, 默认二进制
}))
if picture != '':
break
except Exception as e:
self.log.info('截图获取失败')
return picture
browser = await launch({'headless': True, 'timeout': 500, 'args': ['--disable-extensions',
'--hide-scrollbars',
'--disable-bundled-ppapi-flash',
'--mute-audio',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu',
'--proxy-server={}'.format(get_ip()),
], })
page = await browser.newPage()
await page.setUserAgent("Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36")
await page.setViewport({'width': 1000,'height': 3480,})
执行JS, 返回图片的二进制的Base64编码, 参照: https://www.w3ctech.com/topic/767
'''
() => {
var img = document.getElementById("%s");
var canvas = document.createElement("canvas");
canvas.width = %s;
canvas.height = %s;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var dataURL = canvas.toDataURL("image/png");
return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");}''' % (id, width, height)
在Pyppeteer中每一个标签页就是一个page对象, 切换page对象就是切换标签页
for _page in await browser.pages() :
if _page != page:
await _page.close()