基於Apify+node+react/vue搭建一個有點意思的爬蟲平臺

2021-02-16 慕課訓練營

上圖所示的就是我們要實現的爬蟲平臺, 我們可以輸入指定網址來抓取該網站下的數據,並生成整個網頁的快照.在抓取完之後我們可以下載數據和圖片.網頁右邊是用戶抓取的記錄,方便二次利用或者備份.
在開始文章之前,我們有必要了解爬蟲的一些應用. 我們一般了解的爬蟲, 多用來爬取網頁數據, 捕獲請求信息, 網頁截圖等,如下圖:

當然爬蟲的應用遠遠不止如此,我們還可以利用爬蟲庫做自動化測試, 服務端渲染, 自動化表單提交, 測試谷歌擴展程序, 性能診斷等. 任何語言實現的爬蟲框架原理往往也大同小異, 接下來筆者將介紹基於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();
});

程序會自動打開瀏覽器並打開滿足條件的url頁面. 我們還可以使用它提供的cli工具實現更加便捷的爬蟲服務管理等功能,感興趣的朋友可以嘗試一下. apify提供了很多有用的api供開發者使用, 如果想實現更加複雜的能力,可以研究一下,下圖是官網api截圖:筆者要實現的爬蟲主要使用了Apify集成的Puppeteer能力, 如果對Puppeteer不熟悉的可以去官網學習了解, 本文模塊會一一列出項目使用的技術框架的文檔地址.
我們要想實現一個爬蟲平臺, 要考慮的一個關鍵問題就是爬蟲任務的執行時機以及以何種方式執行. 因為爬取網頁和截圖需要等網頁全部加載完成之後再處理, 這樣才能保證數據的完整性, 所以我們可以認定它為一個耗時任務.當我們使用nodejs作為後臺伺服器時, 由於nodejs本身是單線程的,所以當爬取請求傳入nodejs時, nodejs不得不等待這個"耗時任務"完成才能進行其他請求的處理, 這樣將會導致頁面其他請求需要等待該任務執行結束才能繼續進行, 所以為了更好的用戶體驗和流暢的響應,我們不德不考慮多進程處理. 好在nodejs設計支持子進程, 我們可以把爬蟲這類耗時任務放入子進程中來處理,當子進程處理完成之後再通知主進程. 整個流程如下圖所示:nodejs有3種創建子進程的方式, 這裡我們使用fork來處理, 具體實現方式如下:

// 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()
})

以上是一個實現父子進程通信的簡單案例, 我們的爬蟲服務也會採用該模式來實現.
使用javascript手動實現控制爬蟲最大並發數
以上介紹的是要實現我們的爬蟲應用需要考慮的技術問題, 接下來我們開始正式實現業務功能, 因為爬蟲任務是在子進程中進行的,所以我們將在子進程代碼中實現我們的爬蟲功能.我們先來整理一下具體業務需求, 如下圖:接下來我會先解決控制爬蟲最大並發數這個問題, 之所以要解決這個問題, 是為了考慮爬蟲性能問題, 我們不能一次性讓爬蟲爬取所以的網頁,這樣會開啟很多並行進程來處理, 所以我們需要設計一個節流裝置,來控制每次並發的數量, 當前一次的完成之後再進行下一批的頁面抓取處理. 

// 異步隊列
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
}
}

以上代碼即可實現每次同時抓取6個網頁, 當第一次任務都結束之後才會執行下一批任務.代碼中的urls指的是用戶輸入的url集合, fetchPage為抓取頁面的爬蟲邏輯, 筆者將其封裝成了promise.
我們都知道puppeteer截取網頁圖片只會截取加載完成的部分,對於一般的靜態網站來說完全沒有問題, 但是對於頁面內容比較多的內容型或者電商網站, 基本上都採用了按需加載的模式, 所以一般手段截取下來的只是一部分頁面, 或者截取的是圖片還沒加載出來的佔位符,如下圖所示:所以為了實現截取整個網頁,需要進行人為幹預.筆者這裡提供一種簡單的實現思路, 可以解決該問題. 核心思路就是利用puppeteer的api手動讓瀏覽器滾動到底部, 每次滾動一屏, 直到頁面的滾動高度不變時則認為滾動到底部.具體實現如下:

