其實爬蟲是一個對計算機綜合能力要求比較高的技術活。javascript
首先是要對網絡協議尤爲是 http
協議有基本的瞭解, 可以分析網站的數據請求響應。學會使用一些工具,簡單的狀況使用 chrome devtools 的 network 面板就夠了。我通常還會配合 postman 或者 charles 來分析,更復雜的狀況可能舉要使用專業的抓包工具好比 wireshark 了。你對一個網站了解的越深,越容易想出簡單的方式來爬取你想獲取的信息。html
除了要了解一些計算機網絡的知識,你還須要具有必定的字符串處理能力,具體來講就是正則表達式玩的溜,其實正則表達式通常的使用場景下用不到不少高級知識,比較經常使用的有點小複雜的就是分組,非貪婪匹配等。俗話說,學好正則表達式,處理字符串都不怕🤣。java
還有就是掌握一些反爬蟲技巧,寫爬蟲你可能會碰到各類各樣的問題,可是不要怕,再複雜的 12306 都有人可以爬,還有什麼是能難到咱們的。常見的爬蟲碰到的問題好比服務器會檢查 cookies, 檢查 host 和 referer 頭,表單中有隱藏字段,驗證碼,訪問頻率限制,須要代理, spa 網站等等。其實啊,絕大多數爬蟲碰到的問題最終均可以經過操縱瀏覽器爬取的。node
這篇使用 nodejs 寫爬蟲系列第二篇。實戰一個小爬蟲,抓取 github 熱門項目。想要達到目標:jquery
咱們的需求是從 github 上抓取熱門項目數據,也就是 star 數排名靠前的項目。可是 github 好像沒有哪一個頁面能夠看到排名靠前的項目。每每網站提供的搜索功能是咱們寫爬蟲的人分析的重點對象。git
我以前在 v2ex 灌水的時候,看到一個討論 996
的帖子上恰好教了一個查看 github stars 數前幾的倉庫的方法。其實很簡單,就是在 github 搜索時加上 star 數的過濾條件好比: stars:>60000
,就能夠搜索到 github 上全部 star 數大於 60000 的倉庫。分析下面的截圖,注意圖片中的註釋:github
分析一下能夠得出如下信息:ajax
Doc
過濾而後我又想 github 會不會檢查 cookies 和其它請求頭好比 referer,host 等,根據是否有這些請求頭決定是否返回頁面。正則表達式
比較簡單的測試方法是直接用命令行工具 curl
來測試, 在 gitbash 中輸入下面命令即 curl "請求的url"
chrome
curl "https://github.com/search?p=2&q=stars%3A%3E60000&type=Repositories"
複製代碼
不出意外的正常的返回了頁面的源代碼, 這樣的話咱們的爬蟲腳本就不用加上請求頭和 cookies 了。
經過 chrome 的搜索功能,咱們能夠看到網頁源代碼中就有咱們須要的項目信息
分析到此結束,這其實就是一個很簡單的小爬蟲,咱們只須要配置好查詢參數,經過 http 請求獲取到網頁源代碼,而後利用解析庫解析,獲取源代碼中咱們須要的和項目相關的信息,再處理一下數據成數組,最後序列化成 json 字符串存儲到到 json 文件中。
想要經過 node 獲取源代碼,咱們須要先配置好 url 參數, 再經過 superagent 這個發送 http 請求的模塊來訪問配置好的 url。
'use strict';
const requests = require('superagent');
const cheerio = require('cheerio');
const constants = require('../config/constants');
const logger = require('../config/log4jsConfig').log4js.getLogger('githubHotProjects');
const requestUtil = require('./utils/request');
const models = require('./models');
/** * 獲取 star 數不低於 starCount k 的項目第 page 頁的源代碼 * @param {number} starCount star 數量下限 * @param {number} page 頁數 */
const crawlSourceCode = async (starCount, page = 1) => {
// 下限爲 starCount k star 數
starCount = starCount * 1024;
// 替換 url 中的參數
const url = constants.searchUrl.replace('${starCount}', starCount).replace('${page}', page);
// response.text 即爲返回的源代碼
const { text: sourceCode } = await requestUtil.logRequest(requests.get(encodeURI(url)));
return sourceCode;
}
複製代碼
上面代碼中的 constants 模塊是用來保存項目中的一些常量配置的,到時候須要改常量直接改這個配置文件就好了,並且配置信息更集中,便於查看。
module.exports = {
searchUrl: 'https://github.com/search?q=stars:>${starCount}&p=${page}&type=Repositories',
};
複製代碼
這裏我把項目信息抽象成了一個 Repository 類了。在項目的 models 目錄下的 Repository.js 中。
const fs = require('fs-extra');
const path = require('path');
module.exports = class Repository {
static async saveToLocal(repositories, indent = 2) {
await fs.writeJSON(path.resolve(__dirname, '../../out/repositories.json'), repositories, { spaces: indent})
}
constructor({
name,
author,
language,
digest,
starCount,
lastUpdate,
} = {}) {
this.name = name;
this.author = author;
this.language = language;
this.digest = digest;
this.starCount = starCount;
this.lastUpdate = lastUpdate;
}
display() {
console.log(` 項目: ${this.name} 做者: ${this.author} 語言: ${this.language} star: ${this.starCount} 摘要: ${this.digest} 最後更新: ${this.lastUpdate} `);
}
}
複製代碼
解析獲取到的源代碼咱們須要使用 cheerio 這個解析庫,使用方式和 jquery 很類似。
/** * 獲取 star 數不低於 starCount k 的項目頁表 * @param {number} starCount star 數量下限 * @param {number} page 頁數 */
const crawlProjectsByPage = async (starCount, page = 1) => {
const sourceCode = await crawlSourceCode(starCount, page);
const $ = cheerio.load(sourceCode);
// 下面 cheerio 若是 jquery 比較熟應該沒有障礙, 不熟的話 github 官方倉庫能夠查看 api, api 並非不少
// 查看 elements 面板, 發現每一個倉庫的信息在一個 li 標籤內, 下面的代碼時建議打開開發者工具的 elements 面板, 參照着閱讀
const repositoryLiSelector = '.repo-list-item';
const repositoryLis = $(repositoryLiSelector);
const repositories = [];
repositoryLis.each((index, li) => {
const $li = $(li);
// 獲取帶有倉庫做者和倉庫名的 a 連接
const nameLink = $li.find('h3 a');
// 提取出倉庫名和做者名
const [author, name] = nameLink.text().split('/');
// 獲取項目摘要
const digestP = $($li.find('p')[0]);
const digest = digestP.text().trim();
// 獲取語言
// 先獲取類名爲 .repo-language-color 的那個 span, 在獲取包含語言文字的父 div
// 這裏要注意有些倉庫是沒有語言的, 是獲取不到那個 span 的, language 爲空字符串
const languageDiv = $li.find('.repo-language-color').parent();
// 這裏注意使用 String.trim() 去除兩側的空白符
const language = languageDiv.text().trim();
// 獲取 star 數量
const starCountLinkSelector = '.muted-link';
const links = $li.find(starCountLinkSelector);
// 選擇器爲 .muted-link 還有多是那個 issues 連接
const starCountLink = $(links.length === 2 ? links[1] : links[0]);
const starCount = starCountLink.text().trim();
// 獲取最後更新時間
const lastUpdateElementSelector = 'relative-time';
const lastUpdate = $li.find(lastUpdateElementSelector).text().trim();
const repository = new models.Repository({
name,
author,
language,
digest,
starCount,
lastUpdate,
});
repositories.push(repository);
});
return repositories;
}
複製代碼
有時候搜索結果是有不少頁的,因此我這裏又寫了一個新的函數用來獲取指定頁面數量的倉庫。
const crawlProjectsByPagesCount = async (starCount, pagesCount) => {
if (pagesCount === undefined) {
pagesCount = await getPagesCount(starCount);
logger.warn(`未指定抓取的頁面數量, 將抓取全部倉庫, 總共${pagesCount}頁`);
}
const allRepositories = [];
const tasks = Array.from({ length: pagesCount }, (ele, index) => {
// 由於頁數是從 1 開始的, 因此這裏要 i + 1
return crawlProjectsByPage(starCount, index + 1);
});
// 使用 Promise.all 來併發操做
const resultRepositoriesArray = await Promise.all(tasks);
resultRepositoriesArray.forEach(repositories => allRepositories.push(...repositories));
return allRepositories;
}
複製代碼
只是寫個腳本,在代碼裏面配置參數而後去爬,這有點太簡陋了。這裏我使用了一個能夠同步獲取用戶輸入的庫readline-sync,加了一點用戶交互,後續的爬蟲教程我可能會考慮使用 electron 來作個簡單的界面, 下面是程序的啓動代碼。
const readlineSync = require('readline-sync');
const { crawlProjectsByPage, crawlProjectsByPagesCount } = require('./crawlHotProjects');
const models = require('./models');
const logger = require('../config/log4jsConfig').log4js.getLogger('githubHotProjects');
const main = async () => {
let isContinue = true;
do {
const starCount = readlineSync.questionInt(`輸入你想要抓取的 github 上項目的 star 數量下限, 單位(k): `, { encoding: 'utf-8'});
const crawlModes = [
'抓取某一頁',
'抓取必定數量頁數',
'抓取全部頁'
];
const index = readlineSync.keyInSelect(crawlModes, '請選擇一種抓取模式');
let repositories = [];
switch (index) {
case 0: {
const page = readlineSync.questionInt('請輸入你要抓取的具體頁數: ');
repositories = await crawlProjectsByPage(starCount, page);
break;
}
case 1: {
const pagesCount = readlineSync.questionInt('請輸入你要抓取的頁面數量: ');
repositories = await crawlProjectsByPagesCount(starCount, pagesCount);
break;
}
case 3: {
repositories = await crawlProjectsByPagesCount(starCount);
break;
}
}
repositories.forEach(repository => repository.display());
const isSave = readlineSync.keyInYN('請問是否要保存到本地(json 格式) ?');
isSave && models.Repository.saveToLocal(repositories);
isContinue = readlineSync.keyInYN('繼續仍是退出 ?');
} while (isContinue);
logger.info('程序正常退出...')
}
main();
複製代碼
這裏要提一下 readline-sync 的一個 bug,,在 windows 上, vscode 中使用 git bash 時,中文會亂碼,不管你文件格式是否是 utf-8。搜了一些 issues, 在 powershell 中切換編碼爲 utf-8 就能夠正常顯示,也就是把頁碼切到 65001
。
項目的完整源代碼以及後續的教程源代碼都會保存在個人 github 倉庫: Spiders。若是個人教程對您有幫助,但願不要吝嗇您的 star 😊。後續的教程可能就是一個更復雜的案例,經過分析 ajax 請求來直接訪問接口。