前端与爬虫

发布时间 2024-01-02 18:03:48作者: 秦伟杰

搜索爬虫, 我们会搜到一大堆 Python 相关的结果

问题: 爬虫和前端有关系吗?

爬虫是什么

爬虫程序是一种计算机程序,旨在通过执行自动化或重复性任务来模仿或替代人类的操作。

爬虫程序执行任务的速度和准确性比真实用户高得多。爬虫程序类型众多,可执行各种任务,并且爬虫程序在互联网流量中的比重也越来越大。

今天我们主要讨论的是 网络爬虫

什么是网络爬虫

网络爬虫(英语:web crawler),也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人。其目的一般为编纂网络索引。--维基百科

如果只是做搜索引擎,那么感兴趣的信息就是互联网中尽可能多的高质量网页;如果要获取某一垂直领域的数据或者有明确的检索需求,那么感兴趣的信息就是根据我们的检索和需求所定位的这些信息,此时,需要过滤掉一些无用信息。前者我们称为通用网络爬虫,后者我们称为聚焦网络爬虫

由此观之, 爬虫的主要作用就是获取&处理信息

原则上,只要是浏览器(客户端)能做的事情,爬虫都能够做

一个简单的爬虫

  • 获取 h1 标签
  • 打印 h1 标签的内容
// 获取h1标签
const h1 = document.querySelector('h1');
// 打印h1标签的内容
console.log(h1.textContent);

是不是感觉很熟悉, 这不就是咱们前端日常的 DOM 操作吗?

是的, 通过 dom 操作获取页面信息, 这就是一个爬虫

保存页面爬取的信息

既然咱们已经获取到了数据, 应该如何保存呢?

function downloadTxtFile() {
  const h1 = document.querySelector('h1');
  const text = h1.textContent; // 要保存的文本内容

  const element = document.createElement('a');
  element.setAttribute(
    'href',
    'data:text/plain;charset=utf-8,' + encodeURIComponent(text)
  );
  element.setAttribute('download', 'file.txt');

  element.style.display = 'none';
  document.body.appendChild(element);

  element.click();

  document.body.removeChild(element);
}

如果我想保存 Excel 表格呢?

咱们可以使用第三方库如 xlsx 或 exceljs 来生成 Excel 文件, 然后提供下载链接给用户。(不在讨论范围)

爬取服务端渲染的网页

步骤

  • 打开页面, 找到需要获取的信息 (标题, 内容, 下一页链接) 的 选择器

  • 使用 axios 获取页面的 HTML 字符串

    const { data } = await axios.get('https://www.sumingxs.com/xiaoshuo/1/1/');
    
  • 使用 cheerio 解析 HTML 字符串

    const $ = cheerio.load(data);
    
  • 使用 jQuery 语法获取信息

    const title = $('h1').text();
    
    // const content = $('.con').text()
    const arr = [];
    $('.con p').each((i, item) => {
      arr.push($(item).text());
    });
    const content = arr.join('\n');
    
    const a = $('.prenext a');
    const nextUrl = a.eq(a.length - 1).attr('href');
    

完整代码

import axios from 'axios';
import * as cheerio from 'cheerio';
import fs from 'fs';

async function getPage(
  url = 'https://www.sumingxs.com/xiaoshuo/1/1/',
  count = 20
) {
  if (count === 0) {
    return;
  }
  const { data } = await axios.get(url);
  const $ = cheerio.load(data);
  const title = $('h1').text();
  console.log(title);

  const content = [];
  $('.con p').each((i, item) => {
    content.push($(item).text());
  });

  const a = $('.prenext a');
  const next = a.eq(a.length - 1).attr('href');
  console.log(next);
  const { host, protocol } = new URL(url);
  const nextUrl = `${protocol}//${host}${next}`;
  console.log(nextUrl);
  fs.writeFileSync(`res/${title}.txt`, content.join('\n'));
  getPage(nextUrl, count - 1);
}

getPage();

编码问题

目标: http://58xs8.com/html/196/196757/26829864.html
这是一个 gbk 编码的页面
老规矩, 咱一上来就

const { data } = await axios.get(
  'http://58xs8.com/html/196/196757/26829864.html'
);
const $ = cheerid.load(data);
const title = $('h1').text();
console.log(title);

这是什么情况

处理编码问题

这里不过多讨论编码的原理, 简单说明一下解决方法吧

  • 采用 buffer 的数据格式获取页面
  • 将 buffer 转换为 utf8 编码字符串

commonJS 模块 iconv-lite

安装

npm i iconv-lite

使用

const { decode } = require('iconv-lite');

const { data } = await axios.get(
  'http://58xs8.com/html/196/196757/26829864.html',
  { responseType: 'arraybuffer' }
);
const newData = decode(data, 'gbk').toString('utf8');
const $ = cheerio.load(newData);
const title = $('h1').text();
console.log(title);

EMS 模块 encoding

安装

npm i encoding

使用

import { convert } from 'encoding';

const { data } = await axios.get(
  'http://58xs8.com/html/196/196757/26829864.html',
  { responseType: 'arraybuffer' }
);
const newData = convert(data, 'utf8', 'gbk');
const $ = cheerio.load(newData);
const title = $('h1').text();
console.log(title);

爬取客户端渲染的网页

目标 https://appid.surge.sh/

我一上来就

const { data } = await axios.get('https://appid.surge.sh/');

不出意外的话, 我们拿到了一个

...
<div id="root"></div>
...

是的, 我们可以直接去抓接口

但是, 当我们不了解接口字段的时候, 从页面获取也是一种选择

puppeteer

前面我们已经在浏览器手动获取到数据了, 现在咱们用代码来操作浏览器获取页面数据

  • 环境: Node.js
  • 依赖:
    • puppeteer (或者其他能发送 http 请求的库),
  • 目标: https://appid.surge.sh/
  • 任务 获取页面的账号密码

步骤

打开浏览器

const browser = await pupteer.launch({
  // 直接用本地的chrome, window填入chrome快捷方式所指向的路径
  executablePath:
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
});

打开一个新页面, 跳转到 https://appid.surge.sh/

const page = await browser.newPage();
await page.goto('https://appid.surge.sh/');

等待页面加载完成

// 数据是异步加载的, 等到数据加载, 渲染页面后的dom出现
await page.waitForSelector('.apple-id-status');

获取页面上的数据

const res = await page.$$eval(
  '#app > div > div.ant-flex.css-1qb1s0s.ant-flex-wrap-wrap.ant-flex-align-center.ant-flex-justify-center > div',
  (el) => {
    const arr = [];
    el.forEach((item) => {
      const email = item.querySelector(
        '.ant-input.css-1qb1s0s[type=text]'
      ).value;
      const password = item.querySelector(
        '.ant-input.css-1qb1s0s[type=password]'
      ).value;

      arr.push({ email, password });
    });
    return arr;
  }
);

完整代码

const pupteer = require('puppeteer');

+(async () => {
  const browser = await pupteer.launch({
    // headless: false,
    executablePath:
      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  });
  const page = await browser.newPage();
  await page.goto('https://appid.surge.sh/');

  await page.waitForSelector('.apple-id-status');
  const res = await page.$$eval(
    '#app > div > div.ant-flex.css-1qb1s0s.ant-flex-wrap-wrap.ant-flex-align-center.ant-flex-justify-center > div',
    (el) => {
      const res = [];
      el.forEach((item) => {
        const email = item.querySelector(
          '.ant-input.css-1qb1s0s[type=text]'
        ).value;
        const password = item.querySelector(
          '.ant-input.css-1qb1s0s[type=password]'
        ).value;

        res.push({ email, password });
      });
      return res;
    }
  );

  console.log(res);
  await browser.close();
})();

反爬与解决

道高一尺, 魔高一丈

  • 封 IP - 挂代理
  • 封 User-Agent 修改 User-Agent
  • 封 Cookie 无痕模式
  • 动态渲染 浏览器爬虫
  • 异步操作 刷接口, 浏览器爬虫
  • 图片伪装 ocr
  • CSS 偏移 截屏 + ocr
  • SVG 映射 截屏 + ocr
  • 验证用户操作 模拟操作

当爬虫的成本和所得的利益不符时, 自然反爬

参考资料