自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

基于Apify+node+react/vue搭建一個(gè)有點(diǎn)意思的爬蟲平臺(tái)

開發(fā) 前端
本文介紹的內(nèi)容來(lái)自于筆者之前負(fù)責(zé)研發(fā)的爬蟲管理平臺(tái), 專門抽象出了一個(gè)相對(duì)獨(dú)立的功能模塊為大家講解如何使用nodejs開發(fā)專屬于自己的爬蟲平臺(tái).文章涵蓋的知識(shí)點(diǎn)比較多,包含nodejs, 爬蟲框架, 父子進(jìn)程及其通信, react和umi等知識(shí), 筆者會(huì)以盡可能簡(jiǎn)單的語(yǔ)言向大家一一介紹.

前言

熟悉我的朋友可能會(huì)知道,我一向是不寫熱點(diǎn)的。為什么不寫呢?是因?yàn)槲也魂P(guān)注熱點(diǎn)嗎?其實(shí)也不是。有些事件我還是很關(guān)注的,也確實(shí)有不少想法和觀點(diǎn)。但我一直奉行一個(gè)原則,就是:要做有生命力的內(nèi)容。

本文介紹的內(nèi)容來(lái)自于筆者之前負(fù)責(zé)研發(fā)的爬蟲管理平臺(tái), 專門抽象出了一個(gè)相對(duì)獨(dú)立的功能模塊為大家講解如何使用nodejs開發(fā)專屬于自己的爬蟲平臺(tái).文章涵蓋的知識(shí)點(diǎn)比較多,包含nodejs爬蟲框架父子進(jìn)程及其通信reactumi等知識(shí), 筆者會(huì)以盡可能簡(jiǎn)單的語(yǔ)言向大家一一介紹。

你將收獲

  • Apify框架介紹和基本使用
  • 如何創(chuàng)建父子進(jìn)程以及父子進(jìn)程通信
  • 使用javascript手動(dòng)實(shí)現(xiàn)控制爬蟲最大并發(fā)數(shù)
  • 截取整個(gè)網(wǎng)頁(yè)圖片的實(shí)現(xiàn)方案
  • nodejs第三方庫(kù)和模塊的使用
  • 使用umi3 + antd4.0搭建爬蟲前臺(tái)界面

平臺(tái)預(yù)覽

圖片

圖片

圖片

上圖所示的就是我們要實(shí)現(xiàn)的爬蟲平臺(tái), 我們可以輸入指定網(wǎng)址來(lái)抓取該網(wǎng)站下的數(shù)據(jù),并生成整個(gè)網(wǎng)頁(yè)的快照.在抓取完之后我們可以下載數(shù)據(jù)和圖片.網(wǎng)頁(yè)右邊是用戶抓取的記錄,方便二次利用或者備份.

正文

在開始文章之前,我們有必要了解爬蟲的一些應(yīng)用. 我們一般了解的爬蟲, 多用來(lái)爬取網(wǎng)頁(yè)數(shù)據(jù), 捕獲請(qǐng)求信息, 網(wǎng)頁(yè)截圖等,如下圖:

圖片

當(dāng)然爬蟲的應(yīng)用遠(yuǎn)遠(yuǎn)不止如此,我們還可以利用爬蟲庫(kù)做自動(dòng)化測(cè)試服務(wù)端渲染自動(dòng)化表單提交測(cè)試谷歌擴(kuò)展程序性能診斷等. 任何語(yǔ)言實(shí)現(xiàn)的爬蟲框架原理往往也大同小異, 接下來(lái)筆者將介紹基于nodejs實(shí)現(xiàn)的爬蟲框架Apify以及用法,并通過一個(gè)實(shí)際的案例方便大家快速上手爬蟲開發(fā).

Apify框架介紹和基本使用

apify是一款用于JavaScript的可伸縮的web爬蟲庫(kù)。能通過無(wú)頭(headlessChrome 和 Puppeteer 實(shí)現(xiàn)數(shù)據(jù)提取和** Web** 自動(dòng)化作業(yè)的開發(fā)。它提供了管理和自動(dòng)擴(kuò)展無(wú)頭Chrome / Puppeteer實(shí)例池的工具,支持維護(hù)目標(biāo)URL的請(qǐng)求隊(duì)列,并可將爬取結(jié)果存儲(chǔ)到本地文件系統(tǒng)或云端。

我們安裝和使用它非常簡(jiǎn)單, 官網(wǎng)上也有非常多的實(shí)例案例可以參考, 具體安裝使用步驟如下:

安裝

npm install apify --save
復(fù)制代碼

使用Apify開始第一個(gè)案例

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();
});
復(fù)制代碼

