新聞中心
我一直覺(jué)得,爬蟲(chóng)是許多web開(kāi)發(fā)人員難以回避的點(diǎn)。我們也應(yīng)該或多或少的去接觸這方面,因?yàn)榭梢詮呐老x(chóng)中學(xué)習(xí)到web開(kāi)發(fā)中應(yīng)當(dāng)掌握的一些基本知識(shí)。而且,它還很有趣。

我是一個(gè)知乎輕微重度用戶,之前寫(xiě)了一只爬蟲(chóng)幫我爬取并分析它的數(shù)據(jù),我感覺(jué)這個(gè)過(guò)程還是挺有意思,因?yàn)檫@是一個(gè)不斷給自己創(chuàng)造問(wèn)題又去解決問(wèn)題的過(guò)程。其中遇到了一些點(diǎn),今天總結(jié)一下跟大家分享分享。
它都爬了什么?
先簡(jiǎn)單介紹下我的爬蟲(chóng)。它能夠定時(shí)抓取一個(gè)問(wèn)題的關(guān)注量、瀏覽量、回答數(shù),以便于我將這些數(shù)據(jù)繪成圖表展現(xiàn)它的熱點(diǎn)趨勢(shì)。為了不讓我錯(cuò)過(guò)一些熱門(mén)事件,它還會(huì)定時(shí)去獲取我關(guān)注話題下的熱門(mén)問(wèn)答,并推送到我的郵箱。
作為一個(gè)前端開(kāi)發(fā)人員,我必須為這個(gè)爬蟲(chóng)系統(tǒng)做一個(gè)界面,能讓我登陸知乎帳號(hào),添加關(guān)注的題目、話題,看到可視化的數(shù)據(jù)。所以這只爬蟲(chóng)還有登陸知乎、搜索題目的功能。
然后來(lái)看下界面。
下面正兒八經(jīng)講它的開(kāi)發(fā)歷程。
技術(shù)選型
Python得益于其簡(jiǎn)單快捷的語(yǔ)法、以及豐富的爬蟲(chóng)庫(kù),一直是爬蟲(chóng)開(kāi)發(fā)人員的***??上也皇臁.?dāng)然最重要的是,作為一名前端開(kāi)發(fā)人員,node能滿足爬蟲(chóng)需求的話,自然更是***。而且隨著node的發(fā)展,也有許多好用的爬蟲(chóng)庫(kù),甚至有 puppeteer 這樣直接能模擬Chrome訪問(wèn)網(wǎng)頁(yè)的工具的推出,node在爬蟲(chóng)方面應(yīng)該是妥妥能滿足我所有的爬蟲(chóng)需求了。
于是我選擇從零搭建一個(gè)基于koa2的服務(wù)端。為什么不直接選擇egg,express,thinkjs這些更加全面的框架呢?因?yàn)槲覑?ài)折騰嘛。而且這也是一個(gè)學(xué)習(xí)的過(guò)程。如果以前不了解node,又對(duì)搭建node服務(wù)端有興趣,可以看我之前的一篇文章-從零搭建Koa2 Server。
爬蟲(chóng)方面我選擇了 request + cheerio 。雖然知乎有很多地方用到了react,但得益于它絕大部分頁(yè)面還是服務(wù)端渲染,所以只要能請(qǐng)求網(wǎng)頁(yè)與接口(request),解析頁(yè)面(cherrio)即可滿足我的爬蟲(chóng)需求。
其他不一一舉例了,我列個(gè)技術(shù)棧
服務(wù)端
- koajs 做node server框架;
- request + cheerio 做爬蟲(chóng)服務(wù);
- mongodb 做數(shù)據(jù)存儲(chǔ);
- node-schedule 做任務(wù)調(diào)度;
- nodemailer 做郵件推送。
客戶端
- vuejs 前端框架;
- museui Material Design UI庫(kù);
- chart.js 圖表庫(kù)。
技術(shù)選型妥善后,我們就要關(guān)心業(yè)務(wù)了。首要任務(wù)就是真正的爬取到頁(yè)面。
如何能爬取網(wǎng)站的數(shù)據(jù)?
知乎并沒(méi)有對(duì)外開(kāi)放接口能讓用戶獲取數(shù)據(jù),所以想獲取數(shù)據(jù),就得自己去爬取網(wǎng)頁(yè)信息。我們知道即使是網(wǎng)頁(yè),它本質(zhì)上也是個(gè)GET請(qǐng)求的接口,我們只要在服務(wù)端去請(qǐng)求對(duì)應(yīng)網(wǎng)頁(yè)的地址(客戶端請(qǐng)求會(huì)跨域),再把html結(jié)構(gòu)解析下,獲取想要的數(shù)據(jù)即可。
那為什么我要搞一個(gè)登陸呢?因?yàn)榉堑顷憥ぬ?hào)獲取信息,知乎只會(huì)展現(xiàn)有限的數(shù)據(jù),而且也無(wú)法得知自己知乎帳戶關(guān)注的話題、問(wèn)題等信息。而且若是想自己的系統(tǒng)也給其他朋友使用,也必須搞一個(gè)帳戶系統(tǒng)。
模擬登陸
大家都會(huì)用Chrome等現(xiàn)代瀏覽器看請(qǐng)求信息,我們?cè)谥醯牡卿涰?yè)進(jìn)行登陸,然后查看捕獲接口信息就能知道,登陸無(wú)非就是向一個(gè)登陸api發(fā)送賬戶、密碼等信息,如果成功。服務(wù)端會(huì)向客戶端設(shè)置一個(gè)cookie,這個(gè)cookie即是登陸憑證。
所以我們的思路也是如此,通過(guò)爬蟲(chóng)服務(wù)端去請(qǐng)求接口,帶上我們的帳號(hào)密碼信息,成功后再將返回的cookie存到我們的系統(tǒng)數(shù)據(jù)庫(kù),以后再去爬取其他頁(yè)面時(shí),帶上此cookie即可。
當(dāng)然,等我們真正嘗試時(shí),會(huì)受到更多挫折,因?yàn)闀?huì)遇到token、驗(yàn)證碼等問(wèn)題。不過(guò),由于我們有客戶端了,可以將驗(yàn)證碼的識(shí)別交給真正的 人 ,而不是服務(wù)端去解析圖片字符,這降低了我們實(shí)現(xiàn)登陸的難度。
一波三折的是,即使你把正確驗(yàn)證碼提交了,還是會(huì)提示驗(yàn)證碼錯(cuò)誤。如果我們自己做過(guò)驗(yàn)證碼提交的系統(tǒng)就能夠迅速的定位原因。如果沒(méi)做過(guò),我們?cè)俅尾榭吹顷憰r(shí)涉及的請(qǐng)求與響應(yīng),我們也能猜到:
在客戶端獲取驗(yàn)證碼時(shí),知乎服務(wù)端還會(huì)往客戶端設(shè)置一個(gè)新cookie,提交登陸請(qǐng)求時(shí),必須把驗(yàn)證碼與此cookie一同提交,來(lái)驗(yàn)證此次提交的驗(yàn)證碼確實(shí)是當(dāng)時(shí)給予用戶的驗(yàn)證碼。
語(yǔ)言描述有些繞,我以圖的形式來(lái)表達(dá)一個(gè)登陸請(qǐng)求的完整流程。
注:我編寫(xiě)爬蟲(chóng)時(shí),知乎還部分采取圖片字符驗(yàn)證碼,現(xiàn)已全部改為“點(diǎn)擊倒立文字”的形式。這樣會(huì)加大提交正確驗(yàn)證碼的難度,但也并非無(wú)計(jì)可施。獲取圖片后,由 人工 識(shí)別并點(diǎn)擊倒立文字,將點(diǎn)擊的坐標(biāo)提交到登陸接口即可。當(dāng)然有興趣有能力的同學(xué)也可以自己編寫(xiě)算法識(shí)別驗(yàn)證碼。
爬取數(shù)據(jù)
上一步中,我們已經(jīng)獲取到了登陸后的憑證cookie。用戶登陸成功后,我們把登陸的帳戶信息與其憑證cookie存到mongo中。以后此用戶發(fā)起的爬取需求,包括對(duì)其跟蹤問(wèn)題的數(shù)據(jù)爬取都根據(jù)此cookie爬取。
當(dāng)然cookie是有時(shí)間期限的,所以當(dāng)我們存cookie時(shí),應(yīng)該把過(guò)期時(shí)間也記錄下來(lái),當(dāng)后面再獲取此cookie時(shí),多加一步過(guò)期校驗(yàn),若過(guò)期了則返回過(guò)期提醒。
爬蟲(chóng)的基礎(chǔ)搞定后,就可以真正去獲取想要的數(shù)據(jù)了。我的需求是想知道某個(gè)知乎問(wèn)題的熱點(diǎn)趨勢(shì)。先用瀏覽器去看看一個(gè)問(wèn)題頁(yè)面下都有哪些數(shù)據(jù),可以被我爬取分析。舉個(gè)例子,比如這個(gè)問(wèn)題: 有哪些令人拍案叫絕的推理橋段 。
打開(kāi)鏈接后,頁(yè)面上最直接展現(xiàn)出來(lái)的有 關(guān)注者 , 被瀏覽 , 1xxxx個(gè)回答 ,還要默認(rèn)展示的幾個(gè)高贊回答及其點(diǎn)贊評(píng)論數(shù)量。右鍵查看網(wǎng)站源代碼,確認(rèn)這些數(shù)據(jù)是服務(wù)端渲染出來(lái)的,我們就可以通過(guò)request請(qǐng)求網(wǎng)頁(yè),再通過(guò)cherrio,使用css選擇器定位到數(shù)據(jù)節(jié)點(diǎn),獲取并存儲(chǔ)下來(lái)。代碼示例如下:
- async getData (cookie, qid) {
- const options = {
- url: `${zhihuRoot}/question/${qid}`,
- method: 'GET',
- headers: {
- 'Cookie': cookie,
- 'Accept-Encoding': 'deflate, sdch, br' // 不允許gzip,開(kāi)啟gzip會(huì)開(kāi)啟知乎客戶端渲染,導(dǎo)致無(wú)法爬取
- }
- }
- const rs = await this.request(options)
- if (rs.error) {
- return this.failRequest(rs)
- }
- const $ = cheerio.load(rs)
- const NumberBoard = $('.NumberBoard-item .NumberBoard-value')
- const $title = $('.QuestionHeader-title')
- $title.find('button').remove()
- return {
- success: true,
- title: $title.text(),
- data: {
- qid: qid,
- followers: Number($(NumberBoard[0]).text()),
- readers: Number($(NumberBoard[1]).text()),
- answers: Number($('h4.List-headerText span').text().replace(' 個(gè)回答', ''))
- }
- }
- }
這樣我們就爬取了一個(gè)問(wèn)題的數(shù)據(jù),只要我們能夠按一定時(shí)間間隔不斷去執(zhí)行此方法獲取數(shù)據(jù),最終我們就能繪制出一個(gè)題目的數(shù)據(jù)曲線,分析起熱點(diǎn)趨勢(shì)。
那么問(wèn)題來(lái)了,如何去做這個(gè)定時(shí)任務(wù)呢?
定時(shí)任務(wù)
我使用了 node-schedule 做任務(wù)調(diào)度。如果之前做過(guò)定時(shí)任務(wù)的同學(xué),可能對(duì)其類似cron的語(yǔ)法比較熟悉,不熟悉也沒(méi)關(guān)系,它提供了not-cron-like的,更加直觀的設(shè)置去配置任務(wù),看下文檔就能大致了解。
當(dāng)然這個(gè)定時(shí)任務(wù)不是簡(jiǎn)單的不斷去執(zhí)行上述的爬取方法 getData 。因?yàn)檫@個(gè)爬蟲(chóng)系統(tǒng)不僅是一個(gè)用戶,一個(gè)用戶不僅只跟蹤了一個(gè)問(wèn)題。
所以我們此處的完整任務(wù)應(yīng)該是遍歷系統(tǒng)的每個(gè)cookie未過(guò)期用戶,再遍歷每個(gè)用戶的跟蹤問(wèn)題,再去獲取這些問(wèn)題的數(shù)據(jù)。
系統(tǒng)還有另外兩個(gè)定時(shí)任務(wù),一個(gè)是定時(shí)爬取用戶關(guān)注話題的熱門(mén)回答,另一個(gè)是推送這個(gè)話題熱門(mén)回答給相應(yīng)的用戶。這兩個(gè)任務(wù)跟上述任務(wù)大致流程一樣,就不細(xì)講了。
但是在我們做定時(shí)任務(wù)時(shí)會(huì)有個(gè)細(xì)節(jié)問(wèn)題,就是如何去控制爬取時(shí)的并發(fā)問(wèn)題。具體舉例來(lái)說(shuō):如果爬蟲(chóng)請(qǐng)求并發(fā)太高,知乎可能是會(huì)限制此IP的訪問(wèn)的,所以我們需要讓爬蟲(chóng)請(qǐng)求一個(gè)一個(gè)的,或者若干個(gè)若干個(gè)的進(jìn)行。
簡(jiǎn)單思考下,我們會(huì)采取循環(huán)await。我不假思索的寫(xiě)下了如下代碼:
- // 爬蟲(chóng)方法
- async function getQuestionData () {
- // do spider action
- }
- // questions為獲取到的關(guān)注問(wèn)答
- questions.forEach(await getQuestionData)
然而執(zhí)行之后,我們會(huì)發(fā)現(xiàn)這樣其實(shí)還是并發(fā)執(zhí)行的,為什么呢?其實(shí)仔細(xì)想下就明白了。forEach只是循環(huán)的語(yǔ)法糖,如果沒(méi)有這個(gè)方法,讓你來(lái)實(shí)現(xiàn)它,你會(huì)怎么寫(xiě)呢?你大概也寫(xiě)的出來(lái):
- Array.prototype.forEach = function (callback) {
- for (let i = 0; i < this.length; i++) {
- callback(this[i], i, this)
- }
- }
雖然 forEach 本身會(huì)更復(fù)雜點(diǎn),但大致就是這樣吧。這時(shí)候我們把一個(gè)異步方法作為參數(shù) callback 傳遞進(jìn)去,然后循環(huán)執(zhí)行它,這個(gè)執(zhí)行依舊是并發(fā)執(zhí)行,并非是同步的。
所以我們?nèi)绻雽?shí)現(xiàn)真正的同步請(qǐng)求,還是需要用for循環(huán)去執(zhí)行,如下:
- async function getQuestionData () {
- // do spider action
- }
- for (let i = 0; i < questions.length; i++) {
- await getQuestionData()
- }
除了for循環(huán),還可以通過(guò)for-of,如果對(duì)這方面感興趣,可以去多了解下數(shù)組遍歷的幾個(gè)方法,順便研究下ES6的迭代器 Iterator 。
其實(shí)如果業(yè)務(wù)量大,即使這樣做也是不夠的。還需要更加細(xì)分任務(wù)顆粒度,甚至要加代理IP來(lái)分散請(qǐng)求。
合理搭建服務(wù)端
下面說(shuō)的點(diǎn)跟爬蟲(chóng)本身沒(méi)有太大關(guān)系了,屬于服務(wù)端架構(gòu)的一些分享,如果只關(guān)心爬蟲(chóng)本身的話,可以不用再往下閱讀了。
我們把爬蟲(chóng)功能都寫(xiě)的差不多了,后面只要編寫(xiě)相應(yīng)的路由,能讓前端訪問(wèn)到數(shù)據(jù)就好了。但是編寫(xiě)一個(gè)沒(méi)那么差勁的服務(wù)端,還是需要我們深思熟慮的。
合理分層
我看過(guò)一些前端同學(xué)寫(xiě)的node服務(wù),經(jīng)常就會(huì)把系統(tǒng)所有的接口(router action)都寫(xiě)到一個(gè)文件中,好一點(diǎn)的會(huì)根據(jù)模塊分幾個(gè)對(duì)于文件。
但是如果我們接觸過(guò)其他成熟的后端框架、或者大學(xué)學(xué)過(guò)一些J2EE等知識(shí),就會(huì)本能意識(shí)的進(jìn)行一些分層:
- model
- service
- controller
當(dāng)然也有些框架或者人會(huì)將業(yè)務(wù)邏輯 service 實(shí)現(xiàn)在 controller 中,亦或者是 model 層中。我個(gè)人認(rèn)為一個(gè)稍微復(fù)雜的項(xiàng)目,應(yīng)該是單獨(dú)抽離出抽象的業(yè)務(wù)邏輯的。
比如在我這個(gè)爬蟲(chóng)系統(tǒng)中,我將數(shù)據(jù)庫(kù)的添刪改查操作按 model 層對(duì)應(yīng)抽離出 service ,另外再將爬取頁(yè)面的服務(wù)、郵件推送的服務(wù)、用戶鑒權(quán)的服務(wù)抽離到對(duì)應(yīng)的 service 。
最終我們的 api 能夠設(shè)計(jì)的更加易讀,整個(gè)系統(tǒng)也更加易拓展。
分層在koa上的實(shí)踐
如果是直接使用一個(gè)成熟的后端框架,分層這事我們是不用多想的。node這樣的框架也有,我之前介紹的我廠開(kāi)源的 api-mocker 采用的 egg.js ,也幫我們做好了合理的分層。
但是如果自己基于koa從零搭建一個(gè)服務(wù)端,在這方面上就會(huì)遇到一些挫折。koa本身邏輯非常簡(jiǎn)單,就是調(diào)取一系列中間件(就是一個(gè)個(gè)function),來(lái)處理請(qǐng)求。官方自己提供的 koa-router ,即是幫助我們識(shí)別請(qǐng)求路徑,然后加載對(duì)應(yīng)的接口方法。
我們?yōu)榱藚^(qū)分業(yè)務(wù)模塊,會(huì)把一些接口方法寫(xiě)在同一個(gè) controller 中,比如我的 questionController 負(fù)責(zé)處理問(wèn)題相關(guān)的接口; topicController 負(fù)責(zé)處理話題相關(guān)的接口。
那么我們可能會(huì)這樣編寫(xiě)路由文件:
- const Router = require('koa-router')
- const router = new Router()
- const question = require('./controller/question')
- const topic = require('./controller/topic')
- router.post('/api/question', question.create)
- router.get('/api/question', question.get)
- router.get('/api/topic', topic.get)
- router.post('/api/topic/follow', topic.follow)
- module.exports = router
我的question文件可能是這樣寫(xiě)的:
- class Question {
- async get () {
- // return data
- }
- async create () {
- // create question and return data
- }
- }
- module.exports = new Question()
那么問(wèn)題就來(lái)了
單純這樣寫(xiě)是沒(méi)有辦法真正的以面向?qū)ο蟮男问絹?lái)編寫(xiě) controller 的。為什么呢?
因?yàn)槲覀儗uestion對(duì)象的屬性方法作為中間件傳遞到了 koa-router 中,然后由 koa 底層來(lái)合并這些中間件方法,作為參數(shù)傳遞到 http.createServer 方法中,最終由node底層監(jiān)聽(tīng)請(qǐng)求時(shí)調(diào)用。那這個(gè) this 到底會(huì)是誰(shuí),不進(jìn)行調(diào)試,或者查看koa與node源代碼,是無(wú)從得知的。但是無(wú)論如何方法調(diào)用者肯定不是這個(gè)對(duì)象自身了(實(shí)際上它會(huì)是 undefined )。
也就是說(shuō),我們不能通過(guò) this 來(lái)獲取對(duì)象自身的屬性或方法。
那怎么辦呢?有的同學(xué)可能會(huì)選擇將自身一些公共方法,直接寫(xiě)在 class 外部,或者寫(xiě)在某個(gè) utils 文件中,然后在接口方法中使用。比如這樣:
- const error = require('utils/error')
- const success = (ctx, data) => {
- ctx.body = {
- success: true,
- data: data
- }
- }
- class Question {
- async get () {
- success(data)
- }
- async create () {
- error(result)
- }
- }
- module.exports = new Question()
這樣確實(shí)ok,但是又會(huì)有新的問(wèn)題---這些方法就不是對(duì)象自己的屬性,也就沒(méi)辦法被子類繼承了。
為什么需要繼承呢?因?yàn)橛袝r(shí)候我們希望一些不同的 controller 有著公共的方法或?qū)傩?,舉個(gè)例子:我希望我所有的成功or失敗都是這樣的格式:
- {
- success: false,
- message: '對(duì)應(yīng)的錯(cuò)誤消息'
- }
- {
- success: true,
- data: '對(duì)應(yīng)的數(shù)據(jù)'
- }
按照 koa 的核心思想,這個(gè)通用的格式轉(zhuǎn)化,應(yīng)該是專門(mén)編寫(xiě)一個(gè)中間件,在路由中間件之后(即執(zhí)行完controller里的方法之后)去做專門(mén)處理并response。
然而這樣會(huì)導(dǎo)致每有一個(gè)公共方法,就必須要加一個(gè)中間件。而且 controller 本身已經(jīng)失去了對(duì)這些方法的控制權(quán)。這個(gè)中間件是執(zhí)行自身還是直接 next() 將會(huì)非常難判斷。
如果是抽離成 utils 方法再引用,也不是不可以,就是方法多的話,聲明引用稍微麻煩些,而且沒(méi)有抽象類的意義。
更理想的狀態(tài)應(yīng)該是如剛才所說(shuō)的,大家都繼承一個(gè)抽象的父類,然后去調(diào)用父類的公共相應(yīng)方法即可,如:
- class AbstractController {
- success (ctx, data) {
- ctx.body = {
- success: true,
- data: data
- }
- }
- error (ctx, error) {
- ctx.body = {
- success: false,
- msg: error
- }
- }
- }
- class Question extends AbstractController {
- async get (ctx) {
- const data = await getData(ctx.params.id)
- return super.success(ctx, data)
- }
- }
這樣就方便多了,不過(guò)如果寫(xiě)過(guò)koa的人可能會(huì)有這樣的煩惱,一個(gè)上下文 ctx 總是要作為參數(shù)傳遞來(lái)傳遞去。比如上述控制器的所有中間件方法都得傳 ctx 參數(shù),調(diào)用父類方法時(shí),又要傳它,還會(huì)使得方法損失一些可讀性。
所以總結(jié)一下,我們有如下問(wèn)題:
- controller
- ctx
解決它
其實(shí)解決的辦法很簡(jiǎn)單,我們只要想辦法讓 controller 方法中的this指向?qū)嵗瘜?duì)象自身,再把 ctx 掛在到這個(gè) this 上即可。
怎么做呢?我們只要再封裝一下 koa-router 就好了,如下所示:
- const Router = require('koa-router')
- const router = new Router()
- const question = require('./controller/question')
- const topic = require('./controller/topic')
- const routerMap = [
- ['post', '/api/question', question, 'create'],
- ['get', '/api/question', question, 'get'],
- ['get', '/api/topic', topic, 'get'],
- ['post', '/api/topic/follow', topic, 'follow']
- ]
- routerMap.map(route => {
- const [ method, path, controller, action ] = route
- router[method](path, async (ctx, next) =>
- controller[action].bind(Object.assign(controller, { ctx }))(ctx, next)
- )
- })
- module.exports = router
大意就是在路由傳遞 controller 方法時(shí),將 controller 自身與 ctx 合并,通過(guò) bind 指定該方法的 this 。這樣我們就能通過(guò) this 獲取方法所屬 controller 對(duì)象的其他方法。此外子類方法與父類方法也能通過(guò) this.ctx 來(lái)獲取上下文對(duì)象 ctx 。
但是 bind 之前我們其實(shí)應(yīng)該考慮以下,其他中間件以及koa本身會(huì)不會(huì)也干了類似的事,修改了this的值。如何判斷呢,兩個(gè)辦法:
- 調(diào)試。在我們未 bind 之前,在中間件方法中打印一下 this ,是 undefined 的話自然就沒(méi)被綁定。
- 看koa-router/koa/node的源代碼。
事實(shí)是,自然是沒(méi)有的。那我們就放心的 bind 吧。
寫(xiě)在***
上述大概就是編寫(xiě)這個(gè)小工具時(shí),遇到的一些點(diǎn),感覺(jué)可以總結(jié)的。也并沒(méi)有什么技術(shù)難點(diǎn),不過(guò)可以借此學(xué)習(xí)學(xué)習(xí)一些相關(guān)的知識(shí),包括網(wǎng)站安全、爬與反爬、、koa底層原理等等。
這個(gè)工具本身非常的個(gè)人色彩,不一定滿足大家的需要。而且它在半年前就寫(xiě)好了,只不過(guò)最近被我挖墳?zāi)贸鰜?lái)總結(jié)。而且就在我即將寫(xiě)完文章時(shí),我發(fā)現(xiàn)知乎提示我的賬號(hào)不安全了。我估計(jì)是以為同一IP同一賬戶發(fā)起過(guò)多的網(wǎng)絡(luò)請(qǐng)求,我這臺(tái)服務(wù)器IP已經(jīng)被認(rèn)為是不安全的IP了,在這上面登錄的賬戶都會(huì)被提示不安全。所以我不建議大家將其直接拿來(lái)使用。
當(dāng)然,如果還是對(duì)其感興趣,本地測(cè)試下或者學(xué)習(xí)使用,還是沒(méi)什么大問(wèn)題的?;蛘哌€有更深的興趣的話,可以自己嘗試去繞開(kāi)知乎的安全策略。
新聞標(biāo)題:一只node爬蟲(chóng)的升級(jí)打怪之路
網(wǎng)站路徑:http://www.dlmjj.cn/article/dhoospo.html


咨詢
建站咨詢
