Puppeteer(中文翻译”操纵木偶的人”) 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个 Node 库,提供了一个高级的 API 来控制 DevTools协议上的无头版 Chrome 。也能够配置为使用完整(非无头)的 Chrome。Chrome 素来在浏览器界稳执牛耳,所以,Chrome Headless 必将成为 web 应用自动化测试的行业标杆。使用 Puppeteer,至关于同时具备 Linux 和 Chrome 双端的操做能力,应用场景可谓很是之多。此仓库的创建,便是尝试各类折腾使用 GoogleChrome Puppeteer;以期在好玩的同时,学到更多有意思的操做。
而pyppeteer 是对无头浏览器 puppeteer的 Python 封装,可让你使用python来操做Chrome。css
Pyppeteer的GIT
Pyppeteer官方文档html
close()
命令没法真正的关闭浏览器,会形成不少的僵尸进程pyppeteer.errors.NetworkError: Protocol error Network.getCookies: Target close
python3 -m pip install pyppeteer
复制代码
在初次使用pyppeteer的时候他会自动下载chromium(看心情,大部分状况下能够用龟速形容),或者直接去官网下载最新版的浏览器而后在代码中指定浏览器的路径。 chromium下载地址python
import asynciofrom pyppeteer import launch
async def main():
# 建立一个浏览器
browser = await launch({
'executablePath': '你下载的Chromium.app/Contents/MacOS/Chromium',
})
# 打开一个页面,同一个browser能够打开多个页面
page = await browser.newPage()
await page.goto('https://baidu.com') # 访问指定页面
await page.screenshot(path='example.png') # 截图
await page.close() # 关闭页面
await browser.close() # 关闭浏览器(实测中发现打开多个页面会产生大量僵尸进程)
asyncio.get_event_loop().run_until_complete(main())
复制代码
运行上面这一段代码会产生一张页面截图,若是在运行中报错pyppeteer.errors.NetworkError: Protocol error Network.getCookies: Target close
能够经过下降websockets 版原本解决mysql
pip uninstall websockets #卸载websockets
pip install websockets==6.0
或者
pip install websockets==6.0 --force-reinstall #指定安装6.0版本
复制代码
import asynciofrom pyppeteer import launch
async def intercept_request(req):
# 不加载css和img等资源
if req.resourceType in ["image", "media", "eventsource", "websocket", "stylesheet", "font"]:
await req.abort() #链接请求
else:
res = {
"method": req.method,
"url": req.url,
"data": "" if req.postData == None else req.postData,
"res": "" if req.response == None else req.response
}
print(res) # 打印请求的内容
await req.continue_() #继续请求,能够添加参数将请求地址重定向、改变请求的headers
async def intercept_response(res):
resourceType = res.request.resourceType
# 拦截ajax请求获取数据
if resourceType in ['xhr']:
resp = await res.json()
print(resp)# 这里能够操做mysql、redis或者设计一个class来保存数据
async def main():
# 建立一个浏览器
browser = await launch({
'executablePath': '你下载的Chromium.app/Contents/MacOS/Chromium',
'headless': False, # 关闭无头模式。主要在测试环境调试使用
'devtools': True, # 打开 chromium 的 devtools与headless配个使用
'args': [
'--disable-extensions',
'--hide-scrollbars',
'--disable-bundled-ppapi-flash',
'--mute-audio',
'--no-sandbox',# --no-sandbox 在 docker 里使用时须要加入的参数,否则会报错
'--disable-setuid-sandbox',
'--disable-gpu',
],
'dumpio': True, #把无头浏览器进程的 stderr 核 stdout pip 到主程序,也就是设置为 True 的话,chromium console 的输出就会在主程序中被打印出来
})
# 打开一个页面,同一个browser能够打开多个页面
page = await browser.newPage()
# 是否启用JS,enabled设为False,则无渲染效果,若是页面有ajax请求须要开启此项
await page.setJavaScriptEnabled(enabled=True)
# 是否容许拦截请求,若是开启能够注册的两个回调函数,在浏览器发出请求和获取到请求以前指向这两个函数。
await page.setRequestInterception(value=True)
page.on('request', intercept_request) # 请求的内容
page.on('response', intercept_response) # 响应的内容
await page.goto('https://baidu.com') # 访问指定页面
await page.screenshot(path='example.png') # 截图
await page.close() # 关闭页面
await browser.close() # 关闭浏览器(实测中发现打开多个页面会产生大量僵尸进程)
asyncio.get_event_loop().run_until_complete(main())
复制代码
当一个父进程以fork()系统调用创建一个新的子进程后,核心进程就会在进程表中给这个子进程分配一个进入点,而后将相关信息存储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。 而当这个子进程结束的时候(好比调用exit命令结束),其实他并无真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit的做用是使进程退出,可是也仅仅限于一个正常的进程变成了一个僵尸进程,并不能彻底将其销毁)。此时原来进程表中的数据会被该进程的退出码(exit code)、执行时所用的CPU时间等数据所取代,这些数据会一直保留到系统将它传递给它的父进程为止。因而可知,defunct进程的出现时间是在子进程终止后,可是父进程还没有读取这些数据以前。
此时,该僵尸子进程已经放弃了几乎全部的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态信息供其余进程收集,除此以外,僵尸进程再也不占有任何存储空间。他须要他的父进程来为他收尸,若是他的父进程没有安装SIGCHLD信号处理函数调用wait 或 waitpid() 等待子进程结束,也没有显式忽略该信号,那么它就一直保持僵尸状态,若是这时候父进程结束了,那么init进程会自动接手这个子进程,为他收尸,他仍是能被清除掉的。 拿Nginx做为例子,默认是做为后台守护进程。它是这么工做的。第一,Nginx建立一个子进程。第二,原始的Nginx进程退出了。第三,Nginx子进程被init进程给接收了。 linux
CMD ["/bin/bash", "-c", "set -e && 你的任务脚本"]
复制代码
可是这种方法也有问题,不能优雅的结束进程。假设你用kill发送SIGTERM信号给bash.Bash终止了,可是没有发送SIGTERM给它的子进程! 当bash结束了,内核结束整个容器中的全部进程。包扩经过SIGKILL信号没有被干净的终结的进程。SIGKILL不能被捕获,因此进程是没有办法干净的终结。假设你运行的应用程序正忙于写文件;在写的过程当中,应用被不干净的终止了这个文件可能会崩溃。不干净的终止是很坏的事情。很像把服务器的电源给拔掉。 可是为何要关心init进程是否被SIGTERM给终结了呢?那是由于docker stop 发送 SIGTERM信号给init进程了。“docker stop” 应该干净的中止容器,以致于稍后你可以用“docker start”启动它。git
ps -ef | grep defunct_process_pid
/bin/bash
用来给僵尸进程收尸dockerfile文件github
FROM centos:7
RUN set -ex \ # 预安装所需组件 && yum install -y wget tar libffi-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make initscripts \
&& wget https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tgz \
&& tar -zxvf Python-3.6.0.tgz \
&& cd Python-3.6.0 \
&& ./configure prefix=/usr/local/python3 \
&& make \
&& make install \
&& make clean \
&& rm -rf /Python-3.6.0* \
&& yum install -y epel-release \
&& yum install -y python-pip
# 设置默认为python3
RUN set -ex \ # 备份旧版本python && mv /usr/bin/python /usr/bin/python27 \
&& mv /usr/bin/pip /usr/bin/pip-python2.7 \
# 配置默认为python3
&& ln -s /usr/local/python3/bin/python3.6 /usr/bin/python \
&& ln -s /usr/local/python3/bin/pip3 /usr/bin/pip
# 修复因修改python版本致使yum失效问题
RUN set -ex \ && sed -i "s#/usr/bin/python#/usr/bin/python2.7#" /usr/bin/yum \ && sed -i "s#/usr/bin/python#/usr/bin/python2.7#" /usr/libexec/urlgrabber-ext-down \ && yum install -y deltarpm # 基础环境配置
RUN set -ex \ # 修改系统时区为东八区 && rm -rf /etc/localtime \
&& ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& yum install -y vim \
# 安装定时任务组件
&& yum -y install cronie
# 支持中文
RUN localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 # chrome浏览器依赖
RUN yum install kde-l10n-Chinese -y RUN yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y RUN yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y # 更新pip版本
RUN pip install --upgrade pip ENV LC_ALL zh_CN.UTF-8
RUN mkdir -p /usr/src/scrapy COPY requirements.txt /usr/src/scrapy RUN pip install -i https://pypi.douban.com/simple/ -r /usr/src/scrapy/requirements.txt 复制代码
docker-compose文件web
version: '3.3'
services:
scrapy:
privileged: true
build: scrapy
tty: true
volumes:
- type: bind
source: /爬虫文件路径
target: /usr/src/scrapy
ports:
- "9999:9999"
networks:
scrapynet:
ipv4_address: 172.19.0.8
command: [/bin/bash, -c, set -e && python /usr/src/scrapy/job.py]
networks:
scrapynet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.19.0.0/24
复制代码
command: [/bin/bash, -c, set -e && python /usr/src/scrapy/job.py]
命令解释ajax
import asyncio,random,psutil,os,signal,time
from pyppeteer import launcher
# hook 禁用 防止监测webdriver
launcher.AUTOMATION_ARGS.remove("--enable-automation")
from pyppeteer import launch
async def intercept_request(req):
if req.resourceType in ["image"]:
await req.abort()
else:
res = {
"method": req.method,
"url": req.url,
"data": "" if req.postData == None else req.postData,
"res": "" if req.response == None else req.response
}
print(res)
await req.continue_()
async def intercept_response(res):
resourceType = res.request.resourceType
if resourceType in ['xhr']:
resp = await res.json()
print(resp)
class newpage(object):
width, height = 1920, 1080
def __init__(self, page_url,chrome_browser):
self.url = page_url
self.browser = chrome_browser
async def run(self):
t = random.randint(1, 4)
tt = random.randint(t, 10)
await asyncio.sleep(tt)
try:
page = await self.browser.newPage()
await page.setUserAgent(
userAgent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/70.0.3521.2 Safari/537.36')
await page.setViewport(viewport={'width': self.width, 'height': self.height})
# 是否启用JS,enabled设为False,则无渲染效果
await page.setJavaScriptEnabled(enabled=True)
await page.setRequestInterception(value=True)
page.on('request', intercept_request)
page.on('response', intercept_response)
await page.goto(self.url, options={'timeout': 30000})
await page.waitFor(selectorOrFunctionOrTimeout=1000)
try:
await page.close()
return self.url
except BaseException as err:
return "close_newpage: {0}".format(err)
except BaseException as err:
return "newpage: {0}".format(err)
class Browser(object):
width, height = 1920, 1080
browser = None
is_headless = True
url_list = []
def __init__(self,urls):
self.url_list = urls
# 封装了kill()方法杀死chrome主进程,让init 1进程接管其僵尸子进程处理僵尸进程
def kill(self,name):
# win平台
# subprocess.Popen("taskkill /F /IM chrome.EXE ", shell=True)
# linux平台
try:
pid = self.browser.process.pid
pgid = os.getpgid(pid)
# 强制结束
os.kill(pid, signal.SIGKILL)
print("结束进程:%d" % pid)
print("父进程是:%d" % pgid)
print("等待结果:%d" % self.browser.process.wait())
except BaseException as err:
print("close: {0}".format(err))
time.sleep(3)
# 查看是否还有其余进程
for proc in psutil.process_iter():
if name in proc.name():
try:
os.kill(proc.pid, signal.SIGTERM)
print('已杀死[pid:%s]的进程[pgid:%s][名称:%s]' % (proc.pid,pgid,proc.name()))
except BaseException as err:
print("kill: {0}".format(err))
# 打开浏览器
async def newbrowser(self):
try:
self.browser = await launch({
'headless': self.is_headless,
'devtools': not self.is_headless,
'dumpio': True,
'autoClose': True,
# 'userDataDir': './userdata',
'handleSIGTERM': True,
'handleSIGHUP': True,
# 'executablePath':'C:/Users/zhang/Desktop/chrome-win/chrome.exe',
'args': [
'--no-sandbox', # --no-sandbox 在 docker 里使用时须要加入的参数,否则会报错
'--disable-gpu',
'--disable-extensions',
'--hide-scrollbars',
'--disable-bundled-ppapi-flash',
'--mute-audio',
'--disable-setuid-sandbox',
'--disable-xss-auditor',
'--window-size=%d,%d' % (self.width, self.height)
]
})
except BaseException as err:
print("launch: {0}".format(err))
print('----打开浏览器----')
async def open(self):
await self.newbrowser()
try:
tasks = [asyncio.ensure_future(newpage(url,self.browser).run()) for url in self.url_list]
for task in asyncio.as_completed(tasks):
result = await task
print('Task ret: {}'.format(result))
except BaseException as err:
print("open: {0}".format(err))
# browser.close()方法没法完全退出chrome进程,这里咱们本身封装了kill()方法杀死chrome主进程,让init 1进程接管其僵尸子进程
# await self.browser.close()
def main(self):
loop = asyncio.get_event_loop()
loop.run_until_complete(self.open())
print('----关闭浏览器----')
self.kill('chrom')
if __name__ == '__main__':
url_list=[
'https://www.baidu.com/',
'https://www.baidu.com/',
'https://www.baidu.com/',
'https://www.baidu.com/',
]
while True:
# 不停的添加任务
o = Browser(url_list)
print(o.main())
复制代码