Node.js爬虫入门指南:使用API方式爬取Wallhaven壁纸信息并存入mysql (三)

本文介绍了作者改进的两个版本的Wallhaven壁纸爬虫,第一个版本耗时约24小时,而通过采用多线程技术和代理管理,第二个版本缩短至10小时。详细讲述了如何配置、使用多线程、代理设置以及解决网络请求错误的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

到此为止,我写了两个版本的爬虫代码,第一个版本,平均每次爬取完毕 4w 多页的壁纸信息,需用时 24 小时左右。

而第二个版本,只需要10个小时左右

接下来,我会详细介绍我的代码

Wallhaven API 文档地址如下请自行理解查看,不做过多讲解:

API v1 - wallhaven.cc

准备

  • Wallhaven 账号 (需要登录获取 API KEY)
  • NodeJS Version > 16 环境
  • Git
  • mysql 并能够正常连接

申请 Wallhaven API KEY

获取 API KEY 入口:https://wallhaven.cc/settings/account。

将框选位置的内容复制

在这里插入图片描述

V1 版本 (用时24小时左右)

拉取 V1 代码

git clone -b v1 https://github.com/ivwv/splider-wallhaven-api

安装依赖

cd splider-wallhaven-api
npm i

配置环境变量

cp .env.simple .env
vim .env

MYSQL_HOST="192.168.1.2"            # MYSQL 所在服务器 IP
MYSQL_USER="root"                   # MYSQL 用户名
MYSQL_PASSWORD="root"               # MYSQL 密码
MYSQL_DATABASE="wallhaven.v1.0.0"   # 连接的数据库,需要提前创建好
MYSQL_PORT="3306"                   # MYSQL 端口
START="1"                           # 开始页数 
IS_SPLIDER=true                     # 是否爬取,可配置 GITHUB  环境变量,使用 Action 执行脚本
END="100"                           # 结束页数,具体可根据实际情况修改
API_KEY="you_api_key"               # 刚刚复制的 API KEY

初始化数据库表

Navicat 查询中执行 init.sql 内容

CREATE TABLE wallpapers (
    id VARCHAR(255),
    url VARCHAR(255),
    short_url VARCHAR(255),
    views VARCHAR(255),
    favorites VARCHAR(255),
    source VARCHAR(255),
    purity VARCHAR(255),
    category VARCHAR(255),
    dimension_x VARCHAR(255),
    dimension_y VARCHAR(255),
    resolution VARCHAR(255),
    ratio VARCHAR(255),
    file_size VARCHAR(255),
    file_type VARCHAR(255),
    created_at VARCHAR(255),
    colors VARCHAR(255),
    path VARCHAR(255),
    thumbs VARCHAR(255)
);

执行脚本

node app.js

控制台不会输出什么信息,日志会保存在 fetch_log.txt

接下来就可以去 数据库表中刷新查看内容了

V2 版本 (用时10小时左右)

该版本使用到了创建多线程的方式,每一个线程分配一个页数区间,如图:

在这里插入图片描述

特性

  • 使用 worker_threads NodeJS 第三方库创建多线程。
  • 可在程序运行时更改 Wallhaven 代理地址,不用停止程序。
  • 可选使用 HTTP 代理IP请求,减少出现 429, 503, 403 等网络请求错误的概率。
  • 随机生成 UserAgent 用于请求,防止出现请求错误的问题。

部分代码解释

拆分页数区间

// threadNum 线程数 ,可根据本机电脑的 cpu 线程数获取
// allPageNums 需要爬取的总页数
function calculateThreadRanges(threadNum, allPageNums) {
    const ranges = [];
    const pagesPerThread = Math.ceil(allPageNums / threadNum);

    for (let i = 0; i < threadNum; i++) {
      const startPage = i * pagesPerThread + 1;
      let endPage = (i + 1) * pagesPerThread;
      if (endPage > allPageNums) {
        endPage = allPageNums;
      }
      ranges.push([startPage, endPage]);
    }

    return ranges;
  }

在这里插入图片描述

创建多线程

// main.js
for (let i = 0; i < threadRanges.length; i++) {
  const worker = new Worker("./worker_threads.js", {
    workerData: { index: i },
  });

  worker.on("message", (result) => {
    console.log(result);
  });

  worker.postMessage({
    start: threadRanges[i][0],
    end: threadRanges[i][1],
  });
}

单个线程

// worker_threads.js
parentPort.on("message", async (message) => {
  const { start, end } = message;
  domains = await updateDomains();
  // 加载用户代理
  UserAgent = await getAllUserAgent();
  setInterval(async () => (domains = await updateDomains()), interval);
  console.log(start, end, "start, end");
  await fetchDataAndSaveToDB(start, end); // 将接收到的页码区间传入方法
  parentPort.postMessage(`${start}-${end}-完成`);
});

fetchDataAndSaveToDB 方法

通过 try/catch 将所有网络请求错误捕获,并重新发请求。

async function fetchDataAndSaveToDB(page, end) {
  try {
    const random = randomDomain();
    const url = `https://${random}/api/v1/search?apikey=${process.env.API_KEY}&purity=111&page=${page}`;
    const response = await axios.get(url, {
      // 添加代理
      headers: {
        "User-Agent": randomUserAgent(),
      },
      httpsAgent,
      proxy: process.env.HTTP_PROXY ? true : false,
    });
    const { data, meta } = response.data;
    // 保存数据到MySQL
    for (const item of data) {
      item.colors = item.colors.join(",");
      item.thumbs = JSON.stringify(item.thumbs);
      connection.query("INSERT INTO wallpapers SET ?", item, (err, result) => {
        if (err) throw err;
      });
    }
    // 写入日志文件
    logToFile(`Page ${page} data saved to MySQL.-- use ${random}\n`);
    if (page < end) {
      await fetchDataAndSaveToDB(page + 1, end);
    } else if (page === end) {
      console.log("Reached the end page. Exiting the program.");
      process.exit(0); // 退出程序,参数 0 表示正常退出
    }
  } catch (error) {
    if (error.response && error.response.status === 429) {
      // 如果是429错误,则等待一段时间后再次尝试
      console.log("Too Many Requests, waiting...");
      await new Promise((resolve) => setTimeout(resolve, 5000));
      await fetchDataAndSaveToDB(page, end);
    } else {
      // 判断error是否 为400响应码
      if (error.message == "Request failed with status code 400") {
        console.log(`Crawling completed, exit the program,The final valid page is: ${page}`);
        process.exit(0);
      }
      // 写入错误日志文件
      logToFile(`Error fetching data for page ${page}: ${error.message}\n`);
      console.error("Error fetching data: " + error.message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
      await fetchDataAndSaveToDB(page, end);
    }
  }
}

使用

clone 仓库

git clone -b v2 https://github.com/ivwv/splider-wallhaven-api
cd splider-wallhaven-api
npm i

配置环境变量

.env.simple 重命名为 .env

  • MYSQL_HOST: 必填,MySQL 主机地址
  • MYSQL_USER: 必填,MySQL 用户名
  • MYSQL_PASSWORD: 必填,MySQL 密码
  • MYSQL_DATABASE: 必填,MySQL 数据库名
  • MYSQL_PORT: 必填,MySQL 端口号
  • START: 必填,开始时间
  • END: 可选,结束页数,设置需要爬取到的最后一页
  • IS_SPIDER: 可选,是否进行爬取
  • HTTP_PROXY: 可选,HTTP 代理
  • API_KEY: 必填,Wallhaven API 密钥

运行

创建数据库

CREATE TABLE wallpapers (
    id VARCHAR(255),
    url VARCHAR(255),
    short_url VARCHAR(255),
    views VARCHAR(255),
    favorites VARCHAR(255),
    source VARCHAR(255),
    purity VARCHAR(255),
    category VARCHAR(255),
    dimension_x VARCHAR(255),
    dimension_y VARCHAR(255),
    resolution VARCHAR(255),
    ratio VARCHAR(255),
    file_size VARCHAR(255),
    file_type VARCHAR(255),
    created_at VARCHAR(255),
    colors VARCHAR(255),
    path VARCHAR(255),
    thumbs VARCHAR(255)
);

运行脚本

npm run start

提高爬取速度

设置web代理

当前版本使用多线程方式进行爬取,但受网络限制,过多的同时请求可能导致出现 Too Many Requests (429) 错误。为了解决这个问题,需要自行配置反向代理。

请修改 utils/domains.json 文件。该文件每隔1分钟自动读取一次,如果持续请求错误,请适当修改该数组。由于会随机选择数组中的域名进行请求,所以可以将同一个域名设置多次,以增加其请求权重。

[
  "wallhaven.cc"
]

使用HTTP代理

如果是在本地环境进行爬取,可以设置 HTTP_PROXY 环境变量。在这里,我使用的是 clash verge rev

然而,我查看日志时发现 clash verge rev 自动选择的节点并不会经常变化,可能一直都是一个节点,负载均衡也同样会一直使用一个节点,容易导致出现 Too Many Requests (429) 响应。为了解决这个问题,我的建议是使用 clash verge rev 的外部控制接口 ip:port,通过接口的方式切换节点。

请修改 change-proxy.js 文件,设置 clash_api 为你自己的接口地址,以及 proxie 为你希望使用的节点组名称。如有特殊符号,可直接打开订阅文件复制。

const clash_api = "http://127.0.0.1:9097";
const proxie = "🚀 节点选择";

完成以上修改后,在命令行中执行以下命令以自动切换节点:

node change-proxy.js

可以设置切换节点的频率时间,单位为毫秒。可随着线程数的增加而加快频率。

我尝试开启 16 线程数,设置 1000 毫秒为佳

// change-proxy.js
const interval = 3000; 

通过设置 HTTP_PROXY 代理,并自动切换节点,可以极大减少出现 429, 503, 403 等错误的概率。

问题

如何自定义筛选条件

在 app.js 查找 以下内容

const url = `https://${random}/api/v1/search?apikey=${process.env.API_KEY}&purity=111&page=${page}`;

在字符串末拼接上查询参数,更多参数可参考官方文档

API v1 - wallhaven.cc

如果爬取的信息在 mysql 中重复了怎么办

script 路径下有一个脚本: delete_multer_data.js

执行该脚本即可,可能会有些慢,该脚本未优化。

node script/delete_multer_data.js

我想批量筛选并下载图片

script 路径下有一个脚本: write-to-path.js

自行修改sql语句,不会的可以去学

connection.query(
    `
    SELECT path
    FROM wallpapers
    WHERE dimension_x > 8000 AND purity = 'sfw' AND category = 'people'
  `,
  ...

该脚本会将查询到的原图写入一个 txt 文本文件中,保存至项目根目录 output 开头的 txt 文本,每行一个链接。

在这里插入图片描述

然后使用 aria2 工具下载

例如:

aria2c -i ./output-20....txt 

我的博客 https://blog.ivwv.site/

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

vvw&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值