DIY技術資訊抓取工具的實踐與研究

好未來技術團隊發表於2020-04-03

DIY技術資訊抓取工具的實踐與研究

前言

相信每一個技術人員都有周期性獲取技術資訊的訴求,而獲取的方式也多種多樣。例如,用資訊類APP,進行RSS訂閱,參加行業大會,深入技術社群,訂閱期刊雜誌、公眾號,等等,都是可選的方式。這些方式看到資訊的成本都很低,有“開箱即得”的感覺。但缺點也很明顯,有點像“大班課”,可以滿足一類人的需求,但難較好地滿足每個參與者的個性化訴求。通過這些方式,要想真正拿到自己所需要的資訊的成本並不低(雖然智慧推薦在往滿足個性化訴求方面迭代,但離期待仍有較大的差距)。對於個性化訴求,最簡單的方式就是你感興趣哪方面的內容就去逐一主動檢索或者瀏覽,但這種方式的成本顯然太高。


核心的問題是,上面的兩大類路徑,都不是很懂你(瞭解你的意圖和訴求)。而你需要一個既懂你,成本又不是太高的方式。


一、對於技術資訊獲取DIY的框架性思考


相信在當前相當一段時期內,最適合的個性化資訊獲取方式仍然是工具+人工相組合的方式。相比純工具的演算法推薦,一些付費資訊渠道已經在(智慧)工具的基礎上,對資訊進行了人工的篩選、加工處理,質量會更好。如果你是程式設計師,自己編寫一些小爬蟲,在其中注入自己的喜好與智慧,不失為一種懂你且成本不高的方式。而且通過這種方式,你將獲得很好的自我掌控感。本文中,筆者就著重介紹這種方式。值得提醒的是,本文所涉內容,僅為學習討論技術,切勿用作非法用途。


具體來說,分為四部分(如圖1.1所示):


DIY技術資訊抓取工具的實踐與研究


圖1.1


第一,自己控制訊息來源


你可以根據自己的經驗積累,在合法合規的前提下來選取訊息來源。這個選擇的維度可以很多樣,包括質量可靠性、資訊的前瞻性、興趣匹配度、研究方向匹配度、資訊生產頻率、資訊的新穎度,等等。


第二,自己編寫採集和篩選演算法


選定了一些採集渠道,你就可以自己編寫採集和篩選演算法了。採集週期、篩選規則、所需內容項,等等,都可以自己控制。如果你對資料處理、人工智慧等很瞭解,相信還有更多的發揮空間。


第三,自己控制閱讀和互動體驗


由於閱讀是一個長期的過程,對於優質的體驗其實有著很強的需求。難受的閱讀體驗是非常不利於資訊的快速獲取的,甚至會打消獲取資訊的興趣。比如,下面這兩張圖,圖1.2左邊是某頭條的資訊介面,右邊是微信讀書的閱讀介面。


DIY技術資訊抓取工具的實踐與研究

圖1.2


相形之下,作為閱讀者,我個人更喜歡微信閱讀的簡潔,而不太喜歡某頭條那些次要元素的干擾。


第四,自己控制迭代優化


自己既是消費者也一定程度是資訊流通控制者的好處就是:自己可以站在結果環節對資訊獲取全流程進行自主評價,回溯作用到前面的環節,從而形成正向作用閉環。


這麼做有什麼收益呢?


首先,是獲得有價值的資訊。


這一點無需多言。


其次,有助於資訊獲取能力的提升。


就拿技術人員來說,這麼做可以更高效地、持續地獲取滿足個性化訴求的高價值資訊,在對外部技術世界持續保持關注中獲得持續性地成長與提升。

1)關於資訊來源:你將自己總結出一份最有價值的資訊的來源渠道列表,提高資訊的獲取效率,能以較快的速度接觸到相對可靠的資訊。

2)關於資訊處理:你將沉澱出自己的一份或簡單或複雜的資訊採集和篩選演算法,提升資訊的鑑別能力,增強資訊處理的能力。

3)關於資訊體驗:你將獲得適合你自己的資訊獲取、閱讀、互動體驗,增強閱讀興趣和減少疲勞。


