【Python】個人豆瓣短評爬蟲的多線程改寫

對以前個人那個豆瓣的短評的爬蟲,進行了一下架構性的改動。儘量實現了模塊的分離。可是老是感受不完美。暫時也沒心情折騰了。html

同時也添加了多線程的實現。具體過程見下。python

改動

獨立出來的部分:

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

將全部的代碼都置於函數之中,顯得乾淨了許多。(*^__^*) 嘻嘻……git

使用直接調用文件入口做爲程序的起點

if __name__ == "__main__":
    main()

注意,這一句並不表明若是該if以前有其餘直接暴露出來的代碼時,他會首先執行。github

print("首先執行")

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

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

if語句只是表明順序執行到這句話時進行判斷調用者是誰,如果直接運行的該文件,則進入結構,如果其餘文件調用,那就跳過。cookie

多線程

這裏參考了【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()類中傳入要用線程運行的函數及其參數。app

線程列表依次添加對應不一樣參數的線程,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

文件截圖

個人項目

具體文件和對應的結果截圖我放到了個人github上。

mypython

相關文章
相關標籤/搜索