闽公网安备 35020302035485号

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 的内容就可以得到一个遍历的效果。
npm i cheerio axios dayjs首先需要对 axios 进行一层封装
// axios.ts
// 堆代码 duidaima.com
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 中进行拦截,动态更改。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;
}
剩下就是开始编写,首先:// 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 的功能。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 编写具体的爬取逻辑// 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();
下面就介绍一些常见绕过的方法。
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>>>;
};
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,这样就完成了限速相关的编写,当然实际场景中还需要考虑重试等机制。那么除了限速还有其他方式吗?
// 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池,对于验证码之类的措施可以接入到验证码平台,当然这个是收费的。
User-agent: * Disallow: /private/ Allow: /public/上述示例表示,对于所有爬虫(User-agent: *),不允许访问 "/private/" 目录下的页面,但允许访问 "/public/" 目录下的页面。虽然这个不是强制的,但是还是建议遵循这个规则,否则出现法律相关问题可能蹲局子。