使用node執(zhí)行后可能會(huì)出現(xiàn)如下界面:

圖片

程序會(huì)自動(dòng)打開瀏覽器并打開滿足條件的url頁(yè)面. 我們還可以使用它提供的cli工具實(shí)現(xiàn)更加便捷的爬蟲服務(wù)管理等功能,感興趣的朋友可以嘗試一下. apify提供了很多有用的api供開發(fā)者使用, 如果想實(shí)現(xiàn)更加復(fù)雜的能力,可以研究一下,下圖是官網(wǎng)api截圖:

圖片

筆者要實(shí)現(xiàn)的爬蟲主要使用了Apify集成的Puppeteer能力, 如果對(duì)Puppeteer不熟悉的可以去官網(wǎng)學(xué)習(xí)了解, 本文模塊會(huì)一一列出項(xiàng)目使用的技術(shù)框架的文檔地址.

如何創(chuàng)建父子進(jìn)程以及父子進(jìn)程通信

我們要想實(shí)現(xiàn)一個(gè)爬蟲平臺(tái), 要考慮的一個(gè)關(guān)鍵問題就是爬蟲任務(wù)的執(zhí)行時(shí)機(jī)以及以何種方式執(zhí)行. 因?yàn)榕廊【W(wǎng)頁(yè)和截圖需要等網(wǎng)頁(yè)全部加載完成之后再處理, 這樣才能保證數(shù)據(jù)的完整性, 所以我們可以認(rèn)定它為一個(gè)耗時(shí)任務(wù).

當(dāng)我們使用nodejs作為后臺(tái)服務(wù)器時(shí), 由于nodejs本身是單線程的,所以當(dāng)爬取請(qǐng)求傳入nodejs時(shí), nodejs不得不等待這個(gè)"耗時(shí)任務(wù)"完成才能進(jìn)行其他請(qǐng)求的處理, 這樣將會(huì)導(dǎo)致頁(yè)面其他請(qǐng)求需要等待該任務(wù)執(zhí)行結(jié)束才能繼續(xù)進(jìn)行, 所以為了更好的用戶體驗(yàn)和流暢的響應(yīng),我們不得不考慮多進(jìn)程處理. 好在nodejs設(shè)計(jì)支持子進(jìn)程, 我們可以把爬蟲這類耗時(shí)任務(wù)放入子進(jìn)程中來(lái)處理,當(dāng)子進(jìn)程處理完成之后再通知主進(jìn)程. 整個(gè)流程如下圖所示:

圖片

nodejs有3種創(chuàng)建子進(jìn)程的方式, 這里我們使用fork來(lái)處理, 具體實(shí)現(xiàn)方式如下:

// child.js
function computedTotal(arr, cb) {
    // 耗時(shí)計(jì)算任務(wù)
}

// 與主進(jìn)程通信
// 監(jiān)聽主進(jìn)程信號(hào)
process.on('message', (msg) => {
  computedTotal(bigDataArr, (flag) => {
    // 向主進(jìn)程發(fā)送完成信號(hào)
    process.send(flag);
  })
});

// main.js
const { fork } = require('child_process');

app.use(async (ctx, next) => {
  if(ctx.url === '/fetch') {
    const data = ctx.request.body;
    // 通知子進(jìn)程開始執(zhí)行任務(wù),并傳入數(shù)據(jù)
    const res = await createPromisefork('./child.js', data)
  }
  
  // 創(chuàng)建異步線程
  function createPromisefork(childUrl, data) {
    // 加載子進(jìn)程
    const res = fork(childUrl)
    // 通知子進(jìn)程開始work
    data && res.send(data)
    return new Promise(reslove => {
        res.on('message', f => {
            reslove(f)
        })
    })
  }
  
  await next()
})
復(fù)制代碼

以上是一個(gè)實(shí)現(xiàn)父子進(jìn)程通信的簡(jiǎn)單案例, 我們的爬蟲服務(wù)也會(huì)采用該模式來(lái)實(shí)現(xiàn).

使用javascript手動(dòng)實(shí)現(xiàn)控制爬蟲最大并發(fā)數(shù)

