用 Spynner 來抓 8Comic 的漫畫 (4): 多線程

Posted by tjwei on 星期日, 11月 30, 2014 with No comments

平行化的好處

之前抓檔案的方式是,
  1. 用瀏覽器抓 .html,找到圖片 url。
  2. 下載圖片
  3. 換下一頁,跳到第一步。
但是網路可以同時開好幾個連線。所以我們要利用這點來加速。我們的策略是,
  1. 用瀏覽器抓 .html,找到圖片 url。
  2. 把圖片 url 丟進一個 Thread Pool 裡面(平行下載,但是不等待)。
  3. 換下一頁,跳到第一步。
當然也有其他的方式平行化,比方連抓 .html 的工作也一併丟入 Thread Pool。不過這樣代表要多開好幾個 browser,而且相對來說,抓網頁會比抓圖片快,所以我們選擇上面的方式。

import 將會用到的 module

因為使用 python 2.7, 我們利用 urllib2 來抓圖, ThreadPool 來 multi threading 平行處理。
In []:
import spynner
import os, sys
from PyQt4.QtWebKit import QWebSettings # 用來設定 QtWebKit
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest # 控制 browser 的網路連線
from PyQt4.QtCore import QUrl # Qt 的 Url 類別

# 下面是新增的兩個 module
import urllib2
from multiprocessing.pool import ThreadPool

# 下面是 IPython 相關
from IPython.display import display, Image
from IPython.html.widgets import ImageWidget, IntProgressWidget

建立瀏覽器

這部份一樣
In []:
# 建立瀏覽器
browser = spynner.Browser(debug_level=spynner.ERROR, debug_stream=sys.stderr)

# 建立一個 webview
browser.create_webview()
settings = browser.webview.settings()
# settings.setAttribute(QWebSettings.AutoLoadImages, False)
settings.setAttribute(QWebSettings.JavaEnabled, False)        # 不需要  Java
settings.setAttribute(QWebSettings.DnsPrefetchEnabled, True)  # 試著節省 Dns 花的時間
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) # 不需要瀏覽紀錄或者 cookie

# 建立一個空的  url
BLANK_REQUEST = QNetworkRequest(QUrl())
# 建立一個空的圖片 url
DUMMY_IMG_REQUEST = QNetworkRequest(QUrl("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="))

# 客製化的 NetworkAccessManager
class EightComicNetworkAccessManager(QNetworkAccessManager):
    # 只需要取代  createRequest 這個 method 即可 
    def createRequest(self, op, request, device=None):        
        url = str(request.url().toString()) # 參數很多,但只取 url 就夠用        
        if 'comic' not in url[:20]: 
            # 用很醜的方式來判斷非 8comic 網站的 url 
            # 用空的 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, BLANK_REQUEST)
        elif not url.endswith('js') and not url.endswith('css') and '.html' not in url:
            # 凡是  .js .css .html 之外的,都用空的圖片 url  取代原本的 url
            return QNetworkAccessManager.createRequest(self, self.GetOperation, DUMMY_IMG_REQUEST)
        else:
            # 傳回原本的 url
            return QNetworkAccessManager.createRequest(self, op, request, device)

# 設定  browser 的 NetworkAccessManager
browser.webpage.setNetworkAccessManager(EightComicNetworkAccessManager())

設定 Widget

增加一個 Progress Bar, 分別來顯示分析過的 .html 數字以及已經下載的圖片數
In []:
browser.show()
# 漫畫的網頁
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='

# 要下載第一本
book_no = 1

# 取得總頁數
browser.load(base_url+str(book_no))
total_pages = browser.runjs('ps').toInt()[0] 

# 建立 Image Widget 用來顯示圖片預覽
img = ImageWidget()
img.set_css("height", 300) # 讓圖片不要太大

# 顯示下載進度的 Progress bar
html_progress = IntProgressWidget(min=1, value=1, max=total_pages)
img_progress = IntProgressWidget(min=1, value=1, max=total_pages)

# 顯示 Widget
display(html_progress)
display(img_progress)
display(img)

利用 ThreadPool 來下載

另用 ThreadPool 來達成 multithreading (多線程) 即為容易,只要將想丟進 pool 的程式碼包進函數裡面即可。
In []:
# 建立一個下載目錄
dir_name = "download/{:02d}".format(book_no)
if not os.path.exists(dir_name):
            os.makedirs(dir_name) 
print "Download to {}/{}".format(os.getcwd(), dir_name)
sys.stdout.flush()

# 建立 ThreadPool, 5 條 thread
pool = ThreadPool(5)
        
# 開始下載
downloaded_images = 0
for page in range(1, total_pages+1):
    # 取得 image url
    browser.load("{}{}-{}".format(base_url, book_no, page))
    img_url = str(browser.runjs('document.getElementById("TheImg").getAttribute("src")').toString())

    # 將下載圖片的工作包成 save_img,推進 pool 裡
    def save_img(img_url, page):
        global downloaded_images
        fn = "{}/{:03d}.jpg".format(dir_name, page)
        data = urllib2.urlopen(img_url).read()
        with open(fn, "wb") as f:
            f.write(data)
        # 更新 widget 的狀態
        downloaded_images += 1
        img_progress.description = "img: %d/%d"%(downloaded_images, total_pages)
        img_progress.value = downloaded_images
        img.value = Image(filename=fn).data
    pool.apply_async(save_img, (img_url, page))
    
    # 更新 Widget 的狀態
    html_progress.description = "html: %d/%d"%(page, total_pages)
    html_progress.value = page

    # 等待所有任務結束
pool.close()
pool.join()
    

結果

到這裡,探索期正式結束,該有的技術已經完整。
有興趣的話,也可以實測比較一下有 multithreading 和沒有 multithreading 的差異。但這裡就發現我們需要封裝了,因為沒有封裝、整理好的關係,要測試兩種程式碼,必須要把程式碼寫兩遍,而無法共用相同的部份。
另外,程式碼裡面用到了 global 這個 keyword, 常常也代表我們需要封裝了。



Categories: