[Swift] 漫畫爬蟲

Reading time ~5 minutes

最近搬家又讓我挖出了Amazon Kindle, 又覺得拿來看漫畫很方便(這戲演了幾次了呀?), 雖然說好像也有網站可以下載漫畫.mobi檔, 不過似乎是會員制的, 不喜歡

因此又讓我想寫漫畫的爬蟲了, 這次的目標是: 無限動漫 (他們的app實在做得有夠差)

這次幾個需求是:

  1. Command line下就可以跑了(這也沒必要做UI吧?)
  2. 在os x下可以執行(我自己電腦是mac)
  3. 出來的檔案可以放到kindle看(.mobi檔或epub)

mobi或epub的檔案格式似乎有點麻煩, 也不太好做得好, 所以決定用cbz檔再用Calibre轉mobi

Calibre有一個方便的command line tool叫ebook-convert, 可以用來轉檔, 而cbz本身非常的簡單 , 它就是一個zip檔, 裡面的圖片檔名照編號就好, 這code還算好寫

再來就是看一下怎麼解析無限動漫的內容了, 它的URL是長這樣的:

http://v.comicbus.com/online/comic-653.html?ch=1

以上範例是名偵探柯南第一卷, 大膽猜測, 653是漫畫編號, ch是集數, 選到第二頁, URL會變成這樣

http://v.comicbus.com/online/comic-653.html?ch=4-2

這樣其實就很明顯了, 接下來是內容的部分

每一集的頭上有一個"正在觀看:[ 名偵探柯南 1 ]", “[]“內就是標題了吧, 另外還有一個"select”, 裡面有這集所有的頁數資訊, 而圖片的id是"TheImg”

不過麻煩的是, 這些資訊似乎隱藏在javascript中, page載入後才會出現

這如果使用headless browser像是Phantomjs就沒啥問題, 但這邊我不想用它, 因為使用這工具還要再裝它

我下一個選擇是Go + Webloop, Webloop是一個Go的headless browser lib, 它是基於WebkitGtk+做成的, 不過我在mac上裝WebkitGTK+裝好久一直有問題, 所以…放棄….

接下來的選擇呢? 還有其他的headless browser嗎?有的! Erik, 這是一個Swift的head less browser, 用Swift寫爬蟲好像挺酷的, 查了一下, 有人用Alamofire + Kanna, 不過這在這例子不適用, 這例子還是比較適合Erik

成品

先給成果: ComicGo

這已經是一個OS X的可執行檔, 在Command line下執行 ComicGo 653 1就可以抓名偵探柯南第一集, 相關的漫畫編號集數, 就去無限動漫查吧

抓完會在你的Download目錄出現"名偵探柯南 1.cbz"再用ebook-covert去轉成你要的格式就可以了

少少的時間隨便寫寫而已, 有bug就見諒囉

OS X Command line tool

XCode + Swift是可以拿來寫command line tool的, 新增一個專案選"Command line tool":

這樣就可以開始寫了

一開始在專案內部會發現一個"main.swift", 由於用swift寫command line app並沒有像其他語言有main function這類的東西 所以程式就寫在這吧

開發Command line tool的坑

坑…真的不少

首先, 你不能使用任何的framework, 因為command line tool產出會是一個可執行檔, 不是一個app bundle, 所以不能包含任何的framework

第二, swift framework不能static link, 像是Erik, Kanna這些swift module, 都是dynamic lib

慘, 光前面這兩點就麻煩了, 開發這個ComicGo, 我用到了Erik, Kanna, Zip等等 , 這樣到底要怎麼辦? 跑起來就image not found

所以呢?土法煉鋼, 把這些module的codes全部引入到我的專案內(所以沒打算Open ssource, 太醜了), 這樣一來就解決掉問題了, 不過這功不算小, 因為Kanna相依libxml, Zip相依libz這些native lib

第三個坑, Erik是利用OS X裡面原生的WebKit去讀取網頁的, 因此他的設計是把載入網頁放到另一個DispatchQueue(javascript執行又是另一個), 但Command line邏輯很單線, 它並不會等callback回來才結束程式, 因此會發現怎麼Erik都沒動作就結束程式了, 因此必須要有個機制來卡住

這個機制就是RunLoop, 關於RunLoop這邊不多做解釋, 看一下官方文件 在程式內則是這樣:

let rl = RunLoop.current
var finished = false

while !finished {
    rl.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 2))
}

當callback完畢後, 把finished設成true就可以結束整個程式了

Erik

好像還沒介紹Erik喔?其實有點想偷懶跳過了 :P

使用Erik來爬網頁其實很簡單,

Erik.visit(url: url) { object, error in
    if let e = error {

    } else if let doc = object {
        // HTML Inspection
		for link in doc.querySelectorAll("a, link") {
    		print(link.text)
    		print(link["href"])
		}
    }
}

只要有些CSS selector的觀念就可以了, 連querySelectorAll這名字都是一樣的, Erik並不是直接用Webkit去做CSS query的, 而是把webkit的內容拿來用Kanna解析, javascript的執行也一樣, 因此如果對html node有任何變動, 是不會反映到webkit裡面去的, 用Erik來爬的優點是專門針對那些動態網頁的, 有這個就簡單太多了!