python高級爬蟲筆記(2)

提高爬蟲效率主要從三個方面開始複習。

  1. 併發
  2. ip
  3. cookies

併發必然引發的一個結果就是反爬蟲機制,這種時候爬蟲的效率不會因爲併發而提高,反而會因爲網站的防禦機制拖累爬蟲的速度。

自然而然地就引出了2,代理爬蟲。代理爬蟲能夠從多個ip發送請求,減小了單個ip的請求頻率,自然觸發反爬蟲機制的概率也就小了很多。

但是新的問題又出現了,對於需要 登錄 的網站,需要提交cookies來模擬登錄情況,模擬登錄不難,但是同一個cookies從不同的ip同時發送請求很明顯不合常理,依然會觸發反爬蟲機制。

這是到目前爲止我所遇到的影響爬蟲效率的問題,就在這裏做一個總結吧,如果後續遇到新的效率相關的問題,再做補充。

併發

前言

在2019年,我閱讀了python cookbook,其中對這一方面有較爲詳細且透徹的講述,比較適合有python基礎的人學習。
多進程、多線程是python程序員的必修課之一。因爲,即使脫離了爬蟲,機器學習、web開發等方面,多線程、多進程依舊有着舉足輕重的地位。
這是開發者的一個小分水嶺,它在一定程度上決定了程序效率的高低。

python中的多進程方法

多線程、多進程、協程爬蟲

對於操作系統來說,一個任務就是一個進程(Process),比如打開一個瀏覽器就是啓動一個瀏覽器進程,打開一個記事本就啓動了一個記事本進程,打開兩個記事本就啓動了兩個記事本進程,打開一個Word就啓動了一個Word進程。

有些進程還不止同時幹一件事,比如Word,它可以同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就需要同時運行多個“子任務”,我們把進程內的這些“子任務”稱爲線程(Thread)。

進程、線程、協程的區別

多進程模式最大的優點就是穩定性高,因爲一個子進程崩潰了,不會影響主進程和其他子進程。(當然主進程掛了所有進程就全掛了,但是Master進程只負責分配任務,掛掉的概率低)著名的Apache最早就是採用多進程模式。

多進程模式的缺點是創建進程的代價大,在Unix/Linux系統下,用fork調用還行,在Windows下創建進程開銷巨大。另外,操作系統能同時運行的進程數也是有限的,在內存和CPU的限制下,如果有幾千個進程同時運行,操作系統連調度都會成問題。

多線程模式通常比多進程快一點,但是也快不到哪去,而且,多線程模式致命的缺點就是任何一個線程掛掉都可能直接造成整個進程崩潰,因爲所有線程共享進程的內存。

協程的優勢:

最大的優勢就是協程 極高的執行效率 。因爲子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。

第二大優勢就是 不需要多線程的鎖機制 ,因爲只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

多進程,使用Pool

import time
import requests
from multiprocessing import Pool

task_list = [
    'https://www.jianshu.com/p/91b702f4f24a',
    'https://www.jianshu.com/p/8e9e0b1b3a11',
    'https://www.jianshu.com/p/7ef0f606c10b',
    'https://www.jianshu.com/p/b117993f5008',
    'https://www.jianshu.com/p/583d83f1ff81',
    'https://www.jianshu.com/p/91b702f4f24a',
    'https://www.jianshu.com/p/8e9e0b1b3a11',
    'https://www.jianshu.com/p/7ef0f606c10b',
    'https://www.jianshu.com/p/b117993f5008',
    'https://www.jianshu.com/p/583d83f1ff81'
]

header = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
                      '(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
    }

def download(url):
    response = requests.get(url,
                            headers=header,
                            timeout=30
                            )
    return response.status_code

def timeCul(processNumberList):
    for processNumber in processNumberList:
        p = Pool(processNumber)
        time_old = time.time()
        print('res:',p.map(download, task_list))
        time_new = time.time()
        time_cost = time_new - time_old
        print("Prcess number {},Time cost {}".format(processNumber,time_cost))
        time.sleep(20)

timeCul([1,3,5,7,10])
res: [200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
Prcess number 1,Time cost 10.276863813400269
res: [200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
Prcess number 3,Time cost 2.4015071392059326
res: [200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
Prcess number 5,Time cost 2.639281988143921
res: [200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
Prcess number 7,Time cost 1.357300043106079
res: [200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
Prcess number 10,Time cost 0.7208449840545654

可以看到,隨着進程數量的提升,爬蟲的效率得到了顯著的提高

多進程,使用Process對象

from multiprocessing import Process

def f(name):
    print('hello', name)
    
p_1 = Process(target=f, args=('bob',))
p_1.start()
p_1.join()

p_2 = Process(target=f, args=('alice',))
p_2.start()
p_2.join()
hello bob
hello alice

關於多線程

純粹的多線程爬蟲不適合複雜的任務

當某一個線程的爬蟲出現故障,由於內存共享機制,所有的線程會受到牽連

from concurrent.futures import ThreadPoolExecutor
import time

def sayhello(a):
    print("hello: "+a)
    time.sleep(2)

def main():
    seed=["a","b","c"]
    start1=time.time()
    for each in seed:
        sayhello(each)
    end1=time.time()
    print("time1: "+str(end1-start1))
    start2=time.time()
    with ThreadPoolExecutor(3) as executor:
        for each in seed:
            executor.submit(sayhello,each)
    end2=time.time()
    print("time2: "+str(end2-start2))
    start3=time.time()
    with ThreadPoolExecutor(3) as executor1:
        executor1.map(sayhello,seed)
    end3=time.time()
    print("time3: "+str(end3-start3))

if __name__ == '__main__':
    main()

關於協程

協程的作用

簡單總結一下協程的優缺點:

優點:

  1. 無需線程上下文切換的開銷(還是單線程);

  2. 無需原子操作的鎖定和同步的開銷;

  3. 方便切換控制流,簡化編程模型;

  4. 高併發+高擴展+低成本:一個cpu支持上萬的協程都沒有問題,適合用於高併發處理。

缺點:

  1. 無法利用多核的資源,協程本身是個單線程,它不能同時將單個cpu的多核用上,協程需要和進程配合才能運用到多cpu上(協程是跑在線程上的);

  2. 進行阻塞操作時會阻塞掉整個程序:如io;

示例演示

協程是我這次複習的一個重頭戲,所以給它一個完整的演示流程。這對於理解併發以及併發應該如何應用有着很大的意義。

首先,爲了體現協程的高效率,我將傳統的串行爬蟲和協程爬蟲進行一個效率對比。

共同部分

import re
import asyncio
import aiohttp
import requests
import ssl
from lxml import etree
from asyncio.queues import Queue

from aiosocksy.connector import ProxyConnector, ProxyClientRequest
links_list = []
for i in range(1, 18):
    url = 'http://www.harumonia.top/index.php/page/{}/'.format(i)
    header = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
                      '(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
    }
    response = requests.get(url,
                            headers=header,
                            timeout=5
                            )
    tree = etree.HTML(response.text)
    article_links = tree.xpath('//*[@id="post-panel"]/div/div[@class="panel"]/div[1]/a/@href')
    for article_link in article_links:
        links_list.append(article_link)

以上,獲取url列表,是兩隻爬蟲的共同部分,所以就摘出來,不加入計時。

傳統方法,順序爬蟲

%%timeit
word_sum = 0
for link in links_list:
    res = requests.get(link,headers=header)
    tree = etree.HTML(res.text)
    word_num = re.match('\d*', tree.xpath('//*[@id="small_widgets"]/ul/li[5]/span/text()')[0]).group()
    word_sum+=int(word_num)
47.9 s ± 6.06 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

協程方法

result_queue_1 = []

async def session_get(session, url):
    headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'}
    timeout = aiohttp.ClientTimeout(total=20)
    response = await session.get(
        url,
        timeout=timeout,
        headers=headers,
        ssl=ssl.SSLContext()
    )
    return await response.text(), response.status


async def download(url):
    connector = ProxyConnector()
    async with aiohttp.ClientSession(
            connector=connector,
            request_class=ProxyClientRequest
    ) as session:
        ret, status = await session_get(session, url)
        if 'window.location.href' in ret and len(ret) < 1000:
            url = ret.split("window.location.href='")[1].split("'")[0]
            ret, status = await session_get(session, url)
        return ret, status


async def parse_html(content):
    tree = etree.HTML(content)
    word_num = re.match('\d*', tree.xpath('//*[@id="small_widgets"]/ul/li[5]/span/text()')[0]).group()
    return int(word_num)


def get_all_article_links():
    links_list = []
    for i in range(1, 18):
        url = 'http://www.harumonia.top/index.php/page/{}/'.format(i)
        header = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
                          '(KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'
        }
        response = requests.get(url,
                                headers=header,
                                timeout=5
                                )
        tree = etree.HTML(response.text)
        article_links = tree.xpath('//*[@id="post-panel"]/div/div[@class="panel"]/div[1]/a/@href')
        for article_link in article_links:
            links_list.append(article_link)
            print(article_link)
    return links_list


async def down_and_parse_task(url):
    error = None
    for retry_cnt in range(3):
        try:
            html, status = await download(url)
            if status != 200:
                print('false')
                html, status = await download(url)
            word_num = await parse_html(html)
            print('word num:', word_num)
            return word_num
        except Exception as e:
            error = e
            print(retry_cnt, e)
            await asyncio.sleep(1)
            continue
    else:
        raise error


async def main(all_links):
    task_queue = Queue()
    task = []
    for item in set(all_links):
        await task_queue.put(item)
    while not task_queue.empty():
        url = task_queue.get_nowait()
        print('now start', url)
        task.append(asyncio.ensure_future(down_and_parse_task(url)))
    tasks = await asyncio.gather(*task)
    for foo in tasks:
        result_queue_1.append(foo)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

time cost 16.03649091720581 s
字數 = 291738

ps.由於jupyter自身的限制,所以這裏使用pycharm運行並計時

總結

可以看出,協程方法下,代碼的運行效率大約是傳統串行方式的3倍,並且,隨着運行量級的增加,效率將會呈指數級提升。

由進程到線程,由線程到協程,任務的劃分越來越精細,但是代價是什麼呢?

補充說明

  1. 無論是串行還是協程,都會面臨爬取頻率過高而觸發反爬蟲機制的問題。這在高效率的協程狀況下尤爲明顯,這裏就要使用代理來規避這一問題。
  2. 兩者的代碼量存在很大的差異,這裏主要是因爲在寫協程的時候進行了代碼規範,只是看上去代碼量多了很多而已。(當然,協程的代碼量必然是比傳統方法多的)
  3. 爬蟲不要玩的太狠,曾經有人將爬蟲掛在服務器上日夜爬取某網站,被判定爲攻擊,最終被反制(病毒攻擊)的先例。同時,也要兼顧一些法律方面的問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章