// 滾動高度
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});

爬蟲代碼的其他部分因為不是核心重點,這裡不一一舉例, 我已經放到github上,大家可以交流研究.有關如何提取網頁文本, 也有現成的api可以調用, 大家可以選擇適合自己業務的api去應用,筆者這裡拿puppeteer的page.$eval來舉例:

const txt = await page.$eval('body', el => {
// el即為dom節點, 可以對body的子節點進行提取,分析
return {...}
})


為了搭建完整的node服務平臺,筆者採用了koa-body 獲取請求體數據 有關如何使用這些模塊實現一個完整的服務端應用, 筆者在代碼裡做了詳細的說明, 這裡就不一一討論了. 具體代碼如下:

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)


該爬蟲平臺的前端界面筆者採用umi3+antd4.0開發, 因為antd4.0相比之前版本確實體積和性能都提高了不少, 對於組件來說也做了更合理的拆分. 因為前端頁面實現比較簡單,整個前端代碼使用hooks寫不到200行,這裡就不一一介紹了.大家可以在筆者的github上學習研究.github項目地址: 基於Apify+node+react搭建的有點意思的爬蟲平臺大家可以自己克隆本地運行, 也可以基於此開發屬於自己的爬蟲應用.項目使用的技術文檔地址apify 一款用於JavaScript的可伸縮的web爬蟲庫koa -- 基於nodejs平臺的下一代web開發框架

