---
title: "وب اسکرپینگ با Cheerio در Node.js: آموزش گام‌به‌گام"
description: "Cheerio کد HTML را در Node.js با انتخابگرهای شبیه jQuery تجزیه می‌کند. یک اسکرپر آزموده با fetch، صفحه‌بندی، محدودیت همزمانی، تلاش مجدد و پروکسی بسازید."
url: https://proxynet.io/fa/blog/cheerio-web-scraping
date: 2026-09-28
author: "Acar Diveroli"
category: "آموزش‌ها, وب اسکرپینگ"
lang: fa
---

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

تیم شما از قبل با Node.js کار می‌کند و کسی عنوان، قیمت و تعداد موجودی همه کتاب‌های یک دسته از یک کاتالوگ را می‌خواهد. کد منبع صفحه در مرورگر نشان می‌دهد که داده‌ها داخل تگ‌های ساده `<article>` قرار دارند، پس برای خواندن آن‌ها به مرورگر نیازی ندارید. آنچه لازم دارید راهی است برای دانلود HTML، انتخاب عنصرهای درست، دنبال کردن پیوند «next» و جلوگیری از اینکه اسکریپت سایت را زیر بار ببرد یا با اولین مهلت زمانی (timeout) از کار بیفتد. مسیر کلی از صفحه تا فایل در [چگونه از یک وب‌سایت داده استخراج کنیم](/fa/blog/extract-data-from-website) آمده است؛ این آموزش همان کار را در 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 یک کتابخانه Node.js است که HTML را به درختی تبدیل می‌کند و شما آن درخت را با انتخابگرهای CSS به سبک jQuery جست‌وجو می‌کنید. این کتابخانه JavaScript اجرا نمی‌کند و جز تابع کمکی `fromURL` صفحه‌ای دانلود نمی‌کند. الگوی معمول این است: صفحه را با `fetch` دانلود کنید، متن را به `cheerio.load` بدهید، مقدارها را با `$(selector).text()` و `.attr()` بخوانید و روی فهرست‌ها با `.map()` یا `.each()` حلقه بزنید. برای صفحه‌های زیاد، پیوند «next» را دنبال کنید، تعداد درخواست‌های همزمان را محدود کنید، مهلت‌های زمانی و پاسخ‌های ⁦5xx⁩ یا 429 را با تأخیری رو به افزایش دوباره امتحان کنید و ردیف‌ها را در JSON ذخیره کنید. برای پروکسی، `fetch` و `ProxyAgent` را از همان بسته undici وارد کنید و agent را به‌عنوان `dispatcher` بدهید.

## Cheerio چیست؟

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

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

Cheerio فقط HTMLی را می‌بیند که سرور فرستاده است. اگر فهرست محصولات بعداً با JavaScript پر شود، داده‌ها در آن HTML نیستند و هیچ انتخابگری آن‌ها را پیدا نمی‌کند. [صفحه‌های ایستا و پویا](/fa/blog/static-vs-dynamic-pages) نشان می‌دهد چگونه پیش از نوشتن هر کدی نوع صفحه را بررسی کنید.

## اسکرپر مبتنی بر 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؟](/fa/blog/web-scraping-javascript-vs-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](https://cheerio.js.org/docs/basics/loading/)):

| متد | ورودی | چه زمانی استفاده شود |
|---|---|---|
| `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](/fa/blog/css-selector-vs-xpath) آمده است.

### متد extract

Cheerio 1.0 متد `$.extract()` را اضافه کرد که کل رکورد را به‌صورت یک شیء توصیف می‌کند ([مستندات extract در Cheerio](https://cheerio.js.org/docs/basics/extract/)). یک رشته متن نخستین تطابق را برمی‌گرداند، کروشه همه تطابق‌ها را جمع می‌کند و `{ 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های «بارگذاری بیشتر») در [صفحه‌بندی در وب اسکرپینگ](/fa/blog/pagination-web-scraping) آمده‌اند.
- **محدودیت همزمانی.** `mapLimit` چهار worker راه می‌اندازد که هر کدام مورد بعدی را از یک شمارنده مشترک برمی‌دارند. `Promise.all` روی هر 32 نشانی، 32 درخواست را یک‌جا می‌فرستد؛ با 1,000 نشانی این کار برای سرور مثل یک هجوم ناگهانی دیده می‌شود. عدد چهار برای یک سایت کوچک نقطه شروعی محترمانه است.
- **تلاش مجدد.** فقط خطاهایی دوباره امتحان می‌شوند که ممکن است خودبه‌خود برطرف شوند: خطای شبکه، مهلت زمانی 15 ثانیه‌ای، 429 و ⁦5xx⁩. پاسخ 404 بلافاصله شکست می‌خورد. سرآیند عددی `Retry-After` بر تأخیر محاسبه‌شده مقدم است؛ backoff از حدود یک ثانیه دوبرابر می‌شود و یک مقدار تصادفی (jitter) به آن اضافه می‌شود تا workerهای موازی هم‌زمان تلاش مجدد نکنند. علت بروز 429 و نحوه خواندن این سرآیند در [⁦HTTP 429 Too Many Requests⁩](/fa/blog/http-429-too-many-requests) توضیح داده شده است.
- **شکست جزئی.** صفحه جزئیاتی که پس از سه تلاش مجدد هنوز شکست می‌خورد، به جای متوقف کردن اجرا به ردیفی با فیلد `error` تبدیل می‌شود. بعداً می‌توانید فقط همین ردیف‌ها را دوباره اجرا کنید.
- **JSON.** فایل شامل `scrapedAt` و `count` است که در مقایسه اجراها کمک می‌کند. برای CSV، JSON Lines یا SQLite با upsert به [ذخیره داده‌های اسکرپینگ در CSV، JSON و SQLite](/fa/blog/save-scraped-data-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://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md)). برای مقصدهای 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 از پروکسی](https://nodejs.org/api/http.html#built-in-proxy-support)). مستندات این قابلیت را در حال توسعه فعال معرفی می‌کند. در آزمایش ما روی 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](/fa/blog/nodejs-proxy) هر دو را پوشش می‌دهد. برای چرخش IP خروجی در هر درخواست یا نشست‌های ثابتی که یک IP را برای مدتی نگه می‌دارند، [پروکسی مسکونی](https://proxynet.io/fa/residential-proxy) و [پروکسی چرخشی](https://proxynet.io/fa/rotating-proxy) همان نشانی `user:pass@host:port` را می‌پذیرند.

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

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

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

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

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

- **پیگیری قیمت:** خواندن زمان‌بندی‌شده قیمت‌ها از صفحه‌های محصولی که در سرور رندر می‌شوند ([پایش قیمت](/fa/price-monitoring)).
- **داده‌های کاتالوگ و بازار:** جمع‌آوری سبد محصولات و سطح موجودی در چند فروشگاه ([تحقیقات بازار](/fa/market-research)).
- **دیده‌شدن در جست‌وجو:** بررسی عنوان‌ها، متاتگ‌ها و سرفصل‌های صفحه‌های خودتان ([پروکسی SEO](/fa/seo-proxy)).
- **خط لوله داده:** رساندن ردیف‌های تجزیه‌شده به یک خزنده یا فرایند ETL بزرگ‌تر ([استخراج داده](/fa/data-scraping)، [خزنده وب](/fa/web-crawler)).
- **تجزیه HTML ذخیره‌شده:** تبدیل صفحه‌های بایگانی‌شده به رکوردهای ساختاریافته؛ بخش تجزیه در [تجزیه داده (parsing) چیست؟](/fa/blog/what-is-data-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](/fa/blog/robots-txt) و [آیا وب اسکرپینگ قانونی است؟](/fa/blog/is-data-web-scraping-legal) قواعد را پوشش می‌دهند؛ [وب اسکرپینگ بدون مسدود شدن](/fa/blog/web-scraping-without-getting-blocked) به خزش محترمانه می‌پردازد.

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

| نیاز | پیشنهاد |
|---|---|
| داده در کد منبع صفحه هست | `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](/fa/proxy) متصل کنید.
