ProxynetProxynet

وب اسکرپینگ با Cheerio در Node.js: آموزش گام‌به‌گام

تاریخ انتشار:

15 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
فرمان node scrape-books.mjs بالای چهار مسیر worker با یک تلاش مجدد که به یک رکورد JSON با برچسب 32 BOOKS می‌رسند

تیم شما از قبل با Node.js کار می‌کند و کسی عنوان، قیمت و تعداد موجودی همه کتاب‌های یک دسته از یک کاتالوگ را می‌خواهد. کد منبع صفحه در مرورگر نشان می‌دهد که داده‌ها داخل تگ‌های ساده <article> قرار دارند، پس برای خواندن آن‌ها به مرورگر نیازی ندارید. آنچه لازم دارید راهی است برای دانلود HTML، انتخاب عنصرهای درست، دنبال کردن پیوند «next» و جلوگیری از اینکه اسکریپت سایت را زیر بار ببرد یا با اولین مهلت زمانی (timeout) از کار بیفتد. مسیر کلی از صفحه تا فایل در چگونه از یک وب‌سایت داده استخراج کنیم آمده است؛ این آموزش همان کار را در JavaScript با Cheerio انجام می‌دهد.

در این نوشته می‌بینیم Cheerio چیست و چه نیست، بارگذاری HTML با load و fromURL، انتخابگرها و فهرست‌ها، متد جدیدتر extract، صفحه‌بندی، محدودیت همزمانی، تلاش مجدد با تأخیر فزاینده (backoff)، نوشتن JSON و ارسال درخواست‌ها از طریق پروکسی با ProxyAgent کتابخانه undici. بخش آخر توضیح می‌دهد چه زمانی Cheerio ابزار مناسبی نیست. همه نمونه‌ها در 28 سپتامبر 2026 با cheerio 1.2.0، undici 8.11.2 و Node.js 24.11.1 روی books.toscrape.com اجرا شدند؛ سایتی آزمایشی که برای تمرین وب اسکرپینگ ساخته شده است.

Cheerio چیست؟

Cheerio یک تجزیه‌گر HTML و XML برای Node.js است که API آن از jQuery الگو گرفته است. شما کد نشانه‌گذاری را به آن می‌دهید، درخت سند را می‌سازد و شما آن درخت را با $("css selector") جست‌وجو می‌کنید. سرعتش از این جهت است که هر کاری را که مرورگر پس از تجزیه انجام می‌دهد کنار می‌گذارد: چیدمان صفحه محاسبه نمی‌شود، CSS اعمال نمی‌شود، تصویری بارگذاری نمی‌شود و اسکریپتی اجرا نمی‌شود.

نسخه فعلی 1.2.0 است (cheerio در npm) و به Node.js 20.18.1 یا بالاتر نیاز دارد. نسخه 1.0 که در اوت 2024 منتشر شد، به دوره نسخه‌های نامزد انتشار (release candidate) پایان داد که از 2017 آغاز شده بود. این بسته خروجی پیش‌فرض ندارد، پس باید بنویسید import * as cheerio from "cheerio". آموزش‌هایی که require("cheerio").default را صدا می‌زنند برای نسخه‌های قدیمی نوشته شده‌اند.

Cheerio فقط HTMLی را می‌بیند که سرور فرستاده است. اگر فهرست محصولات بعداً با JavaScript پر شود، داده‌ها در آن HTML نیستند و هیچ انتخابگری آن‌ها را پیدا نمی‌کند. صفحه‌های ایستا و پویا نشان می‌دهد چگونه پیش از نوشتن هر کدی نوع صفحه را بررسی کنید.

اسکرپر مبتنی بر Cheerio چگونه کار می‌کند؟

اسکرپری که بر پایه Cheerio ساخته شده، برای هر صفحه همین پنج گام را تکرار می‌کند:

  1. دانلود. یک کلاینت HTTP (در اینجا fetch) نشانی را درخواست می‌کند و HTML را به‌صورت متن دریافت می‌کند.
  2. تجزیه. cheerio.load(html) درخت را می‌سازد و تابع $ متصل به همان سند را برمی‌گرداند.
  3. انتخاب. $("article.product_pod") همه عنصرهای منطبق را برمی‌گرداند؛ .find() و .text() و .attr() درون آن‌ها را می‌خوانند.
  4. دنبال کردن. اسکرپر نشانی بعدی را از صفحه می‌خواند (پیوند صفحه‌بندی یا پیوند صفحه جزئیات) و آن را نسبت به نشانی فعلی کامل می‌کند.
  5. ذخیره. ردیف‌ها در حافظه جمع می‌شوند و در پایان در فایل یا پایگاه داده نوشته می‌شوند.

گام‌های 2 و 3 هرگز به شبکه دست نمی‌زنند. این جدایی در اشکال‌زدایی مهم است: اگر انتخابگری چیزی برنگرداند، HTML را در فایلی ذخیره کنید و انتخابگر را روی همان فایل امتحان کنید، بی‌آنکه درخواست دیگری بفرستید.

مقایسه Cheerio، jsdom و Playwright

سه ابزاری که برای اسکرپینگ در Node.js بیش از همه با هم مقایسه می‌شوند، کارهای متفاوتی انجام می‌دهند:

ابزارچه می‌کنداجرای JavaScript صفحههزینه هر صفحهکاربرد مناسب
Cheerioتجزیه HTML، پرس‌وجو به سبک jQueryخیرکمترین: فقط تجزیهHTML رندرشده در سرور، تعداد صفحه زیاد
jsdomساخت DOM شبیه مرورگر در Node.jsاختیاری، محدودبیشتر از Cheerioکدی که document و APIهای DOM را انتظار دارد
Playwrightکنترل یک Chromium، Firefox یا WebKit واقعیبلهبیشترین: مرورگر کاملصفحه‌هایی که محتوا را با JavaScript می‌سازند، کلیک، ورود به حساب

یک چیدمان رایج از هر دو سر استفاده می‌کند: Playwright برای معدود صفحه‌هایی که به مرورگر نیاز دارند و Cheerio برای بقیه. انتخاب زبان پرسش جداگانه‌ای است که در وب اسکرپینگ: JavaScript یا Python؟ به آن پرداخته شده است.

نصب Cheerio و بارگذاری نخستین صفحه

یک پروژه بسازید و بسته را نصب کنید. افزودن "type": "module" به شما امکان می‌دهد از import و await در سطح بالا استفاده کنید:

bash
mkdir book-scraper && cd book-scraper
npm init -y
npm pkg set type=module
npm install cheerio

Node.js 18 و نسخه‌های بعدی fetch را همراه دارند، پس نخستین اسکریپت به چیز دیگری نیاز ندارد:

js
import * as cheerio from "cheerio";

const url = "https://books.toscrape.com/";
const response = await fetch(url, {
  headers: { "user-agent": "book-research/1.0 (+mailto:you@example.com)" },
});
if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);

const $ = cheerio.load(await response.text());

console.log($("title").text().trim());
console.log($("article.product_pod").length, "books on this page");

$("article.product_pod").slice(0, 3).each((i, el) => {
  const card = $(el);
  const title = card.find("h3 a").attr("title");
  const price = card.find(".price_color").text();
  console.log(i + 1, title, price);
});
text
All products | Books to Scrape - Sandbox
20 books on this page
1 A Light in the Attic £51.77
2 Tipping the Velvet £53.74
3 Soumission £50.10

عنوان از ویژگی title پیوند می‌آید، نه از متن آن: در این سایت متن نمایان پیوند کوتاه شده است («In a Dark, Dark ...») در حالی که ویژگی، عنوان کامل را نگه می‌دارد. پیش از انتخاب، هر دو را در کد منبع بررسی کنید. سرآیند user-agent اسکریپت شما را معرفی می‌کند و به مالک سایت راهی برای تماس با شما می‌دهد.

روش‌های بارگذاری

Cheerio ⁦1.x⁩ پنج راه برای بارگذاری سند دارد (مستندات بارگذاری Cheerio):

متدورودیچه زمانی استفاده شود
load(html)یک رشتهصفحه را خودتان دانلود کرده‌اید (حالت معمول)
loadBuffer(buffer)بایت‌های خامکدگذاری نامعلوم است؛ Cheerio آن را تشخیص می‌دهد
stringStream(options, cb)جریان متن رمزگشایی‌شدهفایل‌های بزرگ با کدگذاری معلوم
decodeStream(options, cb)جریان بایت خامفایل‌های بزرگ با کدگذاری نامعلوم
fromURL(url, options)یک نشانیاسکریپت‌های سریع؛ Cheerio خودش صفحه را دانلود می‌کند

fromURL راحت است، اما برای مبدأ صفحه کلاینت undici جداگانه‌ای باز می‌کند. در آزمایش ما، این تابع dispatcher ارسال‌شده در requestOptions را نادیده گرفت و مستقیم وصل شد، حتی وقتی آن dispatcher به پروکسی‌ای اشاره می‌کرد که همه درخواست‌ها را رد می‌کرد. برای هر کاری که به پروکسی، تلاش مجدد یا مهلت زمانی نیاز دارد، با fetch دانلود کنید و از load استفاده کنید.

انتخاب عنصرها و خواندن مقدارها

بیشتر کدهای اسکرپینگ فقط بخش کوچکی از API را به کار می‌برند:

  • $(selector) در کل سند انتخاب می‌کند؛ el.find(selector) درون یک عنصر جست‌وجو می‌کند.
  • .text() متن ترکیبی انتخاب را برمی‌گرداند؛ .attr("href") یک ویژگی از نخستین عنصر را برمی‌گرداند.
  • .each((i, el) => …) حلقه می‌زند؛ .map((i, el) => value).get() انتخاب را به یک آرایه ساده تبدیل می‌کند.
  • .first() و .eq(n) و .slice(a, b) انتخاب را محدودتر می‌کنند.

انتخابگری که با هیچ عنصری منطبق نشود خطا نمی‌دهد. .text() رشته خالی و .attr() مقدار undefined برمی‌گرداند، پس تغییر نام یک کلاس به جای خطا، فیلدهای خالی تولید می‌کند. ردیف‌هایی را که جمع می‌کنید اعتبارسنجی کنید (جزئیات بیشتر در فهرست اشتباهات پایین‌تر). نحو انتخابگرها و دلیل نداشتن XPath در Cheerio در انتخابگر CSS یا XPath آمده است.

متد extract

Cheerio 1.0 متد $.extract() را اضافه کرد که کل رکورد را به‌صورت یک شیء توصیف می‌کند (مستندات extract در Cheerio). یک رشته متن نخستین تطابق را برمی‌گرداند، کروشه همه تطابق‌ها را جمع می‌کند و { selector, value } یک ویژگی را می‌خواند یا تابعی را اجرا می‌کند:

js
import * as cheerio from "cheerio";

const $ = await cheerio.fromURL("https://books.toscrape.com/");

const data = $.extract({
  heading: "h1",
  books: [
    {
      selector: "article.product_pod",
      value: {
        title: { selector: "h3 a", value: "title" },
        price: ".price_color",
        link: { selector: "h3 a", value: "href" },
        rating: {
          selector: "p.star-rating",
          value: (el) => $(el).attr("class").replace("star-rating", "").trim(),
        },
      },
    },
  ],
});

console.log(data.heading, data.books.length);
console.log(data.books[0]);
text
All products 20
{
  title: 'A Light in the Attic',
  price: '£51.77',
  link: 'catalogue/a-light-in-the-attic_1000/index.html',
  rating: 'Three'
}

انتخابگرهای درون value نسبت به هر article اجرا می‌شوند و همین باعث می‌شود فیلدهای یک کتاب کنار هم بمانند. پیوند نسبی باقی می‌ماند، پس پیش از درخواست آن را با new URL(link, pageUrl) کامل کنید.

یک اسکرپر کامل: صفحه‌بندی، همزمانی، تلاش مجدد و JSON

اسکریپت زیر همه کتاب‌های دسته Mystery را جمع می‌کند. با دنبال کردن پیوند «next» صفحه‌های فهرست را می‌پیماید، صفحه جزئیات هر کتاب را با حداکثر چهار درخواست همزمان باز می‌کند، خطاهای شبکه، مهلت‌های زمانی و پاسخ‌های 429 و ⁦5xx⁩ را با backoff نمایی دوباره امتحان می‌کند و فایل books.json را می‌نویسد. این اسکریپت از fetch کتابخانه undici استفاده می‌کند تا پروکسی اختیاری بخش بعد بدون تغییر کار کند. آن را با npm install cheerio undici نصب کنید (undici 8 به Node.js 22.19 یا بالاتر نیاز دارد).

js
import * as cheerio from "cheerio";
import { fetch, ProxyAgent } from "undici";
import { writeFile } from "node:fs/promises";

const START_URL =
  "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html";
const CONCURRENCY = 4;   // detail pages fetched at the same time
const MAX_RETRIES = 3;   // extra attempts after the first one
const HEADERS = { "user-agent": "book-research/1.0 (+mailto:you@example.com)" };

// Optional proxy: PROXY_URL=http://user:pass@pr.proxynet.io:8000
const dispatcher = process.env.PROXY_URL
  ? new ProxyAgent(process.env.PROXY_URL)
  : undefined;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const backoff = (attempt) => 1000 * 2 ** (attempt - 1) + Math.random() * 250;

class HttpError extends Error {
  constructor(status, url) {
    super(`HTTP ${status} for ${url}`);
    this.status = status;
  }
}

async function fetchHtml(url) {
  for (let attempt = 1; ; attempt++) {
    let wait;
    try {
      const res = await fetch(url, {
        headers: HEADERS,
        dispatcher,
        signal: AbortSignal.timeout(15_000),
      });
      if (res.ok) return await res.text();
      const retryable = res.status === 429 || res.status >= 500;
      if (!retryable || attempt > MAX_RETRIES) throw new HttpError(res.status, url);
      const retryAfter = Number(res.headers.get("retry-after"));
      wait = retryAfter > 0 ? retryAfter * 1000 : backoff(attempt);
    } catch (err) {
      if (err instanceof HttpError || attempt > MAX_RETRIES) throw err;
      wait = backoff(attempt); // network error or timeout
    }
    console.warn(`retry ${attempt}/${MAX_RETRIES} in ${Math.round(wait)} ms: ${url}`);
    await sleep(wait);
  }
}

// Run fn over items with at most `limit` calls in flight.
async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;
      results[i] = await fn(items[i], i);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}

function parseListPage(html, pageUrl) {
  const $ = cheerio.load(html);
  const books = $("article.product_pod")
    .map((_, el) => {
      const card = $(el);
      const link = card.find("h3 a");
      return {
        title: link.attr("title"),
        price: Number(card.find(".price_color").text().replace(/[^0-9.]/g, "")),
        rating: card.find("p.star-rating").attr("class").split(" ").pop(),
        url: new URL(link.attr("href"), pageUrl).href,
      };
    })
    .get();
  const nextHref = $("li.next a").attr("href");
  return { books, nextUrl: nextHref ? new URL(nextHref, pageUrl).href : null };
}

function parseDetailPage(html) {
  const $ = cheerio.load(html);
  const info = {};
  $("table.table-striped tr").each((_, row) => {
    info[$(row).find("th").text().trim()] = $(row).find("td").text().trim();
  });
  const stock = info["Availability"]?.match(/\((\d+) available\)/);
  return {
    upc: info["UPC"],
    inStock: stock ? Number(stock[1]) : 0,
    description: $("#product_description + p").text().trim(),
  };
}

// 1. Walk the listing pages by following the "next" link.
const listed = [];
for (let url = START_URL; url; ) {
  const { books, nextUrl } = parseListPage(await fetchHtml(url), url);
  listed.push(...books);
  console.log(`${url} -> ${books.length} books`);
  url = nextUrl;
}

// 2. Open every detail page, four at a time.
const books = await mapLimit(listed, CONCURRENCY, async (book) => {
  try {
    return { ...book, ...parseDetailPage(await fetchHtml(book.url)) };
  } catch (err) {
    console.error(`skipped ${book.url}: ${err.message}`);
    return { ...book, error: err.message };
  }
});

// 3. Save the result.
await writeFile(
  "books.json",
  JSON.stringify({ scrapedAt: new Date().toISOString(), count: books.length, books }, null, 2),
);
console.log(`saved ${books.length} books to books.json`);
text
https://books.toscrape.com/catalogue/category/books/mystery_3/index.html -> 20 books
https://books.toscrape.com/catalogue/category/books/mystery_3/page-2.html -> 12 books
saved 32 books to books.json

یک رکورد از books.json (توضیح کوتاه‌شده):

json
{
  "title": "Sharp Objects",
  "price": 47.82,
  "rating": "Four",
  "url": "https://books.toscrape.com/catalogue/sharp-objects_997/index.html",
  "upc": "e00eb4fd7b871a48",
  "inStock": 20,
  "description": "…"
}

کار هر بخش:

  • صفحه‌بندی. حلقه وقتی متوقف می‌شود که صفحه li.next a نداشته باشد. پیوند صفحه 1 برابر page-2.html و نسبت به پوشه دسته است؛ به همین دلیل هر نشانی از new URL(href, pageUrl) می‌گذرد. الگوهای دیگر (شماره صفحه در query string، کرسرها، APIهای «بارگذاری بیشتر») در صفحه‌بندی در وب اسکرپینگ آمده‌اند.
  • محدودیت همزمانی. mapLimit چهار worker راه می‌اندازد که هر کدام مورد بعدی را از یک شمارنده مشترک برمی‌دارند. Promise.all روی هر 32 نشانی، 32 درخواست را یک‌جا می‌فرستد؛ با 1,000 نشانی این کار برای سرور مثل یک هجوم ناگهانی دیده می‌شود. عدد چهار برای یک سایت کوچک نقطه شروعی محترمانه است.
  • تلاش مجدد. فقط خطاهایی دوباره امتحان می‌شوند که ممکن است خودبه‌خود برطرف شوند: خطای شبکه، مهلت زمانی 15 ثانیه‌ای، 429 و ⁦5xx⁩. پاسخ 404 بلافاصله شکست می‌خورد. سرآیند عددی Retry-After بر تأخیر محاسبه‌شده مقدم است؛ backoff از حدود یک ثانیه دوبرابر می‌شود و یک مقدار تصادفی (jitter) به آن اضافه می‌شود تا workerهای موازی هم‌زمان تلاش مجدد نکنند. علت بروز 429 و نحوه خواندن این سرآیند در ⁦HTTP 429 Too Many Requests⁩ توضیح داده شده است.
  • شکست جزئی. صفحه جزئیاتی که پس از سه تلاش مجدد هنوز شکست می‌خورد، به جای متوقف کردن اجرا به ردیفی با فیلد error تبدیل می‌شود. بعداً می‌توانید فقط همین ردیف‌ها را دوباره اجرا کنید.
  • JSON. فایل شامل scrapedAt و count است که در مقایسه اجراها کمک می‌کند. برای CSV، JSON Lines یا SQLite با upsert به ذخیره داده‌های اسکرپینگ در CSV، JSON و SQLite مراجعه کنید.

استفاده از پروکسی با Cheerio (undici ProxyAgent)

در این اسکریپت Cheerio هرگز اتصالی باز نمی‌کند، پس پروکسی به کلاینت HTTP مربوط است. با undici یک ProxyAgent می‌سازید و آن را به‌عنوان dispatcher به fetch می‌دهید. اسکریپت کامل بالا وقتی PROXY_URL تنظیم شده باشد همین کار را انجام می‌دهد:

bash
PROXY_URL=http://user:pass@pr.proxynet.io:8000 node scrape-books.mjs

undici سرآیند Proxy-Authorization را از نام کاربری و گذرواژه درون نشانی می‌سازد و پیش از آن، آن‌ها را از حالت URL-encoded رمزگشایی می‌کند؛ بنابراین نویسه‌های خاص در گذرواژه باید percent-encoded شوند (مستندات ProxyAgent در undici). برای مقصدهای HTTPS، agent یک تونل CONNECT باز می‌کند و TLS تا سایت درون همین تونل برقرار می‌شود.

ما اسکریپت را از طریق یک پروکسی محلی کوچک اجرا کردیم که user:pass را الزامی می‌کرد و هر تونل را ثبت می‌کرد. هر 34 درخواست (دو صفحه فهرست و 32 صفحه جزئیات) از طریق یک CONNECT books.toscrape.com:443 رسیدند: agent تونل را باز نگه داشت و دوباره از آن استفاده کرد. با گذرواژه اشتباه، پروکسی پاسخ 407 داد و undici پیام Proxy response (407) !== 200 when HTTP Tunneling را گزارش کرد. اسکریپت پیش از تسلیم شدن سه بار دوباره تلاش کرد؛ گذرواژه اشتباه هیچ‌وقت خودبه‌خود درست نمی‌شود، پس به جای بالا بردن تعداد تلاش‌ها، اطلاعات ورود را بررسی کنید.

اگر نمی‌خواهید undici را به وابستگی‌ها اضافه کنید، Node.js 24.5 و 22.21 پشتیبانی داخلی از پروکسی را اضافه کرده‌اند که با تنظیم NODE_USE_ENV_PROXY=1 متغیرهای HTTP_PROXY و HTTPS_PROXY و NO_PROXY را می‌خواند (پشتیبانی داخلی Node.js از پروکسی). مستندات این قابلیت را در حال توسعه فعال معرفی می‌کند. در آزمایش ما روی Node.js 24.11.1، همان fetch سراسری معمولی با این تنظیم از پروکسی محلی عبور کرد:

bash
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://user:pass@pr.proxynet.io:8000 node first-page.mjs

Axios و node-fetch به جای dispatcher از agent استفاده می‌کنند؛ استفاده از پروکسی در Node.js هر دو را پوشش می‌دهد. برای چرخش IP خروجی در هر درخواست یا نشست‌های ثابتی که یک IP را برای مدتی نگه می‌دارند، پروکسی مسکونی و پروکسی چرخشی همان نشانی user:pass@host:port را می‌پذیرند.

وقتی Cheerio کافی نیست

Cheerio نمی‌تواند کلیک کند، صفحه را پیمایش کند یا منتظر درخواستی بماند که صفحه پس از بارگذاری می‌فرستد. نشانه‌هایی که می‌گویند به مرورگر نیاز دارید:

  • کد منبع صفحه (Ctrl+U) داده‌ای را که صفحه رندرشده نشان می‌دهد ندارد.
  • HTML یک ظرف خالی مثل <div id="root"></div> و یک بسته اسکریپت بزرگ دارد.
  • داده‌ها فقط پس از فرم ورود، بنر کوکی یا «پیمایش بی‌پایان» ظاهر می‌شوند.

پیش از راه‌اندازی مرورگر، زبانه Network را در ابزارهای توسعه‌دهنده باز کنید. بسیاری از صفحه‌های «پویا» داده‌هایشان را از یک endpoint از نوع JSON بارگذاری می‌کنند و درخواست مستقیم آن endpoint با fetch سبک‌تر از رندر کردن صفحه است. اگر واقعاً به مرورگر نیاز دارید، Playwright می‌تواند صفحه را رندر کند و HTML نهایی را با cheerio.load(await page.content()) به Cheerio بدهد، و کد تجزیه شما همان می‌ماند.

کاربردهای اسکرپرهای Cheerio

  • پیگیری قیمت: خواندن زمان‌بندی‌شده قیمت‌ها از صفحه‌های محصولی که در سرور رندر می‌شوند (پایش قیمت).
  • داده‌های کاتالوگ و بازار: جمع‌آوری سبد محصولات و سطح موجودی در چند فروشگاه (تحقیقات بازار).
  • دیده‌شدن در جست‌وجو: بررسی عنوان‌ها، متاتگ‌ها و سرفصل‌های صفحه‌های خودتان (پروکسی SEO).
  • خط لوله داده: رساندن ردیف‌های تجزیه‌شده به یک خزنده یا فرایند ETL بزرگ‌تر (استخراج داده، خزنده وب).
  • تجزیه HTML ذخیره‌شده: تبدیل صفحه‌های بایگانی‌شده به رکوردهای ساختاریافته؛ بخش تجزیه در تجزیه داده (parsing) چیست؟ توضیح داده شده است.

اشتباهات رایج و روش تشخیص آن‌ها

  • رشته‌های خالی همه‌جا. انتخابگر با هیچ عنصری منطبق نشده یا داده‌ها را JavaScript اضافه می‌کند. HTML را با writeFile("page.html", html) ذخیره کنید و در آن دنبال مقداری بگردید که در مرورگر می‌بینید.
  • require(...).default is not a function یا does not provide an export named 'default'. شیوه وارد کردن قدیمی است. از import * as cheerio from "cheerio" استفاده کنید.
  • TypeError: fetch failed همراه با invalid onRequestStart method. یک ProxyAgent از بسته undici در npm را به fetch سراسری Node.js داده‌اید. Node 24.11.1 نسخه undici 7.16.0 را درون خود دارد و این دو نسخه رابط dispatcher یکسانی ندارند. fetch و ProxyAgent را از یک بسته وارد کنید.
  • پروکسی‌ای که «هیچ کاری نمی‌کند». agent را به cheerio.fromURL داده‌اید که کلاینت خودش را به کار می‌برد. با fetch دانلود کنید و cheerio.load را صدا بزنید.
  • پیوندهای نسبی شکست می‌خورند. fetch("catalogue/…") خطای Failed to parse URL می‌دهد. با new URL(href, pageUrl) نشانی را کامل کنید.
  • درخواست‌های بیش از حد به‌طور همزمان. Promise.all(urls.map(fetch)) همه را موازی می‌فرستد و پاسخ‌های 429 را به دنبال دارد. از محدودیتی مثل mapLimit استفاده کنید.
  • جابه‌جایی بی‌صدای داده. سایت نام یک کلاس را عوض می‌کند و قیمت‌ها NaN می‌شوند. هر اجرا را بررسی کنید: ردیف‌ها و قیمت‌های NaN را بشمارید و اگر اعداد ناگهان افت کردند، اجرا را متوقف کنید.

پیش از بزرگ‌تر کردن مقیاس، robots.txt و شرایط استفاده سایت را بخوانید، اگر API رسمی وجود دارد آن را ترجیح دهید و نرخ درخواست‌ها را معتدل نگه دارید. توضیح robots.txt و آیا وب اسکرپینگ قانونی است؟ قواعد را پوشش می‌دهند؛ وب اسکرپینگ بدون مسدود شدن به خزش محترمانه می‌پردازد.

راهنمای انتخاب

نیازپیشنهاد
داده در کد منبع صفحه هستfetch + cheerio.load
اسکریپت یک‌باره، بدون پروکسیcheerio.fromURL
رکوردهای زیاد با ساختار یکسان$.extract با توصیفگر آرایه‌ای
صدها صفحهمحدودیت همزمانی 2 تا 5 به‌علاوه تلاش مجدد با backoff
درخواست از طریق پروکسیfetch کتابخانه undici + ProxyAgent، یا NODE_USE_ENV_PROXY=1 روی Node.js 24.5+
داده فقط پس از اجرای JavaScript ظاهر می‌شوداول endpoint مربوط به JSON را پیدا کنید، وگرنه Playwright + Cheerio
کد به DOM کامل (document، رویدادها) نیاز داردjsdom

پرسش‌های متداول

آیا Cheerio در 2026 هنوز نگهداری می‌شود؟

بله. رجیستری npm نسخه 1.2.0 را که در ژانویه 2026 منتشر شد به‌عنوان آخرین نسخه نشان می‌دهد و سایت مستندات API نسخه ⁦1.x⁩ را با extract و fromURL پوشش می‌دهد.

آیا Cheerio کد JavaScript اجرا می‌کند؟

خیر. فقط رشته HTMLی را که به آن می‌دهید تجزیه می‌کند و کار دیگری انجام نمی‌دهد. اسکریپت‌های صفحه مثل متن در نظر گرفته می‌شوند. برای صفحه‌هایی که محتوایشان را در مرورگر می‌سازند، از Playwright استفاده کنید یا endpoint داده‌ای را که صفحه صدا می‌زند پیدا کنید.

آیا کنار Cheerio به Axios نیاز دارم؟

خیر. Node.js 18 و نسخه‌های بعدی fetch را دارند که نیاز بیشتر اسکرپرها را برآورده می‌کند. Axios انتخابی سلیقه‌ای است؛ اگر از آن استفاده می‌کنید، بدنه پاسخ (response.data) را به cheerio.load بدهید.

چگونه با Cheerio چند صفحه را اسکرپ کنم؟

پیوند صفحه بعد را از هر صفحه بخوانید، آن را نسبت به نشانی فعلی کامل کنید و تا وقتی پیوند وجود دارد حلقه را ادامه دهید، همان‌طور که در اسکریپت کامل بالا آمده است. وقتی تعداد صفحه‌ها معلوم است، می‌توانید فهرست نشانی‌ها را از پیش بسازید و آن را با محدودیت همزمانی اجرا کنید.

چگونه با Cheerio از پروکسی استفاده کنم؟

پروکسی را روی کلاینت HTTP تنظیم کنید، نه روی Cheerio. با undici: new ProxyAgent("http://user:pass@pr.proxynet.io:8000") را به‌عنوان dispatcher به fetch همین کتابخانه بدهید. روی Node.js 24.5 یا بالاتر می‌توانید به جای آن NODE_USE_ENV_PROXY=1 و HTTPS_PROXY را تنظیم کنید.

آیا Cheerio از Puppeteer یا Playwright سریع‌تر است؟

برای صفحه‌هایی که داده‌شان در HTML است، بله؛ چون فقط متن را تجزیه می‌کند، در حالی که مرورگر علاوه بر آن منابع را دانلود می‌کند، اسکریپت‌ها را اجرا می‌کند و چیدمان صفحه را می‌سازد. ما این تفاوت را بنچمارک نکردیم و به صفحه بستگی دارد، پس اگر اعداد برایتان مهم است، روی سایت‌های هدف خودتان اندازه بگیرید.

خلاصه

Cheerio کد HTML دانلودشده را به درختی تبدیل می‌کند که می‌توانید با انتخابگرهای CSS جست‌وجو کنید و در نسخه ⁦1.x⁩ متدهای fromURL و extract را هم اضافه کرده است. یک اسکرپر قابل اتکا کار شبکه را بیرون از Cheerio نگه می‌دارد: fetch با مهلت زمانی، تلاش مجدد برای 429، ⁦5xx⁩ و خطاهای شبکه، محدودیت همزمانی کوچک و یک فایل JSON با برچسب زمانی. وقتی درخواست‌ها باید از IP یا کشور دیگری خارج شوند، یک ProxyAgent از undici را به‌عنوان dispatcher بدهید و آن را به یک پروکسی Proxynet متصل کنید.

پرسش از ChatGPTپرسش از Claude