最近搬家又讓我挖出了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來爬的優點是專門針對那些動態網頁的, 有這個就簡單太多了!

好, 這算我以為我寫過但實際上沒有系列….啥? 剛剛把我以前寫的一個Go package - gopttcrawler更新後, 想說之前好像有寫過相關文章, 但實際上又沒找到(老了?)

沒關係, 把Readme拿來貼一貼就可以混一篇文了(混蛋!!!偷懶!!!)

gopttcrawler

這是我在新聞萬事通裡面用來抓取ptt文章的, 用法也很簡單, 基本上看最後兩種用法就好, 自己個人覺得那比較好用

原本的版本並沒處理18+那個擋在前面的畫面, 查了一下很多人(在python)的做法是去發post取得cookie, 再帶cookie去取, 後來發現, cookie是固定的, 只要這樣就好:

	cookie := http.Cookie{
		Name:  "over18",
		Value: "1",
	}
	req.AddCookie(&cookie)

所以現在像是八卦版這種18+的也可以抓取了

安裝方法: go get -u github.com/julianshen/gopttcrawler

使用方法請參考sample.go或是ptt_test.go

資料結構

type Article struct {
	ID       string //Article ID
	Board    string //Board name
	Title    string
	Content  string
	Author   string //Author ID
	DateTime string
	Nrec     int //推文數(推-噓)
}

type ArticleList struct {
	Articles     []*Article //Articles
	Board        string //Board
	PreviousPage int //Previous page id
	NextPage     int //Next page id
}

載入文章列表

  1. 載入最新一頁表特版文章
    articleList, _ := gopttcrawler.GetArticles("Beauty", 0)
    // the 1st parameter is the board name
    // the 2nd parameter is the page id. 0 indicates the latest page
  1. 載入前一頁文章列表
    prevArticleList, _ := articleList.GetFromPreviousPage()

載入文章內容

  1. 載入單篇文章詳細內容
    article := articleList.Articles[0]
    article.Load()
    fmt.Println(article.Content) //印出內文(html)
  1. 取得文章中所有圖片連結
    images := article.GetImageUrls()
  1. 取得文章中的連結
    links := article.GetLinks()

Iterator

新增Iterator功能:

	n := 100

	articles, e := gopttcrawler.GetArticles("movie", 0)
	
	if e != nil {
		....
	}

	iterator := articles.Iterator()

	i := 0
	for {
		if article, e := iterator.Next(); e == nil {
			if i >= n {
				break
			}
			i++

			log.Printf("%v %v", i, article)
		}
	}

上面這範例是抓取最新的100篇文章, 不用管第幾頁, 或是上一頁下一頁, 反正就一直抓

Go routine版本的GetArticles

	ch, done := gopttcrawler.GetArticlesGo("Beauty", 0)
	n := 100
	i := 0
	for article := range ch {
		if i >= n {
			done <- true
			break
		}
		i++
		log.Printf("%v %v", i, article)
	}

這範例一樣也是抓一百篇, 只是抓文章的部分被放到go routine去了, 會立即回傳兩個channel, 第一個是receive only channel, 跟Iterator類似, 一次拿一篇文章, 可以用range, 第二個是一個bool channel, 拿夠了送個訊息通知他終止go routine, 如果把receive部分放到select去, 就是non blocking了, 不會被讀上一頁下一頁的IO給卡住

之前寫了一些爬蟲, 想說來補一篇這樣的文章好了

可能是之前"所謂"的大數據(Big Data)太過流行, 以至於網路爬蟲好像是一種顯學, 隨便Google一下都可以找到一堆用python加上BeautifulSoup 相關的文章, 這可能也是因為, 現在網路上的資料, Open data的, 提供API的, 在比例上還是非常的少數, 但網頁的數量真的多到很難統計(想到早期還真是屈指可數) 要取得網頁內的內容, 解析HTML, 做字串的處理就是一個必要的基礎, 這也難怪python + BeautifulSoup 一直廣泛的被採用

