到此为止,我写了两个版本的爬虫代码,第一个版本,平均每次爬取完毕 4w 多页的壁纸信息,需用时 24 小时左右。
而第二个版本,只需要10个小时左右
接下来,我会详细介绍我的代码
Wallhaven API 文档地址如下请自行理解查看,不做过多讲解:
准备
- 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}`;
在字符串末拼接上查询参数,更多参数可参考官方文档
如果爬取的信息在 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