第三,有助於進行技術探索,提升技術應用能力。


在這個過程中,實際上也是自己在運用技術解決實際問題的探索過程,可以作為技術甚至產品建構探索的實驗田。比如說,Flutter這一技術有很多公司在進行嘗試和應用,但是你所做的專案暫時還是用的Electron做的,目前並沒有遷移到Flutter的打算。那麼如果你對Flutter感興趣的話,完全可以把採集到的技術資訊嘗試用Flutter做成一個APP,先試水一下怎麼用(只是舉個“栗子”,如果你恰好真感興趣的話,後面有彩蛋一枚,繼續往下看準能找到?)。這樣就相當於是先期業餘做了一些儲備和實踐。


二、對於技術資訊獲取DIY的實踐探索


上面囉嗦了這麼多,還是講點實在的吧。我們們來真實地爬取點技術資訊。要抓取的內容存在形式是多種多樣的,有的被內容服務端直接渲染到了HTML頁面上,有的則是在頁面中通過JavaScript請求資料,然後再渲染出來的。


首先來看第一種。


1、HTML頁面中內容的抓取


第一步,資訊來源的選擇。


要不我們就比較有代表性的網際網路公司BAT裡隨便找一家吧,看看他們都有些什麼高價值的技術資訊。不如就選那個比較高調(非常樂於向業界分享自己技術)的阿里巴巴,因為高調的可能比較好找。他們有個雲棲社群,裡面有個欄目叫阿里技術(https://yq.aliyun.com/articles/721143),這是一個一直在有規律更新,而且文章質量不錯的欄目,介面如下所示。


DIY技術資訊抓取工具的實踐與研究

圖2.1


第二步,資訊的採集和篩選。


假設我們準備爬取最近一週阿里技術這個欄目下都有些什麼新的文章釋出。我們主要獲取其標題、文章連結地址、釋出時間和文章簡介,希望只抓取最近7天內釋出的文章。即期望爬取出來的結果如圖2.2所示。

DIY技術資訊抓取工具的實踐與研究 圖2.2


目標清楚了,下一步就是怎麼實現,筆者選擇使用Node.js。這裡需要介紹用到的兩個工具:request-promise(https://www.npmjs.com/package/request-promise)和cheerio(https://www.npmjs.com/package/cheerio)。所以首先你需要用 yarn init 命令建立一個專案,再用 yarn add request request-promise cheerio 命令安裝上這幾個依賴模組。


關於request-promise,官方介紹是:


The simplified HTTP request client 'request' with Promise support. Powered by Bluebird.


通過request-promise,可以很輕易地抓到頁面的HTML,如下所示:


const rp = require('request-promise');rp(' // 略去了地址   .then(function (htmlString) {   // Process html...     }) .catch(function (err) {  // Crawling failed...     });複製程式碼


抓到HTML後,我們還是希望對其進行處理,把其中的我們所需要的標題、文章連結地址和文章簡介等資訊提取出來。這時需要用到另一個工具——cheerio。用它與request-promise結合,可以讓對於抓取到的HTML的處理基本上像用jQuery那樣進行。因為cheerio實現了jQuery的核心子集。兩者結合後的用法如下:


const rp = require('request-promise');const cheerio = require('cheerio');const targetURL = ' // 略去了地址  const options = {  uri: targetURL,  transform: (body) => {    return cheerio.load(body);  }};function getArticles() {  rp(options)    .then(($) => {      // Process html like you would with jQuery...       console.log($('title').text());    })    .catch((err) => {      // Crawling failed or Cheerio choked...    });}// 入口getArticles();複製程式碼


上面程式碼中,


console.log($('title').text())複製程式碼


會log出來頁面title標籤內部的文字,就像使用jQuery操作頁面DOM一樣。


接著我們就可以用Chrome開啟阿里技術(https://yq.aliyun.com/articles/721143)頁面,藉助Chrome DevTools輕而易舉找到文章的標題所對應的HTML元素(如圖2.3所示)。進而通過將上述程式碼中的


console.log($('title').text())複製程式碼


這一行替換為:


console.log console.log($('.yq-new-item h3 a').eq(1).text())($('.yq-new-item h3 a').eq(1).text())複製程式碼


從而log出來其中一篇技術資訊文章的標題。


DIY技術資訊抓取工具的實踐與研究

圖2.3


舉一反三,用同樣的方法可以獲取到文章連結地址和文章簡介。但是,我們還想獲取到每篇文章的釋出時間,但是當前頁面中並沒有,怎麼辦呢?點進去每篇文章的連結,我們發現文章內部是有這個資訊的(如圖2.4)。於是,實現思路就有了。每抓取到一篇文章的連結之後,再針對抓到的連結地址再進行一次抓取,把該篇文章中的釋出時間也抓取出來。


DIY技術資訊抓取工具的實踐與研究 圖2.4


另外,因為Promise在程式碼中用多了之後,看起有點醜陋,所以我們將之改成用async和await的方式實現。並且把抓取到的資訊寫入到一個JSON檔案(result.json)中。最終實現的演示程式碼如下:

/** * 爬取技術資訊學習舉例1 */const fs = require('fs');const rp = require('request-promise');const cheerio = require('cheerio');const  targetURL = 'https://xxxxxxxxxxxxxx'; // 略去了地址const maxDeltaDay = 7;/** * 抓取目標網頁中的技術資訊 * @param {string} url - 抓取的目標網頁的網址  * @param {number} maxDeltaDay - 抓取距離當前時間多少天以內的資訊  */ async function getArticles(url, maxDeltaDay) {     const options = generateOptions(url);   const $ = await rp(options);     const elements = $('.yq-new-item h3 a');      // 拿到包含文章標題、連結等的標籤     const result = [];     const promises = [];   elements.map((index, el) => {         const $el = $(el);         const linkObj = {};              // 獲取標題和連結         linkObj.title = $el.text();         const link = $el.attr('href');         linkObj.link = `https://yq.aliyun.com${link}`;              // 處理文章簡介     let brief = $el.parent().parent().find('.new-desc-two').text();         brief = brief.replace(/\s*/g, '');         linkObj.brief = brief;         promises.push(          getDeltaDay(linkObj.link).then((deltaDay) => {                 if (deltaDay < maxDeltaDay) {                     linkObj.deltaDay = deltaDay;                     result.push(linkObj);                 }             })     );   });        Promise.all(promises).then(() => {         if (result.length) {             console.log(result);             result.sort((a, b) => {                 return a.deltaDay - b.deltaDay;             })                    fs.writeFileSync('./result.json', JSON.stringify(result));         }     });}/**  * 生成用於發起request-promise抓取用的options引數  * @param {string} url - 抓取的目標地址  */function generateOptions(url) {    return {        uri: url,        transform: (body) => {            return cheerio.load(body);        }    };}/**  * 抓取文章的釋出時間  * @param {string} url - 文章的地址  */async function getDeltaDay(url) {    const options = generateOptions(url);    const $ = await rp(options);    const $time = $('.yq-blog-detail .b-time');    const dateTime = $time.text();    let deltaDay = (new Date() - new Date(dateTime)) / (24 * 60 * 60 * 1000);    deltaDay = deltaDay.toFixed(1);    return deltaDay;}// 入口getArticles(targetURL, maxDeltaDay);複製程式碼


其中,getDeltaDay函式就是用來處理髮布時間抓取的。我們最終的目的不是抓取該文章的釋出時間,而是看該釋出時間距離當前時間之間的差值是不是在7天之內。當然,如果想進一步篩選的話,你還可以抓取到閱讀量、點贊量、收藏量等來進行判斷。


2、資料介面中內容的抓取


上面是這個對於靜態HTML頁面上資料的抓取。下面再來看第二種,對於介面中資料的抓取。這裡以對知名技術社群掘金的資料抓取為例。


DIY技術資訊抓取工具的實踐與研究 圖2.5


如圖2.5所示,掘金的資訊分了推薦、後端、前端、Android、iOS、人工智慧、開發工具、程式碼人生、閱讀等多個類目。通過Chrome DevTools檢視網路請求我們發現,頁面中的文章列表資料是通過(https://web-api.juejin.im/query)這個介面POST請求返回的。且每個類目下的文章列表資料都是來自這同一個介面,只是請求的時候,Request Payload中的variables下的category(類目ID)欄位不一樣,如圖2.6、圖2.7所示。

DIY技術資訊抓取工具的實踐與研究

圖2.6

DIY技術資訊抓取工具的實踐與研究 圖2.7


所以,整體思路就是,建立一個類目名稱和類目ID的map,使用不同的類目ID逐一去呼叫上述介面。具體的抓取工具仍然採用上面用過的request-promise。由於事先同樣並不複雜,所以不做過多解釋,直接貼上程式碼:

/** * 爬取技術資訊學習舉例2 */const rp = require('request-promise');const fs = require('fs');// 類目對應的IDconst categoryIDMap= {   '推薦': '',  '後端': '5562b419e4b00c57d9b94ae2',    '前端': '5562b415e4b00c57d9b94ac8',    'Android': '5562b410e4b00c57d9b94a92',    'iOS': '5562b405e4b00c57d9b94a41',    '人工智慧': '57be7c18128fe1005fa902de',    '開發工具': '5562b422e4b00c57d9b94b53',    '程式碼人生': '5c9c7cca1b117f3c60fee548',    '閱讀': '5562b428e4b00c57d9b94b9d'};/** * 生成request-promise用到的options引數  * @param {string} categoryID - 類目ID  */function generateOptions(categoryID) {    return {        method: 'POST',        uri: ' // 略去了地址       body: {            'operationName': '',            'query': '',            'variables': {                'tags': [],                'category': categoryID,                'first': 20,                'after': '',                'order': 'POPULAR'            },            'extensions': {                'query': {                    'id': '653b587c5c7c8a00ddf67fc66f989d42'                }            }        },        json: true,        headers: {            'X-Agent': 'Juejin/Web'        },    }};/** * 獲取某一類目下的資訊資料  * @param {string} categoryID - 類目ID  */async function getArtInOneCategory(categoryID, categoryName) {    const options = generateOptions(categoryID);    const res = await rp(options);    const data = res.data.articleFeed.items.edges;    let currentCategoryResult = [];    data.map((item) => {        const linkObj = {};        const {            title,            originalUrl,            updatedAt,            likeCount    } = item.node;            linkObj.title = title;        linkObj.link = originalUrl;        linkObj.likeCount = likeCount;        linkObj.category = categoryName;            let deltaDay = (new Date() - new Date(updatedAt)) / (24 * 60 * 60 * 1000);        deltaDay = deltaDay.toFixed(1);            if (deltaDay < 7) {            linkObj.deltaDay = deltaDay;            currentCategoryResult.push(linkObj);        }    });    return currentCategoryResult;}/** * 獲取所有類目下的資訊資料  */function getAllArticles() {    const promises = [];    let result = [];    Object.keys(categoryIDMap).map((key) => {        const categoryID = categoryIDMap[key];        promises.push(getArtInOneCategory(categoryID, key).then((res) => {            result = result.concat(res);        }));   });      Promise.all(promises).then(() => {        fs.writeFileSync('./result2.json', JSON.stringify(result));    });}  // 入口getAllArticles();複製程式碼


抓取到的結果如圖2.8所示,主要抓取了標題、連結、點贊數、類目以及釋出距離當前的時間差(天為單位):


DIY技術資訊抓取工具的實踐與研究 圖2.8


3、微信公眾號內容的抓取


除了上述兩類內容的抓取外,還有一類資訊的抓取可能也是比較常遇到的,就是對於微信公眾號內容的抓取。比如,以對於“xx早讀課”這一公眾號的抓取為例。微信公眾號的內容如果直接從微信平臺抓,需要登入,估計很容易被封號。因此,可以嘗試另一種方法——通過搜狗搜尋所提供的對於微信公眾號的搜尋結果進行抓取。


首先,通過(https://weixin.sogou.com/weixin?type=1&s_from=input&query=%E5%89%8D%E7%AB%AF%E6%97%A9%E8%AF%BB%E8%AF%BE&ie=utf8&_sug_=y&_sug_type_=&w=01019900&sut=6202&sst0=1571574212479&lkt=0%2C0%2C0)檢索得到該公眾號的英文ID。如圖2.9所示。

DIY技術資訊抓取工具的實踐與研究 圖2.9


接著用該公眾號英文ID搜尋該公眾號的最新文章,並通過點選“搜尋工具”彈出的篩選皮膚中選擇“一週內”,過濾出最近一週的文章(如圖2.10所示)。之所以要用英文ID是為了讓搜出來的結果只來自於該公眾號,資訊更為純粹。

DIY技術資訊抓取工具的實踐與研究 圖2.10


不過,很遺憾,這些資料都是伺服器直接渲染在了HTML頁面中,而不是從介面中返回來的。而且,在呈現這些資訊之前,還得經過圖2.10所示的幾步互動操作。所以不能像上面兩種方法那樣抓取資料。具體實現上可以採用可以puppeteer。puppeteer 是一個Chrome官方出品的headless Chrome node庫。它提供了一系列的API, 可以在無UI的情況下呼叫Chrome的功能, 適用於爬蟲、自動化處理等各種場景(如自動化測試),詳細的使用可以參考官方文件(https://github.com/GoogleChrome/puppeteer)。篇幅所限,這裡就不展開介紹具體實現了。值得注意的是,搜狗搜尋做了很多反爬蟲的工作,所以需要注意:


1)在puppteer的lunch的時候,需要加上headless: false選項,避免要你輸入驗證碼。如下所示:


const browser = await puppeteer.launch({      headless: false});複製程式碼


2)抓取次數宜儘量少,否則當你頻繁抓取時,對方就會要求你輸入驗證碼,這時候抓取工作就無法繼續了。

即便你注意了這兩點,你仍然可能遇到被識別為爬蟲的情況。所以,權當是對puppeteer的學習嘗試吧,畢竟這個工具功能還挺強大的,在前端自動化測試等領域,大有可為。


三、延申性的思考


上面對於資訊的採集做了一些具體的介紹。對於資訊可以做進一步的加工處理,以便更好地自己進行學習和研究,這裡提供一點思路。


DIY技術資訊抓取工具的實踐與研究

圖3.1


如圖3.1所示,通過後臺服務從訊息來源池裡採集到資料之後,可以把資料建立一個庫儲存起來,提供一些資料服務介面供前端業務使用。你可以對資料進行處理、加工,視覺化出來,比如直接以前端Web頁面的形式呈現,也可以做一個原生的APP。甚至加上一些反饋渠道,對資訊進行評價,從而從評價資料反推訊息來源渠道的質量。


至於根據喜好來控制閱讀和互動體驗方面,一般來說,有一些共同的準則。比如,簡潔的整體風格,突出內容本身的沉浸式、無打擾感受;合適的字號、行間距;優美的字型;可調、護眼的背景顏色;提供互動渠道,讓閱讀過程中有人共同參與而不孤獨;操作的流暢性,等等。這方面感興趣的話可以參考下這篇文章對於微信閱讀的分析(http://www.woshipm.com/evaluating/977491.html),這裡不過多贅述。


總結


本文首先分析一些常見資訊獲取方式的優缺點,分享了進行技術資訊獲取DIY的框架性思考,闡明瞭其價值。然後藉助三個具體的抓取案例剖析了抓取思路,並做了部分演示性的程式碼舉例。最後就該主題進行了延申性的思考,基於此可以DIY出來一款簡單的產品,甚至一個系統。


末了,關於Flutter的彩蛋找到了嗎?(在圖2.2中第二條資訊哦)?


作者簡介

鄔明亮為好未來前端開發專家

---------- END ----------

招聘資訊


好未來技術團隊正在熱招視覺/影像演算法、後臺開發、運維開發、後端開發Golang、web前端開發等高階工程師崗位,大家可點選本公眾號“技術招聘”欄目瞭解詳情,歡迎感興趣的夥伴加入我們!


也許你還想看

DIY技術資訊抓取工具的實踐與研究


相關文章