基于Apify+node+react/vue搭建一个有点意思的爬虫平台

简介: 熟悉我的朋友可能会知道,我一向是不写热点的。为什么不写呢?是因为我不关注热点吗?其实也不是。有些事件我还是很关注的,也确实有不少想法和观点。 但我一直奉行一个原则,就是:要做有生命力的内容。


前言


熟悉我的朋友可能会知道,我一向是不写热点的。为什么不写呢?是因为我不关注热点吗?其实也不是。有些事件我还是很关注的,也确实有不少想法和观点。 但我一直奉行一个原则,就是:要做有生命力的内容


本文介绍的内容来自于笔者之前负责研发的爬虫管理平台, 专门抽象出了一个相对独立的功能模块为大家讲解如何使用nodejs开发专属于自己的爬虫平台.文章涵盖的知识点比较多,包含nodejs, 爬虫框架, 父子进程及其通信, reactumi等知识, 笔者会以尽可能简单的语言向大家一一介绍.


你将收获


  • Apify框架介绍和基本使用
  • 如何创建父子进程以及父子进程通信
  • 使用javascript手动实现控制爬虫最大并发数
  • 截取整个网页图片的实现方案
  • nodejs第三方库和模块的使用
  • 使用umi3 + antd4.0搭建爬虫前台界面


平台预览





上图所示的就是我们要实现的爬虫平台, 我们可以输入指定网址来抓取该网站下的数据,并生成整个网页的快照.在抓取完之后我们可以下载数据和图片.网页右边是用户抓取的记录,方便二次利用或者备份.


正文


在开始文章之前,我们有必要了解爬虫的一些应用. 我们一般了解的爬虫, 多用来爬取网页数据, 捕获请求信息, 网页截图等,如下图:



当然爬虫的应用远远不止如此,我们还可以利用爬虫库做自动化测试, 服务端渲染, 自动化表单提交, 测试谷歌扩展程序, 性能诊断等. 任何语言实现的爬虫框架原理往往也大同小异, 接下来笔者将介绍基于nodejs实现的爬虫框架Apify以及用法,并通过一个实际的案例方便大家快速上手爬虫开发.


Apify框架介绍和基本使用



apify是一款用于JavaScript的可伸缩的web爬虫库。能通过无头(headlessChromePuppeteer 实现数据提取和** Web** 自动化作业的开发。 它提供了管理和自动扩展无头Chrome / Puppeteer实例池的工具,支持维护目标URL的请求队列,并可将爬取结果存储到本地文件系统或云端。


我们安装和使用它非常简单, 官网上也有非常多的实例案例可以参考, 具体安装使用步骤如下:


安装

npm install apify --save

使用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();
});

使用node执行后可能会出现如下界面:



程序会自动打开浏览器并打开满足条件的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手动实现控制爬虫最大并发数



以上介绍的是要实现我们的爬虫应用需要考虑的技术问题, 接下来我们开始正式实现业务功能, 因为爬虫任务是在子进程中进行的,所以我们将在子进程代码中实现我们的爬虫功能.我们先来整理一下具体业务需求, 如下图:



j'接下来我会先解决控制爬虫最大并发数这个问题, 之所以要解决这个问题, 是为了考虑爬虫性能问题, 我们不能一次性让爬虫爬取所以的网页,这样会开启很多并行进程来处理, 所以我们需要设计一个节流装置,来控制每次并发的数量, 当前一次的完成之后再进行下一批的页面抓取处理. 具体代码实现如下:


// 异步队列
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去应用,笔者这里拿puppeteerpage.$eval来举例:

const txt = await page.$eval('body', el => {
    // el即为dom节点, 可以对body的子节点进行提取,分析
    return {...}
})

nodejs第三方库和模块的使用



为了搭建完整的node服务平台,笔者采用了


  • koa 一款轻量级可扩展node框架
  • glob 使用强大的正则匹配模式遍历文件
  • koa2-cors 处理访问跨域问题
  • koa-static 创建静态服务目录
  • 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搭建爬虫前台界面



该爬虫平台的前端界面笔者采用umi3+antd4.0开发, 因为antd4.0相比之前版本确实体积和性能都提高了不少, 对于组件来说也做了更合理的拆分. 因为前端页面实现比较简单,整个前端代码使用hooks写不到200行,这里就不一一介绍了.大家可以在笔者的github上学习研究.



