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())
複製代碼