在經過learnyounode的課程初步瞭解nodejs的各大模塊以後,不由感慨於nodejs的強大,讓咱們這些前端小白也能夠進行進階的功能實現,同時發現本身也已經能夠經過nodejs實現一些比較平常的小功能。好比在看完fs模塊以後,我就用nodejs寫了一個批量修改文件名的小demo,仍是至關好用的。技術服務於生活,這纔是硬道理~
上週用nodejs寫了一個小爬蟲,可是因爲當時的認知有限,爬蟲雖然工做了,可是在爬圖片的時候老是丟圖漏圖,也常常出現http請求併發太多形成連接超時致使爬蟲掛掉的狀況。在研究別人的爬蟲代碼以後,我決定用async重寫一個爬安居客租房信息的異步爬蟲,寫下這篇筆記記錄本身的心得~
爬蟲完整代碼:https://github.com/zzuzsj/myN...html
需求:利用爬蟲將安居客杭州市所有區域規定頁數內的租房信息以文件夾形式保存到本地,並將租房的圖片保存到相應文件夾裏。
在寫爬蟲以前,咱們須要整理咱們的思路,首先咱們須要分析安居客租房的網頁跳轉路徑:
租房信息分頁路徑:https://hz.zu.anjuke.com/fang...
租房信息帖子路徑:https://hz.zu.anjuke.com/fang...
租房信息帖子內房源圖片路徑:https://pic1.ajkimg.com/displ...
emmmm,事實證實,只有分頁路徑有跡可循,這也是咱們爬蟲的突破點所在。
在需求中,咱們須要訪問指定頁面的租房信息,規定的頁數咱們能夠經過node傳參傳入指定,拼接成對應分頁的url並儲存。用request模塊請求全部的分頁url,利用cheerio將頁面結構解碼以後獲取全部租房信息帖子的路徑並儲存。將全部帖子url路徑保存以後,繼續請求這些路徑,獲取到全部租房圖片的src路徑並儲存。等將全部圖片路徑儲存以後,利用request和fs在本地創建相應文件夾並將圖片下到本地。而後利用async將上訴的這些操做進行異步化,並在併發請求的時候對併發數進行限制。就醬,完美~前端
cheerio和async是nodejs的第三方模塊,須要先下載,在命令行中運行: npm init
npm install cheerio async --save-dev
安裝完畢以後,咱們在當前目錄下創建一個ajkSpider.js,同時創建一個rent_image文件夾,拿來存放爬蟲所爬到的信息。咱們在ajkSpider.js先引用模塊:node
const fs = require('fs'); const cheerio = require('cheerio'); const request = require('request'); const async = require('async');
咱們須要獲取全部分頁路徑,並將其存到數組裏面,開始頁和結束頁經過在執行文件的時候傳參指定。例如 node ajkSpider.js 1 5
git
let pageArray = []; let startIndex = process.argv[2]; let endIndex = process.argv[2]; for (let i = startIndex; i < endIndex; i++) { let pageUrl = 'https://hz.zu.anjuke.com/fangyuan/quanbu/p' + i; pageArray.push(pageUrl); }
利用async對pageArray進行遍歷操做,獲取到url以後發起request請求,解析頁面結構,獲取全部帖子的dom節點,解析出帖子標題、價格、地段、url存到對象中,將對象存入數組以利於下一步的分析。github
let topicArray = []; function saveAllPage(callback) { let pageindex = startIndex; async.map(pageArray, function (url, cb) { request({ 'url': url, 'method': 'GET', 'accept-charset': 'utf-8', 'headers': { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36" } }, (err, res, body) => { if (err) cb(err, null); let $ = cheerio.load(res.body.toString()); $('.zu-itemmod').each((i, e) => { let topicObj = {}; let title = $(e).find('h3').find('a').attr('title').replace(/[(\s+)\:\、\,\*\\\:]/g, ''); let topicUrl = $(e).find('h3').find('a').attr('href'); let address = $(e).find('address').text().replace(/\<a(.+)a\>/g, '').replace(/\s+/g, ''); let price = $(e).find('.zu-side').find('strong').text(); let fileName = price + '_' + address + '_' + title; topicObj.fileName = fileName; topicObj.topicUrl = topicUrl; topicArray.push(topicObj); if (!fs.existsSync('./rent_image/' + fileName)) { fs.mkdirSync('./rent_image/' + fileName); } // console.log(topicObj.topicUrl + '\n' + topicObj.fileName + '\n'); }) console.log('=============== page ' + pageindex + ' end ============='); cb(null, 'page ' + pageindex); pageindex++; }); }, (err, result) => { if (err) throw err; console.log(topicArray.length); console.log(result + ' finished'); console.log('\n> 1 saveAllPage end'); if (callback) { callback(null, 'task 1 saveAllPage'); } }) }
爲了方便查看,我將帖子的標題價格地段都存了下來,並將價格+地段+貼子標題結合在一塊兒作成了文件名。爲了保證文件路徑的有效性,我對標題進行了特殊符號的去除,因此多了一串冗餘的代碼,不須要的能夠自行去掉。在信息獲取完畢以後,同步建立相應文件,以便於後續存放帖子內的房源圖片。代碼中的cb函數是async進行map操做所必要的內部回調,若是異步操做出錯調用 cb(err,null)
中斷操做,異步操做成功則調用 cb(null,value)
,後續代碼中的cb大體都是這個意思。在async將全部的異步操做遍歷完畢以後,會調用map後的那個回調函數,咱們能夠利用這個回調進行下一個異步操做。web
咱們已經將全部的帖子路徑保存下來了,那麼接下來咱們就要獲取帖子內全部房源圖片的路徑。一樣的,咱們能夠參照上一步的步驟,將全部圖片路徑保存下來。但須要注意的一點是,若是帖子數量不少,用async的map函數來作request請求容易形成致使爬蟲掛掉。因此爲了爬蟲的穩定,我決定用async的mapLimit函數來遍歷帖子路徑,好處是能夠控制同時併發的http請求數目,讓爬蟲更加穩定,寫法也和map函數差很少,增長了第二個參數--併發數目限制。npm
let houseImgUrlArray = []; function saveAllImagePage(topicArray, callback) { async.mapLimit(topicArray, 10, function (obj, cb) { request({ 'url': obj.topicUrl, 'method': 'GET', 'accept-charset': 'utf-8', 'headers': { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36" } }, (err, res, body) => { if (err) cb(err, null); let $ = cheerio.load(res.body.toString()); let index = 0; $('.bigps').find('.picMove').find('li').find('img').each((i, e) => { index++; let imgUrlArray = {}; imgUrlArray.fileName = obj.fileName; var imgsrc = ($(e).attr('src').indexOf('default') != -1 || $(e).attr('src').length <= 0) ? $(e).attr('data-src') : $(e).attr('src'); imgUrlArray.imgsrc = imgsrc; console.log(imgUrlArray.imgsrc + '\n'); imgUrlArray.index = index; houseImgUrlArray.push(imgUrlArray); }); cb(null, obj.topicUrl + '\n'); }); }, (err, result) => { if (err) throw err; console.log(houseImgUrlArray.length); console.log('\n> 2 saveAllImagePage end'); if (callback) { callback(null, 'task 2 saveAllImagePage'); } }) }
因爲頁面中的大圖採用了懶加載的模式,因此大部分圖片咱們沒法直接從dom節點的src屬性上獲取圖片路徑,變通一下,獲取dom節點的data-src屬性便可獲取到。獲取到圖片路徑以後咱們就能夠將其儲存,進行最後一步--下載圖片啦~數組
圖片保存的文件夾信息已經記錄在houseImageUrlArray裏了,發送請求以後咱們只須要將文件寫入到對應文件夾裏就行。不過我在爬蟲啓動的時候常常出現文件夾不存在致使爬蟲中斷,因此在寫入文件以前,我都檢查相應路徑是否存在,若是不存在就直接建立文件,以避免爬蟲常常中斷
。下載圖片是一個較爲繁重的操做,因此咱們不妨將併發請求數控制的低一些,保證爬蟲穩定性。併發
function saveAllImage(houseImgUrlArray, callback) { async.mapLimit(houseImgUrlArray, 4, function (obj, cb) { console.log(obj); if (!fs.existsSync('./rent_image/' + obj.fileName)) { fs.mkdirSync('./rent_image/' + obj.fileName); } request({ 'url': obj.imgsrc, 'method': 'GET', 'accept-charset': 'utf-8', 'headers': { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36", } }).pipe(fs.createWriteStream('./rent_image/' + obj.fileName + '/' + obj.index + '.jpg').on('close', function () { cb(null, obj.title + ' img respose'); })); }, (err, result) => { if (err) throw err; console.log('\n> 3 saveAllImage end'); if (callback) { callback(null, 'task 3 saveAllImage'); } }) }
經過這一步你就能夠把帖子內房源的圖片下載到本地文件夾啦~看到這麼多圖片被保存到本地,開不開心!刺不刺激!學會了你能夠肆意去爬圖啦!好吧,開玩笑的,規模稍微大些的網站都會作一些反爬蟲策略。就拿安居客來講,懶加載勉強也算是一種反爬蟲的方法,更可怕的是,若是同一ip高頻率請求安居客網頁,它會要求圖片驗證碼驗證,因此有時候運行爬蟲什麼東西都爬不到。至於這種高等爬蟲技巧,等之後進階再說吧,如今也是小白練手而已~app
其實靠上面那些步驟經過異步回調組織一下就已經能夠造成一個完整的爬蟲了。不過既然用了async,那就乾脆用到底,將這些操做組織一下,讓代碼更好看、更有邏輯性,async的series方法就能夠很輕易地幫咱們組織好。
function startDownload() { async.series([ function (callback) { // do some stuff ... saveAllPage(process.argv[2], process.argv[3], callback); }, function (callback) { // do some more stuff ... saveAllImagePage(topicArray, callback); }, // function (callback) { // // do some more stuff ... // saveAllImageUrl(imgPageArray, callback); // }, function (callback) { // do some more stuff ... saveAllImage(houseImgUrlArray, callback); } ], // optional callback function (err, results) { // results is now equal to ['one', 'two'] if (err) throw err; console.log(results + ' success'); }); } startDownload();
雖然這只是一個最初級的爬蟲,沒有穩定性的保證,也沒有反爬蟲措施的破解。可是值得開心的是,它已是能夠正常運行的啦~記得寫出的初版本的時候,雖然能夠記錄帖子標題,可是圖片不管如何也是存不全的,最多存一兩百張圖爬蟲就結束了。多方參考以後,引入了async模塊,重構代碼邏輯,終於可以存一千多張圖了,已經挺滿意了~能夠說,async模塊是寫這個爬蟲收穫最多的地方了,大家也能夠用一下。 學習nodejs以後,發現能作的事多了不少,很開心,同時也發現本身能作的還不多,很憂心。做爲一個前端小白,不知道什麼好的學習方法,可是我知道,能作一些對本身有用的東西總歸是好的。利用所學的知識服務於生活則是更好的。每一個走在成長道路上的人,都該爲本身打打氣,堅持走下一步。 常規性的爲本身立一個下一階段的小目標:將nodejs與electron結合,寫一個具備爬蟲功能的桌面軟件~也不知道能不能完成,作了再說~