以上介紹的是要實(shí)現(xiàn)我們的爬蟲應(yīng)用需要考慮的技術(shù)問題, 接下來(lái)我們開始正式實(shí)現(xiàn)業(yè)務(wù)功能, 因?yàn)榕老x任務(wù)是在子進(jìn)程中進(jìn)行的,所以我們將在子進(jìn)程代碼中實(shí)現(xiàn)我們的爬蟲功能.我們先來(lái)整理一下具體業(yè)務(wù)需求, 如下圖:

圖片

接下來(lái)我會(huì)先解決控制爬蟲最大并發(fā)數(shù)這個(gè)問題, 之所以要解決這個(gè)問題, 是為了考慮爬蟲性能問題, 我們不能一次性讓爬蟲爬取所有的網(wǎng)頁(yè),這樣會(huì)開啟很多并行進(jìn)程來(lái)處理, 所以我們需要設(shè)計(jì)一個(gè)節(jié)流裝置,來(lái)控制每次并發(fā)的數(shù)量, 當(dāng)前一次的完成之后再進(jìn)行下一批的頁(yè)面抓取處理. 具體代碼實(shí)現(xiàn)如下:

// 異步隊(duì)列
const queue = []
// 最大并發(fā)數(shù)
const max_parallel = 6
// 開始指針
let start = 0

for(let i = 0; i < urls.length; i++) {
  // 添加異步隊(duì)列
  queue.push(fetchPage(browser, i, urls[i]))
  if(i &&
      (i+1) % max_parallel === 0 
        || i === (urls.length - 1)) {
    // 每隔6條執(zhí)行一次, 實(shí)現(xiàn)異步分流執(zhí)行, 控制并發(fā)數(shù)
    await Promise.all(queue.slice(start, i+1))
    start = i
  }
}
復(fù)制代碼

以上代碼即可實(shí)現(xiàn)每次同時(shí)抓取6個(gè)網(wǎng)頁(yè), 當(dāng)?shù)谝淮稳蝿?wù)都結(jié)束之后才會(huì)執(zhí)行下一批任務(wù).代碼中的urls指的是用戶輸入的url集合, fetchPage為抓取頁(yè)面的爬蟲邏輯, 筆者將其封裝成了promise.

如何截取整個(gè)網(wǎng)頁(yè)快照

我們都知道puppeteer截取網(wǎng)頁(yè)圖片只會(huì)截取加載完成的部分,對(duì)于一般的靜態(tài)網(wǎng)站來(lái)說(shuō)完全沒有問題, 但是對(duì)于頁(yè)面內(nèi)容比較多的內(nèi)容型或者電商網(wǎng)站, 基本上都采用了按需加載的模式, 所以一般手段截取下來(lái)的只是一部分頁(yè)面, 或者截取的是圖片還沒加載出來(lái)的占位符,如下圖所示:

圖片

所以為了實(shí)現(xiàn)截取整個(gè)網(wǎng)頁(yè),需要進(jìn)行人為干預(yù).筆者這里提供一種簡(jiǎn)單的實(shí)現(xiàn)思路, 可以解決該問題. 核心思路就是利用puppeteer的api手動(dòng)讓瀏覽器滾動(dòng)到底部, 每次滾動(dòng)一屏, 直到頁(yè)面的滾動(dòng)高度不變時(shí)則認(rèn)為滾動(dòng)到底部.具體實(shí)現(xiàn)如下:

// 滾動(dòng)高度
let scrollStep = 1080;
// 最大滾動(dòng)高度, 防止無(wú)限加載的頁(yè)面導(dǎo)致長(zhǎng)效耗時(shí)任務(wù)
let max_height = 30000;
let m = {prevScroll: -1, curScroll: 0}

while (m.prevScroll !== m.curScroll && m.curScroll < max_height) {
    // 如果上一次滾動(dòng)和本次滾動(dòng)高度一樣, 或者滾動(dòng)高度大于設(shè)置的最高高度, 則停止截取
    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秒后繼續(xù)滾動(dòng)頁(yè)面, 為了讓頁(yè)面加載充分
    await sleep(3000);
}
// 其他業(yè)務(wù)代碼...
// 截取網(wǎng)頁(yè)快照,并設(shè)置圖片質(zhì)量和保存路徑
const screenshot = await page.screenshot({path: `static/${uid}.jpg`, fullPage: true, quality: 70});
復(fù)制代碼

爬蟲代碼的其他部分因?yàn)椴皇呛诵闹攸c(diǎn),這里不一一舉例, 我已經(jīng)放到github上,大家可以交流研究.

