一個前端小白的"爬蟲"初試

前言

八月。透藍的天空,懸著火球般的太陽,云彩好似被太陽燒化了,也消失得無影無蹤。沒有一絲風,大地活像一個蒸籠。

好熱,好煩躁,好無聊。無意間又打開知乎??,首頁冒出一個問題給好看的女生拍照是種怎樣的體驗?,齊刷刷一大摞好看的小jie姐,看的人好生陶醉。作為一個曾經的理工屌絲男,我相信此刻你的想法和我一樣,要是可以把她們裝進那《學習教程》文件夾就好了。

怎么辦?一張張圖片右鍵保存嗎?不不不,效率低,鼠標按多了“右手”還疼??。差點忘了,我特么是個程序員啊!程序員?。〕绦騿T?。∵@種事難道不應該交給程序去干嘛。

"說干就干"

原文地址 源碼地址

一個前端小白的"爬蟲"初試的圖1

需求

需求很簡單:希望知乎APP自適應用戶手機殼顏色。

啊呸呸呸,應該是

需求很簡單:實現自動下載知乎某個帖子下所有回答的圖片到本地

分析

需求很明確,所以我想只要知道了以下兩點基本就能夠完成了。

  1. 圖片鏈接

能夠獲取帖子下面答題者上傳的圖片鏈接,至于所有圖片,那就是所有回答者上傳的圖片鏈接了

  1. 下載圖片

這個暫時猜想是使用成熟的庫,我只需要傳入圖片鏈接地址,以及圖片下載到哪個目錄就可以完成。如果沒找著這樣的庫,就只能研究原生的nodejs如何做了。

針對1,我們打開chrome瀏覽器的控制臺,發現頁面一打開的時候會有很多個請求發出,但是有一個帶"answers"請求很可疑,是不是它負責返回答題者的答案呢?

一個前端小白的"爬蟲"初試的圖2

在驗證這個想法之前,我們先不去看這個請求的具體響應內容。我們先點擊一下頁面上的查看全部 948 個回答按鈕,如果猜的對,"answers"請求應該會再次發出,并且返回答題者的答案。

一個前端小白的"爬蟲"初試的圖3

點擊按鈕之后發現,“answers”確實再次發出了,并且查看其響應內容大體如下

