基於nodejs編寫小爬蟲

Zsai發表於2019-02-16

nodejs編寫非同步小爬蟲


在通過learnyounode的課程初步瞭解nodejs的各大模組之後,不禁感慨於nodejs的強大,讓我們這些前端小白也可以進行進階的功能實現,同時發現自己也已經可以通過nodejs實現一些比較日常的小功能。比如在看完fs模組之後,我就用nodejs寫了一個批量修改檔名的小demo,還是相當好用的。技術服務於生活,這才是硬道理~
上週用nodejs寫了一個小爬蟲,但是由於當時的認知有限,爬蟲雖然工作了,但是在爬圖片的時候總是丟圖漏圖,也經常出現http請求併發太多造成連結超時導致爬蟲掛掉的情況。在研究別人的爬蟲程式碼之後,我決定用async重寫一個爬安居客租房資訊的非同步爬蟲,寫下這篇筆記記錄自己的心得~
爬蟲完整程式碼:https://github.com/zzuzsj/myN…


需求:利用爬蟲將安居客杭州市全部區域規定頁數內的租房資訊以資料夾形式儲存到本地,並將租房的圖片儲存到相應資料夾裡。  

思路整理

在寫爬蟲之前,我們需要整理我們的思路,首先我們需要分析安居客租房的網頁跳轉路徑:
租房資訊分頁路徑: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將上訴的這些操作進行非同步化,並在併發請求的時候對併發數進行限制。就醬,完美~

1.模組引用

cheerio和async是nodejs的第三方模組,需要先下載,在命令列中執行:
npm init
npm install cheerio async --save-dev
安裝完畢之後,我們在當前目錄下建立一個ajkSpider.js,同時建立一個rent_image資料夾,拿來存放爬蟲所爬到的資訊。我們在ajkSpider.js先引用模組:

const fs = require(`fs`);
const cheerio = require(`cheerio`);
const request = require(`request`);
const async = require(`async`);

2.分頁路徑獲取

我們需要獲取所有分頁路徑,並將其存到陣列裡面,開始頁和結束頁通過在執行檔案的時候傳參指定。例如
node ajkSpider.js 1 5

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);
}

3.帖子路徑獲取

利用async對pageArray進行遍歷操作,獲取到url之後發起request請求,解析頁面結構,獲取所有帖子的dom節點,解析出帖子標題、價格、地段、url存到物件中,將物件存入陣列以利於下一步的分析。

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 + `
` + topicObj.fileName + `
`);
            })
            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(`
> 1 saveAllPage end`);
        if (callback) {
            callback(null, `task 1 saveAllPage`);
        }
    })
}

為了方便檢視,我將帖子的標題價格地段都存了下來,並將價格+地段+貼子標題結合在一起做成了檔名。為了保證檔案路徑的有效性,我對標題進行了特殊符號的去除,所以多了一串冗餘的程式碼,不需要的可以自行去掉。在資訊獲取完畢之後,同步建立相應檔案,以便於後續存放帖子內的房源圖片。程式碼中的cb函式是async進行map操作所必要的內部回撥,如果非同步操作出錯呼叫 cb(err,null) 中斷操作,非同步操作成功則呼叫 cb(null,value) ,後續程式碼中的cb大致都是這個意思。在async將所有的非同步操作遍歷完畢之後,會呼叫map後的那個回撥函式,我們可以利用這個回撥進行下一個非同步操作。

4.房源圖片路徑獲取

我們已經將所有的帖子路徑儲存下來了,那麼接下來我們就要獲取帖子內所有房源圖片的路徑。同樣的,我們可以參照上一步的步驟,將所有圖片路徑儲存下來。但需要注意的一點是,如果帖子數量很多,用async的map函式來做request請求容易造成導致爬蟲掛掉。所以為了爬蟲的穩定,我決定用async的mapLimit函式來遍歷帖子路徑,好處是可以控制同時併發的http請求數目,讓爬蟲更加穩定,寫法也和map函式差不多,增加了第二個引數–併發數目限制。

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 + `
`);
                imgUrlArray.index = index;
                houseImgUrlArray.push(imgUrlArray);
            });
            cb(null, obj.topicUrl + `
`);
        });
    }, (err, result) => {
        if (err) throw err;
        console.log(houseImgUrlArray.length);
        console.log(`
> 2 saveAllImagePage end`);
        if (callback) {
            callback(null, `task 2 saveAllImagePage`);
        }
    })
}

由於頁面中的大圖採用了懶載入的模式,所以大部分圖片我們無法直接從dom節點的src屬性上獲取圖片路徑,變通一下,獲取dom節點的data-src屬性即可獲取到。獲取到圖片路徑之後我們就可以將其儲存,進行最後一步–下載圖片啦~

5.房源圖片下載儲存

圖片儲存的資料夾資訊已經記錄在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(`
> 3 saveAllImage end`);
        if (callback) {
            callback(null, `task 3 saveAllImage`);
        }
    })
}

通過這一步你就可以把帖子內房源的圖片下載到本地資料夾啦~看到這麼多圖片被儲存到本地,開不開心!刺不刺激!學會了你可以肆意去爬圖啦!好吧,開玩笑的,規模稍微大些的網站都會做一些反爬蟲策略。就拿安居客來說,懶載入勉強也算是一種反爬蟲的方法,更可怕的是,如果同一ip高頻率請求安居客網頁,它會要求圖片驗證碼驗證,所以有時候執行爬蟲什麼東西都爬不到。至於這種高等爬蟲技巧,等以後進階再說吧,現在也是小白練手而已~

6.組織非同步流程

其實靠上面那些步驟通過非同步回撥組織一下就已經可以形成一個完整的爬蟲了。不過既然用了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結合,寫一個具有爬蟲功能的桌面軟體~也不知道能不能完成,做了再說~

相關文章