相關焦點

  • 快速在你的vue/react應用中實現ssr(服務端渲染)
    摘要ssr(服務端渲染)技術實現方案接下來筆者將列舉幾個常用的基於vue/react的服務端渲染方案,如下:使用next.js/nuxt.js的服務端渲染方案>使用node+vue-server-renderer實現vue項目的服務端渲染使用node+React renderToStaticMarkup實現react項目的服務端渲染傳統網站通過模板引擎來實現ssr(比如ejs, jade, pug等)
  • 基於Vue實現一個有點意思的拼拼樂小遊戲
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言為了加深大家對vue的了解和vue項目實戰,筆者採用vue生態來重構此項目,方便大家學習和探索。
  • 前端技術:React&Vue對比
    React和vue的業務邏輯是差不多,vue在react上封裝了更簡潔的方法,使用起來更加的便捷,如:提供了便捷的指令(v-for,v-if,v-model),還提供了更多的屬性(computed,watch),我還是比較喜歡用react的,更接近js原生,更容易於理解它。
  • 基於Node.js 的爬蟲丨前端開發者
    前端開發者丨Node.js基於nodejs 的爬蟲 API接口項目,包括前端開發日報、知乎日報、前端top框架排行、妹紙福利、搞笑視頻、各類視頻新聞資訊 熱點詳情接口數據 https://ecitlm.github.io/Node-SpliderApi/#/原文地址:https://gitee.com/ecitlm/splider
  • 最火移動端跨平臺方案盤點:React Native、weex、Flutter
    react native 的打包腳本目錄為/node_modules/react-native/local-cli,打包最後會通過 metro 模塊壓縮 bundle 文件。而bundle文件只會打包js代碼,自然不會包含圖片等靜態資源,所以打包後的靜態資源,其實是被拷貝到對應的平臺資源文件夾中。
  • 三年React 開發經驗的我,遷移到 Vue 的心路歷程
    最初僅有 React,後來使用 Redux 和 React 的其他庫(react-router、react-redux、prop-types 等)配合使用。我喜歡 React 的簡單和方便,使用 React 的時光一直都很快樂。我喜歡這個時代,有太多的好工具幫助我們更快更好地開發應用。
  • vue高級進階系列——用typescript玩轉vue和vuex
    接下來,我不會過多介紹vuex的用法,而是介紹如何基於typescript,用class的方式來使用vue和vuex進行項目開發,相信使用過react的朋友們對class的寫法不會陌生,那就讓我們開始吧!為了省去一些配置上的麻煩,我們直接採用vue-cli3來搭建項目。在創建項目的時候選中typescript即可。
  • 精通React/Vue系列之帶你實現一個功能強大的通知提醒框
    正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先來解構一下Notification組件, 一個Notification分為以下幾個部分:每一個區塊都可以自定義配置, 也可以組合其他組件.並且我們可以配置提醒框出現的位置,就像antd
  • 解密Vue SSR
    首先我們得去構建一個vue的實例,也就是我們前面構建流程中說到的app.js做的事情,但是這裡不同於傳統的客戶端渲染的程序,我們需要用一個工廠函數去封裝它,以便每一個用戶的請求都能夠返回一個新的實例,也就是官網說到的避免交叉汙染了。
  • 手把手教你用JS/Vue/React實現幸運水果機(80後情懷之作)
    項目體驗地址免費視頻教程分別使用原生JS,Vue和React,手把手教你開發一個H5小遊戲,快速上手Vue和React框架的使用。項目截圖在線體驗在線體驗遊戲介紹幸運水果機是一款街機遊戲,遊戲界面由24個方格拼接成一個正方形,每個方格中都有一個不同的水果圖形,方格下都有一個小燈。
  • VUE初體驗篇-安裝
    node的包管理工具,使用node之後你會接觸各種各樣成千上萬的Package(包),就需要一個管理工具能很好的解決它的安裝,更新,依賴包安裝等等的維護。默認安裝完node之後,npm會自動安裝上的。還是cmd,輸入命令 npm -V 。正常出現版本號,就可以了。
  • Vue Element+Node.js開發企業通用管理後臺系統
    1.簡介綜合應用 Vue 和 Node 技術,基於 Element-UI 組件庫搭建「小慕讀書
  • 現場教學,優雅地處理基於 Vue CLI 項目中的 async await 異常
    但是,實際中,使用 loader 並不是這麼簡單,例如當你的項目是基於「Vue CLI」的時候,此時你想改一些配置,那可能夠你喝一壺的...所以,今天我們就來聊聊如何優雅地處理基於「Vue CLI」項目中的 async await 異常~準備一個 Loader其實,對於「loader」而言,這裡可以是「babel-plugin」,也可以是「webpack-loader」。
  • 《精通react/vue組件設計》之實現一個健壯的警告提示(Alert)組件
    正文在開始組件設計之前希望大家對css3和js有一定的基礎,並了解基本的react/vue語法.我們先看看實現後的組件效果:1. 組件設計思路按照之前筆者總結的組件設計原則,我們第一步是要確認需求., 會得出如下線框圖:對於react選手來說,如果沒用typescript,建議大家都用PropTypes, 它是react內置的類型檢測工具,我們可以直接在項目中導入. vue有自帶的屬性檢測方式,這裡就不一一介紹了.
  • 精通react/vue組件設計之實現一個Tag(標籤)和Empty(空狀態)組件
    今天主要帶大家一起實現一個Tag組件和Empty(空狀態)組件,在介紹組件設計之前,先給大家介紹一個免費開源的圖標庫icomoon,可以在線導入SVG格式字體,並進行編輯,然後下載來使用,在組件設計中有具體的使用介紹.
  • 如何在Windows系統安裝最新版本的Node.js
    工具windows作業系統Node.js技術JavaScript在使用vue框架、react框架和angularjs框架時,隨著框架版本不斷更新,對應的Node.js版本也在不斷更新;如果版本不對應,搭建框架的項目就啟動不了。
  • 最全Vue2.0學習路線,各個階段適用
    Vue2.0更是結合nodejs、webpack自動化es6新語法等。這些都是新手很難理解的知識點,所以今天發個乾貨,希望幫助到有心的讀者和粉絲,也算好事一件。下面建議學習順序,從 新手起步,到實戰開發,到進階核心都有介紹,結合了自己查的資料和經驗和vue作者尤大的一些建議匯總而成,覺得好請轉發、收藏。
  • 基於jsoneditor二次封裝一個可實時預覽的json編輯器組件react版
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言做為一名前端開發人員,掌握vue/react/angular等框架已經是必不可少的技能了,我們都知道,vue或react等MVVM框架提倡組件化開發
  • 前端諸神大戰,Vue、React 依舊笑傲江湖
    那麼,項目搭建框架時該如何選擇才能得到更適合的效果呢?且看本文分析。整理 | 彎月 責編 | 張文頭圖 | CSDN 下載自視覺中國自 2010 年 AngularJS 第一版發布以來,前端框架的發展經歷了十個年頭,前端框架大戰也幾乎告一段落。