用 Spynner 來抓 8Comic 的漫畫 (3): 節省頻寬

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

要解決的問題

  • 還是有文字廣告這樣不需要的流量來浪費頻寬和時間。
  • QtWebKit 的 QWebSettings.AutoLoadImages=false 有 bug,記憶體已用不還。

解法

如同這裡 http://stackoverflow.com/questions/21357157/is-there-any-solution-for-the-qtwebkit-memory-leak 以及相關的連結、討論,記憶體的 bug 目前找得到的解法就是定期砍掉 process 重新再開。
理論上,也可以深入 QtWebKit, 找到配置的記憶空間,然後手動釋放,順便將解法回給上游。這樣雖然是正解,但是與我們的主題無關。
既然無解,那要等 QtWebKit 修正 bug 之後再來抓? 太久。
定期砍 Process 重開? 可以,但太醜。
不用 QtWebKit, 改用其他套件來解? 可以,但其他套件可能有其他問題,人生不能一直逃避,會養成習慣的。
仔細思考,其實我們的目標是要讓 browser 不去抓不必要的網路資源,這個原則包含了圖片以及文字廣告,但不僅限於這兩個,還有像是 google 統計等等。
所以只要我們封鎖這些不必要的連線,看起來瀏覽器的圖片就不會被顯示了,不需要真的設定 QWebSettings.AutoLoadImages=false
比方可以設定 proxy,由 proxy 來控制網路資訊流。不過 QtWebKit 有幾個內建的功能,可以讓你控制網路的存取,比 proxy 更裡層一點。一個是 browser.webpage 的 acceptNavigationRequest, loadStarted, loadFinished. 這幾個搭配起來,你可以限制 browser 不抓取子 iframe。 但這個方法對我們來說,不夠用。我們想要控制更多,所以我們用另外一種方式,直接控制比較外層一點的 QNetworkAccessManager 。

一樣 import 所有我們將會用到的東西

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 類別

# 下面這行是 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
# 我們不設定 AutoLoadImages=False, 但增加一些其他設定
# 這裡並不是重點,但適合我們的應用
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) # 不需要瀏覽紀錄

建立一個 QNetworkAccessManager 子類別

當 browser.webpage 在要求網路資源前,會先詢問 QNetworkAccessManager 來確定要不要抓,或者怎麼來抓這個資源。 我們可以用 browser.webpage.setNetworkAccessManager 來指定自己客製過的 manager。
In []:
# 建立一個空的  url
BLANK_REQUEST = QNetworkRequest(QUrl())
# 建立一個空的圖片 url
DUMMY_IMG_REQUEST = QNetworkRequest(QUrl("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="))

# 因為只需要用一次,可以取個又臭又長的名字
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())

後面的程式碼都一樣

In []:
# 漫畫的網頁
base_url = 'http://new.comicvip.com/show/cool-5614.html?ch='

# 顯示瀏覽器,確認 browser 內容乾淨清爽
browser.show()

# 要下載第一本
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
progress = IntProgressWidget(min=1, value=1, max=total_pages)

# 顯示 Widget
display(progress)
display(img)

# 建立一個下載目錄
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()

# 開始下載
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())
    # 下載圖片
    fn = "{}/{:03d}.jpg".format(dir_name, page)
    with open(fn, "wb") as f:
        browser.download(img_url, outfd=f)
    
    # 更新 Widget 的狀態
    progress.description = "%d/%d"%(page, total_pages)
    progress.value = page
    img.value = Image(filename=fn).data

結果

和第一篇的結果比較,
的確清爽很多,廣告和圖片都消失了。速度也快了很多。其實 .css 檔案也可以不用抓,不過因為有 cache 的緣故, .css 和 .js 本來都只會抓一次,所以影響有限。
因為這一部分的探索已經告一段落,所以現在是封裝的時候。不過因為之後我們要介紹兩種不同的封裝方式,所以在這之前,我們要再做一件事情,那就是更加節省一點。
看起來不是我們不是已經夠節儉了嗎?只抓有需要的東西,沒有多抓任何東西。 也許 .html 也可以不用抓?沒錯,也許可以直接反推出每一頁漫畫圖片的 url,但即使這個例子可以,但一般來說,伺服器端完全可以將必要的資訊放在 .html 裡面,讓你必須要抓 .html 才能獲得必要資訊。 這系列的目的是介紹一個簡單的萬用抓資料方式,所以做到這裡就可以了。
那還有什麼可以節省的? 頻寬就這樣了,但是時間還可以節省。 下一篇介紹簡單的 multithreading 抓圖。
In []:



Categories: