کتابخانه‌های پایتون

وب اسکرپینگ با پایتون؛ استخراج داده از سایت با requests و BeautifulSoup

وب اسکرپینگ با پایتون با requests و BeautifulSoup

🗓 آخرین به‌روزرسانی:

وب اسکرپینگ (Web Scraping) یعنی اینکه یک برنامه به جای شما صفحه‌های وب را باز کند، متن و داده‌های مورد نیاز را از داخل HTML بیرون بکشد و آن‌ها را در قالبی مرتب مثل CSV یا JSON ذخیره کند. فرض کنید می‌خواهید قیمت ده‌ها کتاب، عنوان آخرین خبرها یا فهرست آگهی‌ها را هر روز جمع کنید؛ انجام دستی این کار خسته‌کننده و پرخطاست، اما با چند خط پایتون در چند ثانیه انجام می‌شود. در این آموزش وب اسکرپینگ با پایتون را با دو کتابخانه محبوب requests و BeautifulSoup از صفر یاد می‌گیرید؛ همه کدها با پایتون 3.13 اجرا شده‌اند و خروجی زیر هر کد، خروجی واقعی همان اجراست.

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

وب اسکرپینگ چیست و چه کاربردی دارد؟

هر صفحه وب در اصل یک فایل متنی HTML است که مرورگر آن را به شکل زیبا نمایش می‌دهد. وقتی صفحه‌ای را اسکرپ می‌کنید، برنامه شما همان کاری را می‌کند که مرورگر انجام می‌دهد: درخواست HTTP می‌فرستد، HTML را دریافت می‌کند و سپس به جای نمایش، ساختار آن را تحلیل می‌کند تا بخش‌های مشخصی مثل عنوان، قیمت یا لینک را پیدا کند. به این بخش دوم «پارس کردن» (Parsing) می‌گویند.

کاربردهای رایج وب اسکرپینگ در پروژه‌های واقعی این‌هاست:

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

قبل از هر چیز بدانید: اگر سایتی API رسمی دارد، استفاده از API تقریباً همیشه بهتر از اسکرپینگ است؛ داده تمیزتر است، احتمال تغییر ساختار کمتر است و از نظر قوانین سایت هم امن‌تر است. اسکرپینگ را برای جایی نگه دارید که داده عمومی است ولی API ندارد.

نصب requests و BeautifulSoup

ابتدا یک محیط مجازی بسازید و دو کتابخانه را نصب کنید. اگر با این مرحله آشنا نیستید، آموزش نصب کتابخانه در پایتون با pip و pip و محیط مجازی venv را ببینید. نام پکیج در pip beautifulsoup4 است ولی در کد آن را با bs4 وارد می‌کنید؛ این تفاوت نام، علت رایج خطای ModuleNotFoundError است.

python -m pip install requests beautifulsoup4

برای اطمینان از نصب درست، نسخه هر دو کتابخانه را چاپ کنید:

import bs4
import requests

print("requests:", requests.__version__)
print("beautifulsoup4:", bs4.__version__)

خروجی:

requests: 2.34.2
beautifulsoup4: 4.15.0

قدم اول: گرفتن HTML صفحه با requests

کتابخانه requests ساده‌ترین راه ارسال درخواست HTTP در پایتون است. تابع requests.get آدرس را می‌گیرد و یک شیء پاسخ برمی‌گرداند که کد وضعیت، هدرها و متن HTML را در خود دارد. در مثال زیر صفحه اصلی books.toscrape.com را می‌گیریم؛ یک فروشگاه کتاب ساختگی که مخصوص تمرین وب اسکرپینگ ساخته شده و اسکرپ کردن آن آزاد است.

import requests

response = requests.get("https://books.toscrape.com/", timeout=10)

print("Status:", response.status_code)
print("Type:", response.headers["Content-Type"])
print("Detected encoding:", response.encoding)
print("HTML size:", len(response.content) > 1000)

خروجی:

Status: 200
Type: text/html
Detected encoding: ISO-8859-1
HTML size: True

همیشه پارامتر timeout را بنویسید. بدون آن، اگر سرور جواب ندهد برنامه شما ممکن است برای همیشه منتظر بماند. کد وضعیت 200 یعنی درخواست موفق بوده است؛ 404 یعنی صفحه پیدا نشده و 403 یا 429 معمولاً یعنی سرور درخواست شما را رد کرده یا تعداد درخواست‌ها زیاد بوده است.

قدم دوم: پارس کردن HTML با BeautifulSoup

حالا HTML را به BeautifulSoup می‌دهیم تا آن را به یک درخت از تگ‌ها تبدیل کند. پارامتر دوم، نام پارسر است؛ html.parser همراه خود پایتون است و به نصب چیز دیگری نیاز ندارد. بعد از ساختن شیء soup می‌توانید با نام تگ‌ها به آن‌ها دسترسی داشته باشید.

import requests
from bs4 import BeautifulSoup

response = requests.get("https://books.toscrape.com/", timeout=10)
print("Wrong:", BeautifulSoup(response.text, "html.parser").select_one(".price_color").text)

response.encoding = "utf-8"
soup = BeautifulSoup(response.text, "html.parser")

print("Title:", soup.title.get_text(strip=True))
print("H1:", soup.h1.get_text(strip=True))
first = soup.find("article", class_="product_pod")
print("First book:", first.h3.a["title"])
print("Price:", first.find("p", class_="price_color").get_text())
print("Link:", first.h3.a["href"])

خروجی:

Wrong: £51.77
Title: All products | Books to Scrape - Sandbox
H1: All products
First book: A Light in the Attic
Price: £51.77
Link: catalogue/a-light-in-the-attic_1000/index.html

متد سطر اول خروجی را ببینید: علامت پوند به شکل «Â£» خراب شده است. سرور در هدر کدگذاری را اعلام نکرده و requests به‌طور پیش‌فرض ISO-8859-1 فرض کرده است. با تنظیم response.encoding = "utf-8" متن درست خوانده می‌شود؛ همین مشکل در سایت‌های فارسی هم به شکل حروف ناخوانا دیده می‌شود. متد get_text(strip=True) متن داخل تگ را بدون فاصله‌های اضافه برمی‌گرداند و با کروشه مثل link["href"] به ویژگی‌های (attributes) تگ دسترسی پیدا می‌کنید. اگر ویژگی وجود نداشته باشد، کروشه خطای KeyError می‌دهد؛ برای حالت اختیاری از link.get("href") استفاده کنید.

find و find_all: پیدا کردن عناصر

در پروژه واقعی، داده‌ها معمولاً در چند کارت تکراری با کلاس مشخص قرار دارند. برای اینکه مثال‌ها همیشه یکسان اجرا شوند، یک فایل نمونه به نام books.html داریم که شبیه صفحه یک فروشگاه کتاب است. ساختار آن این است:

<html><body>
<h1>Book Store</h1>
<div class="book" data-id="101">
  <h2 class="title">Python Basics</h2>
  <span class="price">12.50</span>
  <span class="stock">In stock</span>
  <a class="more" href="/books/101">Details</a>
</div>
<div class="book" data-id="102">
  <h2 class="title">Web Scraping 101</h2>
  <span class="price">18.00</span>
  <span class="stock">Out of stock</span>
  <a class="more" href="/books/102">Details</a>
</div>
<div class="book" data-id="103">
  <h2 class="title">Data with Pandas</h2>
  <span class="price">21.90</span>
  <span class="stock">In stock</span>
  <a class="more" href="/books/103">Details</a>
</div>
</body></html>

متد find اولین عنصر منطبق و find_all همه عناصر منطبق را برمی‌گرداند. پارامتر class_ (با خط زیر، چون class در پایتون کلمه کلیدی است) برای فیلتر بر اساس کلاس CSS به کار می‌رود.

from pathlib import Path
from bs4 import BeautifulSoup

html = Path("books.html").read_text(encoding="utf-8")
soup = BeautifulSoup(html, "html.parser")

books = soup.find_all("div", class_="book")
print("Books found:", len(books))

for book in books:
    title = book.find("h2", class_="title").get_text(strip=True)
    price = float(book.find("span", class_="price").get_text())
    print(f"{book['data-id']} | {title:<18} | {price:>6.2f}")

خروجی:

Books found: 3
101 | Python Basics      |  12.50
102 | Web Scraping 101   |  18.00
103 | Data with Pandas   |  21.90

دقت کنید که قیمت را با float به عدد تبدیل کردیم تا بتوانیم با آن محاسبه کنیم. در سایت‌های واقعی قیمت اغلب همراه واحد پول، ویرگول هزارگان یا رقم فارسی است؛ قبل از تبدیل آن را تمیز کنید، وگرنه ValueError می‌گیرید.

انتخابگرهای CSS با select و select_one

