1. 程式人生 > >我的豆瓣短評爬蟲的多執行緒改寫

我的豆瓣短評爬蟲的多執行緒改寫

對之前我的那個豆瓣的短評的爬蟲,進行了一下架構性的改動。儘可能實現了模組的分離。但是總是感覺不完美。暫時也沒心情折騰了。

同時也添加了多執行緒的實現。具體過程見下。

改動

獨立出來的部分:

  • MakeOpener
  • MakeRes
  • GetNum
  • IOFile
  • GetSoup
  • main

將所有的程式碼都置於函式之中,顯得乾淨了許多。(^__^) 嘻嘻……

使用直接呼叫檔案入口作為程式的起點

if __name__ == "__main__":
    main()

注意,這一句並不代表如果該if之前有其他直接暴露出來的程式碼時,他會首先執行。

print("首先執行")

if __name__ == "__main__":
    print("次序執行")

# 輸出如下:
# 首先執行
# 次序執行

if語句只是代表順序執行到這句話時進行判斷呼叫者是誰,若是直接執行的該檔案,則進入結構,若是其他檔案呼叫,那就跳過。

多執行緒

這裡參考了【Python資料分析】Python3多執行緒併發網路爬蟲-以豆瓣圖書Top,和我的情況較為類似,參考較為容易。

仔細想想就可以發現,其實爬10頁(每頁25本),這10頁爬的先後關係是無所謂的,因為寫入的時候沒有依賴關係,各寫各的,所以用序列方式爬取是吃虧的。顯然可以用併發來加快速度,而且由於沒有同步互斥關係,所以連鎖都不用上。

正如引用博文所說,由於問題的特殊性,我用了與之相似的較為直接的直接分配給各個執行緒不同的任務,而避免了執行緒互動導致的其他問題。

我的程式碼中多執行緒的核心程式碼不多,見下。

thread = []
for i in range(0, 10):
    t = threading.Thread(
            target=IOFile,
            args=(soup, opener, file, pagelist[i], step)
        )
    thread.append(t)

# 建立執行緒
for i in range(0, 10):
    thread[i].start()

for
i in range(0, 10): thread[i].join()

呼叫執行緒庫threading,向threading.Thread()類中傳入要用執行緒執行的函式及其引數。

執行緒列表依次新增對應不同引數的執行緒,pagelist[i]step兩個引數是關鍵,我是分別為每個執行緒分配了不同的頁面連結,這個地方我想了半天,最終使用了一些數學計算來處理了一下。

同時也簡單試用了下列表生成式:

pagelist = [x for x in range(0, pagenum, step)]

這個和下面是一致的:

pagelist = []
for x in range(0, pagenum, step):
    pagelist.append(x)

threading.Thread的幾個方法

值得參考:多執行緒

  • start() 啟動執行緒
  • jion([timeout]),依次檢驗執行緒池中的執行緒是否結束,沒有結束就阻塞直到執行緒結束,如果結束則跳轉執行下一個執行緒的join函式。在程式中,最後join()方法使得當所呼叫執行緒都執行完畢後,主執行緒才會執行下面的程式碼。相當於實現了一個結束上的同步。這樣避免了前面的執行緒結束任務時,導致檔案關閉。

注意

使用多執行緒時,期間的延時時間應該設定的大些,不然會被網站拒絕訪問,這時你還得去豆瓣認證下”我真的不是機器人”(尷尬)。我設定了10s,倒是沒問題,再小些,就會出錯了。

完整程式碼

# -*- coding: utf-8 -*-
"""
Created on Thu Aug 17 16:31:35 2017

@note: 為了便於閱讀,將模組的引用就近安置了
@author: lart
"""

import time
import socket
import re
import threading
from urllib import parse
from urllib import request
from http import cookiejar
from bs4 import BeautifulSoup
from matplotlib import pyplot
from datetime import datetime


# 用於生成短評頁面網址的函式
def MakeUrl(start):
    """make the next page's url"""
    url = 'https://movie.douban.com/subject/26934346/comments?start=' \
        + str(start) + '&limit=20&sort=new_score&status=P'
    return url


def MakeOpener():
    """make the opener of requset"""
    # 儲存cookies便於後續頁面的保持登陸
    cookie = cookiejar.CookieJar()
    cookie_support = request.HTTPCookieProcessor(cookie)
    opener = request.build_opener(cookie_support)
    return opener


def MakeRes(url, opener, formdata, headers):
    """make the response of http"""
    # 編碼資訊,生成請求,開啟頁面獲取內容
    data = parse.urlencode(formdata).encode('utf-8')
    req = request.Request(
                    url=url,
                    data=data,
                    headers=headers
                )
    response = opener.open(req).read().decode('utf-8')
    return response


def GetNum(soup):
    """get the number of pages"""
    # 獲得頁面評論文字
    totalnum = soup.select("div.mod-hd h2 span a")[0].get_text()[3:-2]
    # 計算出頁數
    pagenum = int(totalnum) // 20
    print("the number of comments is:" + totalnum,
          "the number of pages is: " + str(pagenum))
    return pagenum


def IOFile(soup, opener, file, pagestart, step):
    """the IO operation of file"""
    # 迴圈爬取內容
    for item in range(step):
        start = (pagestart + item) * 20
        print('第' + str(pagestart + item) + '頁評論開始爬取')
        url = MakeUrl(start)
        # 超時重連
        state = False
        while not state:
            try:
                html = opener.open(url).read().decode('utf-8')
                state = True
            except socket.timeout:
                state = False
        # 獲得評論內容
        soup = BeautifulSoup(html, "html.parser")
        comments = soup.select("div.comment > p")
        for text in comments:
            file.write(text.get_text().split()[0] + '\n')
            print(text.get_text())
        # 延時1s
        time.sleep(10)

    print('執行緒採集寫入完畢')


def GetSoup():
    """get the soup and the opener of url"""
    main_url = 'https://accounts.douban.com/login?source=movie'
    formdata = {
        "form_email": "your-email",
        "form_password": "your-password",
        "source": "movie",
        "redir": "https://movie.douban.com/subject/26934346/",
        "login": "登入"
            }
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1)\
            Gecko/20061208 Firefox/2.0.0 Opera 9.50",
        'Connection': 'keep-alive'
            }
    opener = MakeOpener()

    response_login = MakeRes(main_url, opener, formdata, headers)
    soup = BeautifulSoup(response_login, "html.parser")

    if soup.find('img', id='captcha_image'):
        print("有驗證碼")
        # 獲取驗證碼圖片地址
        captchaAddr = soup.find('img', id='captcha_image')['src']
        # 匹配驗證碼id
        reCaptchaID = r'<input type="hidden" name="captcha-id" value="(.*?)"/'
        captchaID = re.findall(reCaptchaID, response_login)
        # 下載驗證碼圖片
        request.urlretrieve(captchaAddr, "captcha.jpg")
        img = pyplot.imread("captcha.jpg")
        pyplot.imshow(img)
        pyplot.axis('off')
        pyplot.show()
        # 輸入驗證碼並加入提交資訊中,重新編碼提交獲得頁面內容
        captcha = input('please input the captcha:')
        formdata['captcha-solution'] = captcha
        formdata['captcha-id'] = captchaID[0]
        response_login = MakeRes(main_url, opener, formdata, headers)
        soup = BeautifulSoup(response_login, "html.parser")

    return soup, opener


def main():
    """main function"""
    timeout = 5
    socket.setdefaulttimeout(timeout)
    now = datetime.now()
    soup, opener = GetSoup()

    pagenum = GetNum(soup)
    step = pagenum // 9
    pagelist = [x for x in range(0, pagenum, step)]
    print('pageurl`s list={}, step={}'.format(pagelist, step))

    # 追加寫檔案的方式開啟檔案
    with open('祕密森林的短評.txt', 'w+', encoding='utf-8') as file:
        thread = []
        for i in range(0, 10):
            t = threading.Thread(
                    target=IOFile,
                    args=(soup, opener, file, pagelist[i], step)
                )
            thread.append(t)

        # 建立執行緒
        for i in range(0, 10):
            thread[i].start()

        for i in range(0, 10):
            thread[i].join()

    end = datetime.now()
    print("程式耗時: " + str(end-now))


if __name__ == "__main__":
    main()

執行結果

效率有提升

對應的單執行緒程式在github上。單執行緒:

單執行緒.jpg

可見時間超過30分鐘。修改後時間縮短到了11分鐘。

多執行緒.jpg

檔案截圖

ScreenShot00080.jpg

我的專案

具體檔案和對應的結果截圖我放到了我的github上。

mypython