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/" 目录下的页面。虽然这个不是强制的,但是还是建议遵循这个规则,否则出现法律相关问题可能蹲局子。