这是一篇鸽了许久的文章,最近失业在家重构博客的时候发现还有一些文章没有来的及写,就有了这篇文章。
准备工作
编写爬虫之前需要调查一下需要爬取的网站是什么形式来渲染的
- 如果是 spa 页面则只需要拿到账号信息,通常是 token 或者 cookie 之类的,之后直接调取接口即可;
- 如果是服务器渲染返回的,则可能需要对 dom 结构解析来获取到正确的答案。
这里主要介绍场景 2,今年的时候买房就是我用爬虫 + 邮箱来定时推送,来保证获取新房的第一手信息,这里也主要结合我做的这个场景来讲解。
观察一下页面,可以发现思路就是把 html 结构解析,然后 forEach 子项,然后添加到数组,循环这个过程就可以。
因为是 node 环境实际上是没有 dom 结构的,所以我们还需要使用特定库来完成解析,这里用的是cheerio,它提供类似 jQuery 的语法
js
const cheerio = require("cheerio");
const $ = cheerio.load('<h2 class="title">Hello world</h2>');
$("h2.title").text("Hello there!");
$("h2").addClass("welcome");
$.html();
//=> <html><head></head><body><h2 class="title welcome">Hello there!</h2></body></html>
最后在观察一下页面 url,发现如果跳转第二页变成了 www.hfzfzlw.com/spf/Scheme/...
所以这里很明显了,只需要更改 p 的内容就可以得到一个遍历的效果。
编写
网络请求库这里使用了 axios,它支持 web 和 node 环境下使用,当然也可以使用其他的请求库。
sh
npm i cheerio axios dayjs
首先需要对 axios 进行一层封装
js
// axios.ts
import axios from "axios";
export const instance = axios.create({
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
},
});
这里 User-Agent 字段是描述请求发起的设备信息,这里多准备一些,然后随机发送,例如可以使用 random-useragent,在 instance.interceptors.request.use 中进行拦截,动态更改。
之后定义一下所需的接口格式
ts
export interface ListProps {
id: string;
// 详情url方便后续拓展需求
url: string;
// 项目名称
entryName: string;
// 楼栋
building: string[];
// 开发商
enterpriseName: string;
// 区域
region: string;
// 开始时间 number
startTime: number;
// 结束时间,number
endTime: number;
// 总数量
total: number;
// 状态
registrationStatus: string;
// 开始时间
start: string;
// 结束时间
end: string;
}
剩下就是开始编写,首先:
- 获取初始页面内容,这里要解析第一页数据,然后查找需要循环次数;
- forEach 其他页面,然后储存结果
js
// api.ts
import { instance } from "./axios";
export const getPage = async (page = 1) => {
const { data } =
(await instance.get) <
string >
`https://www.hfzfzlw.com/spf/Scheme/?p=${page}&xmmc=&qy=&djzt=`;
return data;
};
首先定义一个接口的文件,方便后续添加其他页面的接口,之后定义 utils.ts 文件,添加解析 html 的功能。
ts
import { load } from "cheerio";
import dayjs from "dayjs";
export const BASE_URL = "http://www.hfzfzlw.com";
export interface ListProps {
id: string;
// 详情url方便后续拓展需求
url: string;
// 项目名称
entryName: string;
// 楼栋
building: string[];
// 开发商
enterpriseName: string;
// 区域
region: string;
// 开始时间 number
startTime: number;
// 结束时间,number
endTime: number;
// 总数量
total: number;
// 状态
registrationStatus: string;
// 开始时间
start: string;
// 结束时间
end: string;
}
export const analysis = (html: string): ListProps[] => {
const $ = load(html);
const arr: ListProps[] = [];
$("tr:not(.table_bg)").each((_i, el) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const obj = {} as ListProps;
$(el)
.find("td")
.each((index, item) => {
const a = $(item).find("a");
const value = $(item).text().trim();
switch (index) {
case 0:
obj.id = $(item).find("span").text().trim();
obj.url = `${BASE_URL}${a.attr("href") ?? ""}`;
obj.entryName = a.text().trim();
return;
case 1:
obj.building = value.split(",");
return;
case 2:
obj.enterpriseName = value;
return;
case 3:
obj.region = value;
return;
case 4:
// eslint-disable-next-line no-case-declarations
const [start, end] = value
.split("至")
.map((f) => dayjs(f.trim()).valueOf()) as [number, number];
obj.startTime = start;
obj.endTime = end;
obj.start = dayjs(start).format("YYYY-MM-DD HH:mm:ss");
obj.end = dayjs(end).format("YYYY-MM-DD HH:mm:ss");
return;
case 5:
obj.total = +value;
return;
case 6:
obj.registrationStatus = value;
}
});
arr.push(obj);
});
return arr;
};
export const getTotal = (html: string) => {
const $ = load(html);
return +$(".green-black a")
.eq(-3)
.attr("href")
.match(/p=(\d+)&/)[1];
};
之后在 index.ts 编写具体的爬取逻辑
ts
// index.ts
import { getPage } from "./api";
import { getTotal, analysis } from "./utils";
const App = async () => {
const html = await getPage();
const len = getTotal(html);
const tasks = await Promise.all(
Array.from({ length: len - 1 }).map((_, index) => {
return getPage(index + 2);
})
);
const result = [html, ...tasks].map((f) => {
return analysis(f);
});
return result;
};
App();
...404
上面状态下是理想情况,但是如果你真的这样运行会发现突然服务器没有响应了,然后你用其他 ip 的设备来访问,发现还是可以运行的。
那么问题出在哪里呢?很大概率就是被对方网站进行了拉黑,恶意爬取网站会大量占用服务器的资源和带宽,对于有经验的后端都会考虑到这种场景,对访问频繁的 IP 进行限制,例如 IP 封锁,提示验证码等。
下面就介绍一些常见绕过的方法。
限速
上面的代码我们仔细观察一下,发现其实是一下子并发很多条过去,这种场景下可能会导致触发对方的安全机制,那么换个角度来说,我们给每个任务进行限速,然后让其排队来完成是不是就可以减少被发现的概率呢。
说干就干,更新一下 utils 方法
ts
const wait = (time: number) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
export const rateLimiting = async <T extends (...rest: any) => any>(
arr: T[],
time: number
) => {
let i = 0;
const result: ReturnType<T>[] = [];
for (const iterator of arr) {
const o = await iterator();
result.push(o);
if (++i < arr.length - 1) {
await wait(time);
}
}
return result as Array<Awaited<ReturnType<T>>>;
};
ts
import { getPage } from "./api";
import { getTotal, analysis, rateLimiting } from "./utils";
const App = async () => {
const html = await getPage();
const len = getTotal(html);
const arr = Array.from({ length: len - 1 }).map((_, index) => {
return () => getPage(index + 2);
});
const tasks = await rateLimiting(arr, 3000);
const result = [html, ...tasks].map((f) => {
return () => analysis(f);
});
return result;
};
ok,这样就完成了限速相关的编写,当然实际场景中还需要考虑重试等机制。那么除了限速还有其他方式吗?
IP 池
除了上面的方式,我们还可以维护一个 ip 来进行操作,例如我有大概 100 个代理 IP
- 维护一个队列,每次请求之后记录爬取时间和响应时间;
- 依次请求,排除掉正在爬取的代理 IP;
- 之后对照响应的时间 - 现在时间,如果大于正常人类浏览时间就继续下一次;
这里推荐几个我正在使用的代理池,推荐使用 docker 的形式来进行启动
yml
// docker-compose.yml
version: '2'
services:
proxy1:
image: 'jhao104/proxy_pool'
ports:
- '5010:5010'
depends_on:
- proxy_redis
environment:
DB_CONN: 'redis://@proxy_redis:6379/0'
proxy_redis:
image: 'redis'
proxy2:
image: 'boses/ipproxypool'
restart: always
privileged: true
ports:
- 8000:8000
具体如何维护代理池,然后请求重试这里就不一一写出来了,如果有兴趣可以看我写的这个项目 Hefei-NewHouse。
如果为了稳定也可以考虑一些付费的IP池,对于验证码之类的措施可以接入到验证码平台,当然这个是收费的。
robots.txt
在编写爬虫中,需要注意一下对方网站根目录是否存在 robots.txt 文件,这个相当于一个默认规则来告诉网络爬虫哪些页面可以被抓取,哪些不应该被抓取。这是遵循网络爬虫协议(Robots Exclusion Protocol)的标准做法。
txt
User-agent: *
Disallow: /private/
Allow: /public/
上述示例表示,对于所有爬虫(User-agent: *),不允许访问 "/private/" 目录下的页面,但允许访问 "/public/" 目录下的页面。
虽然这个不是强制的,但是还是建议遵循这个规则,否则出现法律相关问题可能蹲局子。
最后
这里只简单介绍了一些编写爬虫的规则,对于规则 1 没有进行额外的拓展,有兴趣的小伙伴可以思考一下知乎如何写一个爬虫。
最后提醒一下,在法律层面上,未经允许的爬取可能违反计算机犯罪法、数据保护法或其他相关法规,这可能导致法律责任。因此,最好在进行任何形式的爬取之前,仔细阅读目标网站的使用条款和服务协议,并确保你的行为是合法的。