اگر با CSS آشنا باشید، متدهای select و select_one خواناتر و قدرتمندترند. همان انتخابگرهایی که در DevTools مرورگر می‌بینید (مثل div.book span.price) را مستقیم استفاده می‌کنید. برای ساختن آدرس کامل از لینک‌های نسبی هم تابع urljoin را به کار ببرید.

from pathlib import Path
from urllib.parse import urljoin
from bs4 import BeautifulSoup

BASE = "https://shop.example.com"
soup = BeautifulSoup(Path("books.html").read_text(encoding="utf-8"), "html.parser")

for card in soup.select("div.book"):
    title = card.select_one(".title").get_text(strip=True)
    in_stock = card.select_one(".stock").get_text(strip=True) == "In stock"
    url = urljoin(BASE, card.select_one("a.more")["href"])
    print(title, "|", "available" if in_stock else "sold out", "|", url)

cheap = [c.select_one(".title").text for c in soup.select("div.book")
         if float(c.select_one(".price").text) < 20]
print("Under 20:", cheap)

خروجی:

Python Basics | available | https://shop.example.com/books/101
Web Scraping 101 | sold out | https://shop.example.com/books/102
Data with Pandas | available | https://shop.example.com/books/103
Under 20: ['Python Basics', 'Web Scraping 101']

برای پیدا کردن انتخابگر درست، روی عنصر مورد نظر در مرورگر راست‌کلیک کرده و Inspect را بزنید. کلاس‌ها و ساختار والد و فرزند را ببینید و تا جای ممکن از کلاس‌های معنادار (مثل price) استفاده کنید، نه از مسیرهای طولانی و شکننده مثل div > div > span:nth-child(3).

ذخیره داده در CSV و JSON

داده استخراج‌شده تا وقتی ذخیره نشود ارزشی ندارد. ماژول استاندارد csv برای جدول‌ها و json برای داده‌های ساختاریافته کافی است. فایل CSV را می‌توانید مستقیم در اکسل باز کنید؛ برای خوانایی درست متن فارسی در اکسل، کدگذاری utf-8-sig را انتخاب کنید. درباره JSON هم آموزش کار با فایل JSON در پایتون را ببینید.

import csv
import json
from pathlib import Path
from bs4 import BeautifulSoup

soup = BeautifulSoup(Path("books.html").read_text(encoding="utf-8"), "html.parser")
rows = []
for card in soup.select("div.book"):
    rows.append({
        "id": int(card["data-id"]),
        "title": card.select_one(".title").get_text(strip=True),
        "price": float(card.select_one(".price").text),
    })

with open("books.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.DictWriter(f, fieldnames=["id", "title", "price"])
    writer.writeheader()
    writer.writerows(rows)

Path("books.json").write_text(json.dumps(rows, indent=2), encoding="utf-8")
print(Path("books.csv").read_text(encoding="utf-8-sig"))
print("JSON items:", len(json.loads(Path("books.json").read_text(encoding="utf-8"))))

خروجی:

id,title,price
101,Python Basics,12.5
102,Web Scraping 101,18.0
103,Data with Pandas,21.9

JSON items: 3

اسکرپ چند صفحه (صفحه‌بندی)

بیشتر سایت‌ها فهرست‌ها را در چند صفحه نمایش می‌دهند و در پایین هر صفحه لینک «بعدی» دارند. الگوی کلی ساده است: صفحه را بخوانید، داده را استخراج کنید، لینک بعدی را پیدا کنید و تا وقتی لینک بعدی وجود دارد ادامه دهید. در مثال زیر دو فایل محلی page1.html و page2.html نقش صفحه‌های سایت را دارند؛ در پروژه واقعی به جای خواندن فایل، همان requests.get را صدا می‌زنید و بین درخواست‌ها مکث می‌کنید.

from pathlib import Path
from bs4 import BeautifulSoup

def load(name):
    return BeautifulSoup(Path(name).read_text(encoding="utf-8"), "html.parser")

page, titles = "page1.html", []
while page:
    soup = load(page)
    titles += [a.get_text(strip=True) for a in soup.select("#news li a")]
    nxt = soup.select_one("a.next")
    print("Scraped", page)
    page = nxt["href"] if nxt else None

for number, title in enumerate(titles, start=1):
    print(number, title)

خروجی:

Scraped page1.html
Scraped page2.html
1 Python 3.13 released
2 New pip version
3 Requests tips
4 BeautifulSoup tricks

همیشه یک سقف برای تعداد صفحه‌ها بگذارید (مثلاً حداکثر ۵۰ صفحه) تا اگر سایت به‌اشتباه لینک بعدی را به همان صفحه داد، برنامه در حلقه بی‌پایان گیر نکند. منطق حلقه را در آموزش حلقه while کامل‌تر توضیح داده‌ایم.

اسکرپینگ مؤدبانه: هدر، مکث، robots.txt و مدیریت خطا

اسکرپر خوب مثل یک مهمان مؤدب رفتار می‌کند: خودش را معرفی می‌کند، با سرعت معقول درخواست می‌فرستد و قوانین سایت را رعایت می‌کند. فایل robots.txt هر سایت مشخص می‌کند ربات‌ها به کدام مسیرها اجازه دسترسی دارند. ماژول استاندارد urllib.robotparser این قوانین را می‌خواند؛ در مثال زیر برای قابل تکرار بودن، متن robots را مستقیم به آن داده‌ایم.

from urllib.robotparser import RobotFileParser

rules = """
User-agent: *
Disallow: /cart/
Disallow: /account/
Crawl-delay: 2
""".splitlines()

rp = RobotFileParser()
rp.parse(rules)
for path in ["/books/101", "/cart/", "/account/orders"]:
    print(path, "allowed" if rp.can_fetch("DevSourceBot", path) else "blocked")
print("Crawl delay:", rp.crawl_delay("DevSourceBot"), "seconds")

خروجی:

/books/101 allowed
/cart/ blocked
/account/orders blocked
Crawl delay: 2 seconds

در کد واقعی، درخواست‌ها را داخل یک تابع با هدر User-Agent مشخص، timeout، مدیریت خطا و مکث بین درخواست‌ها قرار دهید. متد raise_for_status برای کدهای 4xx و 5xx استثنا ایجاد می‌کند و با try/except آن را مدیریت می‌کنیم.

import time
import requests

HEADERS = {"User-Agent": "DevSourceBot/1.0 (learning project)"}

def fetch(url, delay=1.0):
    try:
        response = requests.get(url, headers=HEADERS, timeout=10)
        response.raise_for_status()
        return response.text
    except requests.exceptions.HTTPError as error:
        print("HTTP error:", error.response.status_code, url)
    except requests.exceptions.RequestException as error:
        print("Network error:", type(error).__name__)
    finally:
        time.sleep(delay)
    return None

for url in ["https://books.toscrape.com/", "https://books.toscrape.com/missing-page"]:
    html = fetch(url)
    print(url, "->", "OK" if html else "skipped")

خروجی:

https://books.toscrape.com/ -> OK
HTTP error: 404 https://books.toscrape.com/missing-page
https://books.toscrape.com/missing-page -> skipped

وب اسکرپینگ قانونی است؟ نکات اخلاقی و حقوقی

اسکرپ کردن داده‌های عمومی برای یادگیری و پژوهش شخصی معمولاً مشکلی ایجاد نمی‌کند، اما چند خط قرمز وجود دارد که باید جدی بگیرید. این بخش مشاوره حقوقی نیست، ولی رعایت آن شما را از بیشتر دردسرها دور نگه می‌دارد:

  • شرایط استفاده (Terms of Service) سایت را بخوانید؛ بعضی سایت‌ها اسکرپینگ را صریحاً ممنوع کرده‌اند.
  • قوانین robots.txt را رعایت کنید و مسیرهای ممنوع را درخواست ندهید.
  • اطلاعات شخصی کاربران (شماره تلفن، ایمیل، نشانی) را جمع نکنید و منتشر نکنید.
  • محتوای دارای حق نشر (مقاله، عکس، کتاب) را کپی و بازنشر نکنید؛ استخراج داده برای تحلیل با بازنشر محتوا فرق دارد.
  • سرعت درخواست‌ها را پایین نگه دارید تا به سرور فشار نیاورید؛ چند ثانیه مکث بین درخواست‌ها معمول است.
  • از دور زدن کپچا، ورود به حساب دیگران یا عبور از محدودیت‌های امنیتی خودداری کنید.

وقتی BeautifulSoup کافی نیست

BeautifulSoup فقط HTML اولیه‌ای را می‌بیند که سرور برمی‌گرداند. اگر داده‌ها بعد از بارگذاری صفحه با جاوااسکریپت ساخته می‌شوند (مثلاً فهرستی که با اسکرول بیشتر می‌شود)، در response.text خبری از آن‌ها نیست. در این حالت سه راه دارید:

  1. پیدا کردن API پنهان: در DevTools به تب Network بروید و درخواست‌های XHR یا Fetch را ببینید؛ اغلب داده به شکل JSON از یک آدرس جدا می‌آید و می‌توانید همان را با requests بگیرید.
  2. مرورگر خودکار: ابزارهایی مثل Playwright یا Selenium یک مرورگر واقعی را کنترل می‌کنند و صفحه را کامل اجرا می‌کنند؛ کندترند ولی صفحه‌های پویا را پوشش می‌دهند.
  3. فریم‌ورک Scrapy: برای پروژه‌های بزرگ با هزاران صفحه، Scrapy صف درخواست‌ها، تکرار خودکار و خروجی‌گرفتن را مدیریت می‌کند.

برای شروع و برای بیشتر سایت‌های ساده، همان requests و BeautifulSoup کافی است و یادگیری آن‌ها پایه لازم برای ابزارهای پیشرفته‌تر را هم می‌سازد.

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

  • AttributeError: ‘NoneType’ object has no attribute ‘get_text’: یعنی find یا select_one چیزی پیدا نکرده و None برگردانده است. قبل از استفاده، نتیجه را با if element: بررسی کنید. توضیح کامل در رفع AttributeError.
  • داده در HTML نیست: صفحه با جاوااسکریپت ساخته می‌شود؛ بخش قبلی را ببینید.
  • کد وضعیت 403: سرور درخواست را مسدود کرده است. هدر User-Agent را مشخص کنید و سرعت را کم کنید؛ اگر باز هم مسدود بود، سایت نمی‌خواهد اسکرپ شود و باید به آن احترام بگذارید.
  • متن فارسی به‌هم‌ریخته: کدگذاری پاسخ اشتباه تشخیص داده شده است؛ قبل از خواندن response.text مقدار response.encoding = "utf-8" را تنظیم کنید.
  • اسکرپر بعد از مدتی کار نمی‌کند: ساختار HTML سایت تغییر کرده است. انتخابگرها را در یک جا (مثلاً یک دیکشنری) نگه دارید تا اصلاحشان آسان باشد.

تمرین: پروژه کوچک شما

یک سایت ساده و عمومی که اسکرپ کردنش مجاز است انتخاب کنید (مثلاً سایت‌های تمرینی مخصوص اسکرپینگ) و برنامه‌ای بنویسید که عنوان و قیمت همه محصولات صفحه اول را استخراج کند، محصولات زیر یک قیمت مشخص را فیلتر کند و نتیجه را در CSV ذخیره کند. در قدم بعد، صفحه‌بندی را اضافه کنید و برنامه را طوری زمان‌بندی کنید که روزی یک بار اجرا شود. اگر بخواهید نتیجه را در یک صفحه وب نمایش دهید، آموزش Flask نشان می‌دهد چطور با چند خط پایتون یک وب‌سایت ساده بسازید. برای تمرین‌های بیشتر، ۵۰ تمرین پایتون با جواب را ببینید.

سوالات متداول

برای وب اسکرپینگ با پایتون چه کتابخانه‌ای بهتر است؟

برای شروع ترکیب requests و BeautifulSoup ساده‌ترین و رایج‌ترین انتخاب است. برای صفحه‌های پویا Playwright یا Selenium و برای پروژه‌های بزرگ Scrapy مناسب‌تر است.

فرق find و select در BeautifulSoup چیست؟

find و find_all با نام تگ و ویژگی‌ها جست‌وجو می‌کنند، ولی select و select_one انتخابگر CSS می‌گیرند. نتیجه یکسان است و انتخاب بین آن‌ها بیشتر سلیقه‌ای است.

چرا داده‌ای که در مرورگر می‌بینم در response.text نیست؟

چون آن بخش بعد از بارگذاری با جاوااسکریپت ساخته می‌شود. API پنهان را در تب Network پیدا کنید یا از یک مرورگر خودکار مثل Playwright استفاده کنید.

آیا وب اسکرپینگ قانونی است؟

اسکرپ داده‌های عمومی با رعایت قوانین سایت، robots.txt و سرعت معقول معمولاً پذیرفته است؛ اما جمع‌آوری اطلاعات شخصی، بازنشر محتوای دارای حق نشر یا نقض شرایط استفاده سایت می‌تواند مشکل‌ساز باشد.

چطور از مسدود شدن اسکرپر جلوگیری کنم؟

User-Agent مشخص بفرستید، بین درخواست‌ها چند ثانیه مکث کنید، تعداد درخواست‌ها را محدود کنید و مسیرهای ممنوع robots.txt را درخواست ندهید.

آموزش‌های مرتبط

آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *