وب اسکرپینگ با پایتون؛ استخراج داده از سایت با 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 خبری از آنها نیست. در این حالت سه راه دارید:
- پیدا کردن API پنهان: در DevTools به تب Network بروید و درخواستهای XHR یا Fetch را ببینید؛ اغلب داده به شکل JSON از یک آدرس جدا میآید و میتوانید همان را با requests بگیرید.
- مرورگر خودکار: ابزارهایی مثل Playwright یا Selenium یک مرورگر واقعی را کنترل میکنند و صفحه را کامل اجرا میکنند؛ کندترند ولی صفحههای پویا را پوشش میدهند.
- فریمورک 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 را درخواست ندهید.
آموزشهای مرتبط
- آموزش جامع پایتون از صفر تا پیشرفته
- اتوماسیون و ساخت ربات با پایتون
- کار با فایل JSON در پایتون
- مدیریت خطا با try/except
- نصب کتابخانه در پایتون با pip
- دوره پایتون سطح متوسط