有關(guān)如何提取網(wǎng)頁(yè)文本, 也有現(xiàn)成的api可以調(diào)用, 大家可以選擇適合自己業(yè)務(wù)的api去應(yīng)用,筆者這里拿puppeteerpage.$eval來(lái)舉例:

const txt = await page.$eval('body', el => {
    // el即為dom節(jié)點(diǎn), 可以對(duì)body的子節(jié)點(diǎn)進(jìn)行提取,分析
    return {...}
})
復(fù)制代碼

nodejs第三方庫(kù)和模塊的使用

為了搭建完整的node服務(wù)平臺(tái),筆者采用了

  • koa 一款輕量級(jí)可擴(kuò)展node框架
  • glob 使用強(qiáng)大的正則匹配模式遍歷文件
  • koa2-cors 處理訪問跨域問題
  • koa-static 創(chuàng)建靜態(tài)服務(wù)目錄
  • koa-body 獲取請(qǐng)求體數(shù)據(jù) 有關(guān)如何使用這些模塊實(shí)現(xiàn)一個(gè)完整的服務(wù)端應(yīng)用, 筆者在代碼里做了詳細(xì)的說(shuō)明, 這里就不一一討論了. 具體代碼如下:
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();
// 創(chuàng)建靜態(tài)目錄
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')
}

// 設(shè)置跨域
app.use(cors({
  origin: function (ctx) {
      if (ctx.url.indexOf('fetch') > -1) {
        return '*'; // 允許來(lái)自所有域名請(qǐng)求
      }
      return ''; // 這樣就能只允許 http://localhost 這個(gè)域名的請(qǐng)求了
  },
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
  maxAge: 5,  //  該字段可選,用來(lái)指定本次預(yù)檢請(qǐng)求的有效期,單位為秒
  credentials: true,
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'x-requested-with'],
}))

// 創(chuàng)建異步線程
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或者請(qǐng)求超時(shí)或者服務(wù)器內(nèi)部錯(cuò)誤'
    }
  }
  await next()
})

app.listen(80)
復(fù)制代碼

使用umi3 + antd4.0搭建爬蟲前臺(tái)界面

該爬蟲平臺(tái)的前端界面筆者采用umi3+antd4.0開發(fā), 因?yàn)閍ntd4.0相比之前版本確實(shí)體積和性能都提高了不少, 對(duì)于組件來(lái)說(shuō)也做了更合理的拆分. 因?yàn)榍岸隧?yè)面實(shí)現(xiàn)比較簡(jiǎn)單,整個(gè)前端代碼使用hooks寫不到200行,這里就不一一介紹了.大家可以在筆者的github上學(xué)習(xí)研究.

  • github項(xiàng)目地址: 基于Apify+node+react搭建的有點(diǎn)意思的爬蟲平臺(tái)

界面如下:

圖片

大家可以自己克隆本地運(yùn)行, 也可以基于此開發(fā)屬于自己的爬蟲應(yīng)用.

項(xiàng)目使用的技術(shù)文檔地址

  • apify 一款用于JavaScript的可伸縮的web爬蟲庫(kù)
  • Puppeteer
  • koa -- 基于nodejs平臺(tái)的下一代web開發(fā)框架
責(zé)任編輯:姜華 來(lái)源: 趣談前端
相關(guān)推薦

2023-10-06 11:48:37

reactvuenodejs

2021-08-08 21:17:18

管理配置平臺(tái)

2023-01-18 07:49:42

2011-10-25 09:28:30

Node.js

2022-01-27 13:02:46

前端爬蟲工具

2018-05-09 09:44:51

Java分布式系統(tǒng)

2013-06-18 09:51:52

PomeloPomelo平臺(tái)搭建平臺(tái)

2010-03-08 17:20:56

搭建私有云

2022-10-19 08:01:17

Gif動(dòng)圖前端

2023-09-14 08:46:50

ReactVue

2022-04-28 09:05:41

網(wǎng)絡(luò)爬蟲Python

2021-04-26 18:48:48

微應(yīng)用React

2021-03-31 08:01:24

React Portareactcss3

2023-07-14 07:23:21

ReactuseEffect

2018-01-02 16:08:00

AndroidiOSReact Nativ

2021-03-13 12:54:50

Node進(jìn)程Cron

2021-06-07 17:34:39

拓?fù)渑判?/a>排序算法數(shù)據(jù)結(jié)構(gòu)與算法

2024-06-19 08:45:13

2024-05-20 01:10:00

Promise變量

2020-12-12 13:50:16

云開發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)