不過, 當你真的去寫一個爬蟲, 突然就會發現了, 代誌不是這麼單純呀, 不是去抓個html回來解析一下就有自己想要的資料了呀, 而是會發現, 怎麼大家正正當當的網頁不寫, 一堆奇技淫巧, 繞來繞去的, 網頁廣告內容也一堆, 很多網頁都很難抓到自己想要的資料呀!!!

真的來說, 做爬蟲是一種Hacking的活動, 天下網頁萬萬種, 為了總總不同的目的, 早已不是幾個簡單的html標籤就做出來的了, 還搭配了很多程式的技巧在內, 因此要從裡面萃取資料出來, 常常還真的是要無所不用其極

那到底做爬蟲會先需要懂什麼?

  1. HTML
  2. CSS
  3. DOM
  4. DOM Selectors
  5. Javascript
  6. Regular Expression (正規表示式)
  7. Chrome dev tool
  8. Curl

以上這幾個東西可能是基本必須懂得, 而不是程式語言, 那反而其次, 很多程式語言都有很好的能力跟工具來做, 另外需要具備的是耐心, 眼力, 直覺, 和運氣

底下就拿我之前弄過的幾個東西當範例, 由於我寫比較多Go和nodejs, 所以就不用(主流的)Python來做範例了

範例一 : 文章內容分析 (如新聞, 部落格文章等等)

這應該是比較常見的應用, 單純抓取文章內容去做分析, 常碰到的麻煩是現在網站放了大大小小的(補釘)廣告, 那些跟內容不相干, 也不會是我們想拿來分析的目標

底下這個範例是從Yahoo! News的RSS抓取新聞文章連結, 再從這些連結的文章內找出關鍵字:

這段code非常的簡單, 主要也只用到以下這幾種東西:

  1. rss parser
  2. go-readability
  3. 結巴

簡單的來說就是先從rss內找出所有新聞的連結再一個個去爬, 然後用go-readability精簡出網頁內文, 再用結巴取出關鍵字

readability

這一個library以往的用途就是清除不必要的html tags跟內容(像是廣告), 只留下易讀的內文(純文字), 以這個例子來說, 這是一個最適合的工具了

最早的readability應該是arc90這人開源出來的, 最早應該是javascript的版本, 但就算你不是用javascript, 它老早也被翻唱成其他語言的版本了, 像是:

  1. Python - python-readability
  2. Node.js - node.js readability
  3. Java - JReadabilitysnacktory
  4. Go - go-readability

結巴 jieba

結巴 jieba 是一個用在做中文分詞的工具, 英文每個單詞都是用空白分開的, 但中文就不是那麼回事了, 結巴 jieba 可以幫忙作掉這部份的工作, 這可以拿來做文章分析或是找關鍵字用

一樣有好幾種語言的版本 - Java, C++, Node.js, Erlang, R, iOS, PHP, C#, Go (參照結巴的說明內文), 算蠻齊全的了

範例二 : 抓取Bilibili的視訊檔位址

這個範例稍微做了點弊, 但還是從頭把分析過程來講一下好了

Bilibili視頻網頁長得就像這樣: 範例 - http://www.bilibili.com/video/av6467776/

先簡單的從網址猜一下….“av6467776"應該是某個ID之類的東西, 再進一步, ID可能就是這個"6467776”

接下來我們就需要借助一下Chrome的"開發人員工具", 這是一個強大也重要的工具, 不要只傻傻的用View source而已, View source能看到的也只有原始HTML的內容

開了網頁後, 用Ctrl+Shift+I (windows)或是Cmd+Opt+I (mac)打開他, 打開後先選到elements, 像這樣:

Chrome devtools elements

這邊標示了四個部分, 先點選了1, 再用滑鼠游標點你想知道的元件(以這邊來說是那個視訊框 2 的地方), 然後他就會幫你跳到相關的HTML位置(如3), 而4所標示出的是css屬性

把object這部份點開, 果然, 在flashvars那邊我們可以找到"cid=10519268&aid=6467776&pre_ad=0"這樣的字串, 表示"6467776"的確是某種叫aid的東西, 但, 這不代表找到結案了!! 我們再用curl檢查一下:

