随着大规模模型技术的兴起,我们可以看到百模大战、各种智能体、百花齐放的应用场景,那么作为一名前端开发者,以前端的视角,我们应当如何积极做好技术储备,开拓技术视野,在智能体时代保持一定的竞争力呢?我尝试通过一系列文章来总结一下!

前言

可能很多人会好奇,你写爬虫就写爬虫为啥要和大模型扯上关系,明显的蹭热点吧。其实我们在做大模型应用时,比如问答系统、内容生成等应用时,通常会用到检索增强生成 (RAG) 技术,标准 RAG(以智能问答🌰) 流程图如下,大致分为以下几个步骤:

image-20240803180740374

离线部分

  1. 知识库:数据从各种来源收集并存储在知识库中。
  2. 清洗、装载:数据进行预处理和清洗,以确保数据质量、清洗后的数据被装载为文档。
  3. 文档:将预处理后的数据组织成文档格式。
  4. 切分:将文档切分成更小的段(chunks),以便后续处理。
  5. 向量化:将切分后的文档段通过向量模型转换为向量表示。
  6. 向量数据库:向量化的文档段存储在向量数据库中,便于快速检索。

在线部分

  1. 用户请求:用户发出查询请求。
  2. Prompt: 用户的请求被处理为一个prompt,准备进行向量化。
  3. 向量化:用户请求的prompt通过向量模型转换为向量表示。
  4. 相似度查询:根据向量化的用户请求在向量数据库中进行相似度查询,检索相关的文档段。
  5. 提取相关知识:检索到的相关文档段作为背景知识注入到提示词模板中。
  6. 提示词模板:将检索到的背景知识与用户请求结合,生成完整的提示词。
  7. LLM(大型语言模型):使用提示词生成最终的回答或内容。
  8. 返回用户:最终的生成内容返回给用户。

我们可以看到,我们的回答是否准确,是否具备差异性,来源于我们的知识库内容的丰富程度和相关性。而知识库内容的获取途径,其中重要的组成部分就是爬虫。

关键概念

如果你以前了解过爬虫相关概念,在讲到爬虫的时候,脑子里蹦出来的会有以下关键词

  1. JS 渲染(JavaScript Rendering)

在现代网页中,很多内容是通过 JavaScript 动态加载和渲染的。这意味着传统的静态网页爬虫(如直接抓取 HTML 内容的爬虫)可能无法获取这些动态加载的内容。因此,爬虫需要具备执行 JavaScript 的能力,以便能够完全渲染页面并获取所有需要的数据。

  1. 无头浏览器(Headless Browsers)

无头浏览器是一种在没有图形用户界面的环境中运行的浏览器。这类浏览器能够执行 JavaScript,并可以模拟用户在浏览器中的操作,如点击、输入文本和滚动页面。常用的无头浏览器有 Puppeteer 和 Playwright,它们基于 Chrome 和 Firefox 等主流浏览器内核,提供了强大的网页自动化和爬取功能。

  1. 等待元素渲染(Waiting for Elements to Render)

由于网页内容可能是动态加载的,爬虫在抓取页面内容时需要等待特定元素加载完成。无头浏览器通常提供了等待元素出现的方法(如 waitForSelector),以确保页面完全加载后再进行数据提取。这样可以避免抓取到不完整的内容或空白页面。

  1. 代理服务器(Proxy Server)

为了防止被目标网站封禁和提高爬取效率,爬虫通常会使用代理服务器。代理服务器可以隐藏爬虫的真实 IP 地址,并通过轮换多个代理 IP 来模拟不同的访问来源,避免爬取行为被检测到并封禁。使用代理服务器还能绕过地理限制,访问特定区域的网站内容。

接下来我给大家介绍一个爬虫框架,并实现一个简单的例子,让大家了解下这个框架。

功能介绍

Crawlee 是一个高效的网页爬虫和抓取工具,旨在帮助开发者快速构建可靠的爬虫系统。它兼具HTTP请求和无头浏览器的爬取能力,适用于各种动态和静态网页内容的抓取。包含 node 和 python两个版本

Crawlee_presentation_final

🌈 主要功能
  • HTTP和无头浏览器爬取:支持传统的HTTP请求和现代无头浏览器(如Playwright和Puppeteer),可以抓取动态生成的内容。
  • 持久化队列:自动管理和持久化URL队列,确保高效和可靠的抓取过程。(广度优先和深度优先)
  • 自动扩展:支持根据需求动态调整爬取规模,提高抓取效率。
  • 代理轮换:内置代理轮换功能,避免IP被封,提高爬取的稳定性。
  • 生命周期管理:提供灵活的生命周期管理,允许自定义爬虫的各个阶段。
  • 错误处理和重试机制:自动处理爬取过程中遇到的错误,并进行重试,确保数据完整性。
👍 优势
  • 单一接口:统一的接口处理HTTP和浏览器爬取,简化开发过程,减少学习成本。
  • JavaScript渲染支持:通过无头浏览器渲染页面,抓取动态内容,使得爬取结果更完整。
  • 丰富的配置选项:提供多种配置选项,适应不同的爬取需求,增强灵活性。http2浏览器请求他fingerprints
  • TypeScript编写:利用TypeScript的强类型检查和自动补全功能,提高代码质量和开发效率。
  • 内置快速HTML解析器,如Cheerio和JSDOM
  • CLI和Docker支持:提供命令行工具和Docker支持,方便集成和部署,适应不同的开发环境。
🆚 与其他爬虫框架对比
  • Scrapy:Scrapy 是一个强大的传统HTTP爬取框架,但不支持动态内容的抓取。Crawlee 在处理动态内容方面更有优势,特别是对现代Web应用的抓取。
  • BeautifulSoup:BeautifulSoup 主要用于解析HTML,功能相对简单,不具备Crawlee 的自动扩展、代理轮换和生命周期管理等高级功能。
  • Selenium:Selenium 可以抓取动态内容,但缺少持久化队列和自动扩展功能,且性能相对较低。Crawlee 提供了更高效和集成的解决方案。

Crawlee 主打一个高效、可高、速度极快,接下来我们通过一个 demo,来了解它有哪些功能!

Live Demo

目标:挑战 20 分钟,完成掘金前端话题内容的爬取,并完成结构化存储

⚠️注意以下示例代码,大部分都是 github 的 copilot 帮我补全的,我只提供思路,他就帮我写完了!!!

初始化项目

打开一个终端,执行以下命令

crawlee 提供 3 个类,方便用户针对不同场景选择不同的类

  • CheerioCrawler 主要针对普通 http 的爬取,并通过 cheerio 库来解释 html,快速高效,但无法处理 js 渲染
  • PuppeteerCrawler google 开源的无头浏览器Puppeteer,提供标准 API 来控制 Chrome/Firefox
  • PlaywrightCrawler microsoft 开源的无头浏览器**playwright** ,提供标准 API 来控制 Chrome/Firefox、Webkit 等浏览器,功能更加齐全

由于笔者原来一直使用 puppeteer,以下会使用 PuppeteerCrawler 来完成 demo,其中 cheerio 可以理解为 node 版本的 jQuery,提供强大 dom 操作

# 你也可以通过以下命令,通过官方模版初始化应用
# npx crawlee create juejin-crawler

cd juejin-crawler
npm init
npm install --save crawlee cheerio puppeteer
安装依赖、启动脚本

打开 package.json修改以下内容,关键两个改动

  • 为了简单,我们是用 esm 来解析 js 文件,需增加 "type": "module",
  • 增加一个启动脚本 "start": "rm -rf storage/ && node src/main.mjs", 其中 main 文件在后续创建
{
  "name": "juejin-crawler",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "rm -rf storage/ && node src/main.mjs",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "type": "module",
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "cheerio": "^1.0.0-rc.12",
    "crawlee": "^3.11.1",
    "puppeteer": "^22.15.0"
  }
}
增加入口文件

增加一个入口文件 src/main.mjs

  • 需要引入一个 router 文件,用来处理整体 handler 操作,包括如何处理列表页、如何处理详情页等等
  • 需要引入一个 config 文件,主要用来配置结构化数据所需的配置,比如爬取页面 URL、页面对于的选择器,如 title 对应详情页的 article-title
  • 初始化一个PuppeteerCrawler实例,具体配置作用,见代码
  • 把请求入口链接加到队列,并启动任务

main.mjs 整体内容如下

import { PuppeteerCrawler, ProxyConfiguration, log } from 'crawlee';
import { router } from './routes.mjs';
import config from './config.mjs';

