原文來自 : https://segmentfault.com/a/1190000014811373?utm_source=tag-newest
一個簡單的百度新聞爬蟲
確定爬取對象(網站/頁面)
百度新聞 (
http://news.baidu.com/
)
確定開發語言、框架、工具等
node.js (express) + WebStorm
Let’s start
初始化package.json
- 新建項目目錄BaiduNewsSpider
- 在DOS命令行中進入項目根目錄 baiduNews
- 執行npm init,初始化package.json文件
- 安裝依賴
npm isntall express npm isntallsuperagent npm isntall cheerio
依賴說明
express
使用express來搭建一個簡單的Http服務器。當然,你也可以使用node中自帶的http模塊
superagent
superagent是node裏一個非常方便的、輕量的、漸進式的第三方客戶端請求代理模塊,用他來請求目標頁面
cheerio
cheerio相當於node版的jQuery,用過jQuery的同學會非常容易上手。它主要是用來獲取抓取到的頁面元素和其中的數據信息
開始coding
一、使用express啓動一個簡單的本地Http服務器
1、 在項目根目錄下創建index.js文件(後面都會在這個index文件中進行coding)
2、 創建好index.js後,我們首先實例化一個express對象,用它來啓動一個本地監聽3000端口的Http服務。
const express = require('express'); //導入express模塊
const app = express();
let server = app.listen(3000, function () {
let host = server.address().address;
console.log("host的結果爲=", host);
let port = server.address().port;
console.log('Your App is running at http://%s:%s', host, port);
});
上述代碼中
let host = server.address().address;
會讀取本機的ip地址,由於windows有個優先解析列表,當ipv6的優先級高於ipv4時, 會出現:
> baidunews@1.0.0 start G:\BaiduNewsSpider
> node index.js
host的結果爲= ::
Your App is running at http://:::3000
解決方法有兩種:
- 第一種修改註冊表 : 詳情請移步博客win10 localhost 解析爲::1 的解決辦法
- 第二種,直接使用字符串”localhost“:
const express = require('express');
const app = express();
let server = app.listen(3000, function () {
let port = server.address().port;
console.log('Your App is running at http://%s:%s', "localhost", port);
});
對,就是這麼簡單,不到10行代碼,搭建啓動一個簡單的本地Http服務。
3、按照國際慣例,希望在訪問本機地址http://localhost:3000的時候,這個服務能給我們返回一個Hello World!在index.js中加入如下代碼:
app.get('/', function (req, res) {
res.send('Hello World!');
});
此時,在DOS中項目根目錄baiduNews下執行node index.js,讓項目跑起來。之後,打開瀏覽器,訪問http://localhost:3000,你就會發現頁面上顯示’Hellow World!'字樣。
這樣,在後面我們獲取到百度新聞首頁的信息後,就可以在訪問http://localhost:3000時看到這些信息。
二、抓取百度新聞首頁的新聞信息
百度新聞首頁大體上分爲“熱點新聞”、“本地新聞”、“國內新聞”、“國際新聞”…等。這次我們先來嘗試抓取左側“熱點新聞
”和下方的“本地新聞
”兩處的新聞數據。
審查頁面元素,經過查看左側“
熱點新聞
”信息所在DOM的結構,所有的“熱點新聞
”信息用jQuery的選擇器表示爲:#pane-news ul li a
。
// 引入所需要的第三方包
const superagent= require('superagent');
let hotNews = []; // 熱點新聞
let localNews = []; // 本地新聞
/**
* index.js
* [description] - 使用superagent.get()方法來訪問百度新聞首頁
*/
superagent.get('http://news.baidu.com/').end((err, res) => {
if (err) {
// 如果訪問失敗或者出錯,會這行這裏
console.log(`熱點新聞抓取失敗 - ${err}`)
} else {
// 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
// 抓取熱點新聞數據
hotNews = getHotNews(res)
}
});
3、獲取頁面信息後,我們來定義一個函數getHotNews()來抓取頁面內的“熱點新聞”數據。
/**
* index.js
* [description] - 抓取熱點新聞頁面
*/
// 引入所需要的第三方包
const cheerio = require('cheerio');
let getHotNews = (res) => {
let hotNews = [];
// 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res.text中。
/* 使用cheerio模塊的cherrio.load()方法,將HTMLdocument作爲參數傳入函數
以後就可以使用類似jQuery的$(selectior)的方式來獲取頁面元素
*/
let $ = cheerio.load(res.text);
// 找到目標數據所在的頁面元素,獲取數據
$('div#pane-news ul li a').each((idx, ele) => {
// cherrio中$('selector').each()用來遍歷所有匹配到的DOM元素
// 參數idx是當前遍歷的元素的索引,ele就是當前便利的DOM元素
let news = {
title: $(ele).text(), // 獲取新聞標題
href: $(ele).attr('href') // 獲取新聞網頁鏈接
};
hotNews.push(news) // 存入最終結果數組
});
return hotNews
};
這裏要多說幾點:
async/await
據說是異步編程的終級解決方案,它可以讓我們以同步的思維方式來進行異步編程。Promise
解決了異步編程的“回調地獄”,async/await同時使異步流程控制變得友好而有清晰,有興趣的同學可以去了解學習一下,真的很好用。superagent
模塊提供了很多比如get、post、delte
等方法,可以很方便地進行Ajax請求操作。在請求結束後執行.end()
回調函數。.end()
接受一個函數作爲參數,該函數又有兩個參數error
和res
。請求失敗
,error會包含返回的錯誤信息
,請求成功
,error值爲null
,返回的數據會包含在res參數中
。
cheerio
模塊的.load()
方法,將HTML document作爲參數傳入函數
,以後就可以使用類似jQuery的$(selectior)的方式來獲取頁面元素。同時可以使用類似於jQuery中的.each()來遍歷元素。此外,還有很多方法,大家可以自行Google/Baidu。
4、將抓取的數據返回給前端瀏覽器
前面,const app = express(); 實例化了一個express對象app。
app.get(’’, async() => {})接受兩個參數,第一個參數接受一個String類型的路由路徑,表示Ajax的請求路徑。第二個參數接受一個函數Function,當請求此路徑時就會執行這個函數中的代碼。
/**
* [description] - 根路由
*/
// 當一個get請求 http://localhost:3000時,就會後面的async函數
app.get('/', async (req, res, next) => {
res.send(hotNews);
});
在DOS中項目根目錄baiduNews下執行node index.js,讓項目跑起來。之後,打開瀏覽器,訪問http://localhost:3000,你就會發現抓取到的數據返回到了前端頁面。我運行代碼後瀏覽器展示的返回信息如下:
圖片涉政不能貼出來
OK!!這樣,一個簡單的百度“熱點新聞”的爬蟲就大功告成啦!!
簡單總結一下,其實步驟很簡單:
- express啓動一個簡單的Http服務
- 分析目標頁面DOM結構,找到所要抓取的信息的相關DOM元素
- 使用superagent請求目標頁面
- 使用cheerio獲取頁面元素,獲取目標數據
- 返回數據到前端瀏覽器
現在,繼續我們的目標,抓取“本地新聞”數據(編碼過程中,我們會遇到一些有意思的問題)
有了前面的基礎,我們自然而然的會想到利用和上面相同的方法“本地新聞”數據。
1、 分析頁面中“本地新聞”部分的DOM結構,如下圖:
F12打開控制檯,審查“本地新聞”DOM元素,我們發現,“本地新聞”分爲兩個主要部分,“左側新聞”和右側的“新聞資訊”。這所有目標數據都在id爲#local_news的div中。
“左側新聞”數據又在id爲#localnews-focus的ul標籤下的li標籤下的a標籤中,包括新聞標題和頁面鏈接。
“本地資訊”數據又在id爲#localnews-zixun的div下的ul標籤下的li標籤下的a標籤中,包括新聞標題和頁面鏈接。
2、OK!分析了DOM結構,確定了數據的位置,接下來和爬取“熱點新聞”一樣,按部就班,定義一個getLocalNews()函數,爬取這些數據。
/**
* [description] - 抓取本地新聞頁面
*/
let getLocalNews = (res) => {
let localNews = [];
let $ = cheerio.load(res);
// 本地新聞
$('ul#localnews-focus li a').each((idx, ele) => {
let news = {
title: $(ele).text(),
href: $(ele).attr('href'),
};
localNews.push(news)
});
// 本地資訊
$('div#localnews-zixun ul li a').each((index, item) => {
let news = {
title: $(item).text(),
href: $(item).attr('href')
};
localNews.push(news);
});
return localNews
};
對應的,在superagent.get()中請求頁面後,我們需要調用getLocalNews()函數,來爬去本地新聞數據。
superagent.get()函數修改爲:
superagent.get('http://news.baidu.com/').end((err, res) => {
if (err) {
// 如果訪問失敗或者出錯,會這行這裏
console.log(`熱點新聞抓取失敗 - ${err}`)
} else {
// 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
// 抓取熱點新聞數據
hotNews = getHotNews(res)
localNews = getLocalNews(res)
}
});
同時,我們要在app.get()路由中也要將數據返回給前端瀏覽器。app.get()路由代碼修改爲:
/**
* [description] - 跟路由
*/
// 當一個get請求 http://localhost:3000時,就會後面的async函數
app.get('/', async (req, res, next) => {
res.send({
hotNews: hotNews,
localNews: localNews
});
});
編碼完成,激動不已!!DOS中讓項目跑起來,用瀏覽器訪問http://localhost:3000
一個有意思的問題
爲了找到原因,首先,我們看看用superagent.get(‘http://news.baidu.com/’).end((err, res) => {})請求百度新聞首頁在回調函數.end()中的第二個參數res中到底拿到了什麼內容?
// 新定義一個全局變量 pageRes
let pageRes = {}; // supergaent頁面返回值
// superagent.get()中將res存入pageRes
superagent.get('http://news.baidu.com/').end((err, res) => {
if (err) {
// 如果訪問失敗或者出錯,會這行這裏
console.log('熱點新聞抓取失敗 - ${err}')
} else {
// 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
// 抓取熱點新聞數據
// hotNews = getHotNews(res)
// localNews = getLocalNews(res)
pageRes = res
}
});
// 將pageRes返回給前端瀏覽器,便於查看
app.get('/', async (req, res, next) => {
res.send({
// {}hotNews: hotNews,
// localNews: localNews,
pageRes: pageRes
});
});
可以看到,返回值中的text字段應該就是整個頁面的HTML代碼的字符串格式。爲了方便觀察,可以直接把這個text字段值返回給前端瀏覽器,這樣我們就能夠清晰地看到經過瀏覽器渲染後的頁面。
修改給前端瀏覽器的返回值
app.get('/', async (req, res, next) => {
res.send(
pageRes.text // 注意此處返回的不是字典形式,刪除{ }
)
}
訪問瀏覽器http://localhost:3000,頁面展示如下內容:
審查元素才發現,原來我們抓取的目標數據所在的DOM元素中是空的,裏面沒有數據!
到這裏,一切水落石出!在我們使用superagent.get()訪問百度新聞首頁時,res中包含的獲取的頁面內容中,我們想要的“本地新聞”數據還沒有生成,DOM節點元素是空的,所以出現前面的情況!抓取後返回的數據一直是空數組[ ]。
在控制檯的Network中我們發現頁面請求了一次這樣的接口:
http://localhost:3000/widget?id=InternationalNews&t=1589378105207
,接口狀態 404
。
這應該就是百度新聞獲取“本地新聞”的接口,到這裏一切都明白了!“本地新聞”是在頁面加載後動態請求上面這個接口獲取的
,所以我們用superagent.get()請求的頁面再去請求這個接口時,接口URL中hostname部分變成了本地IP地址,而本機上沒有這個接口,所以404,請求不到數據。
找到原因,我們來想辦法解決這個問題!!
- 直接使用superagent訪問正確合法的百度“本地新聞”的接口,獲取數據後返回給前端瀏覽器。
- 使用第三方npm包,模擬瀏覽器訪問百度新聞首頁,在這個模擬瀏覽器中當“本地新聞”加載成功後,抓取數據,返回給前端瀏覽器。
以上方法均可,我們來試試比較有意思的第二種方法。
使用Nightmare自動化測試工具
Electron可以讓你使用純JavaScript調用Chrome豐富的原生的接口來創造桌面應用。你可以把它看作一個專注於桌面應用的Node.js的變體,而不是Web服務器。其基於瀏覽器的應用方式可以極方便的做各種響應式的交互
Nightmare是一個基於Electron的框架,針對Web自動化測試和爬蟲,因爲其具有跟PlantomJS一樣的自動化測試的功能可以在頁面上模擬用戶的行爲觸發一些異步數據加載,也可以跟Request庫一樣直接訪問URL來抓取數據,並且可以設置頁面的延遲時間,所以無論是手動觸發腳本還是行爲觸發腳本都是輕而易舉的。
安裝依賴
// 安裝nightmare
yarn add nightmare
爲獲取“本地新聞”,繼續coding…
給index.js中新增如下代碼:
const Nightmare = require('nightmare'); // 自動化測試包,處理動態頁面
const nightmare = Nightmare({ show: true }); // show:true 顯示內置模擬瀏覽器
/**
* [description] - 抓取本地新聞頁面
* [nremark] - 百度本地新聞在訪問頁面後加載js定位IP位置後獲取對應新聞,
* 所以抓取本地新聞需要使用 nightmare 一類的自動化測試工具,
* 模擬瀏覽器環境訪問頁面,使js運行,生成動態頁面再抓取
*/
// 抓取本地新聞頁面
nightmare
.goto('http://news.baidu.com/')
.wait("div#local_news")
.evaluate(() => document.querySelector("div#local_news").innerHTML)
.then(htmlStr => {
// 獲取本地新聞數據
localNews = getLocalNews(htmlStr)
})
.catch(error => {
console.log(`本地新聞抓取失敗 - ${error}`);
})
修改getLocalNews()
函數爲:
/**
* [description]- 獲取本地新聞數據
*/
let getLocalNews = (htmlStr) => {
let localNews = [];
let $ = cheerio.load(htmlStr);
// 本地新聞
$('ul#localnews-focus li a').each((idx, ele) => {
let news = {
title: $(ele).text(),
href: $(ele).attr('href'),
};
localNews.push(news)
});
// 本地資訊
$('div#localnews-zixun ul li a').each((index, item) => {
let news = {
title: $(item).text(),
href: $(item).attr('href')
};
localNews.push(news);
});
return localNews
}
修改app.get('/')
路由爲:
/**
* [description] - 跟路由
*/
// 當一個get請求 http://localhost:3000時,就會後面的async函數
app.get('/', async (req, res, next) => {
res.send({
hotNews: hotNews,
localNews: localNews
})
});
此時,DOS命令行中重新讓項目跑起來,瀏覽器訪問https://localhost:3000,看看頁面展示的信息,看是否抓取到了“本地新聞”數據!
至此,一個簡單而又完整的抓取百度新聞頁面“熱點新聞”和“本地新聞”的爬蟲就大功告成啦!!
最後總結一下,整體思路如下:
- express啓動一個簡單的Http服務
- 分析目標頁面DOM結構,找到所要抓取的信息的相關DOM元素
- 使用superagent請求目標頁面
- 動態頁面(需要加載頁面後運行JS或請求接口的頁面)可以使用Nightmare模擬瀏覽器訪問
- 使用cheerio獲取頁面元素,獲取目標數據
完整代碼如下:
const express = require('express');
const app = express();
let server = app.listen(3000, function () {
let port = server.address().port;
console.log('Your App is running at http://%s:%s', "localhost", port);
});
// 引入所需要的第三方包
const superagent = require('superagent');
let hotNews = []; // 熱點新聞
let localNews = []; // 本地新聞
/**
* index.js
* [description] - 使用superagent.get()方法來訪問百度新聞首頁
*/
superagent.get('http://news.baidu.com/').end((err, res) => {
if (err) {
// 如果訪問失敗或者出錯,會這行這裏
console.log('熱點新聞抓取失敗 - ${err}')
} else {
// 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
// 抓取熱點新聞數據
hotNews = getHotNews(res);
}
});
const cheerio = require('cheerio');
let getHotNews = (res) => {
let hotNews = [];
// 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res.text中。
/* 使用cheerio模塊的cherrio.load()方法,將HTMLdocument作爲參數傳入函數
以後就可以使用類似jQuery的$(selectior)的方式來獲取頁面元素
*/
let $ = cheerio.load(res.text);
// 找到目標數據所在的頁面元素,獲取數據
$('div#pane-news ul li a').each((idx, ele) => {
// cherrio中$('selector').each()用來遍歷所有匹配到的DOM元素
// 參數idx是當前遍歷的元素的索引,ele就是當前便利的DOM元素
let news = {
title: $(ele).text(), // 獲取新聞標題
href: $(ele).attr('href') // 獲取新聞網頁鏈接
};
hotNews.push(news) // 存入最終結果數組
});
return hotNews
};
const Nightmare = require('nightmare'); // 自動化測試包,處理動態頁面
const nightmare = Nightmare({show: false}); // show:true 顯示內置模擬瀏覽器
/**
* [description] - 抓取本地新聞頁面
* [nremark] - 百度本地新聞在訪問頁面後加載js定位IP位置後獲取對應新聞,
* 所以抓取本地新聞需要使用 nightmare 一類的自動化測試工具,
* 模擬瀏覽器環境訪問頁面,使js運行,生成動態頁面再抓取
*/
// 抓取本地新聞頁面
nightmare
.goto('http://news.baidu.com/')
.wait("div#local_news")
.exists("div#local_news")
.evaluate(() => document.querySelector("div#local_news").innerHTML)
.then(htmlStr => {
// 獲取本地新聞數據
localNews = getLocalNews(htmlStr);
console.log('本地新聞抓取成功');
})
.catch(error => {
console.log('本地新聞抓取失敗 - ${error}');
});
/**
* [description]- 獲取本地新聞數據
*/
let getLocalNews = (htmlStr) => {
let localNews = [];
let $ = cheerio.load(htmlStr);
// 本地新聞
$('ul#localnews-focus li a').each((idx, ele) => {
let news = {
title: $(ele).text(),
href: $(ele).attr('href'),
};
localNews.push(news)
});
// 本地資訊
$('div#localnews-zixun ul li a').each((index, item) => {
let news = {
title: $(item).text(),
href: $(item).attr('href')
};
localNews.push(news);
});
return localNews
};
app.get('/', async (req, res, next) => {
res.send({
hotNews: hotNews,
localNews: localNews
});
});