爬蟲之多工非同步協程

冀未然發表於2024-03-26

gevent模組

示例程式碼:

特點: 可以識別所有阻塞

from gevent import monkey
monkey.patch_all()
import gevent
import requests
from lxml import etree
import time

# 傳送請求
def get_request(url):
    page_text = requests.get(url).text
    tree = etree.HTML(page_text)
    print(len(tree.xpath('//div[1]//text()')))
    return page_text

# 解析資料
def parse(page_text):
    tree = etree.HTML(page_text)
    print(tree.xpath('//div[1]//text()'))

start = time.time()
g1 = gevent.spawn(get_request, 'http://127.0.0.1:5000/index')
g2 = gevent.spawn(get_request, 'http://127.0.0.1:5000/hxbs')
gevent.joinall([g1, g2])
print(time.time() - start)

asyncio模組

安裝: pip install asyncio

特點: 只能識別支援非同步的模組的阻塞

協程物件

import asyncio
from time import sleep

# 特殊的函式: 如果一個函式的定義被async關鍵字修飾,則該函式是一個特殊函式
async def get_request(url):
    print('正在請求:', url)
    sleep(1)
    print('請求結束:', url)

# 特殊函式被呼叫後,函式內部的語句不會立即執行
# 特殊函式的呼叫返回一個協程物件
c = get_request('www.1.com')

# 結論1: 協程物件 == 特殊的函式

任務物件

任務物件其實就是對協程物件的進一步封裝,並且可以給任務物件繫結回撥

結論2: 任務物件 == 高階的協程物件 == 特殊的函式

# 定義回撥函式,接收的引數是任務物件,任務物件執行結束後執行
def parse(task):
    ret = task.result()  # 取得任務物件(特殊函式)的返回值
# 創一個任務物件: 基於協程物件建立
# task就是一個任務物件
task = asyncio.ensure_future(c)
# 繫結回撥函式
task.add_done_callback(parse)

事件迴圈物件

作用: 將其內部註冊的任務物件進行非同步執行

# 建立一個事件迴圈物件
loop = asyncio.get_event_loop()
# 將任務物件註冊到事件迴圈物件中並且開啟事件迴圈
loop.run_until_complete(task)

注意: 事件迴圈物件中註冊多個任務物件時,需要使用async.wait()對任務列表進行掛起操作

loop.run_until_complete(async.wait(task_list))

編碼流程:

  • 定義特殊函式
  • 建立協程物件
  • 封裝任務物件
  • 建立事件迴圈物件
  • 將任務物件註冊到事件迴圈物件中並且開啟事件迴圈
    注意: 在特殊函式內部的實現語句中,不可以出現不支援非同步的模組對應的程式碼,否則就會終止多工非同步協程的非同步效果

注意: reuqests模組不支援非同步,需要使用aiothttp模組進行爬取,此模組使用方法和requests高度相似,

示例程式碼:
import asyncio
import time

async def get_request(url):
    print('正在請求:', url)
    # time模組不支援非同步
    # time.sleep(1)
    # 需要使用await關鍵字對阻塞操作進行等待
    await asyncio.sleep(1)
    print('請求結束:', url)

urls = [
    'www.1.com',
    'www.2.com',
    'www.3.com'
]
task_list = []  # 存放多個任務物件的列表
for url in urls:
    c = get_request(url)
    task = asyncio.ensure_future(c)
    task_list.append(task)
# 建立一個事件迴圈物件
loop = asyncio.get_event_loop()
# 將任務物件註冊到事件迴圈物件中並且開啟事件迴圈
loop.run_until_complete(asyncio.wait(task_list))

aiohttp模組

安裝: pip install aiohttp

特點: 支援非同步的網路請求模組

編碼流程:

  • 寫基本架構:
# aiohttp建立的物件都需要關閉,使用with
with aiohttp.ClientSession() as cs:
    # 注意get/post: proxy = 'http://ip:port'
    # 其餘引數與requests模組一致
    with cs.get(url) as response:
        # text()字串形式的響應資料
        # read()二進位制的響應資料
        page_text = response.text()
        return  page_text
  • 補充細節:

新增async關鍵字: 每一個with前加上async

新增await關鍵字: 在每一步的阻塞操作前加上await

  • 請求(get(), post()等)
  • 獲取相應資料(text(), read())

asyncio + aiohttp 實現多工協程爬蟲:

簡易伺服器:

# 搭建簡易伺服器
from flask import Flask, render_template
from time import sleep
app = Flask(__name__)

@app.route('/hxbs')
def index_hxbs():
    sleep(2)
    return render_template('測試.html')

@app.route('/index')
def index_ceshi():
    sleep(2)
    return render_template('測試.html')

