pyppeteer進階技巧


記錄一下在使用pyppeteer過程中慢慢發現的一些稍微高級一點的用法。

一、攔截器簡單用法

攔截器作用於單個Page,即瀏覽器中的一個標簽頁。每初始化一個Page都要添加一下攔截器。攔截器實際上是

通過給各種事件添加回調函數來實現的。

事件列表可參見:pyppeteer.page.Page.Events

常用攔截器:

    • request:發出網絡請求時觸發
    • response:收到網絡響應時觸發
    • dialog:頁面有彈窗時觸發

使用request攔截器修改請求:

# coding:utf8
import asyncio
from pyppeteer import launch

from pyppeteer.network_manager import Request


launch_args = {
    "headless": False,
    "args": [
        "--start-maximized",
        "--no-sandbox",
        "--disable-infobars",
        "--ignore-certificate-errors",
        "--log-level=3",
        "--enable-extensions",
        "--window-size=1920,1080",
        "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36",
    ],
}


async def modify_url(request: Request):
    if request.url == "https://www.baidu.com/":
        await request.continue_({"url": "https://www.baidu.com/s?wd=ip&ie=utf-8"})
    else:
        await request.continue_()


async def interception_test():
    # 啟動瀏覽器
    browser = await launch(**launch_args)
    # 新建標簽頁
    page = await browser.newPage()
    # 設置頁面打開超時時間
    page.setDefaultNavigationTimeout(10 * 1000)
    # 設置窗口大小
    await page.setViewport({"width": 1920, "height": 1040})

    # 啟用攔截器
    await page.setRequestInterception(True)

    # 設置攔截器
    # 1. 修改請求的url
    if 1:
        page.on("request", modify_url)
        await page.goto("https://www.baidu.com")

    await asyncio.sleep(10)

    # 關閉瀏覽器
    await page.close()
    await browser.close()
    return


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(interception_test())

使用response攔截器獲取某個請求的響應:

async def get_content(response: Response):
    """
        # 注意這里不需要設置 page.setRequestInterception(True)
        page.on("response", get_content)
    :param response:
    :return:
    """
    if response.url == "https://www.baidu.com/":
        content = await response.text()
        title = re.search(b"<title>(.*?)</title>", content)
        print(title.group(1))

干掉頁面所有彈窗:

async def handle_dialog(dialog: Dialog):
    """
        page.on("dialog", get_content)
    :param dialog: 
    :return: 
    """
    await dialog.dismiss()

 

二、攔截器實現切換代理

一般情況下瀏覽器添加代理的方法為設置啟動參數:

--proxy-server=http://user:password@ip:port

例如:

launch_args = {
    "headless": False,
    "args": [
"--proxy-server=http://localhost:1080", "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", ], }

但此種方式的缺點很明顯,只能在瀏覽器啟動時設置。當需要切換代理時,只能重啟瀏覽器,這個代價

就太高了,所以我們可以想想其他辦法。

思路很簡單:

  1. request攔截器可以修改請求屬性並且返回自定義響應內容
  2. 使用第三方庫來發送網絡請求,並設置代理。然后封裝響應內容返回給瀏覽器

上代碼:

import aiohttp

aiohttp_session = aiohttp.ClientSession(loop=asyncio.get_event_loop())

proxy = "http://127.0.0.1:1080"
async def use_proxy_base(request: Request):
    """
        # 啟用攔截器
        await page.setRequestInterception(True)
        page.on("request", use_proxy_base)
    :param request:
    :return:
    """
    # 構造請求並添加代理
    req = {
        "headers": request.headers,
        "data": request.postData,
        "proxy": proxy,  # 使用全局變量 則可隨意切換
        "timeout": 5,
        "ssl": False,
    }
    try:
        # 使用第三方庫獲取響應
        async with aiohttp_session.request(
            method=request.method, url=request.url, **req
        ) as response:
            body = await response.read()
    except Exception as e:
        await request.abort()
        return

    # 數據返回給瀏覽器
    resp = {"body": body, "headers": response.headers, "status": response.status}
    await request.respond(resp)
    return

 或者再增加一些緩存來節約一下帶寬:

# 靜態資源緩存
static_cache = {}
async
def use_proxy_and_cache(request: Request): """ # 啟用攔截器 await page.setRequestInterception(True) page.on("request", use_proxy_base) :param request: :return: """ global static_cache if request.url not in static_cache: # 構造請求並添加代理 req = { "headers": request.headers, "data": request.postData, "proxy": proxy, # 使用全局變量 則可隨意切換 "timeout": 5, "ssl": False, } try: # 使用第三方庫獲取響應 async with aiohttp_session.request( method=request.method, url=request.url, **req ) as response: body = await response.read() except Exception as e: await request.abort() return # 數據返回給瀏覽器 resp = {"body": body, "headers": response.headers, "status": response.status} # 判斷數據類型 如果是靜態文件則緩存起來 content_type = response.headers.get("Content-Type") if content_type and ("javascript" in content_type or "/css" in content_type): static_cache[request.url] = resp else: resp = static_cache[request.url] await request.respond(resp) return

 

三、反反爬蟲 

使用pyppeteer來模擬瀏覽器進行爬蟲行動,我們的本意是偽裝自己,讓目標網站認為我是一個真實的人,然而

總有一些很蛋疼的東西會暴露自己。比如當你使用我上面的配置去模擬淘寶登錄的時候,會發現怎么都登錄不上。因

為瀏覽器的navigator.webdriver屬性暴露了你的身份。在正常瀏覽器中,這個屬性是沒有的。但是當你使用pyppeteer

或者selenium時,默認情況下這個參數就會設置為true。

去除這個屬性有兩種方式。

先說簡單的,pyppeteer的啟動參數中,默認會增加一個:--enable-automation

去掉方式如下: 在導入launch之前先把默認參數改了

from pyppeteer import launcher
# hook  禁用 防止監測webdriver
launcher.AUTOMATION_ARGS.remove("--enable-automation")
from pyppeteer import launch

還有個稍微復雜點的方式,就是利用攔截器來實現注入JS代碼。

JS代碼參見:

  https://github.com/dytttf/little_spider/blob/master/pyppeteer/pass_webdriver.js

攔截器代碼:

async def pass_webdriver(request: Request):
    """
        # 啟用攔截器
        await page.setRequestInterception(True)
        page.on("request", use_proxy_base)
    :param request:
    :return:
    """
    # 構造請求並添加代理
    req = {
        "headers": request.headers,
        "data": request.postData,
        "proxy": proxy,  # 使用全局變量 則可隨意切換
        "timeout": 5,
        "ssl": False,
    }
    try:
        # 使用第三方庫獲取響應
        async with aiohttp_session.request(
            method=request.method, url=request.url, **req
        ) as response:
            body = await response.read()
    except Exception as e:
        await request.abort()
        return

    if request.url == "https://www.baidu.com/":
        with open("pass_webdriver.js") as f:
            js = f.read()
        # 在html源碼頭部添加js代碼 修改navigator屬性
        body = body.replace(b"<title>", b"<script>%s</script><title>" % js.encode())

    # 數據返回給瀏覽器
    resp = {"body": body, "headers": response.headers, "status": response.status}
    await request.respond(resp)
    return

這個功能pyppeteer是有專門的函數來做這件事情的:

pyppeteer.page.Page.evaluateOnNewDocument

BUT,這個函數實現的有問題,總是不起作用 。而與之對比,如果你用的是nodejs的puppeteer的話,這個函數

是生效的。 

 

四、使用Xvfb配合實現headless效果

之所以用pyppeteer,很大程度上是為了使用chromium的無頭headless模式。無頭更省資源,限制也少。然而現

實很殘酷,特別是對爬蟲。

類似於navigator.webdriver這樣的東西可以用來檢測是否是機器人。還有更多的手段可以來檢測是否是headless。

比如:headless模式下沒有window.chrome屬性。具體我就不列了,反正好多。可以參見文后鏈接。關於如何偽裝

headless模式,使其不被探測到,網上資料也有很多,也很有用。但是,這個東西細節太多了。。。。。。還得看目

標網站工程師的心情和實力。如果對方有大把時間去檢測各種邊邊角角的東西,不斷提升代碼的混淆程度,死磕到底

的話,就有點得不償失了。

於是,我在死磕了攜程酒店三天后,幡然醒悟。(有興趣的可以嘗試一下,看如何在無頭模式下爬取攜程酒店數據)

既然無頭這么難搞,就不搞了。直接使用Xvfb來實現虛擬顯示器,這樣就變成有頭的了:)。

問題解決。

 

文內Python代碼見:

https://github.com/dytttf/little_spider/blob/master/pyppeteer/use_case.py

 

參考文獻:

無頭瀏覽器相關

MAKING CHROME HEADLESS UNDETECTABLE

Detecting Chrome headless, new techniques

Xvfb

https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml

https://blog.csdn.net/Nobody_Wang/article/details/60887659

https://stackoverflow.com/questions/57298901/unable-to-hide-chrome-is-being-controlled-by-automated-software-infobar-within

 
 
 
 
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM