NODE.JS HTTP CLIENT

Node.js Axios代理采集

Axios通过原生proxy选项连接隧道住宅网关,Cheerio解析分页HTML。案例复用HTTP连接,只对429、5xx与临时网络错误执行有限退避,并输出JSON Lines。

语言Node.js 20+框架Axios + Cheerio测试目标Quotes to Scrape输出quotes-axios.jsonl
打开对应产品提取 查看测试目标
01
END-TO-END RESULT

使用原生代理配置采集分页网页

测试目标Quotes to Scrapehttps://quotes.toscrape.com/ 运行时Node.js 20+Axios + Cheerio 输出文件结构化数据quotes-axios.jsonl

代码直接连接隧道住宅代理固定网关 residential.123proxy.cn:33000。主机与端口无需替换,出口路由由当前产品配置决定;你只需设置控制台生成的完整代理用户名和密码。

02
CREDENTIALS

只把代理账密放进环境变量

PROXY_USER必须是控制台中的代理用户名,不是网站登录手机号。地区和SESSION场景应复制控制台生成的完整路由用户名。

export PROXY_USER="替换为控制台代理用户名"
export PROXY_PASS="替换为控制台代理密码"
# 隧道住宅代理固定网关:residential.123proxy.cn:33000

不要把真实代理密码提交到Git仓库、镜像、日志或前端代码中。

03
COMPLETE PROGRAM

scrape-axios.mjs

以下代码包含完整入口和输出逻辑。安装注释、运行命令与依赖声明保留在文件顶部,复制为对应文件后即可执行。

// requirements:
//   npm init -y
//   npm install axios cheerio
// save as scrape-axios.mjs, then run:
//   node scrape-axios.mjs
import fs from "node:fs/promises";
import http from "node:http";
import https from "node:https";
import axios from "axios";
import * as cheerio from "cheerio";

const PROXY_HOST = "residential.123proxy.cn";
const PROXY_PORT = 33000;
const PROXY_USER = process.env.PROXY_USER;
const PROXY_PASS = process.env.PROXY_PASS;
const START_URL = "https://quotes.toscrape.com/";
const MAX_PAGES = 3;

if (!PROXY_USER || !PROXY_PASS) {
  throw new Error("set PROXY_USER and PROXY_PASS first");
}

const client = axios.create({
  timeout: 30_000,
  proxy: {
    protocol: "http",
    host: PROXY_HOST,
    port: PROXY_PORT,
    auth: {
      username: PROXY_USER,
      password: PROXY_PASS,
    },
  },
  headers: {
    "User-Agent": "123Proxy-Developer-Example/1.0",
    "Accept-Language": "en-US,en;q=0.8",
  },
  httpAgent: new http.Agent({ keepAlive: true, maxSockets: 10 }),
  httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 10 }),
});

function delay(milliseconds) {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

async function getWithRetry(url, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      return await client.get(url);
    } catch (error) {
      const status = error.response?.status;
      const retryable =
        status === 429
        || (status >= 500 && status <= 599)
        || error.code === "ECONNRESET"
        || error.code === "ETIMEDOUT";
      if (!retryable || attempt === maxAttempts) throw error;

      const retryAfter = Number(error.response?.headers?.["retry-after"]);
      const waitMs = Number.isFinite(retryAfter)
        ? Math.min(retryAfter * 1000, 30_000)
        : 500 * (2 ** (attempt - 1));
      await delay(waitMs + Math.floor(Math.random() * 250));
    }
  }
  throw new Error("unreachable");
}

function parsePage(html, sourceUrl) {
  const $ = cheerio.load(html);
  const records = $("div.quote")
    .map((_, node) => ({
      text: $(node).find("span.text").text().trim(),
      author: $(node).find("small.author").text().trim(),
      tags: $(node).find("a.tag")
        .map((__, tag) => $(tag).text().trim())
        .get(),
      source_url: sourceUrl,
    }))
    .get();
  const nextHref = $("li.next > a").attr("href");
  return { records, nextHref };
}

async function main() {
  const records = [];
  let url = START_URL;

  for (let page = 0; page < MAX_PAGES && url; page += 1) {
    const response = await getWithRetry(url);
    const parsed = parsePage(response.data, url);
    records.push(...parsed.records);
    url = parsed.nextHref ? new URL(parsed.nextHref, url).href : null;
    await delay(500);
  }

  const jsonLines = records.map((record) => JSON.stringify(record)).join("\n");
  await fs.writeFile("quotes-axios.jsonl", jsonLines + "\n", "utf8");
  console.log(
    "saved " + records.length + " records to quotes-axios.jsonl",
  );
}

main().catch((error) => {
  console.error(error.response?.status || error.code, error.message);
  process.exitCode = 1;
});
04
ENGINEERING CHECKS

案例没有省略的关键逻辑

01proxy对象分别设置协议、主机、端口和代理账密
02固定住宅网关为residential.123proxy.cn:33000
03HTTP与HTTPS Agent启用keepAlive并限制连接数
04仅重试429、5xx、连接重置与超时
05解析相对分页URL并输出一行一个JSON对象
05
SWITCH PROXY PRODUCT

替换网关,不改采集逻辑

代理产品网关域名端口适合任务
隧道代理proxy.123proxy.cn36923混合池 / 纯住宅池
隧道住宅代理residential.123proxy.cn33000国家地区与SESSION
不限量动态住宅unlimit.residential.123proxy.cn10253不限流量住宅任务
两类静态代理控制台分配的固定代理IP返回端口直连IP,无网关域名
隧道住宅代理的国家与SESSION写在完整代理用户名中

网关固定为 residential.123proxy.cn:33000。例如使用美国SESSION时,直接把控制台生成的完整用户名作为 PROXY_USER,代码无需解析或重组。

06
PRIMARY REFERENCES

框架文档与测试目标

07
TROUBLESHOOTING

常见问题

PROXY_USER应该填写什么?

填写控制台生成的完整代理用户名。需要国家或SESSION时,地区后缀与SESSION参数都属于完整用户名,不要在Axios代码中再次拼接。

为什么没有设置HTTP_PROXY环境变量?

案例使用Axios原生proxy配置,使代理行为与当前采集客户端绑定,避免进程环境代理影响其他请求。

如何使用隧道代理?

把固定地址改为proxy.123proxy.cn:36923,并使用隧道代理控制台生成的代理用户名与密码;其他采集逻辑保持不变。

429是否应该立即重试?

不应该。优先读取Retry-After,否则使用指数退避与随机抖动,并降低目标域名的并发和QPS。