curl http://www.bilibili.com/video/av6467776/ --compressed

抓原始的html檔來比對一下(可以把這指令的輸出存成檔案在來看會方便點), 怎麼沒"object"這標籤呀?到哪去了?可見剛剛那段html是某段javascript去產生的

再回頭看DevTools上object那段, 可以發現它是一個div包起來的, 這div的class是scontent, id是bofqi, 再回頭去看原始的HTML, 整段也只有一個這樣的block, 內容是這樣:

    <div class="scontent" id="bofqi">
    <div id='player_placeholder' class='player'></div>
<script type='text/javascript'>EmbedPlayer('player', "http://static.hdslb.com/play.swf", "cid=10519268&aid=6467776&pre_ad=0");</script>
    </div>

OK, 這邊就很容易可以確定從scontent這區塊就可以找到兩個id - aid和cid , 但這能做什麼用? 還不知道

接著切換到Network那邊去, 然後再重新整理一下頁面:

Chrome devtools network

左下角的部分是瀏覽器在畫出這個頁面所載入的內容跟檔案, 一開使用時序排序的, 當然你可以用其他方式排序, 這邊就是可以挖寶的地方了

先看到上面那一堆長長短短的線, 這是瀏覽器載入檔案的時間線, 點選這邊可以只看特定時間區間的部分, 最後面可以發現有一條長長的藍線, 那可能就是視訊檔了(因為通常較大), 因此我們可知這視訊檔的URL是

http://61.221.181.215/ws.acgvideo.com/3/46/10519268-1.flv?wsTime=1475663926&wsSecret2=893ba83b8f13d8700d2ae0cddab96c55&oi=3699654085&rate=0&wshc_tag=0&wsts_tag=57f467b8&wsid_tag=dc843dc5&wsiphost=ipdbm

但這串怎麼來的, 依我們手上只有兩個id的資訊是拼湊不起來的, 它一定從某個地方由這兩個id轉換出來的(合理的猜測), 因此, 我們可以再把aid, cid拿去搜尋檔案(點選檔案列表那區塊, 按ctrl-f或command-f開搜尋框)

一個個看, 找出可能的檔案, 由於aid可以找出22個結果而cid只有10個, 從cid開始找起會比較簡單點, 這邊篩選出幾個可能性:

  1. http://interface.bilibili.com/player?id=cid:10519268&aid=6467776 - 回傳是一個xml, 有一些相關資訊, 但沒影片位址
  2. http://interface.bilibili.com/playurl?accel=1&cid=10519268&player=1&ts=1475635126&sign=e1e2ae9d2d34e4be94f46f77a4a107ce - 從這回傳裡面有個durl > url, 跟上面url比對, 似乎就是他了, 後面再來講這段
  3. http://comment.bilibili.com/playtag,10519268 - 這應該是"看过该视频的还喜欢"裡的內容
  4. http://comment.bilibili.com/10519268.xml - 喔喔喔, 看起來這就是彈幕檔喔!
  5. http://comment.bilibili.com/recommendnew,6467776 - 這似乎是推薦視頻的內容
  6. http://api.bilibili.com/x/tag/archive/tags?jsonp=jsonp&aid=6467776&nomid=1 - 看起來這是tag

看來, 2 應該就是我們所要的了, 不過這邊有兩個麻煩, 一個是…我討厭XML!!!!!不過這好像還好, 似乎有個HTML5版本, 點點看好了, 果不其然, 發現另一個:

https://interface.bilibili.com/playurl?cid=10519268&appkey=6f90a59ac58a4123&otype=json&type=flv&quality=3&sign=571f239a0a3d4c304e8ea0e0f255992a

表示我們是可以用otype=json來抓取json格式的, 但後面這個更麻煩了, 那個sign是什麼東西? 從他有個appkey來看, 合理的猜測, 他是某種API的signature, 通常這種東西的規則是把所有的參數先依名字排序成新的query string, 加上某個secret, 再算出他的MD5即是他的sign

但如果真是這樣, 這下有點麻煩, 到哪裡找這個secret, 不過凡走過必有痕跡, 這串既然是由瀏覽器端產生的, 那應該會在哪裡找到點線索, 或許可以先用appkey的內容去每個javascript檔案搜尋吧

不過, 不出所料, 找不到, 那, 還有一個可能性, 它寫在flash內, 從上面抓到的資訊來看, 他的flash檔案應該是: http://static.hdslb.com/play.swf

可以把它抓回來反編譯(decompile), 有個工具叫JPEX Flash decompiler的, 可以做到這件事

JPEX

在script裡面有它的程式原始碼, 應該可以在裡面找到, 不過有點辛苦, 因為你也沒辦法從appkey找到那個secret, 這邊直接跳轉答案, 應該就如同截圖所示是"com.bilibili.interfaces.getSign"這邊, 只是被混淆到很難看, 看得很頭痛, 會短命的

理論上, 把這段源碼翻譯一遍後應該就解決了, 但一來我看不太懂action script, 二來我實在懶得看, 想偷懶, 有沒作弊的方法? 凡走過必留下痕跡嘛, 一定還會有前人走過這條路, 所以直接把"6f90a59ac58a4123"這串appkey拿去搜尋, 果然, 找到secret了, 寫個程式驗證一下, 果然是沒錯的

範例三: 楓林網

楓林網是一個非法的電視劇來源, 有相當齊全的電視劇內容, 我這個是之前寫的一個工具了, 可以把一整部劇可以抓回本地端, 原始碼跟使用方法在這:

這邊就不再說明怎麼使用它了, 主要著重它是怎做出來的, 這一個跟上面幾個不一樣的地方在於我是用nodejs寫的, 之所以用nodejs是有原因的, 後面再解釋, 主要的程式碼在88.js

楓林網的電視劇集的網址長成這樣: http://8drama.com/178372/ , 當然178372又是ID了, 但它裡面所有的東西ID長得都是一個樣, 一整部劇跟單一集的ID都是同一個格式, 所以是無法從ID判斷出他是哪類

一樣打開Chrome DevTools, 點選到每一集的列表區塊, 我們可以發現在"“裡面的可以找到每一集的URL, 而他的格式都是http://8drama.com/(ID), 所以我們可以輕易的用regular expression分辨出來

接下來就要掃每一集的內容把影片檔找出來了

當然可以用前面的方法試試看, 不過那方法在這邊沒啥用, 因為這邊影片的網址編碼是用好多亂七八糟的javascript堆積起來, 沒記錯的話, 我是從video.js找到線索的(這有點時間之前做的了, 有點沒印象了)

所以該怎麼面對那一大段javascript的code呢? 這就是我這工具為何選nodejs的原因了, 抄過來就好了!!! 去除掉跟瀏覽器相關的部分, 它就是不折不扣nodejs可以跑的程式碼, 所以這也就是88.js一些怪怪程式碼的由來

除了這版本外, 我本來也有想要做一個Java的版本, 做了一半, 用Java也是可以用類似的技巧, 不過就得要導入Javascript的runtime了, 而我採用的是Mozilla Rhino

更進階一點的

做爬蟲大部分的時間都是要跟html, javascript, css這類的東西搏鬥, 但其實很多東西本來就是原本瀏覽器就會處理的, 因此如果可以直接用瀏覽器或甚至是WebKit來處理, 可以處理的事應該就會多一點, 這時候就可以利用所謂的Headless browser來簡化, 這種東西本來是用在網頁自動化測試的, 不過, 用在這應用也一樣強大, 這類的解決方案有:

  1. Phantomjs - 這是以WebKit為基礎的, 我比較常用
  2. Slimerjs - 這是以Mozilla Geco為基礎的
  3. Selenium
  4. Webloop - Go版本的

關於Phantomjs的應用, 我很久之前已經有寫過一篇了:

颱風天的宅code教學: 抓漫畫

Android上呢?

Java上, 大多是用jsoup來解析html的, 像我之前這篇:

[Android] 土製Play store API

當然應該也是可以用Webkit/WebView做成headless的解決方案, 不過這部份我目前還沒試過, 留待以後有機會再試吧