خانه/مقالات/رفع خطای لیست خالی در اسکریپینگ با BeautifulSoup
سلنیوم
beautifulsoup
ضد بلاک (Anti-bot)
برگشت به مقاله‌ها

رفع خطای لیست خالی در اسکریپینگ با BeautifulSoup

رفع خطای لیست خالی در اسکریپینگ با BeautifulSoup
این مقاله گام‌به‌گام نشان می‌دهد چرا BeautifulSoup ممکن است لیست یا مقدار خالی برگرداند و چگونه با بررسی پاسخ HTTP، استفاده از headless browser یا پراکسی‌های رندرینگ، استخراج JSON مخفی و دیباگ سلکتورها این مشکل را حل کنید. همراه با مثال‌های پایتون و نکات عملی برای بهینه‌سازی، مدیریت خطا و اجتناب از بلاک شدن.
آسان اسکریپ آسان اسکریپ
1405-04-24

مقدمه

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

در این مطلب پوشش می‌دهیم: بررسی محتوای پاسخ HTTP، سایت‌های داینامیک و راه‌حل‌هایشان (headless، پراکسیِ رندرینگ، استخراج JSON مخفی)، محتوای جغرافیایی، بلاک‌شدن درخواست‌ها و دیباگ سلکتورها.

گام اول: بررسی محتوای پاسخ

قبل از تغییر سلکتورها یا کتابخانه‌ها، مطمئن شوید که HTML دریافتی واقعاً شامل داده‌ای است که انتظار دارید. چند کار سریع برای تشخیص:

  • بررسی status_code و response.headers.
  • ذخیره و باز کردن HTML در مرورگر یا جستجوی متنی برای قطعه‌ای از متن مورد انتظار.
  • بررسی اینکه آیا HTML شامل اسکریپت‌های جاوااسکریپتی است که داده‌ها را بعداً بارگذاری می‌کنند.
import requests
from bs4 import BeautifulSoup

url = 'https://example.com/target'
resp = requests.get(url, timeout=30)
# ذخیره‌ی خام پاسخ برای بازرسی دستی
with open('response.html', 'wb') as f:
    f.write(resp.content)

print('status:', resp.status_code)
print('content-type:', resp.headers.get('content-type'))

# نمونه‌ی اولیه‌ی پارس با BeautifulSoup
soup = BeautifulSoup(resp.content, 'html.parser')
print('len html:', len(resp.content))

توضیح: ورودی این کد یک URL است و خروجی فایل response.html و اطلاعات مقدماتی از پاسخ. خط‌به‌خط: دریافت پاسخ با requests.get، نوشتن باینری محتوا روی دیسک، چاپ وضعیت و سپس ساخت شیٔ BeautifulSoup برای مشاهده اولیهٔ DOM.

دلیل ۱: سایت‌های داینامیک و راه‌حل‌ها

اگر سایت با فریم‌ورک‌های مدرن مثل React/Vue/Angular ساخته شده باشد، داده‌ها اغلب پس از بارگذاری اولیه توسط جاوااسکریپت وارد DOM می‌شوند. در این حالت HTML خام که requests می‌گیرد، ممکن است داده‌ها را نداشته باشد.

سه رویکرد اصلی وجود دارد:

  • استفاده از مرورگر بدون سر (headless) مثل Selenium یا Playwright تا صفحه را رندر کنید و سپس page_source را به BeautifulSoup بدهید.
  • استفاده از پراکسی‌های هوشمند که قابلیت JS rendering را ارائه می‌دهند (render_js).
  • استخراج JSON مخفی که اغلب داخل تگ <script> قرار می‌گیرد (مثلاً __NEXT_DATA__).

راه‌حل: Selenium + BeautifulSoup

from selenium import webdriver
from bs4 import BeautifulSoup

# نمونه‌ی ساده با Selenium (نیاز به درایور مناسب دارد)
driver = webdriver.Firefox()
driver.get('https://example.com/dynamic')
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
print(soup.select_one('h1'))
driver.quit()

توضیح: ورودی URL است؛ خروجی DOM رندرشده. خطوط: راه‌اندازی WebDriver، بارگذاری صفحه، خواندن page_source، پارس با BeautifulSoup و بستن مرورگر. نکته: مدیریت زمان‌بندی (explicit waits) برای اطمینان از اینکه داده‌ها بارگذاری شده‌اند ضروری است.

راه‌حل: پراکسی با رندرینگ جاوااسکریپت

import requests
from urllib.parse import urlencode

proxy_api = 'https://proxy-api.example/v1/'  # آدرس نمونه پراکسی (باید سرویس خود را مشخص کنید)
payload = {
    'api_key': 'YOUR_API_KEY',
    'url': 'https://example.com/dynamic',
    'render_js': True,
}
resp = requests.get(proxy_api + '?' + urlencode(payload), timeout=120)
from bs4 import BeautifulSoup
soup = BeautifulSoup(resp.content, 'html.parser')
print(soup.select_one('h1'))

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

راه‌حل: استخراج JSON داخل تگ <script>

import requests
import json
from bs4 import BeautifulSoup

resp = requests.get('https://example.com/product')
soup = BeautifulSoup(resp.text, 'html.parser')
script = soup.find('script', {'id': '__NEXT_DATA__'})
if script:
    data = json.loads(script.get_text())
    # مسیر داده بستگی به سایت دارد؛ مثال فرضی:
    product = data['props']['pageProps']['initialData']['data']['product']
    print(product['title'])
else:
    print('script not found')

توضیح: بعضی سایت‌ها همهٔ داده‌ها را در یک بلوک JSON قرار می‌دهند. ورودی: HTML صفحه؛ خروجی: ساختار JSON قابل پردازش. مزیت: داده‌ها معمولاً پاک و ساختارمند هستند و نیاز به نوشتن سلکتورهای پیچیده کاهش می‌یابد.

دلیل ۲: محتوای مبتنی بر موقعیت جغرافیایی

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

# نمونه‌ی ارسال درخواست با پارامتر کشور به پراکسی
payload = {
    'api_key': 'YOUR_API_KEY',
    'url': 'https://example.com/',
    'country': 'us',
}
resp = requests.get(proxy_api + '?' + urlencode(payload), timeout=60)
# سپس parse با BeautifulSoup

توضیح: اضافه‌کردن پارامتر country یا معادل آن به پراکسی می‌تواند درخواست شما را از IP منطقهٔ مدنظر ارسال کند. اگر از پراکسی عمومی استفاده می‌کنید، مطمئن شوید که قابلیت geotargeting دارد.

دلیل ۳: بلاک‌شدن درخواست‌ها (Ban / CAPTCHA)

گاهی سایت‌ها به‌صورت موفقیت‌آمیز (status 200) صفحات محدودیت یا CAPTCHA را برمی‌گردانند. این صفحات معمولاً در HTML ظاهر می‌شوند اما دادهٔ اصلی وجود ندارد.

راهکارها:

  • بررسی شاخص‌هایی مثل رشته‌های «Access Denied», «CAPTCHA», یا اندازهٔ غیرمعمول محتوا.
  • استفاده از روتیشن پراکسی و روتیشن User-Agent.
  • اضافه‌کردن زمان‌های تصادفی بین درخواست‌ها و استفاده از backoff در مواجهه با خطا.
import random
import time
import requests
from fake_useragent import UserAgent

ua = UserAgent()
for attempt in range(5):
    headers = {'User-Agent': ua.random}
    try:
        resp = requests.get('https://example.com/', headers=headers, timeout=15)
        if 'captcha' in resp.text.lower() or len(resp.content) < 1000:
            raise ValueError('possible block detected')
        break
    except Exception as e:
        wait = (2 ** attempt) + random.random()
        time.sleep(wait)
        continue

توضیح: این قطعه نشان‌دهندهٔ یک استراتژی سادهٔ retry با روتیشن User-Agent است. ورودی: URL و تعداد تلاش‌ها؛ خروجی: پاسخ قابل‌استفاده یا شکست پس از تلاش‌ها. دقت کنید که استفاده از کتابخانه‌هایی مثل fake_useragent باید با احتیاط و رعایت قوانین سایت‌ها انجام شود.

گام دوم: بررسی و دیباگ سلکتورها در BeautifulSoup

اگر HTML شامل داده بود ولی BeautifulSoup هیچ عنصری را پیدا نمی‌کند، احتمالاً سلکتور اشتباه است. رویکردهای مفید:

  • ذخیرهٔ HTML و تست سلکتورها در Jupyter Notebook به‌صورت تعاملی.
  • استفاده از .select() با CSS selectors که در مرورگر با document.querySelectorAll قابل تست است.
  • استفاده از .find() و .find_all() با پارامترهای مناسب مثل نام تگ، کلاس، یا attrs.
// تست سریع در کنسول مرورگر
document.querySelectorAll('div.product > h2.title')

توضیح: اجرای این در تب Console مرورگر به شما کمک می‌کند که ببینید CSS selector موردنظر چند عنصر را برمی‌گرداند. سپس همان سلکتور را در soup.select() استفاده کنید.

# مثال استفاده از select در BeautifulSoup
from bs4 import BeautifulSoup
html = open('response.html', 'r', encoding='utf-8').read()
soup = BeautifulSoup(html, 'html.parser')
items = soup.select('div.product > h2.title')
for it in items:
    print(it.get_text(strip=True))

توضیح: ورودی این کد فایل HTML ذخیره‌شده است. خروجی متنِ عنوان‌های محصول. نکته: همیشه خروجی را با strip() پاک کنید و بررسی کنید که کلاس‌ها یا ساختار DOM تغییر نکرده باشد.

بهترین شیوه‌ها و هشدارهای امنیتی

  • همیشه timeout برای درخواست‌ها تعیین کنید و از مدیریت استثناها برخوردار باشید.
  • در صورت پردازش داده‌های حساس، مطمئن شوید لاگ‌ها اطلاعات محرمانه (API key) را شامل نشوند.
  • بنابر قوانین و اخلاق، قبل از اسکریپ کردن از یک سایت، فایل robots.txt و شرایط استفاده را بررسی کنید.
  • برای حجم بالا از sessionهای ماندگار (requests.Session) و راهکارهای همزمانی (مثل httpx async یا صف‌بندی وظایف) استفاده کنید.
  • برای صفحات بسیار بزرگ از استریمینگ پاسخ و تحلیل جزئی محتوا بهره ببرید تا حافظه کمتر مصرف شود.

جمع‌بندی

اگر BeautifulSoup لیست خالی برمی‌گرداند، ابتدا مطمئن شوید HTML شامل دادهٔ مورد نظر است؛ سپس دلایل احتمالی (JS رندر، محدودیت جغرافیایی، بلاک شدن، یا اشتباه در سلکتورها) را بررسی کنید. برای سایت‌های داینامیک از headless یا پراکسی‌های با رندرینگ استفاده کنید، از استخراج JSON داخلی بهره ببرید و سلکتورها را در مرورگر یا نوت‌بوک آزمایش کنید. در نهایت از استراتژی‌های ضد-بلاک مثل روتیشن پراکسی/UA و backoff استفاده کنید تا پایداری اسکریپر افزایش یابد.

مطالب مرتبط

مقاله‌های مرتبط