استخراج داده از وبسایتهای مدرن SPA (Next.js & React) بدون نیاز به مرورگر
استخراج مستقیم دادههای ساختاریافته از Hydration State، اندپوینتهای مخفی _next/data و استریمهای React Server Components (RSC) با سرعت ۱۰۰ برابر مرورگر.
توهم نیاز به مرورگر در وبسایتهای مدرن
یکی از رایجترین باورهای نادرست در وب اسکرپینگ این است که به محض دیدن یک وبسایت مبتنی بر React، Next.js یا Nuxt.js، باید سراغ مرورگرهای سنگینی مثل Playwright یا Selenium رفت. در واقعیت، بیش از ۹۰ درصد وبسایتهای فروشگاهی، نیازمندیها و پلتفرمهای محتوایی مدرن به دلیل نیاز مبرم به SEO و سرعت بارگذاری اولیه، از معماری SSR (Server-Side Rendering) یا ISR (Incremental Static Regeneration) استفاده میکنند.
در این معماریها، سرور تمام دادههای صفحه را پیش از ارسال پاسخ جمعآوری کرده و علاوه بر رندر HTML، کل درخت داده را به صورت یک شیء JSON تمیز درون سورس صفحه تزریق میکند تا فریمورک در سمت کلاینت بتواند فرآیند Hydration را انجام دهد.
کالبدشکافی Hydration State: از __NEXT_DATA__ تا React Server Components
بسته به نسخه فریمورک مورد استفاده در سایت هدف، دادههای خام در یکی از سه الگو قرار میگیرند:
- معماری Next.js Pages Router: تمام پراپهای صفحه داخل تگ
<script id="__NEXT_DATA__" type="application/json">در انتهای سند HTML قرار دارند. مسیرprops.pagePropsدقیقاً همان خروجی دیتابیس یا API داخلی بکاند است. - اندپوینتهای مخفی `_next/data`: زمانی که کاربر در یک سایت Next.js بین صفحات جابهجا میشود (Client-Side Navigation)، مرورگر کل HTML را دانلود نمیکند، بلکه یک فایل JSON خالص را از مسیر
/_next/data/<buildId>/<path>.jsonدریافت میکند. با استخراجbuildIdدر اولین درخواست، میتوانیم هزاران صفحه بعدی را مستقیماً به صورت JSON و با کسری از حجم ترافیک دریافت کنیم! - معماری Next.js App Router (RSC Flight Payloads): در نسخههای جدیدتر (Next.js 13/14/15)، تگ
__NEXT_DATA__حذف شده و دادهها به صورت تکههای استریم در فراخوانیهایself.__next_f.push([1, "..."])تزریق میشوند.
ارسال هدر RSC: 1 در Next.js App Router
یک ترفند فوقالعاده حرفهای در مواجهه با سایتهای مبتنی بر App Router این است که در درخواست HTTP خود، هدر RSC: 1 را اضافه کنید. با ارسال این هدر، سرور Next.js به جای رندر کردن کل تگهای سنگین HTML، مستقیماً استریم متنی React Flight را برمیگرداند که پارس کردن آن بسیار سریعتر و کمحجمتر است.
پیادهسازی استخراجگر ترکیبی Next.js (پشتیبانی از Pages Router و App Router)
کد زیر یک موتور استخراج غیرهمگام (Async) است که به صورت خودکار معماری سایت هدف را تشخیص میدهد، buildId را کش میکند، در صورت دیپلوی نسخه جدید و دریافت خطای 404 به صورت خودکار buildId جدید را واکشی میکند و هر دو ساختار __NEXT_DATA__ و استریمهای self.__next_f را استخراج مینماید:
import json
import re
import asyncio
from typing import Dict, Any, List, Optional
import httpx
class NextJsDirectExtractor:
NEXT_DATA_REGEX = re.compile(r'<script id="__NEXT_DATA__" type="application/json"[^>]*>(.*?)</script>', re.DOTALL)
RSC_FLIGHT_REGEX = re.compile(r'self\.__next_f\.push\(\[\d+,\s*"(.*?)"\]\)', re.DOTALL)
def __init__(self, base_url: str):
self.base_url = base_url.rstrip('/')
self.build_id: Optional[str] = None
self.client = httpx.AsyncClient(http2=True, timeout=15.0, headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
})
async def refresh_build_id(self, sample_path: str = '/') -> Optional[str]:
resp = await self.client.get(f'{self.base_url}{sample_path}')
resp.raise_for_status()
match = self.NEXT_DATA_REGEX.search(resp.text)
if match:
payload = json.loads(match.group(1))
self.build_id = payload.get('buildId')
return self.build_id
return None
async def fetch_page_props(self, route_path: str, query_params: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
clean_route = route_path.strip('/') or 'index'
if not self.build_id:
await self.refresh_build_id()
if self.build_id:
data_url = f'{self.base_url}/_next/data/{self.build_id}/{clean_route}.json'
resp = await self.client.get(data_url, params=query_params)
if resp.status_code == 404:
# Target deployed a new build; refresh buildId once and retry
await self.refresh_build_id()
data_url = f'{self.base_url}/_next/data/{self.build_id}/{clean_route}.json'
resp = await self.client.get(data_url, params=query_params)
if resp.status_code == 200:
return resp.json().get('pageProps', {})
# Fallback to HTML extraction
full_url = f'{self.base_url}/{route_path.lstrip("/")}'
resp = await self.client.get(full_url, params=query_params)
resp.raise_for_status()
match = self.NEXT_DATA_REGEX.search(resp.text)
if match:
return json.loads(match.group(1)).get('props', {}).get('pageProps', {})
raise ValueError('No __NEXT_DATA__ found; target may use App Router RSC.')
async def fetch_rsc_flight_stream(self, route_path: str) -> List[str]:
"""Fetches raw React Server Components payload using the RSC header."""
full_url = f'{self.base_url}/{route_path.lstrip("/")}'
resp = await self.client.get(full_url, headers={'RSC': '1'})
if resp.status_code == 200 and 'text/x-component' in resp.headers.get('content-type', ''):
return resp.text.splitlines()
return [m.encode('utf-8').decode('unicode_escape') for m in self.RSC_FLIGHT_REGEX.findall(resp.text)]
async def close(self):
await self.client.aclose()نکته کلیدی مهندسی: هنگام استفاده از مسیر /_next/data/<buildId>/...، توجه داشته باشید که با هر بار Deploy شدن کد جدید توسط تیم فنی سایت هدف، مقدار buildId تغییر کرده و درخواستهای شما با خطای 404 Not Found مواجه میشوند. همیشه مکانیسم Auto-Refresh BuildID را در لایه Middleware اسکرپر خود پیادهسازی کنید تا پایپلاین بدون توقف به کار خود ادامه دهد.