0%

Pyppeteer-浏览器爬虫初探

pre:

最近打算捣鼓一个黑链扫描器.

当你打开浏览器去访问被挂了黑链的网站的时候,渲染执行完js代码后,就会跳转到恶意的网站.

那么这种情况应该选用什么爬虫的解决方案呢?即技术选型


技术选型:

方案一 静态爬虫:

在python中最常见的就是用requests,urllib2等库来模拟发包,执行简单的HTTP请求并获取HTML页面.

这种方案比较适合于对特定网站进行简单的爬取,优点是:

  • 在CPU和内存消耗方面的开销很低

  • 开发速度快

但是缺点也很明显:

  • 不通用:发包情况千千万,都要模拟请求的话不现实

  • 无法收集通过JavaScript动态生成的内容

基于以上两点,果断弃之.


方案二 动态爬虫:

选型1: 使用Selenium等库来驱动Chrome、Firefox或PhantomJS.

在Chrome的Headless模式刚出现不久,我们当时就调研过用作漏洞扫描器爬虫的需求,但由于当时功能不够完善,以及无法达到稳定可靠的要求。举个例子,对于网络请求,无法区分导航请求和其它请求,而本身又不提供navigation lock的功能,所以很难确保页面的处理不被意外跳转中断。同时,不太稳定的CDP经常意外中断和产生Chrome僵尸进程,所以我们之前一直在使用PhantomJS。

但随着前端的框架使用越来越多,网页内容对爬虫越来越不友好,在不考虑进行服务端渲染的情况下,Vue等框架让静态爬虫彻底失效。同时,由于JS的ES6语法的广泛使用,缺乏维护(创始人宣布归档项目暂停开发)的PhantomJS开始变的力不从心。

----漏扫动态爬虫实践

选型2: pyppeteer+Chromium headless

在去年,puppeteer和Chromium项目在经历了不断迭代后,新增了一些关键功能,Headless模式现在已经能大致胜任扫描器爬虫的任务。

所以我们在去年果断更新了扫描器的动态爬虫,采用Chromium的Headless模式作为网页内容解析引擎
----漏扫动态爬虫实践


总结:

  • Chrome的Headless模式早期不成熟

  • PhantomJS不再维护了

  • puppeteerChromium项目在不断迭代更新

综上,就决定使用pyppeteer+Chromium headless这个搭配方案了.

那么,现在就跑个Demo来试试水.(参考Pyppeteer, the snake charmer)


Demo:

原文章里的代码基本可以运行.
就是url和需要提取信息的xpath发送了一些变化.稍作修改即可


静态爬虫:

先用最原始的方式,确认能够获取得到信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/local/bin/python
# -*- coding:utf-8 -*-
# @Time : 2019/5/18 11:32 PM
# @Author : Jerry
# @Desc :
# @File : pyppeteer1.py


import pprint
import lxml.html
from urllib import request


def get_page(url):
return request.urlopen(url)


def read_document(response):
return response.read()


def extract_data(document):
# Generate document tree
tree = lxml.html.fromstring(document)
# Select tr with a th and td descendant from table
elements = tree.xpath('//*[@id="mw-content-text"]/div/table[1]/tbody/tr[th and td]')
# Extract data
result = {}
for element in elements:
th, td = element.iterchildren()
result.update({
th.text_content(): td.text_content()
})
return result


if __name__ == "__main__":
languages = {
"python": "https://en.wikipedia.org/wiki/Python_(programming_language)",
# "Rust": "https://es.wikipedia.org/wiki/Rust_(lenguaje_de_programaci%C3%B3n)",
# "Java": "https://es.wikipedia.org/wiki/Java_(lenguaje_de_programaci%C3%B3n)",
# "Javascript": "https://es.wikipedia.org/wiki/JavaScript"
}
result = {}
for name, url in languages.items():
response = get_page(url)
document = read_document(response)
result.update({name: extract_data(document)})

pprint.pprint(result)

预期结果:


puppeteer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/local/bin/python
# -*- coding:utf-8 -*-
# @Time : 2019/5/18 11:32 PM
# @Author : Jerry
# @Desc :
# @File : pyppeteer1.py

import pprint
import asyncio
from pyppeteer import launch


async def get_browser(): # 启动浏览器
return await launch({"headless": False})


async def get_page(browser, url):
page = await browser.newPage()
await page.goto(url)
return page


async def extract(browser, name, url):
page = await get_page(browser, url)
return {name: await extract_data(page)}


async def extract_data(page): # 数据提取
# Select tr with a th and td descendant from table
elements = await page.xpath(
'//*[@id="mw-content-text"]/div/table[1]/tbody/tr[th and td]')
# Extract data
result = {}
for element in elements:
title, content = await page.evaluate(
'''(element) =>
[...element.children].map(child => child.textContent)''',
element)
result.update({title: content})
return result


async def extract_all(languages): # 程序入口
browser = await get_browser() # 启动浏览器
result = {}
for name, url in languages.items():
result.update(await extract(browser, name, url))
return result


if __name__ == "__main__":
languages = {
"python": "https://en.wikipedia.org/wiki/Python_(programming_language)",

}

loop = asyncio.get_event_loop()
result = loop.run_until_complete(extract_all(languages))

pprint.pprint(result)

同样能够获取到和预期一样的结果


总结:

从Demo出发,再不断改进优化功能.
便能慢慢有些雏形出来了.
Keeo Moving.


refs: