那些年,我爬過的北科(三)——爬蟲進階之多進程的使用

爬取多個頁面

在爬蟲基礎之環境搭建與入門中,介紹瞭如何用Requests下載(爬取)了一個頁面,並用BeautifulSoup這個HTML解析庫來解析頁面裏面咱們想要的內容。html

顯然,爬蟲確定不是隻讓咱們爬取一個網頁的,這樣的工做,人也能夠作。下面咱們來看:nladuo.cn/scce_site/這個頁面。這個頁面一共有10頁,點擊下一頁以後能夠看到在網頁的url中多了個字段「2.html」,也就是當前頁面時第二頁的意思。python

也就是咱們若是要爬取下全部的新聞,只要爬取形如"nladuo.cn/scce_site/{…"的頁面就行了。算法

這裏使用一個for循環就能夠完成所有頁面的爬取。網絡

import requests
from bs4 import BeautifulSoup
import time


def crawl_one_page(page_num):
    resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
                        format(page=page_num))
    soup = BeautifulSoup(resp.content)
    items = soup.find_all("div", {"class": "every_list"})

    for item in items:
        title_div = item.find("div", {"class": "list_title"})
        title = title_div.a.get_text()
        url = title_div.a["href"]
        date = item.find("div", {"class": "list_time"}).get_text()
        print(date, title, url)

if __name__ == '__main__':
    t0 = time.time()
    for i in range(1, 11):
        print("crawling page %d ......." % i)
        crawl_one_page(i)
    print("used:", (time.time() - t0))
複製代碼

CPU密集型和IO密集型業務

經過上面的代碼,咱們完成了一個順序結構的爬蟲。下面咱們來討論如何爬取的速度瓶頸在哪裏,從而提高爬取速率。多線程

這裏介紹一下CPU密集型業務I/O密集型業務併發

  • CPU密集型業務(CPU-bound):也叫計算密集型,指的是系統的硬盤、內存性能相對CPU要好不少,此時,系統運做大部分的情況是CPU Loading 100%,CPU要讀/寫I/O(硬盤/內存),I/O在很短的時間就能夠完成,而CPU還有許多運算要處理,CPU Loading很高。
  • I/O密集型業務(I/O-bound):指的是系統的CPU性能相對硬盤、內存要好不少,此時,系統運做,大部分的情況是CPU在等I/O (硬盤/內存) 的讀/寫操做,此時CPU Loading並不高。

(上述解釋來自blog.csdn.net/youanyyou/a…app

網絡爬蟲主要有兩個部分,一個是下載頁面,一個是解析頁面。顯然,下載是個長時間的I/O密集操做,而解析頁面則是須要調用算法來查找頁面結構,是個CPU操做。async

對於爬蟲來講,耗時主要在下載一個網頁中,根據網絡的連通性,下載一個網頁可能要幾百毫秒甚至幾秒,而解析一個頁面可能只須要幾十毫秒。因此爬蟲實際上是屬於I/O密集型業務,其瓶頸主要在網絡上面。性能

因此,提高爬蟲的爬取速度,不是把CPU都跑滿。而是要多開幾個下載器,同時進行下載,把網絡I/O跑滿。url

在Python中,使用多線程和多進程均可以實現併發下載。然而在python多線程沒法跑多核(參見:GIL),而多進程能夠。

這裏,咱們主要說一下python中多進程的使用。

多進程

python中調用多進程使用multiprocessing這個包就行了。下面建立了兩個進程,每隔一秒打印一下進程ID。(這裏的time.sleep能夠理解爲耗時的I/O操做。)

import multiprocessing
import time
import os


def process(process_id):
    while True:
        time.sleep(1)
        print('Task %d, pid: %d, doing something' % (process_id, os.getpid()))

if __name__ == "__main__":
    # 進程1
    p = multiprocessing.Process(target=process, args=(1,))
    p.start()

    # 進程2
    p2 = multiprocessing.Process(target=process, args=(2,))
    p2.start()
複製代碼

能夠看到基本上是同時打印兩句話。而在沒用多進程前,咱們的代碼會像下面的代碼的樣子。

while True:
    time.sleep(1)
    print 'Task 1, doing something'  
    time.sleep(1)
    print 'Task 2, doing something'
複製代碼

此時,咱們建立兩個進程,一個進程爬取1-5頁,一個進程爬取6-10頁。再來試試,看看速度有沒有提高一倍。

import multiprocessing
import requests
from bs4 import BeautifulSoup
import time


def crawl_one_page(page_num):
    resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
                        format(page=page_num))
    soup = BeautifulSoup(resp.content)
    items = soup.find_all("div", {"class": "every_list"})

    for item in items:
        title_div = item.find("div", {"class": "list_title"})
        title = title_div.a.get_text()
        url = title_div.a["href"]
        date = item.find("div", {"class": "list_time"}).get_text()
        print(date, title, url)


def process(start, end):
    for i in range(start, end):
        print("crawling page %d ......." % i)
        crawl_one_page(i)


if __name__ == '__main__':
    t0 = time.time()
    p = multiprocessing.Process(target=process, args=(1, 6))  # 任務1, 爬取1-5頁
    p.start()

    p2 = multiprocessing.Process(target=process, args=(6, 11))  # 任務2, 爬取6-10頁
    p2.start()

    p.join()
    p2.join()

    print("used:", (time.time() - t0))
複製代碼

進程池

像上面的方式,咱們建立了兩個進程,分別處理兩個任務。然而有的時候,並非那麼容易的把一個任務分紅兩個任務。考慮一下把一個任務想象爲爬取並解析一個網頁,當咱們有兩個或者多個進程而任務有成千上萬個的時候,代碼應該怎麼寫呢?

這時候,咱們須要維護幾個進程,而後給每一個進程分配一個網頁,如何分配,須要咱們本身定義。在全部的進程都在運行時,要保證有進程結束時,再加入新的進程。

import multiprocessing
import requests
from bs4 import BeautifulSoup
import time


def crawl_one_page(page_num):
    resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
                        format(page=page_num))
    soup = BeautifulSoup(resp.content, "html.parser")
    items = soup.find_all("div", {"class": "every_list"})

    for item in items:
        title_div = item.find("div", {"class": "list_title"})
        title = title_div.a.get_text()
        url = title_div.a["href"]
        date = item.find("div", {"class": "list_time"}).get_text()
        print(date, title, url)


if __name__ == '__main__':
    t0 = time.time()

    p = None  # 進程1
    p2 = None  # 進程2

    for i in range(1, 11):
        if i % 2 == 1:  # 把偶數任務分配給進程1
            p = multiprocessing.Process(target=crawl_one_page, args=(i,))
            p.start()
        else:           # 把奇數任務分配給進程2
            p2 = multiprocessing.Process(target=crawl_one_page, args=(i,))
            p2.start()

        if i % 2 == 0:  # 保證只有兩個進程, 等待兩個進程完成
            p.join()
            p2.join()

    print("used:", (time.time() - t0))
複製代碼

上面的代碼實現了一個簡單的兩進程的任務分配和管理,但其實也存在着一些問題:好比進程2先結束,此時就只有一個進程在運行,但程序還阻塞住,沒法產生新的進程。這裏只是簡單的作個例子,旨在說明進程管理的複雜性。

下面咱們說一說進程池,其實就是爲了解決這個問題而設計的。

既然叫作進程池,那就是有個池子,裏面有一堆公用的進程;當有任務來了,拿一個進程出來;當任務完成了,把進程還回池子裏,給別的任務用;當池子裏面沒有可用進程的時候,那就要等待,等別人把進程歸還了再拿去用。

下面咱們來看一下代碼,讓每一個進程每秒打印一下pid,一共打印兩遍。

from multiprocessing import Pool
import time
import os


def do_something(num):
    for i in range(2):
        time.sleep(1)
        print("doing %d, pid: %d" % (num, os.getpid()))

if __name__ == '__main__':
    p = Pool(3)
    for page in range(1, 11):  # 10個任務
        p.apply_async(do_something, args=(page,))

    p.close()   # 關閉進程池, 再也不接受任務
    p.join()    # 等待子進程結束
複製代碼

運行代碼後能夠看到,咱們能夠看到這裏是三個三個的打印的,咱們成功完成了三併發。同時,進程池一共產生了三個進程:59650、5965一、59652,說明後面的全部任務都是使用這三個進程完成的。

下面,修改爬蟲代碼,用進程池實現併發爬取。

import requests
from bs4 import BeautifulSoup
from multiprocessing import Pool
import time


def crawl_one_page(page_num):
    resp = requests.get("http://nladuo.cn/scce_site/{page}.html".
                        format(page=page_num))
    soup = BeautifulSoup(resp.content, "html.parser")
    items = soup.find_all("div", {"class": "every_list"})

    for item in items:
        title_div = item.find("div", {"class": "list_title"})
        title = title_div.a.get_text()
        url = title_div.a["href"]
        date = item.find("div", {"class": "list_time"}).get_text()
        print(date, title, url)

if __name__ == '__main__':
    t0 = time.time()
    p = Pool(5)
    for page in range(1, 11):  # 1-10頁
        p.apply_async(crawl_one_page, args=(page,))

    # 關閉進程池, 等待子進程結束
    p.close()
    p.join()

    print("used:", (time.time() - t0))
複製代碼

到這裏,多進程的講解就結束了。

相關文章
相關標籤/搜索