用 Spynner 來抓 8Comic 的漫畫 (4): 多線程
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, 常常也代表我們需要封裝了。