當然爬蟲的應用遠遠不止如此,我們還可以利用爬蟲庫做自動化測試, 服務端渲染, 自動化表單提交, 測試谷歌擴展程序, 性能診斷等. 任何語言實現的爬蟲框架原理往往也大同小異, 接下來筆者將介紹基於nodejs實現的爬蟲框架Apify以及用法,並通過一個實際的案例方便大家快速上手爬蟲開發.
apify是一款用於JavaScript的可伸縮的web爬蟲庫。能通過無頭(headless)Chrome 和 Puppeteer 實現數據提取和** Web** 自動化作業的開發。它提供了管理和自動擴展無頭Chrome / Puppeteer實例池的工具,支持維護目標URL的請求隊列,並可將爬取結果存儲到本地文件系統或雲端。我們安裝和使用它非常簡單, 官網上也有非常多的實例案例可以參考, 具體安裝使用步驟如下:
1、安裝2、使用Apify開始第一個案例const Apify = require('apify');
Apify.main(async () => {
const requestQueue = await Apify.openRequestQueue();
await requestQueue.addRequest({ url: 'https://www.iana.org/' });
const pseudoUrls = [new Apify.PseudoUrl('https://www.iana.org/[.*]')];
const crawler = new Apify.PuppeteerCrawler({
requestQueue,
handlePageFunction: async ({ request, page }) => {
const title = await page.title();
console.log(`Title of ${request.url}: ${title}`);
await Apify.utils.enqueueLinks({
page,
selector: 'a',
pseudoUrls,
requestQueue,
});
},
maxRequestsPerCrawl: 100,
maxConcurrency: 10,
});
await crawler.run();
});
// child.js
function computedTotal(arr, cb) {
// 耗時計算任務
}
// 與主進程通信
// 監聽主進程信號
process.on('message', (msg) => {
computedTotal(bigDataArr, (flag) => {
// 向主進程發送完成信號
process.send(flag);
})
});
// main.js
const { fork } = require('child_process');
app.use(async (ctx, next) => {
if(ctx.url === '/fetch') {
const data = ctx.request.body;
// 通知子進程開始執行任務,並傳入數據
const res = await createPromisefork('./child.js', data)
}
// 創建異步線程
function createPromisefork(childUrl, data) {
// 加載子進程
const res = fork(childUrl)
// 通知子進程開始work
data && res.send(data)
return new Promise(reslove => {
res.on('message', f => {
reslove(f)
})
})
}
await next()
})
// 異步隊列
const queue = []
// 最大並發數
const max_parallel = 6
// 開始指針
let start = 0
for(let i = 0; i < urls.length; i++) {
// 添加異步隊列
queue.push(fetchPage(browser, i, urls[i]))
if(i &&
(i+1) % max_parallel === 0
|| i === (urls.length - 1)) {
// 每隔6條執行一次, 實現異步分流執行, 控制並發數
await Promise.all(queue.slice(start, i+1))
start = i
}
}
// 滾動高度
let scrollStep = 1080;
// 最大滾動高度, 防止無限加載的頁面導致長效耗時任務
let max_height = 30000;
let m = {prevScroll: -1, curScroll: 0}
while (m.prevScroll !== m.curScroll && m.curScroll < max_height) {
// 如果上一次滾動和本次滾動高度一樣, 或者滾動高度大於設置的最高高度, 則停止截取
m = await page.evaluate((scrollStep) => {
if (document.scrollingElement) {
let prevScroll = document.scrollingElement.scrollTop;
document.scrollingElement.scrollTop = prevScroll + scrollStep;
let curScroll = document.scrollingElement.scrollTop
return {prevScroll, curScroll}
}
}, scrollStep);
// 等待3秒後繼續滾動頁面, 為了讓頁面加載充分
await sleep(3000);
}
// 其他業務代碼...
// 截取網頁快照,並設置圖片質量和保存路徑
const screenshot = await page.screenshot({path: `static/${uid}.jpg`, fullPage: true, quality: 70});
const txt = await page.$eval('body', el => {
// el即為dom節點, 可以對body的子節點進行提取,分析
return {...}
})
const Koa = require('koa');
const { resolve } = require('path');
const staticServer = require('koa-static');
const koaBody = require('koa-body');
const cors = require('koa2-cors');
const logger = require('koa-logger');
const glob = require('glob');
const { fork } = require('child_process');
const app = new Koa();
// 創建靜態目錄
app.use(staticServer(resolve(__dirname, './static')));
app.use(staticServer(resolve(__dirname, './db')));
app.use(koaBody());
app.use(logger());
const config = {
imgPath: resolve('./', 'static'),
txtPath: resolve('./', 'db')
}
// 設置跨域
app.use(cors({
origin: function (ctx) {
if (ctx.url.indexOf('fetch') > -1) {
return '*'; // 允許來自所有域名請求
}
return ''; // 這樣就能只允許 http://localhost 這個域名的請求了
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
maxAge: 5, // 該欄位可選,用來指定本次預檢請求的有效期,單位為秒
credentials: true,
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'x-requested-with'],
}))
// 創建異步線程
function createPromisefork(childUrl, data) {
const res = fork(childUrl)
data && res.send(data)
return new Promise(reslove => {
res.on('message', f => {
reslove(f)
})
})
}
app.use(async (ctx, next) => {
if(ctx.url === '/fetch') {
const data = ctx.request.body;
const res = await createPromisefork('./child.js', data)
// 獲取文件路徑
const txtUrls = [];
let reg = /.*?(\d+)\.\w*$/;
glob.sync(`${config.txtPath}/*.*`).forEach(item => {
if(reg.test(item)) {
txtUrls.push(item.replace(reg, '$1'))
}
})
ctx.body = {
state: res,
data: txtUrls,
msg: res ? '抓取完成' : '抓取失敗,原因可能是非法的url或者請求超時或者伺服器內部錯誤'
}
}
await next()
})
app.listen(80)