app.run(debug=True)

非同步爬蟲

# 非同步爬蟲
import asyncio
import aiohttp
import time
from lxml import etree

async def get_request(url):

    async with aiohttp.ClientSession() as cs:
        async with await cs.get(url) as response:
            page_text = await response.text()
            return page_text

def parse(task):
    page_text = task.result()
    tree = etree.HTML(page_text)
    data = tree.xpath('//div[2]//text()')[0]
    print(data)
urls = [
    'http://127.0.0.1:5000/hxbs',
    'http://127.0.0.1:5000/hxbs',
    'http://127.0.0.1:5000/index',
    'http://127.0.0.1:5000/index',
]
start = time.time()
task_list = []
for url in urls:
    c = get_request(url)  # 協程物件
    task = asyncio.ensure_future(c)  # 任務物件
    task.add_done_callback(parse)  # 繫結回撥,用於資料解析
    task_list.append(task)

loop = asyncio.get_event_loop()  # 事件迴圈物件
loop.run_until_complete(asyncio.wait(task_list))  # 註冊任務
print('總耗時:', time.time() - start)

selenium模組

概念: 基於瀏覽器自動化的模組,都用於自動化測試

特性: 不支援非同步,效率較低

selenium和爬蟲之間的關聯:

  • 便捷的爬取到動態載入的資料
    可見即可得
  • 便捷的實現模擬登入
    擴充: Appium基於手機的自動化的模組

基本使用:

  • 環境安裝: pip install selenium

  • 下載瀏覽器的驅動程式

# google瀏覽器驅動程式
http://chromedriver.storage.googleapis.com/index.html
# 驅動程式與瀏覽器版本對映關係
https://blog.csdn.net/huilan_same/article/details/51896672

演示程式碼:

from selenium import webdriver
from time import sleep

# 後面是你的瀏覽器驅動位置,記得前面加r'','r'是防止字元轉義的
driver = webdriver.Chrome(r'chromedriver.exe')
# 用get開啟百度頁面
driver.get("http://www.baidu.com")
# 查詢頁面的“設定”選項,並進行點選
driver.find_elements_by_link_text('設定')[0].click()
sleep(2)
# # 開啟設定後找到“搜尋設定”選項,設定為每頁顯示50條
driver.find_elements_by_link_text('搜尋設定')[0].click()
sleep(2)

# 選中每頁顯示50條
m = driver.find_element_by_id('nr')
sleep(2)
m.find_element_by_xpath('//*[@id="nr"]/option[3]').click()
m.find_element_by_xpath('.//option[3]').click()
sleep(2)

# 點選儲存設定
driver.find_elements_by_class_name("prefpanelgo")[0].click()
sleep(2)

# 處理彈出的警告頁面   確定accept() 和 取消dismiss()
driver.switch_to.alert.accept()
sleep(2)
# 找到百度的輸入框,並輸入 美女
driver.find_element_by_id('kw').send_keys('美女')
sleep(2)
# 點選搜尋按鈕
driver.find_element_by_id('su').click()
sleep(2)

# 關閉瀏覽器
driver.quit()

基本使用

from selenium import webdriver
import time
# 例項化某一款瀏覽器物件
browser = webdriver.Chrome(executable_path=r'chromedriver.exe')
# 基於瀏覽器發起請求
browser.get('https://www.jd.com')

# 商品搜尋
# 標籤定位
search_input = browser.find_element_by_id('key')
# 向定位到的標籤中錄入資料
search_input.send_keys('python')
# 點選搜尋按鈕
btn = browser.find_element_by_xpath('//*[@id="search"]/div/div[2]/button/i')
btn.click()

# 滾輪滑動(JS注入)
browser.execute_script('window.scrollTo(0, document.body.scrollHeight)')
time.sleep(2)

# 關閉瀏覽器
browser.quit()

爬取動態載入的資料

from selenium import webdriver
from lxml import etree
import time
# 例項化某一款瀏覽器物件
browser = webdriver.Chrome(executable_path=r'chromedriver.exe')
# 基於瀏覽器發起請求
browser.get('https://www.fjggfw.gov.cn/Website/JYXXNew.aspx')
# page_source: 當前頁面所有的頁面原始碼資料
page_text = browser.page_source
# 儲存前三頁對應的頁面原始碼資料
all_page_text = [page_text]

for i in range(2, 4):
    next_page_btn = browser.find_element_by_xpath(f'//body//a[@onclick="return kkpager._clickHandler({i})"]')
    next_page_btn.click()
    time.sleep(1)
    page_text = browser.page_source
    all_page_text.append(page_text)

for page_text in all_page_text:
    tree = etree.HTML(page_text)
    title = tree.xpath('//*[@id="list"]/div[1]/div/h4/a/text()')[0]
    print(title)

動作鏈

注意:

在使用find系列的函式進行標籤定位時,如果出現NoSuchElementException錯誤怎麼處理?

如果定位的標籤是存在於iframe標籤之下(子頁面)中,則在進行指定標籤定位的時,必須使用switch_to.frame(frame_id)的操作才可.

from selenium import webdriver
from selenium.webdriver import ActionChains
import time

# 例項化某一款瀏覽器物件
browser = webdriver.Chrome(executable_path=r'chromedriver.exe')
# 基於瀏覽器發起請求
browser.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

# NoSuchElementException: 定位標籤時出現此錯誤,考慮是否為子頁面內的標籤
browser.switch_to.frame('iframeResult')  # 引數是iframe標籤的id屬性值
div_tag = browser.find_element_by_id('draggable')

# 基於動作鏈實現滑動操作
# 例項化一個動作鏈物件: 指定瀏覽器
action = ActionChains(browser)
# 點選且長按
action.click_and_hold(div_tag)
for i in range(4):
    # perform()表示讓動作鏈立即執行
    action.move_by_offset(25, 0).perform()  # (x, y)
    time.sleep(0.5)

time.sleep(2)
browser.quit()

無頭瀏覽器

沒有視覺化介面的瀏覽器

  • phantomjs
  • 谷歌無頭瀏覽器(推薦使用)
from selenium import webdriver

from selenium.webdriver.chrome.options import Options
# 建立一個引數物件,用來控制chrome以無介面模式開啟
options = Options()
options.add_argument('--headless')
options.add_argument('--disable-gpu')

# 設定瀏覽器
browser = webdriver.Chrome(executable_path='chromedriver.exe', options=options)
browser.get('https://www.taobao.com/')
print(browser.page_source)

如何規避selenium被監測到的風險?

網站可以根據:window.navigator.webdriver的返回值鑑定是否使用了selenium

  • undefind:正常
  • true:使用了selenium
from selenium import webdriver
from time import sleep
from selenium.webdriver import ChromeOptions

option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])

# 後面是瀏覽器驅動位置,記得前面加r'','r'是防止字元轉義的
browser = webdriver.Chrome(r'chromedriver.exe',options=option)

browser.get('https://www.taobao.com/')

模擬登入12306

from ChaoJiYing import Chaojiying_Client
from selenium import webdriver
from selenium.webdriver import ActionChains
from PIL import Image  # 圖片操作模組
import time

def get_code(imgPath, imgType):
    chaojiying = Chaojiying_Client('賬號', '賬號', '901814')
    im = open(imgPath, 'rb').read()
    return chaojiying.PostPic(im, imgType)['pic_str']

browser = webdriver.Chrome(executable_path='chromedriver.exe')
browser.get('https://kyfw.12306.cn/otn/login/init')
time.sleep(2)

# 輸入使用者名稱和密碼
username_input = browser.find_element_by_id('username')
username_input.send_keys('賬號')
password_input = browser.find_element_by_id('password')
password_input.send_keys('賬號')
# 截圖
browser.save_screenshot('main.png')
# 在main.png中擷取下驗證碼圖片
img_tag = browser.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img')
# 標籤的大小{'height': 190, 'width': 293}
size = img_tag.size
# 標籤左下角的座標{'x': 276, 'y': 274}
location = img_tag.location
# 裁剪範圍
range_xy = (
    int(location['x']), int(location['y']), int(location['x'] + size['width']), int(location['y'] + size['height']))
# 必須是png格式
main_png = Image.open(r'main.png')
code_png = main_png.crop(range_xy)
code_png.save('code.png')
# 105,167|105,267|125,167
code = get_code('./code.png', 9004)
# [[105, 167], [105, 267], [125, 167]]
all_list = []
if '|' in code:
    list_1 = code.split('|')
    count_1 = len(list_1)
    for i in range(count_1):
        xy_list = []
        x = int(list_1[i].split(',')[0])
        y = int(list_1[i].split(',')[1])
        xy_list.append(x)
        xy_list.append(y)
        all_list.append(xy_list)
else:
    x = int(code.split(',')[0])
    y = int(code.split(',')[1])
    xy_list = []
    xy_list.append(x)
    xy_list.append(y)
    all_list.append(xy_list)

for x, y in all_list:
    # move_to_element_with_offset以標籤的左下角為起點進行偏移
    ActionChains(browser).move_to_element_with_offset(img_tag, x, y).click().perform()
    time.sleep(1)

# 點選登入
loginSub = browser.find_element_by_id('loginSub')
loginSub.click()

相關文章