هر روز صبح با دست وارد پنل شرکت خودتان میشوید و یک گزارش دانلود میکنید. پنل دکمه برونبری دارد، API رسمی ندارد و همان پنج کلیک هر روز تکرار میشود. وقتی بخواهید این کار را به یک اسکریپت Python بسپارید، نخستین دیوار صفحه ورود است: صفحهای که requests.get() دانلود میکند گزارش نیست، فرم ورود است. HTTP پروتکلی بدون حالت است و سرور تنها با کوکیای که به هر درخواست میافزایید شما را میشناسد.
در این نوشته توضیح میدهیم کوکی و نشست چیست، چرا توکن CSRF در هر اجرا دوباره خوانده میشود و یک جریان ورود با requests.Session چگونه ساخته میشود. سپس به ذخیره نشست روی دیسک، تازه کردن آن پس از انقضا، بیرون بردن اطلاعات ورود از کد و ماندن روی یک IP خروجی در تمام مدت نشست میپردازیم. در پایان روش storage_state در Playwright برای سایتهایی میآید که فرم را با JavaScript میفرستند. نمونههای کد روی quotes.toscrape.com آزموده شدهاند.
این نوشته به کدام ورودها مربوط است؟
هر چه اینجا میآید بر یک فرض استوار است: حسابی که در آن وارد میشوید از آن شماست، یا دارنده حساب به شما اجازه کتبی داده است. پنل مدیریت شرکت خودتان، حساب فروشگاهی خودتان، سامانه مشتریای که کتباً خواسته است «این گزارش را هر روز برای ما بگیرید». آنچه بیرون از این دایره باشد موضوع این نوشته نیست.
پیش از رفتن سراغ کد، سه پرسش:
- آیا API رسمی یا برونبری وجود دارد؟ اگر هست، رابط کاربری را خودکار نکنید. کلید API پایدار است و با تغییر رابط نمیشکند؛ خودکارسازی ورود راهی است که وقتی به داده دسترسی دیگری ندارید به آن پناه میبرید. چارچوب حقوقی را در آیا اسکرپینگ وب قانونی است؟ یک نمای کلی بررسی کردهایم.
- شرایط استفاده سایت چه میگوید؟ اگر بندی دسترسی خودکار را ممنوع کرده باشد، حساب شما میتواند معلق شود. ممکن بودن از نظر فنی به معنای مجاز بودن نیست.
- اجازه شما کتبی است؟ در کار مشتری، تأیید شفاهی کافی نیست. با کدام حساب، از کدام صفحهها و با چه تناوبی داده میگیرید باید در قرارداد یا ایمیل نوشته شده باشد.
چهار چیز در این نوشته آگاهانه توضیح داده نمیشود:
- آزمودن رمز عبور در کار نیست. کدی که فهرستی را یکییکی در فیلد فرم مینویسد (credential stuffing) یا کدی که ترکیب میسازد (brute force) اینجا نمیآید. این کار را نکنید: دسترسی غیرمجاز به حساب دیگران در هر کشوری جرم است و ممکن بودن فنی آن چیزی را عوض نمیکند.- گذشتن از تأیید دومرحلهای و CAPTCHA در کار نیست. این کنترلها برای محافظت از حساب گذاشته شدهاند؛ تلاش برای رد شدن از آنها با یک اسکریپت امنیت همان حسابی را که با آن کار میکنید پایین میآورد. اگر روی حساب خودتان تأیید دومرحلهای فعال است، راه درست کلید API یا رمز اختصاصی برنامه از سوی سرویسدهنده است؛ اگر هیچکدام نباشد یعنی آن کار خودکار نخواهد شد.
- حساب دیگران در کار نیست. «حساب دوستم»، «حساب کسی که از شرکت رفته» و اطلاعات ورودی که در اینترنت منتشر شده هم در همین دستهاند.
- دزدیدن یا جابهجا کردن کوکی نشست در کار نیست. نشستی که با کوکی کپیشده از مرورگر شخص دیگر باز شود، جعل هویت اوست. فایلهای کوکی این نوشته تنها با ورود خود شما ساخته میشوند و تنها روی رایانه خودتان میمانند.
یک تفکیک هم به سبب شباهت نامها لازم است: احراز هویت در برابر سرور پروکسی (user:pass یا مجوز IP) با ورود به سایت مقصد دو کار جداست. اولی را در احراز هویت پروکسی: نام کاربری و رمز یا لیست سفید IP نوشتهایم؛ موضوع این نوشته دومی است.
کوکی و نشست چیست؟
HTTP بدون حالت است: سرور هیچ چیزی را که دو درخواست را به هم پیوند بزند به یاد نمیآورد. این شکاف را کوکیها پر میکنند. سرور در پاسخ خود سرآیند Set-Cookie میگذارد، کلاینت این مقدار را نگه میدارد و در درخواستهای بعدی به همان دامنه با سرآیند Cookie بازمیفرستد. همه فیلدهای این سرآیند یکییکی در صفحه Set-Cookie در MDN توضیح داده شدهاند.
«نشست» همان چیزی است که به این کوکی معنا میدهد. دو طرح رایج است:
- نشست سمت سرور. در کوکی تنها یک شناسه تصادفی (
sessionid،PHPSESSID،JSESSIONID) حمل میشود؛ اینکه کاربر کیست در رکوردی روی سرور نگهداری میشود. اگر سرور آن رکورد را پاک کند، نشست پایان مییابد حتی اگر کوکی در دست شما باشد. - کوکی امضاشده. اطلاعات کاربر درون خود کوکی است و سرور آن را با یک کلید محرمانه امضا میکند. کوکی پیشفرض
sessionدر Flask چنین است.
اینکه کوکی چقدر عمر میکند با Expires و Max-Age تعیین میشود. بخش 4.1.2.2 از RFC 6265 میگوید اگر کوکی نه 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 «توکن همگامساز» نامیده میشود: سرور در هر صفحه فرم، مقداری تصادفی و وابسته به همان نشست را به شکل یک فیلد پنهان کار میگذارد. هنگام فرستادن فرم، این مقدار باید با همتای خود در کوکی نشست بخواند. فرمی در سایت دیگر نمیتواند توکن نشست شما را بداند، پس تطابق برقرار نمیشود.
این موضوع برای کسی که اسکریپت مینویسد سه نتیجه دارد:
- توکن ثابت نیست. اگر یک بار آن را از مرورگر کپی کنید و در کد بگذارید، فردا کار نمیکند. در هر اجرا باید صفحه ورود را دانلود کنید و فیلد را از همانجا بخوانید.
- توکن به نشست وابسته است. فرستادن توکن در یک درخواست و فرم در اتصالی دیگر بیفایده است. هر دو باید از یک شیء
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 چگونه وارد میشویم؟
پنج گام هست که ترتیبشان اهمیت دارد: چهار گام نخست خودِ ورود است و گام پنجم برای اجراهای بعدی.
- صفحه ورود را با
GETبگیرید. این پاسخ دو چیز میآورد: کوکی نشستی که هنوز ناشناس است و توکن CSRF درون فرم.Sessionکوکی را در ظرف خودش میگذارد. - فیلدهای فرم را واقعاً بخوانید. فرم را در مرورگر پر کنید و بفرستید و در زبانه شبکه ابزار توسعهدهنده بدنه درخواست را ببینید. نام فیلدها حدس زده نمیشود، از همانجا کپی میشود؛ اگر فیلد پنهانی مانند
nextیاredirectهست آن را هم بفرستید. - درخواست
POSTرا با همانSessionبفرستید. کوکی خودبهخود افزوده میشود. اگر فیلدactionفرم مسیر دیگری را نشان میدهد، درخواست را به همانجا بفرستید. - ورود را از محتوا تأیید کنید. کد وضعیت گمراهکننده است: بسیاری از برنامهها با رمز نادرست هم
200میدهند و فرم را با یک پیام خطا دوباره نشان میدهند. معیار عنصری است که تنها هنگام باز بودن نشست دیده میشود: پیوند خروج، نام کاربری، منوی حساب. - کوکیها را ذخیره کنید. اجرای بعدی گام ورود را رد میکند.
بر پایه بخش Session Objects در مستندات Requests، شیء Session افزون بر نگه داشتن کوکیها در طول عمر نمونه، برای درخواستهایی که به یک میزبان میروند اتصال TCP را هم دوباره به کار میگیرد. این الگو در هر سه کتابخانه مقایسه HTTPX، Requests و AIOHTTP یکسان است و تنها نام کلاس فرق میکند.
نمونه کارا: quotes.toscrape.com
کد زیر روی quotes.toscrape.com/login آزموده شده است. اینجا سایتی آموزشی و باز است که برای تمرین اسکرپینگ برپا شده و هر نام کاربری و هر رمزی را که وارد کنید میپذیرد؛ هیچ احراز هویتی انجام نمیدهد و تنها جریان کار را تقلید میکند. دیدن این سازوکار نخست در همینجا جلوی ساختن رشتهای از ورودهای ناموفق روی حساب خودتان را میگیرد.
نخست جریان اصلی در چهار گام:
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()))پیش از اجرا اطلاعات ورود را در محیط بگذارید؛ نوشتن رمز در کد یعنی نوشتن آن در تاریخچه نسخهها:
export SITE_USERNAME="username"
export SITE_PASSWORD="password"
python login.pyدر Windows PowerShell شکل $env:SITE_USERNAME = "username" به کار میرود. اینکه متغیرهای محیطی برای ابزارهای خط فرمان چگونه ماندگار میشوند را در استفاده از پروکسی در wget: دستورها و نمونهها نوشتهایم.
نشست چگونه روی دیسک ذخیره و کی تازه میشود؟
ورود دوباره در هر اجرا، گزارش امنیتی حساب شما را با رکوردهای بیمورد پر میکند؛ در برخی برنامهها ورودهای پیاپی در زمان کوتاه تأیید بیشتری را هم برمیانگیزد. راهحل این است که ظرف کوکی را در یک فایل بنویسید و در اجرای بعدی دوباره بارگذاری کنید.
اسکریپت کامل زیر همین کار را میکند: اگر کوکی ذخیرهشده هست بارگذاری میکند، اگر نیست وارد میشود، در هر درخواست باز بودن نشست را بررسی میکند و اگر نشست افتاده باشد یک بار دوباره وارد میشود. یکباره بودن مهم است: حلقه بدون شرط، وقتی مشکل از اطلاعات ورود باشد، به تلاش بیپایان برای ورود تبدیل میشود.
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 چیست و چگونه کار میکند؟ نوشتهایم.
ساختار درست یکی از دو گزینه است:
- با پروکسی با نشست ثابت برای مدتی که خودتان تعیین میکنید، بین 1 تا 60 دقیقه، روی یک IP خروجی میمانید. اگر نشست کوتاه است و پس از پایان کار اتصال را رها میکنید، همین کافی است.
- با پروکسی ISP نشانی از اجرایی به اجرای دیگر هم ثابت میماند. در سامانههایی که دسترسی به پنل را به IPهای مشخص محدود میکنند تنها راه همین است، چون نشانی را یک بار به طرف مقابل اعلام میکنید و در فهرست او ثبت میشود.
سناریوی ثبت نشانی در فهرست مجاز یک API در IP ثابت برای API: خطای مجوز IP چگونه رفع میشود؟ آمده است و دلیل عمومی IP ثابت در بخش «IP چسبان آنجا که نشست لازم است» از وب اسکرپینگ بدون مسدود شدن: راهنمای عملی.
جلوی یک برداشت نادرست را بگیریم: IP ثابت ابزاری برای گذشتن از کنترل امنیتی نیست. کاری که میکند این است که ترافیک خود شما را در حسابی که از پیش برای آن مجاز هستید یکدست نشان دهد. اگر حساب از آن شما نباشد، IP ثابت هم دسترسی قانونی نمیسازد.
محدودیت نرخ: مؤدب نگه داشتن اسکریپت
اسکریپتی که وارد شده از اسکریپت ناشناس آشکارتر است: درخواستهای شما دیگر به یک نشانی IP نوشته نمیشود، مستقیم به حساب شما نوشته میشود. رعایت محدودیت نرخ اینجا یک ترجیح فنی نیست، بخشی از محافظت از حساب شماست.
- با یک نخ شروع کنید. برای روزی یک گزارش موازیسازی نسازید.
- میان درخواستها مکث بگذارید. مقدار
DELAY_SECONDSدر اسکریپت بالا سادهترین شکل همین کار است. - وقتی
429دیدید بایستید. سرآیندRetry-Afterدر پاسخ میگوید چقدر باید صبر کنید. کد کامل در 429 Too Many Requests چیست؟ خطای Rate Limit و رفع آن و کدهای وضعیت HTTP در وب اسکرپینگ: 403، 407، 429، 503 آمده است. - خودتان را معرفی کنید. نوشتن نام اسکریپت و یک نشانی تماس در فیلد
User-Agentکاری میکند که مدیر سایت بتواند با شما حرف بزند؛ کارکرد این فیلد در User-Agent چیست و چگونه آن را ببینیم و تغییر دهیم؟ آمده است.
اگر ورود با JavaScript انجام میشود: storage_state در Playwright
در برخی برنامهها فرم ورود یک POST کلاسیک نمیفرستد: JavaScript فیلدها را میخواند، مرورگر توکن را میسازد و پاسخ با یک فراخوانی API میآید. در چنین صفحهای فرمی که با requests فرستاده شود بیصدا شکست میخورد. اینجا باید یک مرورگر واقعی اجرا شود.
Playwright میتواند وضعیت نشست را در یک فایل JSON بنویسد. بر پایه مستندات احراز هویت Playwright این فایل کوکیها و localStorage را با هم حمل میکند؛ یعنی در برنامههایی که نشست را به جای کوکی در localStorage نگه میدارند هم به کار میآید. اگر برنامه توکن را در IndexedDB ذخیره میکند، باید جداگانه آن را بخواهید: context.storage_state(path=..., indexed_db=True).
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 چیست و چگونه با پروکسی استفاده میشود؟ نوشتهایم؛ برای معادل آن در Selenium به Selenium با پروکسی: راهاندازی در Python و Java نگاه کنید.
الگوی ترکیبی از همینجا بیرون میآید: یک بار با مرورگر وارد میشوید و storage_state.json میسازید، سپس کوکیها را به یک requests.Session منتقل میکنید و داده را از راه ارزان میگیرید.
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: کدام را انتخاب کنیم؟ مقایسه کردهایم.
کاربردها
- گرفتن گزارش روزانه از پنل خودتان. نشانی پشت دکمه برونبری بیشتر وقتها یک پیوند ساده به فایل است؛ تا وقتی کوکی نشست پابرجاست،
requestsآن را دانلود میکند. ساختار عمومی در صفحه استخراج داده آمده است. - پایش یک جریان پشت ورود. برای بازبینی گامهای ورود و سبد خرید برنامه خودتان پس از هر استقرار؛ ساختار آن در صفحه تست اپلیکیشن هست.
- انتقال داده از سامانه مشتری. با اجازه کتبی و یک IP خروجی ثابت، تا طرف مقابل بتواند آن نشانی را در فهرست خود ثبت کند.
- جمعآوری داده از فهرست چندصفحهای. پس از باز شدن نشست، کار یک صفحهبندی معمولی است؛ الگوی آن را در صفحهبندی چیست و در اسکرپینگ چگونه پیمایش میشود؟ نوشتهایم.
خطاهای رایج و عیبیابی
- هدایت بیپایان به صفحه ورود. اگر درخواست با
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 خروجی را ثابت کنید: پروکسی با نشست ثابت |
| پنل تنها به IPهای مشخص باز است | نشانی ثابت با پروکسی ISP |
| حساب تأیید دومرحلهای دارد | کلید API یا رمز اختصاصی برنامه بخواهید، از کنترل رد نشوید |
| حساب از آن شما نیست | بایستید؛ از دارنده آن اجازه کتبی بگیرید |
پرسشهای متداول
تفاوت نشست و کوکی چیست؟
کوکی تکه داده کوچکی است که سرور در مرورگر یا کلاینت شما ذخیره میکند و در هر درخواست بازفرستاده میشود. نشست وضعیتی است که آن کوکی به آن اشاره میکند: اینکه شما کیستید، از چه زمانی وارد شدهاید و چه دسترسیهایی دارید. در سمت Python شیء requests.Session این دو را به هم میپیوندد؛ کوکیها را نگه میدارد و میان درخواستها میبرد.
وارد شدم ولی درخواست بعدی دوباره به صفحه ورود میافتد، چرا؟
سه دلیل رایج دارد. نخست اینکه درخواست ورود را بیرون از Session فرستادهاید و کوکی گم شده است. دوم اینکه فیلد Domain یا Path کوکی با نشانی درخواست شما نمیخواند. سوم اینکه برنامه نشست را به IP گره زده و به سبب پروکسی چرخشی، درخواست دوم از نشانی دیگری آمده است.
میتوانم توکن CSRF را یک بار بگیرم و نگه دارم؟
نه. توکن به نشست وابسته است و با تازه شدن نشست عوض میشود. در هر اجرا باید صفحه ورود را دانلود کنید و فیلد را از همانجا بخوانید. آنچه نگه داشته میشود توکن نیست، کوکی نشستی است که پس از ورود میآید.
گرفتن داده از سایتی که رمز میخواهد جرم است؟
تعیینکننده این نیست که سایت رمز دارد، بلکه این است که شما مجوز دسترسی به آن حساب را دارید یا نه. گرفتن داده خودتان از حساب خودتان استفادهای عادی است؛ ورود با اطلاعات کاربری شخص دیگر دسترسی غیرمجاز است و در هر کشوری جرم به شمار میرود. اگر شرایط استفاده دسترسی خودکار را ممنوع کرده باشد، حتی در حساب خودتان هم خلاف قرارداد رفتار کردهاید. جزئیات در آیا اسکرپینگ وب قانونی است؟ یک نمای کلی آمده است.
کوکی نشست چقدر معتبر میماند؟
بسته به برنامه فرق میکند. کوکیای که Max-Age یا Expires ندارد با بسته شدن کلاینت پایان مییابد. آنهایی که مدت دارند از چند ساعت تا چند هفته عمر میکنند، اما وقتی سرور رکورد سمت خودش را پاک کند، نشست پایان مییابد حتی اگر کوکی در دست شما باشد. به همین دلیل در اسکریپت به جای بررسی «منقضی شده است؟» بررسی «نشست هنوز باز است؟» انجام میشود.
پس از ورود میتوانم پروکسی را عوض کنم؟
بهتر است عوض نکنید. اگر IP بازکننده نشست با IP خروجی درخواستهای بعدی فرق کند، برنامه ممکن است نشست را ببندد یا تأیید بیشتری بخواهد. اگر در یک کار هم مرورگر و هم کلاینت HTTP به کار میبرید، به هر دو یک پروکسی بدهید.
خلاصه
هسته فنی کاری که پشت یک ورود است کوچک میماند: requests.Session کوکیها را حمل میکند، توکن CSRF خواندهشده از صفحه ورود به فرم افزوده میشود، نتیجه نه با کد وضعیت بلکه با محتوای صفحه تأیید میشود و نشست در یک فایل ذخیره و به اجرای بعدی سپرده میشود. در سایتهایی که فرم را با JavaScript میفرستند، همین کار را فایل storage_state در Playwright انجام میدهد. برای اینکه نشست نیفتد، IP خروجی در طول آن نباید عوض شود؛ برای این کار پروکسی با نشست ثابت یا نشانی ثابت به کار میرود. پیششرط هم تغییر نمیکند: حساب باید از آن شما باشد یا اجازه کتبی دارندهاش را داشته باشید، و اگر API رسمی هست نخست همان آزموده شود. گونههای مناسب پروکسی را در خدمات پروکسی ما مییابید.




