---
title: "تنظیم پروکسی در Puppeteer: احراز هویت، چرخش IP و SOCKS5"
description: "در Puppeteer پروکسی با پرچم ⁦--proxy-server⁩ در Chrome یا برای هر browser context تعریف می‌شود و ورود با ⁦page.authenticate()⁩ است؛ نمونه‌ها و خطاهای آزموده."
url: https://proxynet.io/fa/blog/puppeteer-proxy
date: 2026-10-06
author: "Acar Diveroli"
category: "یکپارچه‌سازی, وب اسکرپینگ"
lang: fa
---

# تنظیم پروکسی در Puppeteer: احراز هویت، چرخش IP و SOCKS5

مقدار `--proxy-server=http://user:pass@host:port` را همان‌طور که پروکسی را برای cURL می‌نویسید به آرگومان‌های اجرا اضافه می‌کنید و نخستین `page.goto()` با `net::ERR_NO_SUPPORTED_PROXIES` متوقف می‌شود. اطلاعات ورود را برمی‌دارید و خطا به `net::ERR_INVALID_AUTH_CREDENTIALS` تبدیل می‌شود. Puppeteer گزینه پروکسی جداگانه‌ای ندارد. این کتابخانه Chrome را اجرا می‌کند و Chrome است که تصمیم می‌گیرد پروکسی چگونه به کار رود؛ برای همین پاسخ بیشتر پرسش‌ها در قواعد Chrome است، نه در API خود Puppeteer.

این نوشته پرچم اجرا (launch flag) و `page.authenticate()`، پروکسی جداگانه برای هر browser context، نشست‌های ثابت (sticky) و چرخشی، SOCKS5، فهرست استثنا (bypass list)، بررسی IP خروجی، خطاهایی که با آن‌ها روبه‌رو می‌شوید و یک اسکریپت کامل با تلاش دوباره را در بر می‌گیرد. همه نمونه‌ها را در 6 اکتبر 2026 با ⁦Puppeteer 25.12.0⁩ و نسخه ⁦Chrome 154⁩ که همراه آن دانلود می‌شود اجرا کردیم. طرف دیگر آزمون‌ها یک پروکسی HTTP محلی بود که نام کاربری و رمز عبور می‌خواهد، به‌علاوه یک سرور SOCKS5 محلی. پیام‌های خطایی که نقل می‌کنیم همان‌هایی است که خودمان گرفتیم.

> **نکته: پاسخ کوتاه**
>
> Puppeteer پروکسی را به Chrome می‌سپارد: `--proxy-server=http://host:port` را در `args` فراخوانی `puppeteer.launch()` بنویسید، یا برای اینکه یک context پروکسی خودش را داشته باشد `proxyServer` را به `browser.createBrowserContext()` بدهید. نام کاربری و رمز عبور هرگز در این نشانی نوشته نمی‌شوند؛ پیش از نخستین `page.goto()` تابع `page.authenticate({ username, password })` را فراخوانی کنید. Chrome پس از آن همین اطلاعات ورود را برای کل context دوباره به کار می‌برد، پس هر نشست پروکسی یعنی یک context. SOCKS5 فقط بدون رمز عبور کار می‌کند و به همین دلیل همراه با لیست سفید IP به کار می‌رود.

## Puppeteer ترافیک را چگونه از پروکسی عبور می‌دهد؟

Puppeteer یک کتابخانه Node.js است که یک مرورگر واقعی را از درون کد هدایت می‌کند. خودش اتصال شبکه باز نمی‌کند؛ این کار را مرورگر انجام می‌دهد. پس پروکسی یکی از تنظیمات Chrome است که یا هنگام اجرای مرورگر به شکل پرچم خط فرمان داده می‌شود یا هنگام ساختن یک browser context به شکل یک گزینه. browser context نشستی جدا درون یک مرورگر است با کوکی‌ها، حافظه نهان و فضای ذخیره‌سازی خودش؛ چیزی شبیه پنجره ناشناس.

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

1. Chrome درخواست `CONNECT httpbin.org:443` را می‌فرستد و از پروکسی می‌خواهد تونلی به سایت باز کند. برای صفحه `http://` خود درخواست را می‌فرستد.
2. درخواست اطلاعات ورود ندارد، پس پروکسی با `407 Proxy Authentication Required` پاسخ می‌دهد. [⁦RFC 9110⁩](https://www.rfc-editor.org/rfc/rfc9110#status.407) کد 407 را همتای 401 در سمت پروکسی تعریف می‌کند.
3. Puppeteer این درخواست احراز هویت (challenge) را می‌گیرد و با نام کاربری و رمز عبوری که به `page.authenticate()` داده شده پاسخ می‌دهد.
4. Chrome درخواست را با هدر `Proxy-Authorization` تکرار می‌کند، پروکسی `200` برمی‌گرداند و اتصال رمزگذاری‌شده به سایت درون تونل ساخته می‌شود. پروکسی نام میزبان را می‌بیند، نه محتوای صفحه را.
5. Chrome اطلاعات ورود را در حافظه نهان احراز هویت همان context نگه می‌دارد و در اتصال‌های بعدی بی‌درنگ می‌فرستد.

گام 5 دلیل آن است که اطلاعات ورود به context تعلق پیدا می‌کند؛ آزمون‌های پایین‌تر این را نشان می‌دهند.

## پروکسی را چگونه با ⁦--proxy-server⁩ و ⁦page.authenticate()⁩ تنظیم کنیم؟

فرمان `npm i puppeteer` کتابخانه را نصب می‌کند و یک Chrome سازگار هم دانلود می‌کند؛ `puppeteer-core` همان API است بدون این دانلود. ساده‌ترین تنظیم این است:

```js
import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
  args: ["--proxy-server=http://pr.proxynet.io:8000"],
});
const page = await browser.newPage();
await page.authenticate({ username: "user", password: "pass" });

await page.goto("https://httpbin.org/ip");
console.log(await page.$eval("body", (el) => el.innerText)); // the exit IP the site sees

await browser.close();
```

بخش `http://` در پرچم، اتصال به خود پروکسی را توصیف می‌کند، نه صفحه‌ها را. پروکسی HTTP سایت‌های `https://` را هم از درون تونل جابه‌جا می‌کند و محتوای آن‌ها میان Chrome و سایت رمزگذاری‌شده می‌ماند. [مرجع `page.authenticate()`](https://pptr.dev/api/puppeteer.page.authenticate) اضافه می‌کند که Puppeteer برای این کار در پشت صحنه رهگیری درخواست (request interception) را روشن می‌کند که ممکن است کمی از کارایی بکاهد؛ دادن `null` آن را خاموش می‌کند.

در آزمون‌های ما سه چیز خراب شد و از هر کدام قاعده‌ای ثابت به دست آمد:

- **`page.authenticate()` را پیش از نخستین `goto()` فراخوانی کنید.** وقتی بعد از آن فراخوانی شد، نخستین ناوبری با `net::ERR_INVALID_AUTH_CREDENTIALS` شکست خورد و فقط ناوبری بعدی گذشت.
- **اطلاعات ورود را در پرچم ننویسید.** با `user:pass@` در نشانی، ⁦Chrome 154⁩ کل مقدار را با `net::ERR_NO_SUPPORTED_PROXIES` رد کرد. راهنماهای قدیمی‌تر در اینجا از `407` می‌گویند؛ امروز درخواست اصلاً به پروکسی نمی‌رسد.
- **`Proxy-Authorization` را خودتان نفرستید.** تنظیم آن با `page.setExtraHTTPHeaders()` باعث شد Chrome ناوبری را با `net::ERR_INVALID_ARGUMENT` رد کند.

| مقدار `--proxy-server` | نتیجه در ⁦Chrome 154⁩ |
|---|---|
| `http://pr.proxynet.io:8000` | کار می‌کند؛ صفحه‌های HTTPS از تونل `CONNECT` می‌گذرند |
| `pr.proxynet.io:8000` | کار می‌کند؛ بدون طرح‌واره، Chrome پروکسی HTTP در نظر می‌گیرد |
| `http://user:pass@pr.proxynet.io:8000` | `net::ERR_NO_SUPPORTED_PROXIES` |
| `socks5://host:port` | فقط وقتی کار می‌کند که سرور رمز عبور نخواهد |
| `socks5h://host:port` | `net::ERR_NO_SUPPORTED_PROXIES`؛ Chrome این طرح‌واره را نمی‌شناسد |

## چگونه برای هر browser context پروکسی جداگانه به کار ببریم؟

در ⁦Puppeteer 22⁩ نام `createIncognitoBrowserContext()`، که راهنماهای قدیمی هنوز به کار می‌برند، به `browser.createBrowserContext()` تغییر کرد. [`BrowserContextOptions`](https://pptr.dev/api/puppeteer.browsercontextoptions) آن دو فیلد پروکسی دارد: `proxyServer` و `proxyBypassList`. مرورگر می‌تواند بدون پروکسی اجرا شود و هر context پروکسی خودش را بگیرد:

```js
import puppeteer from "puppeteer";

const PROXY = "http://pr.proxynet.io:8000";
// One sticky session per context: same session ID -> same exit IP for up to 600 seconds
const sessions = ["a1b2c3", "d4e5f6"];

const browser = await puppeteer.launch(); // the browser itself starts without a proxy
for (const id of sessions) {
  const context = await browser.createBrowserContext({ proxyServer: PROXY });
  const page = await context.newPage();
  await page.authenticate({ username: `user-session-${id}-ttl-600`, password: "pass" });

  for (let i = 0; i < 2; i++) {
    await page.goto("https://httpbin.org/ip");
    console.log(id, await page.$eval("body", (el) => el.innerText));
  }
  await context.close(); // cookies, cache and the proxy setting go with the context
}
await browser.close();
```

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

- **اطلاعات ورود به context تعلق دارد، نه به صفحه.** صفحه دومی در همان context تابع `page.authenticate()` را با نام کاربری دیگری فراخوانی کرد، اما همه درخواست‌هایش باز هم نام کاربری نخست را داشتند. صفحه‌ای هم که اصلاً آن را فراخوانی نکرد با همان اطلاعات ورود ذخیره‌شده گذشت.
- **context بدون `proxyServer` پروکسی پرچم اجرا را به ارث می‌برد، اما اطلاعات ورود خودش را نگه می‌دارد.** با `--proxy-server` روی مرورگر، دو context با شناسه‌های نشست جداگانه باز هم از دو نقطه خروج متفاوت خارج شدند.

| | پرچم `--proxy-server` | `createBrowserContext({ proxyServer })` |
|---|---|---|
| دامنه اثر | کل مرورگر | فقط همان context |
| نقطه‌های خروج جداگانه در یک مرورگر | بله، یک نام کاربری برای هر context | بله، حتی میزبان‌های پروکسی جداگانه |
| عوض کردن پروکسی | راه‌اندازی دوباره مرورگر | بستن context و باز کردن یکی تازه |
| مناسب برای | یک اسکریپت، یک نقطه خروج | نشست‌های مستقل پرشمار به‌صورت موازی |

قاعده عملی این است: یک نشست، یک context، یک نام کاربری.

## چرخشی یا sticky: کدام نوع نشست به کار مرورگر می‌آید؟

مرورگر برای هر صفحه فقط یک درخواست نمی‌فرستد. یک صفحه محصول سند HTML، اسکریپت‌ها، فایل‌های سبک، تصویرها و فراخوانی‌های API پس‌زمینه را می‌گیرد، اغلب از چند میزبان و روی چند اتصال. در گیت‌وی چرخشی هر اتصال تازه می‌تواند IP خروجی تازه‌ای بگیرد؛ در آزمون ما حتی آیکون زبانه (favicon) یک صفحه از نشانی دیگری جز خود صفحه خارج شد. برای صفحه‌هایی که به هم ربطی ندارند این بی‌ضرر است. اما در ورود به حساب، سبد خرید یا فرم چندمرحله‌ای، سایت می‌بیند که یک بازدیدکننده میان نشانی‌ها جابه‌جا می‌شود و ممکن است نشست را پایان دهد.

گیت‌وی نوع نشست را از نام کاربری می‌خواند و سازنده نقطه اتصال در پنل کاربری آن را برایتان می‌نویسد. بخش‌های آن به‌راحتی خوانده می‌شوند: `-country-de` کشور خروج را انتخاب می‌کند (شهر هم در همان سازنده انتخاب می‌شود)، `-session-a1b2c3-ttl-600` یک نقطه خروج را برای آن شناسه نشست به تعداد ثانیه‌ای که می‌نویسید نگه می‌دارد، در بازه 1 تا 60 دقیقه، و نام کاربری بدون بخش نشست چرخشی است.

برای صفحه‌های مستقل مانند فهرست دسته‌بندی یا نتیجه جست‌وجو که در context‌های کوتاه‌عمر باز می‌شوند، از [پروکسی چرخشی](https://proxynet.io/fa/rotating-proxy) استفاده کنید.

برای هر کاری که وضعیت نگه می‌دارد از [پروکسی با نشست ثابت](https://proxynet.io/fa/sticky-proxy) استفاده کنید: یک context، یک شناسه نشست، با مدتی بلندتر از زمانی که آن روند طول می‌کشد. وقتی کار تمام شد context را ببندید تا کوکی‌ها و IP با هم پایان یابند.

چرخش، کارهای مستقل را میان نقطه‌های خروج پخش می‌کند؛ راهی برای گذشتن از محدودیت‌های یک سایت نیست. پاسخ `429 Too Many Requests` یا `403 Forbidden` از شما می‌خواهد آهسته‌تر پیش بروید و IP تازه این پاسخ را عوض نمی‌کند.

## آیا Puppeteer با پروکسی SOCKS5 کار می‌کند؟

بله، اما بدون نام کاربری و رمز عبور. [مستندات پروکسی Chromium](https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md) می‌گوید که برای SOCKSv5 هیچ روش احراز هویتی پشتیبانی نمی‌شود. سرور SOCKS5 محلی ما همین را از سوی دیگر دید: پیام آغازین Chrome فقط روش `0x00` یعنی «بدون احراز هویت» را پیشنهاد کرد. سروری که روش نام کاربری و رمز عبور (`0x02`) را الزامی می‌دانست ناچار بود این پیشنهاد را رد کند و ناوبری با `net::ERR_SOCKS_CONNECTION_FAILED` شکست خورد. `page.authenticate()` هم تفاوتی ایجاد نکرد، چون در SOCKS5 درخواست احراز هویتی از نوع 407 وجود ندارد که Puppeteer به آن پاسخ دهد.

راه‌حل این است که هویت خود را با آدرس IP ثابت کنید. IP عمومی دستگاهی را که Chrome روی آن اجرا می‌شود در پنل کاربری به لیست سفید IP اضافه کنید (حداکثر 10 نشانی)، پورت SOCKS5 را که سازنده نقطه اتصال نشان می‌دهد بردارید و مرورگر را بدون اطلاعات ورود اجرا کنید:

```js
import puppeteer from "puppeteer";

// No username or password: this machine's public IP must be on the IP whitelist.
// SOCKS5_PORT: the SOCKS5 port shown in the endpoint builder.
const { SOCKS5_PORT } = process.env;

const browser = await puppeteer.launch({
  args: [`--proxy-server=socks5://pr.proxynet.io:${SOCKS5_PORT}`],
});
const page = await browser.newPage();
await page.goto("https://httpbin.org/ip");
console.log(await page.$eval("body", (el) => el.innerText));
await browser.close();
```

Chrome به سرور SOCKS5 نام میزبان فرستاد، نه آدرس IP؛ پس DNS در سمت پروکسی تحلیل می‌شود و عادت `socks5h://` که از cURL می‌شناسید در اینجا لازم نیست. برای صفحه‌های وب، پروکسی HTTP معمولاً گزینه ساده‌تری است، چون با `page.authenticate()` کار می‌کند.

## چگونه برخی نشانی‌ها را از پروکسی بیرون نگه داریم؟

پرچم `--proxy-bypass-list`، یا `proxyBypassList` به شکل آرایه در یک context، میزبان‌هایی را فهرست می‌کند که Chrome مستقیم به آن‌ها وصل می‌شود. در پرچم، قاعده‌ها با نقطه‌ویرگول یا ویرگول از هم جدا می‌شوند: `--proxy-bypass-list=*.internal.example;192.168.0.0/16`. در آزمون ما میزبانی که در فهرست بود اصلاً از پروکسی نگذشت و نامش روی همان دستگاه محلی تحلیل شد.

Chrome قاعده‌های ضمنی هم دارد: `localhost`، `*.localhost`، `127.0.0.1/8` و `[::1]` حتی با فهرست خالی هرگز از پروکسی استفاده نمی‌کنند. اگر در برابر یک سرور محلی آزمون می‌گیرید و گزارش پروکسی ساکت می‌ماند، دلیلش همین است. قاعده ویژه `<-loopback>` این قاعده‌های ضمنی را برمی‌دارد؛ با آن، درخواست ما به `127.0.0.1` از پروکسی گذشت.

## IP خروجی را چگونه بررسی کنیم؟

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

```js
import puppeteer from "puppeteer";

async function exitIp(context, credentials) {
  const page = await context.newPage();
  if (credentials) await page.authenticate(credentials);
  await page.goto("https://httpbin.org/ip");
  const { origin } = JSON.parse(await page.$eval("body", (el) => el.innerText));
  await page.close();
  return origin;
}

const browser = await puppeteer.launch();
const direct = await browser.createBrowserContext();
const proxied = await browser.createBrowserContext({ proxyServer: "http://pr.proxynet.io:8000" });

const own = await exitIp(direct);
const viaProxy = await exitIp(proxied, { username: "user", password: "pass" });
console.log({ own, viaProxy, proxyWorks: own !== viaProxy });
await browser.close();
```

اگر کشور خاصی را هدف گرفته‌اید، IP خروجی را در یک سرویس موقعیت‌یابی جغرافیایی هم بررسی کنید. وقتی بررسی شکست می‌خورد، Chrome را از ماجرا کنار بگذارید: آزمون‌های خط فرمانی که مشکل شبکه را از مشکل کد جدا می‌کنند در نوشته [آیا پروکسی کار می‌کند؟ چگونه پروکسی را آزمایش کنیم](/fa/blog/how-to-test-a-proxy) آمده است.

## با چه خطاهایی روبه‌رو می‌شوید و معنای هر کدام چیست؟

هر یک از این خطاها را در محیط آزمایشی محلی خودمان پدید آوردیم:

| آنچه می‌بینید | چه اتفاقی افتاد | چه باید کرد |
|---|---|---|
| `net::ERR_PROXY_CONNECTION_FAILED` | Chrome به پروکسی نرسید: میزبان یا پورت نادرست است یا ترافیک خروجی مسدود است | نشانی و پورت را بررسی کنید؛ تلاش دوباره کمکی نمی‌کند |
| `net::ERR_INVALID_AUTH_CREDENTIALS` | پروکسی ورود خواست و Chrome اطلاعات ورودی نداشت | `page.authenticate()` را پیش از نخستین `goto()` فراخوانی کنید |
| `goto()` با وضعیت `407` برمی‌گردد | رمز عبور نادرست بود؛ Puppeteer یک بار تلاش کرد و دست کشید | `response.status()` را بررسی و اطلاعات ورود را درست کنید |
| `net::ERR_TUNNEL_CONNECTION_FAILED` | ورود موفق بود، اما پروکسی نتوانست تونل را تا سایت باز کند | نشانی مقصد را بررسی کنید |
| `net::ERR_NO_SUPPORTED_PROXIES` | Chrome مقدار `--proxy-server` را رد کرد | `user:pass@` را بردارید و از `http://` یا `socks5://` استفاده کنید |
| `net::ERR_SOCKS_CONNECTION_FAILED` | سرور SOCKS5 رمز عبور می‌خواهد یا IP شما در لیست سفید نیست | IP خود را به لیست سفید اضافه کنید یا از پورت HTTP استفاده کنید |
| صفحه باز می‌شود اما IP همان IP شماست | یک قاعده استثنا یا میزبان loopback باعث شد درخواست از پروکسی نگذرد | فهرست استثنا و `<-loopback>` را بررسی کنید |

سطر رمز عبور نادرست از همه دیرتر خود را نشان می‌دهد: هیچ خطایی پرتاب نمی‌شود و اسکریپت با صفحه 407 پروکسی به کار ادامه می‌دهد. در مرورگر دسکتاپ خطای تونل علت‌های بیشتری دارد، از اسکن HTTPS آنتی‌ویروس تا تنظیم پروکسی سیستمی کهنه؛ نوشته [خطای ⁦err_tunnel_connection_failed⁩ چیست و چگونه رفع می‌شود؟](/fa/blog/err-tunnel-connection-failed) را ببینید. در زمان توسعه، این دو شنونده خطای واقعی پشت یک پایان مهلت را نشان می‌دهند:

```js
page.on("requestfailed", (req) => console.log("FAILED", req.url(), req.failure()?.errorText));
page.on("response", (res) => res.status() >= 400 && console.log(res.status(), res.url()));
```

## نمونه کامل: context‌های موازی، نشست‌های ثابت و تلاش دوباره

این اسکریپت شش صفحه را که با JavaScript ساخته می‌شوند باز می‌کند و هم‌زمان حداکثر سه context باز نگه می‌دارد. هر تلاش یک context تازه و یک نشست ثابت تازه می‌گیرد تا کوکی‌ها و IP خروجی با هم عوض شوند. تصویر، رسانه و فونت برای صرفه‌جویی در ترافیک مسدود می‌شوند؛ `page.setRequestInterception()` این کار را بدون خراب کردن `page.authenticate()` انجام می‌دهد. پایان مهلت و شکست تونل با انتظار نمایی (exponential backoff) دوباره تلاش می‌شوند، اما خطاهای پیکربندی و پاسخ `403` یا `429` از سوی سایت کار را متوقف می‌کنند.

```js
import puppeteer from "puppeteer";

const PROXY = "http://pr.proxynet.io:8000";
const USER = "user";
const PASS = "pass";
const URLS = Array.from({ length: 6 }, (_, i) => `https://quotes.toscrape.com/js/page/${i + 1}/`);
const CONCURRENCY = 3; // contexts open at the same time
const ATTEMPTS = 3;
const BLOCKED = new Set(["image", "media", "font"]);
// Configuration errors: waiting and retrying will not fix them
const FATAL = ["ERR_PROXY_CONNECTION_FAILED", "ERR_NO_SUPPORTED_PROXIES", "ERR_INVALID_AUTH_CREDENTIALS"];

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function scrape(browser, url) {
  for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
    // One context per attempt: its own cookies and its own sticky session (one exit IP)
    const context = await browser.createBrowserContext({ proxyServer: PROXY });
    try {
      const page = await context.newPage();
      const session = crypto.randomUUID().slice(0, 8);
      await page.authenticate({ username: `${USER}-session-${session}-ttl-300`, password: PASS });
      await page.setRequestInterception(true);
      page.on("request", (req) => (BLOCKED.has(req.resourceType()) ? req.abort() : req.continue()));

      const res = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
      const status = res ? res.status() : 0;
      if (status === 403 || status === 429) {
        // The site is refusing or slowing you down: a new IP is not the answer
        throw Object.assign(new Error(`HTTP ${status}: stop and lower the request rate`), { fatal: true });
      }
      if (status >= 400) throw new Error(`HTTP ${status}`);

      await page.waitForSelector("div.quote span.text", { timeout: 10_000 }); // JavaScript-rendered
      return await page.$$eval("div.quote span.text", (els) => els.map((el) => el.textContent));
    } catch (err) {
      err.fatal ||= FATAL.some((code) => err.message.includes(code));
      if (err.fatal || attempt === ATTEMPTS) throw err;
      await sleep(2 ** attempt * 1000 + Math.random() * 1000); // back off before the next attempt
    } finally {
      await context.close();
    }
  }
}

const browser = await puppeteer.launch();
const queue = [...URLS];
const results = new Map();
await Promise.all(
  Array.from({ length: CONCURRENCY }, async () => {
    while (queue.length) {
      const url = queue.shift();
      try {
        results.set(url, `${(await scrape(browser, url)).length} quotes`);
      } catch (err) {
        results.set(url, `ERROR ${err.message.split("\n")[0]}`);
        if (err.fatal) queue.length = 0; // stop the whole job, not just this page
      }
    }
  }),
);
await browser.close();
for (const url of URLS) console.log(url, results.get(url) ?? "skipped");
```

از طریق پروکسی آزمایشی محلی، هر شش صفحه در کمتر از پنج ثانیه و هر کدام با ده نقل‌قول برگشتند. وقتی پورت پروکسی را عمداً نادرست نوشتیم، سه صفحه‌ای که در جریان بودند بی‌درنگ با `net::ERR_PROXY_CONNECTION_FAILED` متوقف شدند و سه صفحه دیگر با برچسب `skipped` کنار گذاشته شدند؛ یک صفحه آزمایشی محلی که با `429` پاسخ می‌داد هم کار را به همین شکل پایان داد. با `CONCURRENCY` کم آغاز کنید: هر context حافظه نگه می‌دارد و سایت مقصد هم محدودیت‌های خودش را دارد.

## کاربردها

- صفحه‌های کاتالوگ و قیمت که با JavaScript ساخته می‌شوند و یک کلاینت ساده HTTP آن‌ها را خالی می‌بیند.
- بررسی اینکه سایت، قیمت‌ها یا بنرهای رضایت کوکی خودتان از کشوری دیگر چگونه دیده می‌شوند؛ برای هر کشور یک context.
- تصویر صفحه و PDF از صفحه‌های عمومی، به همان شکلی که بازدیدکننده‌ای در کشوری مشخص آن‌ها را می‌بیند.
- پایش صفحه‌ها و روندهای پرداخت خودتان از بیرون شبکه‌تان.

چارچوب همه این کارها یکی است: به `robots.txt` و شرایط استفاده سایت پایبند باشید، اگر API رسمی وجود دارد از آن استفاده کنید و سرعت درخواست‌ها را در سطحی نگه دارید که سایت تاب بیاورد.

## اشتباهات رایج

- **نوشتن `user:pass@` در `--proxy-server`.** Chrome مقدار را با `net::ERR_NO_SUPPORTED_PROXIES` رد می‌کند.
- **انتظار نام کاربری تازه برای هر صفحه.** Chrome نخستین اطلاعات ورود را برای کل context نگه می‌دارد؛ برای هر نشست یک context به کار ببرید.
- **امتحان کردن رمز عبور در SOCKS5.** Chrome فقط «بدون احراز هویت» را پیشنهاد می‌کند؛ از لیست سفید IP یا پورت HTTP استفاده کنید.
- **آزمودن در برابر `localhost` و اعتماد به نتیجه.** نشانی‌های loopback از پروکسی نمی‌گذرند، مگر اینکه `<-loopback>` را اضافه کنید.
- **صفحه دانستن پاسخ `407`.** رمز عبور نادرست خطا پرتاب نمی‌کند؛ `response.status()` را بررسی کنید.
- **عوض کردن IP پس از `429` یا `403`.** به جای آن آهسته‌تر پیش بروید؛ محدودیت به رفتار شما مربوط است، نه به نشانی‌تان.
- **باز گذاشتن context‌ها.** هر کدام حافظه نگه می‌دارد؛ آن‌ها را در بلوک `finally` ببندید.

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

| نیاز | پیشنهاد |
|---|---|
| یک اسکریپت، یک نقطه خروج | پرچم `--proxy-server` همراه با `page.authenticate()` |
| نشست‌های مستقل پرشمار در یک مرورگر | `createBrowserContext({ proxyServer })`، یک شناسه نشست برای هر context |
| صفحه‌های انبوهی که به هم وابسته نیستند | نام کاربری چرخشی، context‌های کوتاه‌عمر |
| ورود به حساب، سبد خرید یا فرم چندمرحله‌ای | نشست ثابت بلندتر از روند، یک context |
| SOCKS5 الزامی است | لیست سفید IP، بدون اطلاعات ورود |
| ابزاری که فقط پرچم اجرا را می‌پذیرد | لیست سفید IP، یا یک پروکسی واسط محلی مانند `proxy-chain` |
| میزبان‌های داخلی باید مستقیم بمانند | `--proxy-bypass-list` یا `proxyBypassList` |

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

### آیا ⁦puppeteer.launch()⁩ گزینه پروکسی دارد؟

خیر. پروکسی به شکل پرچم `--proxy-server` در Chrome درون `args` قرار می‌گیرد و ورود در `page.authenticate()` انجام می‌شود. API خود Puppeteer فقط در `browser.createBrowserContext()` فیلد پروکسی دارد: `proxyServer` و `proxyBypassList`.

### آیا هر صفحه می‌تواند پروکسی خودش را داشته باشد؟

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

### proxy-chain چیست و آیا به آن نیاز دارید؟

`proxy-chain` یک بسته متن‌باز Node.js است که تابع `anonymizeProxy()` آن یک پروکسی محلی بدون رمز عبور راه می‌اندازد و ترافیک را به یک پروکسی بالادستی دارای اطلاعات ورود می‌فرستد؛ به این ترتیب Chrome یک نشانی ساده `http://127.0.0.1:<port>` می‌گیرد. در آزمون ما هم با پروکسی بالادستی HTTP و هم با SOCKS5 که هر دو رمز عبور می‌خواستند کار کرد. اگر از `page.authenticate()` یا لیست سفید IP استفاده می‌کنید به آن نیازی ندارید.

### آیا می‌توان پروکسی را بدون راه‌اندازی دوباره مرورگر عوض کرد؟

بله، از راه context‌ها. context را ببندید، یکی تازه با `proxyServer` یا شناسه نشست دیگری باز کنید و `page.authenticate()` را دوباره فراخوانی کنید. فقط پرچم `--proxy-server` در تمام عمر مرورگر ثابت می‌ماند.

### چرا گزارش پروکسی درخواست‌هایی را نشان می‌دهد که شما نفرستاده‌اید؟

ترافیک پس‌زمینه خود Chrome، مانند بررسی به‌روزرسانی و درخواست‌های سرویس‌های گوگل، به همان پروکسی می‌رود. در گزارش ما بیشتر این درخواست‌ها بدون اطلاعات ورود رسیدند و `407` گرفتند، چون `page.authenticate()` فقط به درخواست‌های احراز هویتی پاسخ می‌دهد که از ترافیک صفحه می‌آیند. این موضوع روی صفحه‌های شما اثری ندارد.

### آیا پروکسی جلوی CAPTCHA را در Puppeteer می‌گیرد؟

خیر. پروکسی فقط جایی را که ترافیک از آن می‌آید عوض می‌کند، در حالی که CAPTCHA به سرعت درخواست‌ها، نشانه‌های مرورگر و نشست‌های ناسازگار هم واکنش نشان می‌دهد. علت‌ها و راه‌های مشروع کم کردن آن در نوشته [Puppeteer و CAPTCHA: چرا ظاهر می‌شود و چگونه کم می‌شود؟](/fa/blog/puppeteer-captcha) آمده است.

## خلاصه

در Puppeteer پروکسی یکی از تنظیمات Chrome است: پرچم `--proxy-server` برای کل مرورگر یا `proxyServer` برای یک context، و ورود با `page.authenticate()` پیش از نخستین ناوبری. اطلاعات ورود برای هر context جداگانه ذخیره می‌شود؛ پس هر نشست را به شکل یک context بسازید، ثابت یا چرخشی بودن را با نام کاربری انتخاب کنید و برای SOCKS5 از لیست سفید IP استفاده کنید. `response.status()` را بررسی کنید و سرعت درخواست‌ها را در حدی مؤدبانه نگه دارید. برای صفحه‌هایی که باید از اتصال‌های خانگی معمولی باز شوند، نقطه‌های خروج از استخر [پروکسی مسکونی](https://proxynet.io/fa/residential-proxy) می‌آیند.