界面如下:



大家可以自己克隆本地运行, 也可以基于此开发属于自己的爬虫应用.


项目使用的技术文档地址





目录
相关文章
|
6月前
|
前端开发 JavaScript NoSQL
使用 Node.js、Express 和 React 构建强大的 API
本文详细介绍如何使用 Node.js、Express 和 React 构建强大且动态的 API。从开发环境搭建到集成 React 前端,再到利用 APIPost 高效测试 API,适合各水平开发者。内容涵盖 Node.js 运行时、Express 框架与 React 库的基础知识及协同工作方式,还涉及数据库连接和前后端数据交互。通过实际代码示例,助你快速上手并优化应用性能。
|
JavaScript 前端开发 开发者
VUE 开发——Node.js学习(一)
VUE 开发——Node.js学习(一)
233 2
|
8月前
|
数据采集 JavaScript Android开发
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
277 7
【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
|
7月前
|
数据采集 监控 数据可视化
11.7K Star!这个分布式爬虫管理平台让多语言协作如此简单!
分布式爬虫管理平台Crawlab,支持任何编程语言和框架的爬虫管理,提供可视化界面、任务调度、日志监控等企业级功能,让爬虫开发管理效率提升300%!
237 1
|
9月前
|
JavaScript 前端开发 jenkins
抛弃node和vscode,如何用记事本开发出一个完整的vue前端项目
本文探讨了在不依赖Node和VSCode的情况下,仅使用记事本和浏览器开发一个完整的Vue3前端项目的方法。通过CDN引入Vue、Vue Router、Element-UI等库,直接编写HTML文件实现页面功能,展示了前端开发的本质是生成HTML。虽然日常开发离不开现代工具,但掌握这种基础方法有助于快速实现想法或应对特殊环境限制。文章还介绍了如何用Node简单部署HTML文件到服务器,提供了一种高效、轻量的开发思路。
195 10
|
JavaScript 应用服务中间件 Linux
宝塔面板部署Vue项目、服务端Node___配置域名
本文介绍了如何使用宝塔面板在阿里云服务器上部署Vue项目和Node服务端项目,并配置域名。文章详细解释了安装宝塔面板、上传项目文件、使用pm2启动Node项目、Vue项目打包上传、以及通过Nginx配置域名和反向代理的步骤。
3380 1
宝塔面板部署Vue项目、服务端Node___配置域名
|
JavaScript 前端开发
Vue、ElementUI配合Node、multiparty模块实现图片上传并反显_小demo
如何使用Vue和Element UI配合Node.js及multiparty模块实现图片上传并反显的功能,包括前端的Element UI组件配置和后端的Node.js服务端代码实现。
190 1
Vue、ElementUI配合Node、multiparty模块实现图片上传并反显_小demo
|
JavaScript 前端开发
vue配合axios连接express搭建的node服务器接口_简单案例
文章介绍了如何使用Express框架搭建一个简单的Node服务器,并使用Vue结合Axios进行前端开发和接口调用,同时讨论了开发过程中遇到的跨域问题及其解决方案。
225 0
vue配合axios连接express搭建的node服务器接口_简单案例
|
前端开发 JavaScript API
构建高效Web应用:React与Node.js的完美结合
【8月更文挑战第29天】在当今快速变化的软件开发领域,构建高性能、可扩展的Web应用成为开发者的首要任务。本文将深入探讨如何利用React和Node.js这两大技术栈,打造一个高效且响应迅速的现代Web应用。从前端的用户界面设计到后端的服务逻辑处理,我们将一步步分析这两种技术如何协同工作,提升应用性能,并确保用户体验的流畅性。通过实际代码示例和架构设计的解析,本篇文章旨在为读者提供一套清晰的指南,帮助他们在项目开发中做出更明智的技术选择。
|
JavaScript
成功解决node、node-sass和sass-loader版本冲突问题、不需要降低node版本。如何在vue项目中安装node-sass,以及安装node-sass可能遇到的版本冲突问题
这篇文章介绍了在Vue项目中安装node-sass和sass-loader时遇到的版本冲突问题,并提供了解决这些问题的方法,包括在不降低node版本的情况下成功安装node-sass。
成功解决node、node-sass和sass-loader版本冲突问题、不需要降低node版本。如何在vue项目中安装node-sass,以及安装node-sass可能遇到的版本冲突问题