一個前端小白的"爬蟲"初試的圖4
 {   data: [     {       // xxx       // 答題者的信息       author: {         // ...       },       // 答題內容       content: '"就是覺得太美好了呀<br><br><figure><noscript><img data-rawheight="1080" src="https://pic4.zhimg.com/v2-a7da381efb1775622c497fb07cc40957_b.jpg" data-rawwidth="720" class="origin_image zh-lightbox-thumb" width="720" data-original="https://pic4.zhimg.com/v2-a7da381efb1775622c497fb07cc40957_r.jpg"></noscript></figure>',       // 帖子描述       question: {}       // xxx 等等     },     {       /// xxx     }   ],   paging: {     // 是否結束     is_end:false,     // 是否是剛開始     is_start:false,     // 查看下一頁內容的api地址     next: "https://www.zhihu.com/api/v4/questions/49364343/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=5&offset=8&sort_by=default",     // 上一頁內容的api地址     previous: "https://www.zhihu.com/api/v4/questions/49364343/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=5&offset=0&sort_by=default",     // 總回答數     totals: 948   } } 復制代碼

從響應中我們拿到總的回答數量,以及當前請求返回的答題者的內容也就是content字段,我們要的圖片地址就在noscript標簽下的img標簽的data-original屬性中。所以針對要求1,我們似乎已經拿到了50%的信息,還有另一半的信息是,我們如何獲取所有答題者的內容?,別忘了剛才的響應中還有paging字段,其中。

// 是否結束 is_end:false, // 查看下一頁內容的api地址 next: 'https://www.zhihu.com/api/v4/questions/49364343/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=5&offset=8&sort_by=default"', // 總回答數 totals: '' 復制代碼

再結合"answers"這個請求的路徑

https://www.zhihu.com/api/v4/questions/49364343/answers?include=data[*].is_normal,admin_closed_comment,reward_info,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,voteup_count,reshipment_settings,comment_permission,created_time,updated_time,review_info,relevant_info,question,excerpt,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp;data[*].mark_infos[*].url;data[*].author.follower_count,badge[?(type=best_answerer)].topics&offset=3&limit=5&sort_by=default

其中路徑部分 https://www.zhihu.com/api/v4/questions/49364343/answers,49364343應該就是帖子的id

query請求部分總共有三個參數

{   include: 'xxxx', // 這個參數可能是知乎后臺要做的各種驗證吧   offset: 3, // 頁碼   limit: 5, // 每頁內容數量   sort_by: 'default' // 排序方式 } 復制代碼

所以看起來,咱們把offset設置為0,limit設置為totals的值,是不是就可以拿到所有數據了呢?嘗試之后發現,最多只能拿到20個答題者的數據,所以我們還是根據is_end以及next兩個響應值,多次請求,逐步獲取所有數據吧。

針對2. 最后一頓google搜索發現還真有這么一個庫request,比如要下載一張在線的圖片到本地只需要些如下代碼

 const request = require('request) request('http://google.com/doodle.png')   .pipe(fs.createWriteStream('doodle.png')) 復制代碼

到這里1和2兩個條件都具備了,接下來要做的就是擼起來,寫代碼實現了。

預覽

在說代碼實現之前,我們先看一個錄制的gif,以及如何使用crawler.js

點擊查看gif

使用

 require('./crawler')({   dir: './imgs', // 圖片存放位置   questionId: '34078228', // 知乎帖子id,比如https://www.zhihu.com/question/49364343/answer/157907464,輸入49364343即可   proxyUrl: 'https://www.zhihu.com' // 當請求知乎的數量達到一定的閾值的時候,會被知乎認為是爬蟲(好像是封ip),這時如果你如果有一個代理服務器來轉發請求數據,便又可以繼續下載了。 }) 復制代碼

proxyUrl先不關注,后面會仔細說明這個字段的作用

源碼實現

點擊查看crawler.js

let path = require('path') let fs = require('fs') let rp = require('request-promise') let originUrl = 'https://www.zhihu.com' class Crawler {   constructor (options) {     // 構造函數中主要是一些屬性的初始化     const { dir = './imgs', proxyUrl = originUrl, questionId = '49364343', offset = 0, limit = 100, timeout = 10000 } = options     // 非代理模式下請求知乎的原始url默認是 https://www.zhihu.com     this.originUrl = originUrl     // 代理模式下請求的實際路徑, 這里默認也是https://www.zhihu.com     // 當你的電腦ip被封了之后,可以通過代理服務器,請求知乎,而我們是向代理服務器獲取數據     this.proxyUrl = proxyUrl     // 請求的最終url     this.uri = `${proxyUrl}/api/v4/questions/${questionId}/answers?limit=${limit}&offset=${offset}&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&sort_by=default`     // 是否已經是最后的數據     this.isEnd = false     // 知乎的帖子id     this.questionId = questionId     // 設置請求的超時時間(獲取帖子答案和下載圖片的超時時間目前相同)     this.timeout = timeout     // 解析答案后獲取的圖片鏈接     this.imgs = []     // 圖片下載路徑的根目錄     this.dir = dir     // 根據questionId和dir拼接的最終圖片下載的目錄     this.folderPath = ''     // 已下載的圖片的數量     this.downloaded = 0     // 初始化方法     this.init()   }   async init () {     if (this.isEnd) {       console.log('已經全部下載完成, 請欣賞')       return     }     // 獲取帖子答案     let { isEnd, uri, imgs, question } = await this.getAnswers()     this.isEnd = isEnd     this.uri = uri     this.imgs = imgs     this.downloaded = 0     this.question = question     console.log(imgs, imgs.length)     // 創建圖片下載目錄     this.createFolder()     // 遍歷下載圖片     this.downloadAllImg(() => {       // 當前請求回來的所有圖片都下載完成之后,繼續請求下一波數據       if (this.downloaded >= this.imgs.length) {         setTimeout(() => {           console.log('休息3秒鐘繼續下一波')           this.init()         }, 3000)       }     })   }   // 獲取答案   async getAnswers () {     let { uri, timeout } = this     let response = {}     try {       const { paging, data } = await rp({ uri, json: true, timeout })       const { is_end: isEnd, next } = paging       const { question } = Object(data[0])       // 將多個答案聚合到content中       const content = data.reduce((content, it) => content + it.content, '')       // 匹配content 解析圖片url       const imgs = this.matchImg(content)       response = { isEnd, uri: next.replace(originUrl, this.proxyUrl), imgs, question }     } catch (error) {       console.log('調用知乎api出錯,請重試')       console.log(error)     }     return response   }   // 匹配字符串,從中找出所有的圖片鏈接   matchImg (content) {     let imgs = []     let matchImgOriginRe = /<img.*?data-original="(.*?)"/g     content.replace(matchImgOriginRe, ($0, $1) => imgs.push($1))     return [ ...new Set(imgs) ]   }   // 創建文件目錄   createFolder () {     let { dir, questionId } = this     let folderPath = `${dir}/${questionId}`     let dirs = [ dir, folderPath ]     dirs.forEach((dir) => !fs.existsSync(dir) && fs.mkdirSync(dir))     this.folderPath = folderPath   }   // 遍歷下載圖片   downloadAllImg (cb) {     let { folderPath, timeout } = this     this.imgs.forEach((imgUrl) => {       let fileName = path.basename(imgUrl)       let filePath = `${folderPath}/${fileName}`       rp({ uri: imgUrl, timeout })         .on('error', () => {           console.log(`${imgUrl} 下載出錯`)           this.downloaded += 1           cb()         })         .pipe(fs.createWriteStream(filePath))         .on('close', () => {           this.downloaded += 1           console.log(`${imgUrl} 下載完成`)           cb()         })     })   } } module.exports = (payload = {}) => {   return new Crawler(payload) } 復制代碼

源碼實現基本上很簡單,大家看注釋就可以很快明白。

ip被封

正當我用寫好的crawler.js下載多個帖子下面的圖片的時候,程序報了一個這個提示。

系統檢測到您的帳號或IP存在異常流量,請進行驗證用于確認這些請求不是自動程序發出的"

完蛋了,知乎不讓我請求了??????。

完蛋了,知乎不讓我請求了??????。

完蛋了,知乎不讓我請求了??????。

折騰了半天,最后被當做爬蟲給封了。網上找了一些解決方法,例如爬蟲怎么解決封IP?

基本上是兩個思路

1、放慢抓取速度,減小對于目標網站造成的壓力。但是這樣會減少單位時間類的抓取量。

2、第二種方法是通過設置代理IP等手段,突破反爬蟲機制繼續高頻率抓取。但是這樣需要多個穩定的代理IP。

一個前端小白的"爬蟲"初試的圖5
一個前端小白的"爬蟲"初試的圖6

繼續用本機并且在ip沒有發生變化的情況下,直接請求知乎是不可能了,不過我們可以嘗試一下2.使用代理服務器。突然想起自己去年在搬瓦工買了一個服務器,??。平時除了用它作為vpn存在訪問一些被墻的網站外,就只放了一個resume-native程序。雖然沒法做到像上面兩張圖一樣,哪個代理服務被封,及時再切換另一個代理服務器。但是至少可以通過代理服務器再次下載圖片,擼起來。。。

另找出路

一個前端小白的"爬蟲"初試的圖7

代理程序proxy.js運行在服務器上,會監測路徑為/proxy*的請求,請求到來的時候通過自己以前寫的請求轉發httpProxy中間件去知乎拉取數據,再返回響應給我們本地。用一張圖表示如下

一個前端小白的"爬蟲"初試的圖8

所以我們原來的請求路徑是(為了簡化把include這個很長的query參數去除了) https://www.zhihu.com/api/v4/questions/49364343/answers?offset=3&limit=5&sort_by=default

經過代理服務器后變成了(其中xxx.yyy.zzz可以是你自己的代理服務器的域名或者ip加端口) https://xxx.yyy.zzz/proxy/api/v4/questions/49364343/answers?offset=3&limit=5&sort_by=default

點擊查看代理模式gif圖左側是服務器上打印的信息,右側是本地打印的信息

這樣我們間接地繞過了知乎封ip的尷尬,不過這只是臨時方案,終究代理服務器也會被封ip。

原文地址 源碼地址

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP