تقریباً هر کسی که اسکرپینگ را آغاز میکند با یک لحظه یکسان روبهرو میشود: محصول، قیمت و نظرها در مرورگر جلوی چشمتان است، اما وقتی همان آدرس را در پایتون با requests میگیرید، انتخابگرها چیزی نمییابند. در HTML صفحه بهجای فهرست محصول، عبارت «در حال بارگذاری» و چند تگ <script> میبینید. مشکل در کد شما نیست؛ صفحه پویا است و جاوااسکریپتی که در مرورگر اجرا میشود محتوا را بعداً میآورد.
در این نوشته تفاوت صفحه ایستا و پویا، شیوه تشخیص پویا بودن صفحه در چند دقیقه و اینکه مرورگر headless چیست را توضیح میدهیم. بیشترین تمرکز ما بر این است که در بیشتر حالتها چگونه بدون مرورگر headless به داده برسید: با یافتن درخواست API که صفحه در پسزمینه میفرستد و خواندن JSON جاسازیشده در HTML. وقتی مرورگر واقعاً لازم باشد، راهبردهای درست انتظار در Playwright و راههای کاهش هزینه را هم نشان میدهیم. نمونهها را روی صفحه آزمون محلی که محتوایش را با جاوااسکریپت بارگذاری میکرد اجرا کردیم.
صفحه ایستا چیست؟
در صفحه ایستا هر چه مرورگر نشان میدهد در HTML نخستین پاسخ سرور وجود دارد. سرور ممکن است این HTML را از فایل آماده بخواند یا در هر درخواست از پایگاه داده بسازد؛ آنچه برای اسکرپینگ اهمیت دارد این است که محتوا هنگام رسیدن به مرورگر درون HTML باشد.
برای گرفتن داده از صفحه ایستا به مرورگر نیاز ندارید:
import requests
from bs4 import BeautifulSoup
html = requests.get("https://example.com/deals", timeout=20).text
cards = BeautifulSoup(html, "html.parser").select("div.product")
print("Product cards found:", len(cards))سایتهای خبری، وبلاگها، بسیاری از سایتهای سازمانی و فروشگاههای اینترنتی که در سمت سرور رندر میشوند در این گروهاند.
صفحه پویا (بارگذاریشده با جاوااسکریپت) چیست؟
در صفحه پویا نخستین پاسخ سرور یک اسکلت است: چیدمان صفحه، ظرف خالی فهرست و فایلهای جاوااسکریپت. محتوا بعداً در این گامها میآید:
- مرورگر اسکلت HTML را میگیرد و جانگاهی مانند «در حال بارگذاری» روی صفحه میکشد.
- فایلهای جاوااسکریپت دانلود و اجرا میشوند. اغلب یک فریمورک مانند React یا Vue.
- جاوااسکریپت درخواست API میفرستد. مثلاً با
fetchبه/api/products?category=headphones&page=1. - سرور داده را بهصورت JSON برمیگرداند. نام محصول، قیمت، اطلاعات موجودی.
- جاوااسکریپت از این JSON، HTML میسازد و در صفحه جا میدهد. کارتهای محصول تازه در این گام دیده میشوند.
- پیمایش یا کلیک درخواستهای تازه میسازد. پیمایش بیپایان، دکمه «بیشتر ببینید»، فیلترها.
کلاینت HTTP مانند requests در پایتون فقط گام نخست را انجام میدهد و جاوااسکریپت اجرا نمیکند. به همین دلیل HTML دریافتی کارت محصول ندارد. در صفحه آزمون محلی ما، کد requests بالا صفر کارت یافت؛ همان صفحه در مرورگر دو محصول نشان میداد.
ساختار ترکیبی هم وجود دارد: فریمورکهایی مانند Next.js و Nuxt وضعیت نخست صفحه را در سرور رندر میکنند و همان داده را بهصورت بلوک JSON هم در HTML جاسازی میکنند تا جاوااسکریپت به کار ببرد. در این صفحهها داده بدون اجرای جاوااسکریپت در HTML هست، اما گاهی درون تگ <script> است نه در کارتهای قابلدیدن.
چگونه بفهمیم صفحه پویاست؟
چند دقیقه تشخیص پیش از راهاندازی مرورگر headless به انتخاب راه درست کمک میکند:
- کد منبع را ببینید.
Ctrl + Uدر صفحه (در macOSCmd + Option + U) HTML خامی را که سرور فرستاده نشان میدهد. نام محصول یا قیمتی را که در صفحه میبینید آنجا جستوجو کنید. اگر پیدا شد، صفحه احتمالاً ایستاست. - با پنل Elements مقایسه کنید. پنل Elements ابزارهای توسعهدهنده صفحه را پس از اجرای جاوااسکریپت نشان میدهد. اگر متن در Elements هست اما در کد منبع نیست، محتوا با جاوااسکریپت آمده است.
- جاوااسکریپت را خاموش کنید و دوباره بارگذاری کنید. در ابزارهای توسعهدهنده کروم منوی فرمان را با
Ctrl + Shift + Pباز کنید، Disable JavaScript را بنویسید و صفحه را دوباره بارگذاری کنید. اگر محتوا ناپدید شد، صفحه پویاست. - با کد بررسی کنید. در HTML دریافتی با
requestsمتنی را که انتظار دارید جستوجو کنید. اگر نبود و HTML بسیار کوتاه بود، اسکلت گرفتهاید. - به فیلتر Fetch/XHR در پنل Network نگاه کنید. اگر با بارگذاری دوباره صفحه در این فیلتر پاسخ JSON دیدید، داده احتمالاً در یکی از همین درخواستهاست.
گام چهارم یک دام دارد: اگر محتوا در HTML نباشد، علت همیشه جاوااسکریپت نیست. ممکن است سایت صفحه بررسی یا محتوای دیگری برگردانده باشد. کد وضعیت و محتوای پاسخ را بررسی کنید؛ این تشخیص را در وب اسکرپینگ بدون مسدود شدن مفصل توضیح دادهایم.
مرورگر headless چیست؟
مرورگر headless مرورگری واقعی است که بدون باز کردن پنجره روی صفحه اجرا میشود. موتور Chromium، Firefox یا WebKit صفحه را مانند مرورگر معمولی بارگذاری میکند، جاوااسکریپت اجرا میکند، درخواست API میفرستد و صفحه را میسازد؛ شما آن صفحه را با کد میخوانید. ابزارهای رایج Playwright، Puppeteer و Selenium هستند.
مرورگر headless کارهای بیشتری از کلاینت HTTP انجام میدهد، اما هزینه بیشتری هم دارد:
- پردازنده و حافظه. هر زبانه مرورگر چندین برابر یک درخواست HTTP منابع مصرف میکند. تعداد صفحههای همزمان روی یک ماشین را هستهها و حافظه محدود میکنند نه شبکه.
- زمان. دانلود و اجرای همه جاوااسکریپت یک صفحه ممکن است ثانیهها طول بکشد.
- تعداد درخواست و باند. یک صفحه دهها درخواست اضافه برای تصویر، فونت، برگه سبک، اسکریپت آمار و تبلیغ میسازد. این هم ترافیک شما و هم بار سایت مقصد را زیاد میکند.
- نگهداری. نسخه مرورگر، درایورها و منطق انتظار پیچیدگی میافزایند.
به همین دلیل مرورگر headless باید آخرین راه باشد نه نخستین انتخاب. برای راهاندازی Selenium، استفاده از پروکسی با Selenium را ببینید.
نخست درخواست API یا XHR را پیدا کنید
داده صفحه پویا از جایی بهصورت JSON به مرورگر میرسد. اگر آن درخواست را پیدا کنید، بدون رندر کردن صفحه مستقیم به داده میرسید. مرجع پنل Network کروم فیلترها و گزینههای کپی را مفصل توضیح میدهد؛ راه کوتاه چنین است:
- ابزارهای توسعهدهنده (F12) را باز کنید و به زبانه Network بروید.
- در نوار فیلتر Fetch/XHR را علامت بزنید.
- صفحه را دوباره بارگذاری کنید یا کاری را که داده را بارگذاری میکند انجام دهید (پیمایش صفحه، انتخاب فیلتر، رفتن به صفحه بعد).
- روی درخواستهایی که نامشان واژههایی مانند
api،graphql،searchیاproductsدارد بزنید و JSON را در زبانه Response ببینید. - وقتی درخواستی را که داده مورد نیازتان دارد یافتید، آدرس، پارامترها و هدرهای لازم را از زبانه Headers یادداشت کنید. با راستکلیک روی درخواست و Copy > Copy as cURL میتوانید آن را در خط فرمان بیازمایید.
تکرار درخواست یافتهشده در پایتون معمولاً چند سطر است:
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "ExamplePriceBot/1.0 (+https://example.com/about-our-bot)",
"Accept": "application/json",
})
response = session.get(
"https://example.com/api/products",
params={"category": "headphones", "page": 1},
timeout=20,
)
response.raise_for_status()
for product in response.json()["products"]:
print(product["name"], product["price"])برتریهای این راه بزرگ است: داده ساختیافته میرسد، به انتخابگر HTML نیاز ندارید، کد شما با تغییر طراحی صفحه نمیشکند و یک درخواست کسر کوچکی از بار کل صفحه است. صفحهبندی معمولاً با پارامتر page، offset یا cursor انجام میشود.
آنچه باید مراقبش باشید:
- احراز هویت و توکن. برخی درخواستهای API کوکی، توکن نشست یا امضاهای کوتاهعمر لازم دارند. کپی دستی اینها مدتی کار میکند و سپس میشکند.
- شرایط سایت. API داخلی که سایت برای رابط کاربری خودش به کار میبرد API عمومی نیست. شرایط استفاده و قواعد robots.txt سایت را برای این درخواستها هم اعمال کنید؛ اگر API رسمی ارائه شده، آن را ترجیح دهید. برای چارچوب قانونی آیا وب اسکرپینگ قانونی است؟ را ببینید.
- سرعت. چون درخواستهای API سبکاند، میتوان آنها را بسیار سریع فرستاد و همین یعنی آسانتر از محدودیتهای سایت میگذرید. همان قواعد سرعت را اعمال کنید.
خواندن JSON جاسازیشده در HTML
در سایتهایی که با Next.js، Nuxt و فریمورکهای مشابه ساخته شدهاند، داده اغلب آماده درون تگ <script> در HTML است. کد منبع را ببینید و __NEXT_DATA__، __NUXT__، __INITIAL_STATE__ یا application/ld+json را جستوجو کنید.
import json
import requests
from bs4 import BeautifulSoup
html = requests.get("https://example.com/deals", timeout=20).text
soup = BeautifulSoup(html, "html.parser")
data = json.loads(soup.select_one("script#__NEXT_DATA__").string)
for product in data["props"]["pageProps"]["products"]:
print(product["name"], product["price"])بلوکهای application/ld+json داده ساختیافتهای مانند نام محصول، قیمت، موجودی و امتیاز را در قالب schema.org دارند و چون برای موتورهای جستوجو آماده میشوند معمولاً پایدارند. پایدار کردن انتخابگرها در برابر تغییر طراحی را در انتخابگر CSS یا XPath بررسی کردهایم.
راهبردهای انتظار در Playwright
اگر داده نه در API است و نه در JSON جاسازیشده، یا رسیدن به محتوا تعاملهایی مانند کلیک، پیمایش و پر کردن فرم لازم دارد، مرورگر headless به کار میرود. رایجترین اشتباه در این حالت زمان خواندن است: اگر بیدرنگ پس از باز شدن صفحه بخوانید محتوا هنوز نرسیده؛ اگر زمان ثابتی صبر کنید یا وقت هدر میدهید یا با پاسخ کند باز نتیجه خالی میگیرید.
ابزارهای درست انتظار در Playwright:
- انتظار خودکار locatorها. عملیاتی مانند
page.locator("div.product h2").inner_text()تا timeout پیشفرض منتظر میماند عنصر در صفحه ظاهر شود. مستندات بررسیهای actionability در Playwright فهرست چیزهایی را که منتظرشان میماند آورده است. - انتظار برای عنصر مشخص.
page.locator("div.product").first.wait_for()منتظر ظاهر شدن نخستین کارت محصول میماند. - انتظار برای پاسخ مشخص.
page.expect_response(...)منتظر برگشتن درخواست داده صفحه میماند و JSON را مستقیم به شما میدهد. - به کار نبردن انتظار ثابت.
page.wait_for_timeout(5000)در آزمون سودمند است اما در محیط واقعی قابلاعتماد نیست.
مستندات Playwright وضعیت بارگذاری networkidle، یعنی انتظار برای آرام شدن ترافیک شبکه برای مدتی، را توصیه نمیکند؛ در صفحههایی که اسکریپتهای آمار و تبلیغ پیوسته درخواست میفرستند، این وضعیت ممکن است هرگز نرسد.
نمونه زیر صفحه را از طریق پروکسی باز میکند، برای کاهش بار تصویرها و فونتها را مسدود میکند، پاسخ درخواست داده را میگیرد و سپس کارتهای رندرشده را میخواند:
from playwright.sync_api import sync_playwright
PROXY = {"server": "http://pr.proxynet.io:8000", "username": "user", "password": "pass"}
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY)
page = browser.new_page()
# Don't download images and fonts, to cut time and traffic
page.route("**/*.{png,jpg,jpeg,webp,gif,woff2}", lambda route: route.abort())
with page.expect_response(lambda r: "/api/products" in r.url and r.ok) as response:
page.goto("https://example.com/deals")
api_data = response.value.json() # get the JSON directly
page.locator("div.product").first.wait_for() # or wait for the rendered cards
names = page.locator("div.product h2").all_inner_texts()
print(len(api_data["products"]), names)
browser.close()در این نمونه JSON گرفتهشده با expect_response اغلب بهتنها کافی است و نیازی به خواندن کارتها نیست. در نهایت مرورگر را فقط برای این به کار میبرید که صفحه آن درخواست را با پارامترها و کوکیهای درست بفرستد.
در Playwright اطلاعات پروکسی هنگام اجرای مرورگر داده میشود و نام کاربری و رمز در فیلدهای جداگانهاند. برای جریانهایی که باید در طول نشست همان IP حفظ شود، آدرس خروجی ثابت و برای صفحههای مستقل فراوان، پروکسی چرخشی که در هر اتصال IP متفاوتی میدهد به کار میرود.
هزینه: مرورگر headless کی لازم نیست؟
| رویکرد | کی کار میکند | سرعت و منابع | شکنندگی | باید مراقب بود |
|---|---|---|---|---|
| کلاینت HTTP همراه HTML | صفحه ایستاست و محتوا در کد منبع است | بسیار سریع، بسیار سبک | حساس به تغییر طراحی | انتخابگرها را به ویژگیهای پایدار ببندید |
| JSON جاسازیشده در HTML | صفحههای ترکیبی مانند Next.js و Nuxt | بسیار سریع، سبک | ساختار داده ممکن است عوض شود | مدیریت خطا که مسیر JSON را بررسی کند |
| درخواست API پسزمینه | محتوا بهصورت JSON میآید | بسیار سریع، سبکترین | مستقل از طراحی، API ممکن است عوض شود | شرایط سایت، توکن، محدودیت نرخ |
| مرورگر headless | تعامل لازم است، داده از راه دیگر در دسترس نیست | کند، سنگین | حساس به منطق انتظار | تصویرها را مسدود کنید، همروندی را کم نگه دارید |
حالتهای رایجی که مرورگر headless لازم نیست:
- محتوا از قبل در کد منبع صفحه هست.
- داده از درخواست JSON میآید که با پارامترهای ساده قابل تکرار است.
- داده در بلوک
__NEXT_DATA__یا JSON-LD است. - جاوااسکریپت فقط برای جابهجایی میان صفحهها به کار میرود و محتوا در هر صفحه از سرور میآید.
حالتهایی که مرورگر headless واقعاً لازم است:
- محتوا فقط با پیمایش، کلیک یا تعامل با فرم بارگذاری میشود و درخواست API مربوط قابل تکرار نیست.
- درخواستها امضا یا توکن کوتاهعمری لازم دارند که جاوااسکریپت صفحه میسازد.
- خروجی بصری صفحه (تصویر صفحه، بررسی چیدمان) خود کار است.
اگر مرورگر headless به کار میبرید، تعداد صفحههای همزمان را متناسب با منابع ماشین تنظیم کنید؛ این موضوع را در همروندی و موازیسازی بررسی کردهایم.
کاربردها
- پایش قیمت در تجارت الکترونیک: صفحه دستهبندی پویاست، اما فهرست محصول از یک درخواست
/api/productsمیآید. درخواست API بهجای مرورگر headless؛ برای دیدن قیمت در کشورهای مختلف پروکسی مسکونی که از اتصالهای خانگی واقعی خارج میشود. ساختار آن در صفحه راهحل پروکسی تجارت الکترونیک آمده است. - سایت آگهی ساختهشده با Next.js: داده در
__NEXT_DATA__است. کلاینت HTTP و تحلیل JSON کافی است. - فهرست نظرها با پیمایش بیپایان: درخواست صفحهبندی که هنگام پیمایش فرستاده میشود پیدا میشود و با پارامتر
cursorدر حلقه تکرار میشود. - پنل حسابی که تعامل لازم دارد (حساب خودتان): گامهای ورود، انتخاب فیلتر و دانلود گزارش با مرورگر headless؛ همان IP در طول نشست. ساختار کلی در صفحه راهحل استخراج داده آمده است.
اشتباهات رایج
- رفتن نخست به سراغ مرورگر headless. درخواست JSON در پنل Network اغلب همان داده را بسیار ارزانتر میدهد.
- انتظار زمان ثابت.
time.sleep(5)در پاسخهای سریع وقت هدر میدهد و در پاسخهای کند باز نتیجه خالی برمیگرداند. - انتظار برای
networkidle. در صفحههایی که پیوسته درخواست میفرستند ممکن است هرگز نرسد. - دانلود همه منابع. تصویر، فونت و ویدئو برای داده لازم نیستند؛ مسدود کردن آنها زمان و ترافیک را کم میکند.
- پویا دانستن هر نتیجه خالی. صفحه بررسی یا مسدودسازی هم نتیجه خالی میدهد؛ کد وضعیت و محتوا را بررسی کنید.
- به کار بردن API داخلی مانند API عمومی. شرایط و محدودیتهای نرخ سایت برای این درخواستها هم معتبر است.
- رفتن به سراغ ابزارهای گریز از شناسایی. افزونههایی که میکوشند مرورگر را «انسانی» نشان دهند ریشه مشکل را پنهان میکنند؛ بازبینی سرعت، دامنه و اجازه راه استوارتری است. دلیل مشکلساز بودن این ابزارها را در استفاده از undetected ChromeDriver در وب اسکرپینگ بررسی کردهایم.
راهنمای انتخاب
| نتیجه تشخیص | پیشنهاد |
|---|---|
| متن در کد منبع صفحه هست | کلاینت HTTP همراه تحلیل HTML |
کد منبع __NEXT_DATA__ یا JSON-LD دارد | کلاینت HTTP همراه تحلیل JSON |
| پنل Network درخواست JSON حاوی داده دارد | درخواست را مستقیم تکرار کنید |
| درخواست JSON امضای کوتاهعمر لازم دارد | مرورگر headless همراه expect_response |
| محتوا فقط با تعامل میآید | مرورگر headless همراه انتظار locator |
| مرورگر headless کند و سنگین است | تصویرها را مسدود کنید، همروندی را کم کنید |
| HTML محتوا ندارد و کد وضعیت 403 یا 429 است | پویا نیست، مسدودسازی است؛ تشخیص دهید |
پرسشهای متداول
چرا requests محتوایی را که در مرورگر میبینم برنمیگرداند؟
چون requests فقط نخستین HTML فرستادهشده سرور را میگیرد و جاوااسکریپت اجرا نمیکند. اگر محتوای صفحه بعداً با جاوااسکریپت بارگذاری شود، آن HTML فقط اسکلت را دارد. در پنل Network درخواستی را که داده از آن میآید یا JSON جاسازیشده در HTML را پیدا کنید.
تفاوت مرورگر headless و مرورگر معمولی چیست؟
موتور یکسان است؛ تفاوت این است که در حالت headless هیچ پنجرهای روی صفحه کشیده نمیشود. بارگذاری صفحه، اجرای جاوااسکریپت و درخواستهای شبکه مانند مرورگر معمولی کار میکنند. برخی سایتها میتوانند مرورگرهای headless را از روی تفاوتهای کوچک تشخیص دهند.
Playwright یا Selenium؟
هر دو صفحههای پویا را اجرا میکنند. Playwright قابلیتهای سودمند اسکرپینگ مانند انتظار خودکار، گرفتن پاسخهای شبکه و مسدود کردن درخواست را در یک API میدهد؛ Selenium قدیمیتر و چندزبانه است و بومسازگان گستردهای دارد. ادامه دادن با ابزاری که پروژه فعلی شما به کار میبرد معمولاً کاربردیترین انتخاب است.
آیا به کار بردن درخواست API پنهان قانونی است؟
عمومی بودن فنی یک درخواست استفاده از آن را بیقیدوشرط نمیکند. شرایط استفاده سایت، قواعد robots.txt و قانونهای داده شخصی را در نظر بگیرید؛ اگر API رسمی وجود دارد آن را ترجیح دهید. اگر مطمئن نیستید، مشاوره حقوقی بگیرید.
چگونه مرورگر headless را با پروکسی به کار ببرم؟
در Playwright پروکسی هنگام اجرای مرورگر با پارامتر proxy داده میشود؛ آدرس سرور، نام کاربری و رمز فیلدهای جداگانهاند. در Puppeteer و Selenium بهصورت آرگومان راهاندازی به مرورگر داده میشود و احراز هویت جداگانه مدیریت میشود.
داده صفحههای با پیمایش بیپایان را چگونه بگیریم؟
نخست در پنل Network درخواست صفحهبندی را که هنگام پیمایش فرستاده میشود پیدا کنید. معمولاً شماره صفحه یا مقدار cursor دارد و در حلقه قابل تکرار است. اگر درخواست قابل تکرار نباشد، صفحه را در مرورگر headless کمکم پیمایش کنید و در هر گام منتظر رسیدن کارتهای تازه بمانید.
خلاصه
در صفحه ایستا محتوا آماده در HTML است و درخواست HTTP ساده کافی است؛ در صفحه پویا محتوا بعداً از راه جاوااسکریپت میآید و درخواست HTTP اسکلت برمیگرداند. پیش از رفتن به مرورگر headless، کد منبع را ببینید، در پنل Network درخواست JSON حاوی داده را بجویید و داده جاسازیشده در HTML را بررسی کنید. وقتی مرورگر واقعاً لازم باشد، بهجای انتظار ثابت از انتظار locator و expect_response استفاده کنید، تصویرها را مسدود کنید و همروندی را کم نگه دارید. انواع پروکسی متناسب با کار جمعآوری داده را در خدمات پروکسی ما مییابید.




