用 Mathematica 寫一個爬蟲

用 Mathematica 寫一個爬蟲

來自專欄 CODE Viens Vanité84 人贊了文章

所謂爬蟲, 說白了就是替你瀏覽網頁, 然後幫你把你要的信息存下來的腳本.

下面我們就以 B站 為例, 寫一個靜態爬蟲.

構造請求

所謂靜態爬蟲, 就是只管靜態部分, 不管各種載入的簡單爬蟲.

通過分析網頁的請求可以找到各種各樣的介面, 然後直接訪問就行了.

比如 tag 標籤, 可以發現瀏覽器向http://api.bilibili.com/tags/info_description?id=12發送了一個 GET 請求, 那麼我們訪問這個地址就能獲得數據了.

>> URLExecute["http://api.bilibili.com/tags/info_description?id=12","RawJSON"]<| "code"->0, "result"-><| "tag_id"->1, "name"->"公告", "cover"->"", "subscribed"->0, "visit_count"->433, "subscribe_count"->17 |>|>

ok, 爬蟲教程結束...


好吧開玩笑的, 這種一行的爬蟲半分鐘都活不過去的...

爬蟲重點不是在爬, 而是如何與反爬程序鬥智斗勇.

雖說直接寫地址也沒關係, 但是如果他們突然改了加了驗證啥的就不好玩了...

所以最好充分解耦, 用一個 HTTPRequest 來替代.

TagURL[tid_]:=HTTPRequest[ "http://api.bilibili.com/tags/info_description?id=" <> ToString[tid], TimeConstraint->5];

有時候會很複雜, 又要 POST, 又要查 appkey 啥的, 比如這個用戶信息.

MemberURL[mid_] := HTTPRequest[ "https://space.bilibili.com/ajax/member/GetInfo", <| "Method" -> "POST", "Headers" -> { "content-type" -> "application/x-www-form-urlencoded;charset=UTF-8", "user-agent" -> RandomSample@$BilibiliLinkUA, "Referer" -> "https://space.bilibili.com/" <> ToString[mid] }, "Body" -> "mid=" <> ToString[mid] <> "&csrf=" |>, TimeConstraint -> 5];

然後搞個 UA 表, 像這樣的:

$BilibiliLinkUA = { "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)", "Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10"};

這就穩了, 對面再也沒法通過 URL 請求來判定你的身份了.

雖說 Heads 都是君子協議, 一般也不看, 但是有的介面他偏要驗人也沒辦法...

任務劃分

接下來切分任務, 任務的最小單位是一次請求, 最大單位就是所需要的一切數據了.

但是呢, 我們也不指望它一次完成, 講道理我們甚至希望他是分散式的...

好吧分散式這個有點超綱了, 但我們還是要按這個標準來寫.

假如現在 B站 有800萬 tag 吧.

我們把這個大任務劃分為小任務, 100萬為一個批次 , 每個批次分 1000 個區塊, 每個區塊包含 1000 個請求.

分散式情況下這個區塊就是發給每個節點的任務, 每個節點本身是並行或者非同步的.

批次這個概念是沒必要的, 但是單機的情況下有必要, 個人 PC 的內存和硬碟讀寫都有限, 比如現在用戶數快4億了, 單機甚至沒法開這麼大個隊列出來, 只能分批處理.

我們來看如何寫一個區塊:

TagGet[i_, t_ /; t <= 0] := AppendTo[i, $fail];TagGet[i_, try_] := Block[ {get = URLExecute[TagURL@i, "RawJSON"]}, If[TagGetQ[get], AppendTo[$block, Inactive[TagGet][i, try - 1]]; retry++; Return[Nothing] ]; finish++; TagFormat[get["result"]]];

i 表示 id 編號, try 表示剩餘的重試次數, 降到0就不再重試了.

這裡還有幾個輔助函數:

$fail = {};retry = 0;finish = 0;TagGetQ[get_] := Or[FailureQ@get, get["code"] != 0];TagFormat[asc_] := <| "ID" -> asc["tag_id"], "Name" -> asc["name"], "Count" -> asc["visit_count"], "Watch" -> asc["subscribe_count"]|>;

retry 和 finish 來自最上層, 用於監督進度.

用 $block 來保存這次的任務隊列, $fail 用來保存重試還失敗的任務.

Format 格式化輸出, 反正都在等 IO, 不如先把要的都提取出來再說.

  • 如果是用資料庫的話這裡寫插入操作就行, 不過我不建議這麼搞, 一是不好處理異常, 二是雙向等 IO 沒法優化.
  • 如果要限速或者隨機等待的話可以末尾加這個函數, 但是如果有代理就可以不用等待了.

TagRange[a_, b_] := Block[ {$fail = {}, $this = {}, $now = Now, $block, $query}, $block = Table[Inactive[TagGet][i, 3], {i, a, b}]; While[Length[$block] > 0, {$query, $block} = {RandomSample[$block], {}}; $this = Join[$this, Activate[$query]] ]; <| "Now" -> $now, "Data" -> $this, "Failed" -> $fail, "Time" -> Now - $now |>];

然後把這個函數分發一下就行了, 無論是並行還是分散式都行.

使用代理

代理這個比較麻煩了, Mathematica 沒有內置代理選項, 那就只能底層 hack 一下了.

不過查查源碼還是不難找到的, 這個$InternetProxyRules就是代理設置.

$WhoAmI[ip_ : Nothing, port_ : Nothing] := Block[ {$InternetProxyRules = { "UseProxy" -> True, "HTTP" -> {ip, port}, "HTTPS" -> {ip, port}, "FTP" -> {}, "Socks" -> {}, "UseWPAD" -> True }}, URLExecute[HTTPRequest["http://ip-api.com/json", TimeConstraint -> 2], "RawJSON"]];

可以直接測試一下. 啊不錯, 我已經到德國去了.

代理比較推薦的方式是掛個SS, 開本地代理, 全局代理以及隨機策略負載均衡, 這樣好處就是不用變這個設置, 有20個以上節點開滿線程都不會被 ban IP 了.

反正我平常每個月流量也用不了幾百個G, 速度也可以, 比野代理高到不知道哪裡去了...


專欄高亮不太好看, 我放了一個 notebook 的版本在 github 上:

https://github.com/Moe-Net/BilibiliLink/blob/master/Packages/Crawlers/TagCrawlers.nb?

github.com

哦, 還有開頭那個一行爬蟲能活5分鐘的樣子...單線程的話...


推薦閱讀:

在 KVM 中測試 IPv6 網路:第 2 部分
進程間的通信方式:簡介
關於博彩公司BET365的限制問題
BA網路matlab代碼及度分布可視化1
Linux 入侵檢測分析技術,純乾貨!

TAG:WolframMathematica | 計算機網路 | 網頁爬蟲 |