---
title: "نشست و کوکی در Python: ورود با requests"
description: "requests.Session کوکی‌ها را میان درخواست‌ها می‌برد و نشست را باز نگه می‌دارد. خواندن توکن CSRF از فرم، تأیید ورود و ذخیره نشست را نشان می‌دهیم."
url: https://proxynet.io/fa/blog/python-login-session-cookies
date: 2026-09-19
author: "Acar Diveroli"
category: "آموزش‌ها, وب اسکرپینگ"
lang: fa
---

# نشست و کوکی در Python: ورود با requests

هر روز صبح با دست وارد پنل شرکت خودتان می‌شوید و یک گزارش دانلود می‌کنید. پنل دکمه برون‌بری دارد، API رسمی ندارد و همان پنج کلیک هر روز تکرار می‌شود. وقتی بخواهید این کار را به یک اسکریپت Python بسپارید، نخستین دیوار صفحه ورود است: صفحه‌ای که `requests.get()` دانلود می‌کند گزارش نیست، فرم ورود است. HTTP پروتکلی بدون حالت است و سرور تنها با کوکی‌ای که به هر درخواست می‌افزایید شما را می‌شناسد.

در این نوشته توضیح می‌دهیم کوکی و نشست چیست، چرا توکن CSRF در هر اجرا دوباره خوانده می‌شود و یک جریان ورود با `requests.Session` چگونه ساخته می‌شود. سپس به ذخیره نشست روی دیسک، تازه کردن آن پس از انقضا، بیرون بردن اطلاعات ورود از کد و ماندن روی یک IP خروجی در تمام مدت نشست می‌پردازیم. در پایان روش `storage_state` در Playwright برای سایت‌هایی می‌آید که فرم را با JavaScript می‌فرستند. نمونه‌های کد روی `quotes.toscrape.com` آزموده شده‌اند.

> **نکته: پاسخ کوتاه**
>
> یک شیء `requests.Session` ظرف کوکی خودش را همراه دارد: کوکی نشستی را که هنگام `GET` روی فرم ورود می‌آید نگه می‌دارد، آن را با کوکی تازه‌ای که در پاسخ `POST` شما می‌آید به‌روز می‌کند و در هر درخواست بعدی خودش آن را می‌افزاید. جریان کار چهار گام دارد: فرم را بخواهید، توکن CSRF درون آن را بخوانید، آن را همراه نام کاربری و رمز بفرستید و از محتوای صفحه تأیید کنید که ورود واقعاً انجام شده است. همه این‌ها تنها برای **حساب خودتان** یا حسابی معتبر است که دارنده‌اش به شما اجازه کتبی داده باشد.

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

هر چه اینجا می‌آید بر یک فرض استوار است: حسابی که در آن وارد می‌شوید از آن شماست، یا دارنده حساب به شما اجازه کتبی داده است. پنل مدیریت شرکت خودتان، حساب فروشگاهی خودتان، سامانه مشتری‌ای که کتباً خواسته است «این گزارش را هر روز برای ما بگیرید». آنچه بیرون از این دایره باشد موضوع این نوشته نیست.

پیش از رفتن سراغ کد، سه پرسش:

1. **آیا API رسمی یا برون‌بری وجود دارد؟** اگر هست، رابط کاربری را خودکار نکنید. کلید API پایدار است و با تغییر رابط نمی‌شکند؛ خودکارسازی ورود راهی است که وقتی به داده دسترسی دیگری ندارید به آن پناه می‌برید. چارچوب حقوقی را در [آیا اسکرپینگ وب قانونی است؟ یک نمای کلی](/fa/blog/is-data-web-scraping-legal) بررسی کرده‌ایم.
2. **شرایط استفاده سایت چه می‌گوید؟** اگر بندی دسترسی خودکار را ممنوع کرده باشد، حساب شما می‌تواند معلق شود. ممکن بودن از نظر فنی به معنای مجاز بودن نیست.
3. **اجازه شما کتبی است؟** در کار مشتری، تأیید شفاهی کافی نیست. با کدام حساب، از کدام صفحه‌ها و با چه تناوبی داده می‌گیرید باید در قرارداد یا ایمیل نوشته شده باشد.

چهار چیز در این نوشته آگاهانه توضیح داده نمی‌شود:

- **آزمودن رمز عبور در کار نیست.** کدی که فهرستی را یکی‌یکی در فیلد فرم می‌نویسد (credential stuffing) یا کدی که ترکیب می‌سازد (brute force) اینجا نمی‌آید. این کار را نکنید: دسترسی غیرمجاز به حساب دیگران در هر کشوری جرم است و ممکن بودن فنی آن چیزی را عوض نمی‌کند.- **گذشتن از تأیید دومرحله‌ای و CAPTCHA در کار نیست.** این کنترل‌ها برای محافظت از حساب گذاشته شده‌اند؛ تلاش برای رد شدن از آن‌ها با یک اسکریپت امنیت همان حسابی را که با آن کار می‌کنید پایین می‌آورد. اگر روی حساب خودتان تأیید دومرحله‌ای فعال است، راه درست کلید API یا رمز اختصاصی برنامه از سوی سرویس‌دهنده است؛ اگر هیچ‌کدام نباشد یعنی آن کار خودکار نخواهد شد.
- **حساب دیگران در کار نیست.** «حساب دوستم»، «حساب کسی که از شرکت رفته» و اطلاعات ورودی که در اینترنت منتشر شده هم در همین دسته‌اند.
- **دزدیدن یا جابه‌جا کردن کوکی نشست در کار نیست.** نشستی که با کوکی کپی‌شده از مرورگر شخص دیگر باز شود، جعل هویت اوست. فایل‌های کوکی این نوشته تنها **با ورود خود شما** ساخته می‌شوند و تنها روی رایانه خودتان می‌مانند.

یک تفکیک هم به سبب شباهت نام‌ها لازم است: احراز هویت در برابر سرور پروکسی (`user:pass` یا مجوز IP) با ورود به سایت مقصد دو کار جداست. اولی را در [احراز هویت پروکسی: نام کاربری و رمز یا لیست سفید IP](/fa/blog/proxy-authentication-methods) نوشته‌ایم؛ موضوع این نوشته دومی است.

## کوکی و نشست چیست؟

HTTP بدون حالت است: سرور هیچ چیزی را که دو درخواست را به هم پیوند بزند به یاد نمی‌آورد. این شکاف را کوکی‌ها پر می‌کنند. سرور در پاسخ خود سرآیند `Set-Cookie` می‌گذارد، کلاینت این مقدار را نگه می‌دارد و در درخواست‌های بعدی به همان دامنه با سرآیند `Cookie` بازمی‌فرستد. همه فیلدهای این سرآیند یکی‌یکی در [صفحه Set-Cookie در MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie) توضیح داده شده‌اند.

«نشست» همان چیزی است که به این کوکی معنا می‌دهد. دو طرح رایج است:

- **نشست سمت سرور.** در کوکی تنها یک شناسه تصادفی (`sessionid`، `PHPSESSID`، `JSESSIONID`) حمل می‌شود؛ اینکه کاربر کیست در رکوردی روی سرور نگهداری می‌شود. اگر سرور آن رکورد را پاک کند، نشست پایان می‌یابد حتی اگر کوکی در دست شما باشد.
- **کوکی امضاشده.** اطلاعات کاربر درون خود کوکی است و سرور آن را با یک کلید محرمانه امضا می‌کند. کوکی پیش‌فرض `session` در Flask چنین است.

اینکه کوکی چقدر عمر می‌کند با `Expires` و `Max-Age` تعیین می‌شود. [بخش 4.1.2.2 از ⁦RFC 6265⁩](https://www.rfc-editor.org/rfc/rfc6265#section-4.1.2.2) می‌گوید اگر کوکی نه `Max-Age` داشته باشد و نه `Expires`، کلاینت آن را تا «پایان نشست جاری» نگه می‌دارد. در مرورگر این یعنی «تا وقتی مرورگر را ببندید» و در یک اسکریپت Python یعنی «تا وقتی فرایند زنده است». پس اگر اسکریپت شما در هر اجرا دوباره وارد می‌شود تعجب نکنید: کوکی‌ای که نگه داشته بود از اول ماندگار نبود.

اینکه کوکی به کدام درخواست‌ها افزوده می‌شود را این فیلدهای سرآیند `Set-Cookie` تعیین می‌کنند:

| فیلد | چه می‌کند | معنای آن در اسکریپت |
|---|---|---|
| `Domain` | کوکی به کدام دامنه می‌رود | کوکی `panel.example.com` به درخواست `example.com` افزوده نمی‌شود |
| `Path` | زیر کدام مسیر معتبر است | کوکی روی `/report` در درخواست به `/` فرستاده نمی‌شود |
| `Secure` | تنها روی HTTPS فرستاده می‌شود | درخواستی که به HTTP بیفتد کوکی را حمل نمی‌کند |
| `HttpOnly` | JavaScript درون صفحه نمی‌تواند آن را بخواند | در کنسول مرورگر با `document.cookie` دیده نمی‌شود، کلاینت HTTP همچنان می‌بیند |
| `SameSite` | اینکه به درخواست‌های آمده از سایت دیگر افزوده شود یا نه | با `Strict` نخستین درخواست از یک پیوند بیرونی بدون نشست دیده می‌شود |

این جدول نظری نیست، فهرست عیب‌یابی است: بیشتر گزارش‌های «ورود موفق به نظر می‌رسد ولی درخواست بعدی دوباره به صفحه ورود می‌افتد» از ناسازگاری `Domain` یا `Path` بیرون می‌آید.

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

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

رایج‌ترین دفاع روشی است که در [صفحه پیشگیری از CSRF در OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) «توکن همگام‌ساز» نامیده می‌شود: سرور در هر صفحه فرم، مقداری تصادفی و وابسته به همان نشست را به شکل یک فیلد پنهان کار می‌گذارد. هنگام فرستادن فرم، این مقدار باید با همتای خود در کوکی نشست بخواند. فرمی در سایت دیگر نمی‌تواند توکن نشست شما را بداند، پس تطابق برقرار نمی‌شود.

این موضوع برای کسی که اسکریپت می‌نویسد سه نتیجه دارد:

- **توکن ثابت نیست.** اگر یک بار آن را از مرورگر کپی کنید و در کد بگذارید، فردا کار نمی‌کند. در هر اجرا باید صفحه ورود را دانلود کنید و فیلد را از همان‌جا بخوانید.
- **توکن به نشست وابسته است.** فرستادن توکن در یک درخواست و فرم در اتصالی دیگر بی‌فایده است. هر دو باید از یک شیء `Session` بگذرند؛ نخستین دلیل استفاده از `Session` هم همین است.
- **نام فیلد بسته به سایت فرق می‌کند.** `csrf_token`، `csrfmiddlewaretoken` (Django)، `authenticity_token` (Rails) و `_token` (Laravel) همگی یک کار می‌کنند. پیش از نوشتن کد به کد منبع فرم نگاه کنید.

در برنامه‌هایی که توکن را به جای فیلد پنهان در یک سرآیند (`X-CSRF-Token`) می‌خواهند، مقدار را باز هم از صفحه می‌خوانید، اما آن را نه در بدنه بلکه در دیکشنری `headers` می‌گذارید.

## کدام راه را انتخاب کنیم؟

برای کاری که پشت یک ورود است سه راه وجود دارد و انتخاب را فرم ورود سایت تعیین می‌کند.

| راه | چه وقت | هزینه | نقطه ضعف |
|---|---|---|---|
| `requests.Session` | فرم HTML ساده است و فیلدها در صفحه دیده می‌شوند | کمترین، بدون مرورگر | فیلدهای ساخته‌شده با JavaScript را نمی‌بیند |
| `storage_state` در Playwright | فرم با JavaScript فرستاده می‌شود یا توکن با اسکریپت ساخته می‌شود | بالا، یک مرورگر واقعی باز می‌شود | حافظه و زمان، بار نصب |
| ترکیبی: با مرورگر وارد شوید، با HTTP ادامه دهید | ورود پیچیده است ولی صفحه‌های داده HTML ساده‌اند | یک بار مرورگر، پس از آن ارزان | کوکی‌ها میان دو محیط جابه‌جا می‌شوند، یکسانی IP الزامی است |

انتخاب را با حدس نزنید، با نگاه به کد منبع صفحه انجام دهید: اگر درون `<form method="post">` فیلدهای `input` دارای `name` می‌بینید، `requests` کافی است. اگر فیلدها خالی‌اند یا فرم با `fetch()` فرستاده می‌شود، به راه دوم یا سوم بروید.

## با requests.Session چگونه وارد می‌شویم؟

پنج گام هست که ترتیبشان اهمیت دارد: چهار گام نخست خودِ ورود است و گام پنجم برای اجراهای بعدی.

1. **صفحه ورود را با `GET` بگیرید.** این پاسخ دو چیز می‌آورد: کوکی نشستی که هنوز ناشناس است و توکن CSRF درون فرم. `Session` کوکی را در ظرف خودش می‌گذارد.
2. **فیلدهای فرم را واقعاً بخوانید.** فرم را در مرورگر پر کنید و بفرستید و در زبانه شبکه ابزار توسعه‌دهنده بدنه درخواست را ببینید. نام فیلدها حدس زده نمی‌شود، از همان‌جا کپی می‌شود؛ اگر فیلد پنهانی مانند `next` یا `redirect` هست آن را هم بفرستید.
3. **درخواست `POST` را با همان `Session` بفرستید.** کوکی خودبه‌خود افزوده می‌شود. اگر فیلد `action` فرم مسیر دیگری را نشان می‌دهد، درخواست را به همان‌جا بفرستید.
4. **ورود را از محتوا تأیید کنید.** کد وضعیت گمراه‌کننده است: بسیاری از برنامه‌ها با رمز نادرست هم `200` می‌دهند و فرم را با یک پیام خطا دوباره نشان می‌دهند. معیار عنصری است که تنها هنگام باز بودن نشست دیده می‌شود: پیوند خروج، نام کاربری، منوی حساب.
5. **کوکی‌ها را ذخیره کنید.** اجرای بعدی گام ورود را رد می‌کند.

بر پایه [بخش Session Objects در مستندات Requests](https://requests.readthedocs.io/en/latest/user/advanced/)، شیء `Session` افزون بر نگه داشتن کوکی‌ها در طول عمر نمونه، برای درخواست‌هایی که به یک میزبان می‌روند اتصال TCP را هم دوباره به کار می‌گیرد. این الگو در هر سه کتابخانه [مقایسه HTTPX، Requests و AIOHTTP](/fa/blog/httpx-vs-requests-vs-aiohttp) یکسان است و تنها نام کلاس فرق می‌کند.

## نمونه کارا: quotes.toscrape.com

کد زیر روی `quotes.toscrape.com/login` آزموده شده است. اینجا سایتی آموزشی و باز است که برای تمرین اسکرپینگ برپا شده و **هر نام کاربری و هر رمزی را که وارد کنید می‌پذیرد**؛ هیچ احراز هویتی انجام نمی‌دهد و تنها جریان کار را تقلید می‌کند. دیدن این سازوکار نخست در همین‌جا جلوی ساختن رشته‌ای از ورودهای ناموفق روی حساب خودتان را می‌گیرد.

نخست جریان اصلی در چهار گام:

```python
import os

import requests
from bs4 import BeautifulSoup

BASE_URL = "https://quotes.toscrape.com"

session = requests.Session()

# 1) Request the login form: the session cookie and the CSRF token arrive with this response
page = session.get(f"{BASE_URL}/login", timeout=20)
page.raise_for_status()
token = BeautifulSoup(page.text, "html.parser").select_one('input[name="csrf_token"]')["value"]

# 2) Credentials are not hard-coded, they are read from environment variables
payload = {
    "csrf_token": token,
    "username": os.environ["SITE_USERNAME"],
    "password": os.environ["SITE_PASSWORD"],
}

# 3) Send the form with the same Session; the Session attaches the cookie itself
result = session.post(f"{BASE_URL}/login", data=payload, timeout=20)
result.raise_for_status()

# 4) Verify from the page content, not from the status code
if BeautifulSoup(result.text, "html.parser").select_one('a[href="/logout"]') is None:
    raise SystemExit("Could not verify the login")

print("Logged in, cookies:", list(session.cookies.keys()))
```

پیش از اجرا اطلاعات ورود را در محیط بگذارید؛ نوشتن رمز در کد یعنی نوشتن آن در تاریخچه نسخه‌ها:

```bash
export SITE_USERNAME="username"
export SITE_PASSWORD="password"
python login.py
```

در Windows PowerShell شکل `$env:SITE_USERNAME = "username"` به کار می‌رود. اینکه متغیرهای محیطی برای ابزارهای خط فرمان چگونه ماندگار می‌شوند را در [استفاده از پروکسی در wget: دستورها و نمونه‌ها](/fa/blog/wget-proxy) نوشته‌ایم.

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

ورود دوباره در هر اجرا، گزارش امنیتی حساب شما را با رکوردهای بی‌مورد پر می‌کند؛ در برخی برنامه‌ها ورودهای پیاپی در زمان کوتاه تأیید بیشتری را هم برمی‌انگیزد. راه‌حل این است که ظرف کوکی را در یک فایل بنویسید و در اجرای بعدی دوباره بارگذاری کنید.

اسکریپت کامل زیر همین کار را می‌کند: اگر کوکی ذخیره‌شده هست بارگذاری می‌کند، اگر نیست وارد می‌شود، در هر درخواست باز بودن نشست را بررسی می‌کند و اگر نشست افتاده باشد **یک بار** دوباره وارد می‌شود. یک‌باره بودن مهم است: حلقه بدون شرط، وقتی مشکل از اطلاعات ورود باشد، به تلاش بی‌پایان برای ورود تبدیل می‌شود.

```python
import json
import os
import time
from pathlib import Path

import requests
from bs4 import BeautifulSoup

BASE_URL = "https://quotes.toscrape.com"
COOKIE_FILE = Path("session_cookies.json")
DELAY_SECONDS = 2.0

class LoginError(Exception):
    """The login did not complete; look at the cause instead of retrying."""

def build_session():
    session = requests.Session()
    session.headers["User-Agent"] = "report-script/1.0 (contact: you@example.com)"
    proxy_url = os.environ.get("PROXY_URL")  # e.g. http://user:pass@pr.proxynet.io:8000
    if proxy_url:
        session.proxies = {"http": proxy_url, "https": proxy_url}
    return session

def save_cookies(session, path=COOKIE_FILE):
    cookies = [
        {"name": c.name, "value": c.value, "domain": c.domain, "path": c.path,
         "expires": c.expires, "secure": c.secure}
        for c in session.cookies
    ]
    path.write_text(json.dumps(cookies), encoding="utf-8")
    try:
        path.chmod(0o600)  # the file is as sensitive as a password; limited effect on Windows
    except OSError:
        pass

def load_cookies(session, path=COOKIE_FILE):
    if not path.exists():
        return False
    try:
        cookies = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return False
    now = time.time()
    for c in cookies:
        if c["expires"] and c["expires"] < now:
            continue  # never load an expired cookie
        session.cookies.set(c["name"], c["value"], domain=c["domain"], path=c["path"],
                            expires=c["expires"], secure=c["secure"])
    return True

def is_logged_in(html):
    return BeautifulSoup(html, "html.parser").select_one('a[href="/logout"]') is not None

def login(session):
    page = session.get(f"{BASE_URL}/login", timeout=20)
    page.raise_for_status()
    field = BeautifulSoup(page.text, "html.parser").select_one('input[name="csrf_token"]')
    if field is None:
        raise LoginError("No csrf_token field in the form; the page structure may have changed")
    payload = {
        "csrf_token": field["value"],
        "username": os.environ["SITE_USERNAME"],
        "password": os.environ["SITE_PASSWORD"],
    }
    result = session.post(f"{BASE_URL}/login", data=payload, timeout=20)
    result.raise_for_status()
    if not is_logged_in(result.text):
        raise LoginError("Could not verify the login; check the credentials and the form fields")
    save_cookies(session)

def get_page(session, path):
    """Fetches the page; if the session dropped, logs in again only once."""
    for attempt in range(2):
        response = session.get(f"{BASE_URL}{path}", timeout=20)
        response.raise_for_status()
        if is_logged_in(response.text):
            return response
        if attempt == 0:
            print("Session invalid, logging in again")
            session.cookies.clear()
            login(session)
    raise LoginError("Still not logged in after a second attempt; stop and look at the cause")

def main():
    session = build_session()
    if load_cookies(session):
        print("Saved cookies loaded")
    else:
        print("No saved session, logging in")
        login(session)

    for number in range(1, 4):
        response = get_page(session, f"/page/{number}/")
        quotes = BeautifulSoup(response.text, "html.parser").select("div.quote")
        print(f"Page {number}: {len(quotes)} records")
        time.sleep(DELAY_SECONDS)

if __name__ == "__main__":
    main()
```

اجرای نخست می‌گوید نشست ذخیره‌شده‌ای نیست و وارد می‌شود، اجرای دوم می‌گوید کوکی‌های ذخیره‌شده بارگذاری شدند و مستقیم سراغ داده می‌رود. سه نکته را در نظر داشته باشید:

- **فایل کوکی یک راز است.** مقدار درون آن برای همان نشست جای رمز عبور را می‌گیرد. فایل را به `.gitignore` بیفزایید و در پشتیبان‌ها و پوشه‌های اشتراکی نگذارید؛ مستندات Playwright هم برای فایل وضعیت خود همین هشدار را می‌دهد.
- **کوکی منقضی‌شده را بارگذاری نکنید.** تابع `load_cookies` این‌ها را با نگاه به فیلد `expires` کنار می‌گذارد؛ وگرنه اسکریپت با نشستی نامعتبر راه می‌افتد.
- **افتادن نشست را از محتوا بفهمید.** برخی برنامه‌ها با `302` به صفحه ورود هدایت می‌کنند و برخی با `200` فرم را نشان می‌دهند. یک معیار واحد مانند `is_logged_in` هر دو را می‌گیرد.

## چرا در طول نشست به یک IP یکسان نیاز است؟

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

در اسکریپتی که از پروکسی استفاده می‌کند این یعنی: **استخر چرخشی در کارهایی که ورود می‌خواهند نشست را خراب می‌کند.** پروکسی چرخشی IP خروجی را در هر درخواست یا در فاصله‌های کوتاه عوض می‌کند؛ نشانی‌ای که با آن وارد شده‌اید با نشانی‌ای که گزارش را از آن می‌گیرید فرق می‌کند و برنامه شما را نمی‌شناسد. چگونگی کار چرخش و اینکه برای کدام کارها درست است را در [چرخش IP چیست و چگونه کار می‌کند؟](/fa/blog/ip-rotation-explained) نوشته‌ایم.

ساختار درست یکی از دو گزینه است:

- با [پروکسی با نشست ثابت](https://proxynet.io/fa/sticky-proxy) برای مدتی که خودتان تعیین می‌کنید، بین 1 تا 60 دقیقه، روی یک IP خروجی می‌مانید. اگر نشست کوتاه است و پس از پایان کار اتصال را رها می‌کنید، همین کافی است.
- با [پروکسی ISP](https://proxynet.io/fa/static-isp-residential-proxy) نشانی از اجرایی به اجرای دیگر هم ثابت می‌ماند. در سامانه‌هایی که دسترسی به پنل را به IPهای مشخص محدود می‌کنند تنها راه همین است، چون نشانی را یک بار به طرف مقابل اعلام می‌کنید و در فهرست او ثبت می‌شود.

سناریوی ثبت نشانی در فهرست مجاز یک API در [IP ثابت برای API: خطای مجوز IP چگونه رفع می‌شود؟](/fa/blog/static-ip-for-api-access) آمده است و دلیل عمومی IP ثابت در بخش «IP چسبان آنجا که نشست لازم است» از [وب اسکرپینگ بدون مسدود شدن: راهنمای عملی](/fa/blog/web-scraping-without-getting-blocked).

جلوی یک برداشت نادرست را بگیریم: IP ثابت ابزاری برای گذشتن از کنترل امنیتی نیست. کاری که می‌کند این است که ترافیک خود شما را در حسابی که از پیش برای آن مجاز هستید یکدست نشان دهد. اگر حساب از آن شما نباشد، IP ثابت هم دسترسی قانونی نمی‌سازد.

## محدودیت نرخ: مؤدب نگه داشتن اسکریپت

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

- **با یک نخ شروع کنید.** برای روزی یک گزارش موازی‌سازی نسازید.
- **میان درخواست‌ها مکث بگذارید.** مقدار `DELAY_SECONDS` در اسکریپت بالا ساده‌ترین شکل همین کار است.
- **وقتی `429` دیدید بایستید.** سرآیند `Retry-After` در پاسخ می‌گوید چقدر باید صبر کنید. کد کامل در [⁦429 Too Many Requests⁩ چیست؟ خطای Rate Limit و رفع آن](/fa/blog/http-429-too-many-requests) و [کدهای وضعیت HTTP در وب اسکرپینگ: 403، 407، 429، 503](/fa/blog/http-status-codes-web-scraping) آمده است.
- **خودتان را معرفی کنید.** نوشتن نام اسکریپت و یک نشانی تماس در فیلد `User-Agent` کاری می‌کند که مدیر سایت بتواند با شما حرف بزند؛ کارکرد این فیلد در [User-Agent چیست و چگونه آن را ببینیم و تغییر دهیم؟](/fa/blog/what-is-user-agent) آمده است.

## اگر ورود با JavaScript انجام می‌شود: storage_state در Playwright

در برخی برنامه‌ها فرم ورود یک `POST` کلاسیک نمی‌فرستد: JavaScript فیلدها را می‌خواند، مرورگر توکن را می‌سازد و پاسخ با یک فراخوانی API می‌آید. در چنین صفحه‌ای فرمی که با `requests` فرستاده شود بی‌صدا شکست می‌خورد. اینجا باید یک مرورگر واقعی اجرا شود.

Playwright می‌تواند وضعیت نشست را در یک فایل JSON بنویسد. بر پایه [مستندات احراز هویت Playwright](https://playwright.dev/python/docs/auth) این فایل کوکی‌ها و `localStorage` را با هم حمل می‌کند؛ یعنی در برنامه‌هایی که نشست را به جای کوکی در `localStorage` نگه می‌دارند هم به کار می‌آید. اگر برنامه توکن را در `IndexedDB` ذخیره می‌کند، باید جداگانه آن را بخواهید: `context.storage_state(path=..., indexed_db=True)`.

```python
import os
from pathlib import Path

from playwright.sync_api import sync_playwright

BASE_URL = "https://quotes.toscrape.com"
STATE_FILE = Path("storage_state.json")
PROXY = {"server": "http://pr.proxynet.io:8000", "username": "user", "password": "pass"}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    if STATE_FILE.exists():
        context = browser.new_context(storage_state=STATE_FILE)  # open with the saved session
    else:
        context = browser.new_context()
    page = context.new_page()
    page.goto(BASE_URL)

    if page.locator('a[href="/logout"]').count() == 0:
        page.goto(f"{BASE_URL}/login")
        page.fill("#username", os.environ["SITE_USERNAME"])
        page.fill("#password", os.environ["SITE_PASSWORD"])
        page.click('input[type="submit"]')
        page.wait_for_selector('a[href="/logout"]')  # proof of the login
        context.storage_state(path=STATE_FILE)  # cookies and localStorage in one file
        print("Logged in, state saved")
    else:
        print("Session open with the saved state")

    browser.close()
```

اجرای نخست وارد می‌شود و فایل را می‌نویسد، اجرای دوم گام ورود را اصلاً نمی‌بیند. تنظیم پروکسی در Playwright و نصب مرورگر را با جزئیات در [Playwright چیست و چگونه با پروکسی استفاده می‌شود؟](/fa/blog/playwright-proxy) نوشته‌ایم؛ برای معادل آن در Selenium به [Selenium با پروکسی: راه‌اندازی در Python و Java](/fa/blog/selenium) نگاه کنید.

الگوی ترکیبی از همین‌جا بیرون می‌آید: یک بار با مرورگر وارد می‌شوید و `storage_state.json` می‌سازید، سپس کوکی‌ها را به یک `requests.Session` منتقل می‌کنید و داده را از راه ارزان می‌گیرید.

```python
import json
from pathlib import Path

import requests

state = json.loads(Path("storage_state.json").read_text(encoding="utf-8"))
session = requests.Session()
for c in state["cookies"]:
    session.cookies.set(c["name"], c["value"], domain=c["domain"], path=c["path"])
```

این الگو یک شرط دارد: هم مرورگر و هم کلاینت HTTP پس از آن باید از یک IP خروجی بیرون بروند. اگر به این دو پروکسی متفاوت بدهید، نشست در نخستین درخواست می‌افتد. اینکه کدام خودکارسازی مرورگر به کدام کار می‌خورد را در [تفاوت Playwright و Selenium: کدام را انتخاب کنیم؟](/fa/blog/playwright-vs-selenium) مقایسه کرده‌ایم.

## کاربردها

- **گرفتن گزارش روزانه از پنل خودتان.** نشانی پشت دکمه برون‌بری بیشتر وقت‌ها یک پیوند ساده به فایل است؛ تا وقتی کوکی نشست پابرجاست، `requests` آن را دانلود می‌کند. ساختار عمومی در صفحه [استخراج داده](/fa/data-scraping) آمده است.
- **پایش یک جریان پشت ورود.** برای بازبینی گام‌های ورود و سبد خرید برنامه خودتان پس از هر استقرار؛ ساختار آن در صفحه [تست اپلیکیشن](/fa/app-testing) هست.
- **انتقال داده از سامانه مشتری.** با اجازه کتبی و یک IP خروجی ثابت، تا طرف مقابل بتواند آن نشانی را در فهرست خود ثبت کند.
- **جمع‌آوری داده از فهرست چندصفحه‌ای.** پس از باز شدن نشست، کار یک صفحه‌بندی معمولی است؛ الگوی آن را در [صفحه‌بندی چیست و در اسکرپینگ چگونه پیمایش می‌شود؟](/fa/blog/pagination-web-scraping) نوشته‌ایم.

## خطاهای رایج و عیب‌یابی

- **هدایت بی‌پایان به صفحه ورود.** اگر درخواست با `302` به `/login` بازمی‌گردد، کوکی یا اصلاً فرستاده نمی‌شود یا سمت سرور نامعتبر است. نخست محتوای `session.cookies` را ببینید، سپس فیلد `Domain` کوکی را با نشانی مقصد بسنجید.
- **استفاده از `requests.get` به جای `Session`.** فراخوانی `requests.get()` در سطح ماژول در هر بار یک اتصال تازه و ظرف کوکی خالی باز می‌کند. در جریان ورود کار نمی‌کند.
- **تأیید ورود با نگاه به کد وضعیت.** با رمز نادرست هم ممکن است `200` بیاید. دنبال گواهی در محتوا بگردید.
- **گذاشتن توکن CSRF در کد.** یک روز کار می‌کند و روز بعد خطای توکن نامعتبر می‌دهد.
- **عوض کردن IP در میانه نشست.** ورود با استخر چرخشی و سپس گرفتن داده، شایع‌ترین دلیل افتادن نشست است.
- **افتادن در حلقه پس از ورود ناموفق.** اگر رمز نادرست باشد تکرار آن را درست نمی‌کند؛ در برخی سامانه‌ها حساب را قفل می‌کند. اسکریپت باید در نخستین شکست بایستد.

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

| وضعیت | کاری که باید کرد |
|---|---|
| سایت API رسمی دارد | اصلاً سراغ خودکارسازی ورود نروید، از کلید API استفاده کنید |
| فرم HTML ساده است و فیلدها در صفحه‌اند | با `requests.Session` وارد شوید و کوکی‌ها را در فایل ذخیره کنید |
| در فرم فیلد پنهانی مانند `csrf_token` هست | در هر اجرا از صفحه بخوانید، در کد نگذارید |
| فرم با JavaScript فرستاده می‌شود | با Playwright وارد شوید و فایل `storage_state` را نگه دارید |
| ورود پیچیده است و صفحه‌های داده ساده | ترکیبی: با مرورگر وارد شوید و کوکی‌ها را به `requests` ببرید |
| نشست در میانه کار بسته می‌شود | IP خروجی را ثابت کنید: [پروکسی با نشست ثابت](https://proxynet.io/fa/sticky-proxy) |
| پنل تنها به IPهای مشخص باز است | نشانی ثابت با [پروکسی ISP](https://proxynet.io/fa/static-isp-residential-proxy) |
| حساب تأیید دومرحله‌ای دارد | کلید API یا رمز اختصاصی برنامه بخواهید، از کنترل رد نشوید |
| حساب از آن شما نیست | بایستید؛ از دارنده آن اجازه کتبی بگیرید |

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

### تفاوت نشست و کوکی چیست؟

کوکی تکه داده کوچکی است که سرور در مرورگر یا کلاینت شما ذخیره می‌کند و در هر درخواست بازفرستاده می‌شود. نشست وضعیتی است که آن کوکی به آن اشاره می‌کند: اینکه شما کیستید، از چه زمانی وارد شده‌اید و چه دسترسی‌هایی دارید. در سمت Python شیء `requests.Session` این دو را به هم می‌پیوندد؛ کوکی‌ها را نگه می‌دارد و میان درخواست‌ها می‌برد.

### وارد شدم ولی درخواست بعدی دوباره به صفحه ورود می‌افتد، چرا؟

سه دلیل رایج دارد. نخست اینکه درخواست ورود را بیرون از `Session` فرستاده‌اید و کوکی گم شده است. دوم اینکه فیلد `Domain` یا `Path` کوکی با نشانی درخواست شما نمی‌خواند. سوم اینکه برنامه نشست را به IP گره زده و به سبب پروکسی چرخشی، درخواست دوم از نشانی دیگری آمده است.

### می‌توانم توکن CSRF را یک بار بگیرم و نگه دارم؟

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

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

تعیین‌کننده این نیست که سایت رمز دارد، بلکه این است که شما مجوز دسترسی به آن حساب را دارید یا نه. گرفتن داده خودتان از حساب خودتان استفاده‌ای عادی است؛ ورود با اطلاعات کاربری شخص دیگر دسترسی غیرمجاز است و در هر کشوری جرم به شمار می‌رود. اگر شرایط استفاده دسترسی خودکار را ممنوع کرده باشد، حتی در حساب خودتان هم خلاف قرارداد رفتار کرده‌اید. جزئیات در [آیا اسکرپینگ وب قانونی است؟ یک نمای کلی](/fa/blog/is-data-web-scraping-legal) آمده است.

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

بسته به برنامه فرق می‌کند. کوکی‌ای که `Max-Age` یا `Expires` ندارد با بسته شدن کلاینت پایان می‌یابد. آن‌هایی که مدت دارند از چند ساعت تا چند هفته عمر می‌کنند، اما وقتی سرور رکورد سمت خودش را پاک کند، نشست پایان می‌یابد حتی اگر کوکی در دست شما باشد. به همین دلیل در اسکریپت به جای بررسی «منقضی شده است؟» بررسی «نشست هنوز باز است؟» انجام می‌شود.

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

بهتر است عوض نکنید. اگر IP بازکننده نشست با IP خروجی درخواست‌های بعدی فرق کند، برنامه ممکن است نشست را ببندد یا تأیید بیشتری بخواهد. اگر در یک کار هم مرورگر و هم کلاینت HTTP به کار می‌برید، به هر دو یک پروکسی بدهید.

## خلاصه

هسته فنی کاری که پشت یک ورود است کوچک می‌ماند: `requests.Session` کوکی‌ها را حمل می‌کند، توکن CSRF خوانده‌شده از صفحه ورود به فرم افزوده می‌شود، نتیجه نه با کد وضعیت بلکه با محتوای صفحه تأیید می‌شود و نشست در یک فایل ذخیره و به اجرای بعدی سپرده می‌شود. در سایت‌هایی که فرم را با JavaScript می‌فرستند، همین کار را فایل `storage_state` در Playwright انجام می‌دهد. برای اینکه نشست نیفتد، IP خروجی در طول آن نباید عوض شود؛ برای این کار [پروکسی با نشست ثابت](https://proxynet.io/fa/sticky-proxy) یا نشانی ثابت به کار می‌رود. پیش‌شرط هم تغییر نمی‌کند: حساب باید از آن شما باشد یا اجازه کتبی دارنده‌اش را داشته باشید، و اگر API رسمی هست نخست همان آزموده شود. گونه‌های مناسب پروکسی را در [خدمات پروکسی](/fa/proxy) ما می‌یابید.
