用 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哦, 還有開頭那個一行爬蟲能活5分鐘的樣子...單線程的話...
推薦閱讀:
※在 KVM 中測試 IPv6 網路:第 2 部分
※進程間的通信方式:簡介
※關於博彩公司BET365的限制問題
※BA網路matlab代碼及度分布可視化1
※Linux 入侵檢測分析技術,純乾貨!
TAG:WolframMathematica | 計算機網路 | 網頁爬蟲 |