log.setLevel(log.LEVELS.DEBUG);

log.debug('Setting up crawler.');
// const proxyConfiguration = new ProxyConfiguration({
//   proxyUrls: [],
// });

// 创建 PuppeteerCrawler 实例
const crawler = new PuppeteerCrawler({
  // 是否使用会话池,启用后爬虫会重用会话和 Cookie
  // useSessionPool: true,

  // 是否为每个会话持久化 Cookie,启用后每个会话会有自己的 Cookie 存储
  // persistCookiesPerSession: true,

  // 代理配置,用于通过代理服务器发送请求,可以用于绕过 IP 限制或地理位置限制
  // proxyConfiguration,

  // 最大并发请求数,控制同时进行的请求数量
  maxConcurrency: 10,

  // 每分钟最大请求数,控制每分钟发送的请求数量
  maxRequestsPerMinute: 20,

  // 启动上下文配置
  launchContext: {
    launchOptions: {
      // 是否以无头模式启动浏览器(无头模式:没有图形界面),设置为 false 表示有图形界面
      headless: false,
    },
  },

  // 爬虫运行时的最大请求数量,当达到这个数量时,爬虫会停止
  maxRequestsPerCrawl: 1000,

  // requestHandler 定义了爬虫在访问每个页面时要做的操作
  // 可以在这里提取数据、处理数据、保存数据、调用 API 等
  requestHandler: router,

  // 处理请求失败的情况,记录失败的请求并输出错误日志
  failedRequestHandler({ request, log }) {
    log.error(`Request ${request.url} failed too many times.`);
  },
});

// 添加要爬取的请求,config 是一个包含 URL 和标签的数组
// 这里将每个 item 的 url 和 label 添加到爬虫的请求队列中
await crawler.addRequests(config.map(item => { return { url: item.url, label: item.label } }));

// 启动爬虫
await crawler.run();
增加配置文件,增加灵活性

增加一个配置文件 src/config.mjs ,具体实现以下功能

  • 配置要爬取的页面 URL,给定一个标签,如 juejin
  • 配置各个字段的选择器,这个对于前端工程师来说,小菜一碟,截图看看例子

image-20240803191950244

具体配置信息如下:

export default [
  {
    label: 'juejin',
    url: 'https://juejin.cn/frontend',
    selector: {
      detail: '.entry-list .title-row a',
      title: 'h1.article-title',
      author: '.author-info-box .author-name a span',
      modifiedDate: '.author-info-box .meta-box time',
      hit: '.author-info-box .meta-box .views-count',
      readTime: '.author-info-box .meta-box .read-time',
      description: '.article-header p',
      content: '.article-viewer',
    },
  }
]
增加处理逻辑

增加真正处理逻辑文件 src/routes 这里增加两个处理逻辑,列表页和详情页

  • 列表页:通过模拟滚动的方式,触发滚动底部加载更多,并把列表页所有文章加入爬取队列
  • 详情页:通过配置的选择器,把详情页的数据结构化,并存储到数据集
  • 如果这里为了实现知识库相关功能,也可以把拿到的数据存储到向量数据库,这里不做演示
import { createPuppeteerRouter, Dataset } from 'crawlee';

// import { Article } from './db.mjs';
export const router = createPuppeteerRouter();

import config from './config.mjs';

config.map(async item => {
  const dataset = await Dataset.open(item.label);
  // 打开列表页操作
  router.addHandler(`${item.label}`, async ({ request, page, enqueueLinks, log }) => {
    page.setDefaultTimeout(5000);
    log.debug(`Enqueueing pagination: ${request.url}`)

    // 由于掘金是滚动加载,这里通过模拟滚动到底部,实现页面切换
    const scrollToBottom = async (page, scrollDelay = 5000) => {
      let previousHeight;
      let newHeight;
      let reachedEnd = false;
      let count = 0;

      // 这里只滚动2次,如果还没有到底部,可以根据实际情况调整
      while (!reachedEnd && count < 2) {
        previousHeight = await page.evaluate('document.body.scrollHeight');
        await page.evaluate(`window.scrollBy(0, ${previousHeight})`);
        await new Promise(resolve => setTimeout(resolve, scrollDelay));
        newHeight = await page.evaluate('document.body.scrollHeight');

        count++;

        if (previousHeight === newHeight) {
          reachedEnd = true;
        }
      }
    };

    // 模拟滚动加载
    await scrollToBottom(page);

    // 等待页面加载完成,这里只为演示,在这个例子中,不需要等待
    await page.waitForSelector(item.selector.detail);

    // 把列表页的详情页链接加入到爬虫队列中
    await enqueueLinks({
      selector: item.selector.detail,
      label: `${item.label}-DETAIL`,
    });

    await page.close();
  });

  // 打开详情页操作
  router.addHandler(`${item.label}-DETAIL`, async ({ request, page, log }) => {
    page.setDefaultTimeout(5000);

    log.debug(`Extracting data: ${request.url}`)

    const urlParts = request.url.split('/').slice(-2);
    const url = request.url;

    await page.waitForSelector(item.selector.content);

    const details = await page.evaluate((url, urlParts, item) => {
      return {
        url,
        uniqueIdentifier: urlParts.join('/'),
        type: urlParts[0],
        title: document.querySelector(item.selector.title).innerText,
        author: document.querySelector(item.selector.author).innerText,
        modifiedDate: document.querySelector(item.selector.modifiedDate).innerText,
        hit: document.querySelector(item.selector.hit).innerText,
        readTime: document.querySelector(item.selector.readTime).innerText,
        content: document.querySelector(item.selector.content).innerText,
        contentHTML: document.querySelector(item.selector.content).innerHTML,
      };
    }, url, urlParts, item);

    log.debug(`Saving data: ${request.url}`)
    // await Article.create(details);
    await dataset.pushData(details);
    await page.close();
  });

});
router.addDefaultHandler(async ({ request, page, enqueueLinks, log }) => {
  log.error(`Unhandled request: ${request.url}`);
});
启动爬虫
npm start

image-20240803202840524

打开 chrome 浏览器,按照逻辑自动打开列表页、详情页

image-20240803202901165

数据存储到 storage 目录下,并按照我们定义的结构,以 json 格式保存起来

image-20240803203053104

增加代理 IP Server

由于上面的例子我做了限制,如果爬取并发数太高的话,大概率会被封禁,所以这个时候就需要用到代理 ip 服务 和 session 管理

  • Proxy 管理:如果有代理 URL,只需以下几行代码,即可实现代理访问,并通过 proxyInfo 方法检查当前代理
import { PuppeteerCrawler, ProxyConfiguration } from 'crawlee';
const proxyConfiguration = new ProxyConfiguration({
    proxyUrls: ['http://proxy-1.com', 'http://proxy-2.com'],
});
const crawler = new PuppeteerCrawler({
    useSessionPool: true,
    persistCookiesPerSession: true,
    proxyConfiguration,
    async requestHandler({ proxyInfo }) {
        console.log(proxyInfo);
    },
});
  • session 管理:使用 session 管理,主要为了过滤掉被阻止或者不工作的代理,大致代码如下:
import { PuppeteerCrawler, ProxyConfiguration } from 'crawlee';

const proxyConfiguration = new ProxyConfiguration({
    /* opts */
});

const crawler = new PuppeteerCrawler({
    // To use the proxy IP session rotation logic, you must turn the proxy usage on.
    proxyConfiguration,
    // Activates the Session pool (default is true).
    useSessionPool: true,
    // Overrides default Session pool configuration
    sessionPoolOptions: { maxPoolSize: 100 },
    // Set to true if you want the crawler to save cookies per session,
    // and set the cookies to page before navigation automatically (default is true).
    persistCookiesPerSession: true,
    async requestHandler({ page, session }) {
        const title = await page.title();

        if (title === 'Blocked') {
            session.retire();
        } else if (title === 'Not sure if blocked, might also be a connection error') {
            session.markBad();
        } else {
            // session.markGood() - this step is done automatically in PuppeteerCrawler.
        }
    },
});

如果你业务中需要代理服务,可以通过快代理购买服务,简单易用,所以推荐

image-20240803204104746


⚠️⚠️⚠️ 本文内容,包含示例代码,部分由 AI 生成!!

以上示例代码均提交到 juejin-crawler 欢迎👏👏👏 star 、点赞👍、关注、讨论

至此,第一篇关于 AIGC 文章已完成,预告下下一篇文章,关于大模型 LangchainJS 工具的介绍,期待(✧∀✧)