Giới thiệu: Tại sao cần giải pháp này?
Trong thế giới số, dữ liệu là vàng. Từ nghiên cứu thị trường, theo dõi đối thủ cạnh tranh, đến tổng hợp tin tức tự động, khả năng thu thập dữ liệu từ các website trở thành kỹ năng thiết yếu. Python, với hệ sinh thái thư viện mạnh mẽ, là lựa chọn số một cho tác vụ này. Bài viết này sẽ cung cấp một Hướng dẫn crawl dữ liệu bằng Python thực chiến, giúp bạn xây dựng công cụ thu thập dữ liệu một cách hiệu quả và bền vững. Chúng ta sẽ tập trung vào các kỹ thuật cốt lõi, đảm bảo code chạy được ngay và dễ dàng mở rộng.
Chuẩn bị: Liệt kê thư viện cần cài
Để bắt đầu, chúng ta cần cài đặt hai thư viện chính: requests để gửi yêu cầu HTTP và BeautifulSoup4 (bs4) để phân tích cú pháp HTML.
pip install requests beautifulsoup4
Phân tích Logic: Giải thích ngắn gọn thuật toán sẽ dùng
Chúng ta sẽ xây dựng một trình crawl đơn giản nhưng mạnh mẽ, tuân thủ các bước sau:
- Gửi yêu cầu HTTP: Sử dụng thư viện
requestsđể gửi yêu cầu GET tới URL đích và nhận về nội dung HTML của trang. Chúng ta sẽ xử lý các lỗi kết nối và HTTP một cách mạnh mẽ. - Phân tích HTML: Sử dụng
BeautifulSoupđể chuyển đổi nội dung HTML thành một đối tượng có thể truy vấn được, giúp chúng ta dễ dàng tìm kiếm các phần tử mong muốn. - Trích xuất dữ liệu: Dựa trên cấu trúc HTML của trang, chúng ta sẽ tìm kiếm các thẻ (tags), lớp (classes), hoặc ID cụ thể để lấy ra thông tin như tiêu đề, liên kết, nội dung.
- Lưu trữ dữ liệu: Sau khi trích xuất, dữ liệu sẽ được lưu trữ tạm thời trong một cấu trúc dữ liệu Python (ví dụ: list of dictionaries) để dễ dàng xử lý hoặc xuất ra sau này.
Hướng dẫn Code (Step-by-step)
Chúng ta sẽ xây dựng các hàm độc lập để dễ quản lý và tái sử dụng.
fetch_html: Xử lý việc tải nội dung HTML từ URL.parse_data_from_html: Xử lý việc phân tích và trích xuất dữ liệu từ HTML. (Trong ví dụ thực tế, hàm này sẽ được tùy chỉnh cho từng website).
1. Hàm fetch_html: Tải nội dung trang web một cách an toàn
import requests
from bs4 import BeautifulSoup
import time
import random
def fetch_html(url: str, headers: dict = None) -> str | None:
"""
Tải nội dung HTML từ một URL cho trước.
Sử dụng try-except để xử lý các lỗi kết nối và HTTP.
Args:
url (str): URL của trang web cần tải.
headers (dict, optional): Custom HTTP headers. Mặc định là None.
Returns:
str | None: Nội dung HTML của trang nếu thành công, ngược lại là None.
"""
if headers is None:
headers = {
# Giả lập một trình duyệt Chrome phổ biến
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1', # Yêu cầu HTTPS
}
try:
# Gửi yêu cầu GET tới URL với headers và timeout
response = requests.get(url, headers=headers, timeout=15) # Tăng timeout lên 15 giây để tránh lỗi
response.raise_for_status() # Ném ngoại lệ cho các mã trạng thái HTTP lỗi (4xx hoặc 5xx)
print(f"✅ Đã tải thành công URL: {url} (Status: {response.status_code})")
return response.text
except requests.exceptions.HTTPError as e:
print(f"❌ Lỗi HTTP khi tải {url}: {e} (Status: {e.response.status_code if e.response else 'N/A'})")
except requests.exceptions.ConnectionError as e:
print(f"❌ Lỗi kết nối khi tải {url}: {e}")
except requests.exceptions.Timeout as e:
print(f"❌ Hết thời gian chờ (Timeout) khi tải {url}: {e}")
except requests.exceptions.RequestException as e:
print(f"❌ Một lỗi request không xác định xảy ra khi tải {url}: {e}")
return None
Giải thích:
headers: Rất quan trọng! Nhiều trang web chặn các yêu cầu không cóUser-Agenthợp lệ. Chúng ta giả lập một trình duyệt Chrome để vượt qua các kiểm tra cơ bản. Các headers khác giúp mô phỏng yêu cầu của trình duyệt thực.requests.get(url, headers=headers, timeout=15): Gửi yêu cầu GET.timeoutgiúp tránh treo chương trình nếu server phản hồi quá lâu.response.raise_for_status(): Kiểm trastatus_codecủa phản hồi. Nếu là lỗi (ví dụ: 404 Not Found, 500 Internal Server Error), nó sẽ tự động ném rarequests.exceptions.HTTPError.try-except: Bắt các loại lỗi phổ biến nhất trong quá trình gửi request, giúp chương trình không bị crash và cung cấp thông báo lỗi rõ ràng.
2. Hàm parse_data_from_html: Phân tích HTML và trích xuất dữ liệu (Ví dụ)
Hàm này sẽ luôn cần được tùy chỉnh dựa trên cấu trúc HTML của website mục tiêu. Dưới đây là một ví dụ hàm parse_articles giả định một cấu trúc blog phổ biến.
def parse_articles(html_content: str) -> list[dict]:
"""
Phân tích nội dung HTML để trích xuất tiêu đề, URL và tóm tắt bài viết.
Sử dụng BeautifulSoup để tìm kiếm các phần tử dựa trên cấu trúc HTML giả định:
- Một div cha có class 'post-list'.
- Bên trong có nhiều div con có class 'post-item'.
- Mỗi 'post-item' chứa h3.post-title (với thẻ a chứa link và text), cùng p.post-summary.
Args:
html_content (str): Nội dung HTML của trang web.
Returns:
list[dict]: Danh sách các dictionary, mỗi dictionary chứa 'title', 'url' và 'summary' của bài viết.
"""
if not html_content:
print("⚠️ Nội dung HTML rỗng, không thể phân tích.")
return []
soup = BeautifulSoup(html_content, 'html.parser')
articles_data = []
try:
# Tìm div cha chứa danh sách bài viết
post_list_div = soup.find('div', class_='post-list')
if not post_list_div:
print("❌ Không tìm thấy div 'post-list'. Có thể cấu trúc HTML đã thay đổi hoặc không tồn tại.")
return []
# Tìm tất cả các bài viết con trong div 'post-list'
article_items = post_list_div.find_all('div', class_='post-item')
if not article_items:
print("❌ Không tìm thấy bài viết nào trong 'post-list'. Kiểm tra lại selector 'post-item'.")
return []
for item in article_items:
title = None
url = None
summary = "N/A" # Giá trị mặc định nếu không tìm thấy tóm tắt
try:
# Trích xuất tiêu đề và URL
title_tag = item.find('h3', class_='post-title')
if title_tag:
link_tag = title_tag.find('a', href=True) # Đảm bảo thẻ a có thuộc tính href
if link_tag:
title = link_tag.get_text(strip=True)
url = link_tag.get('href')
else:
title = title_tag.get_text(strip=True) # Lấy text trực tiếp từ h3 nếu không có link
# Trích xuất tóm tắt
summary_tag = item.find('p', class_='post-summary')
if summary_tag:
summary = summary_tag.get_text(strip=True)
if title and url:
articles_data.append({
'title': title,
'url': url,
'summary': summary
})
else:
# In cảnh báo nếu không đủ dữ liệu từ một item
print(f"⚠️ Không thể trích xuất đầy đủ dữ liệu từ một bài viết. Title: {title}, URL: {url}")
except AttributeError as e:
# Xử lý lỗi khi một thuộc tính không tồn tại (vd: .get_text() trên None)
print(f"❌ Lỗi AttributeError khi phân tích một item: {e}. Có thể một phần tử không tồn tại. Item HTML: {item}")
except Exception as e:
# Xử lý các lỗi không xác định khác trong quá trình xử lý item
print(f"❌ Lỗi không xác định khi xử lý item: {e}. Item HTML: {item}")
except Exception as e:
print(f"❌ Lỗi chung khi phân tích HTML: {e}")
return articles_data
Giải thích:
BeautifulSoup(html_content, 'html.parser'): Khởi tạo đối tượng BeautifulSoup để phân tích HTML.soup.find(),soup.find_all(): Các phương thức chính để tìm kiếm các phần tử HTML. Bạn cần sử dụng “Inspect Element” trong trình duyệt để xác định đúngtag,class,idcần tìm..get_text(strip=True): Lấy nội dung văn bản bên trong thẻ, loại bỏ khoảng trắng thừa..get('href'): Lấy giá trị của thuộc tínhhref(hoặc bất kỳ thuộc tính nào khác).try-exceptlồng nhau: Rất quan trọng trong parsing. Cấu trúc HTML có thể không đồng nhất hoặc một phần tử mong muốn không tồn tại.AttributeErrorthường xảy ra khi bạn cố gắng truy cập một thuộc tính của một đối tượngNone(ví dụ:None.get_text()).
3. Hàm chính main và chạy script với ví dụ thực tế
Trong hàm main, chúng ta sẽ áp dụng các hàm trên vào một website thực tế có thể crawl được là books.toscrape.com. Lưu ý rằng phần parsing cho books.toscrape.com sẽ khác so với hàm parse_articles mẫu ở trên vì cấu trúc HTML khác nhau. Điều này nhấn mạnh rằng mỗi website sẽ yêu cầu một logic parsing riêng.
def main():
"""
Hàm chính để thực hiện quá trình crawl dữ liệu.
Ví dụ này crawl dữ liệu từ http://books.toscrape.com/catalogue/category/books_1/index.html
"""
target_url = "http://books.toscrape.com/catalogue/category/books_1/index.html" # Một trang web ví dụ
print(f"🚀 Bắt đầu crawl dữ liệu từ: {target_url}")
html_content = fetch_html(target_url)
if html_content:
# --- Bắt đầu phần parsing TÙY CHỈNH cho books.toscrape.com ---
# Hàm `parse_articles` ở trên được viết cho cấu trúc blog giả định.
# Với books.toscrape.com, chúng ta cần một logic parsing khác.
books_data = []
try:
soup = BeautifulSoup(html_content, 'html.parser')
# Tìm tất cả các item sách, mỗi cuốn nằm trong thẻ <article> có class 'product_pod'
book_items = soup.find_all('article', class_='product_pod')
if not book_items:
print("❌ Không tìm thấy sách nào trên trang. Kiểm tra lại selector 'product_pod'.")
return
for item in book_items:
title = "N/A"
url = "N/A"
price = "N/A"
try:
# Tiêu đề và URL nằm trong h3 > a
title_tag = item.find('h3')
if title_tag:
link_tag = title_tag.find('a', href=True)
if link_tag:
title = link_tag.get('title', strip=True) # Tiêu đề sách thường nằm ở thuộc tính 'title' của thẻ link
# URL trên books.toscrape.com là tương đối, cần ghép với base URL
relative_url = link_tag.get('href')
# Base URL của trang này: http://books.toscrape.com/catalogue/
# Đối với books.toscrape.com, các relative URL có dạng ../../path/to/book.html
# Ta cần loại bỏ ../../ và ghép với domain
base_domain = "http://books.toscrape.com/catalogue/"
url = base_domain + relative_url.replace('../', '')
# Giá nằm trong p có class 'price_color'
price_tag = item.find('p', class_='price_color')
if price_tag:
price = price_tag.get_text(strip=True)
books_data.append({
'title': title,
'url': url,
'price': price
})
except AttributeError as e:
print(f"❌ Lỗi AttributeError khi phân tích item sách: {e}")
except Exception as e:
print(f"❌ Lỗi không xác định khi xử lý item sách: {e}")
if books_data:
print("n--- Dữ liệu sách đã crawl được ---")
for i, book in enumerate(books_data):
print(f"[{i+1}] Tiêu đề: {book['title']}")
print(f" URL: {book['url']}")
print(f" Giá: {book['price']}")
print("-" * 20)
print(f"n✅ Hoàn thành crawl. Tổng cộng {len(books_data)} cuốn sách được tìm thấy.")
else:
print("⚠️ Không tìm thấy sách nào hoặc có lỗi trong quá trình phân tích.")
except Exception as e:
print(f"❌ Lỗi chung khi phân tích dữ liệu sách: {e}")
# --- Kết thúc phần parsing TÙY CHỈNH ---
# *Lưu ý*: Nếu bạn muốn test hàm `parse_articles` gốc (được thiết kế cho cấu trúc blog giả định),
# bạn có thể tạo một chuỗi HTML mẫu như sau và truyền vào hàm:
# mock_html_content = """
# <div class="post-list">
# <div class="post-item">
# <h3 class="post-title"><a href="/blog/bai-viet-1">Tiêu đề bài viết số 1</a></h3>
# <p class="post-summary">Đây là tóm tắt của bài viết số 1, nói về Python và automation.</p>
# </div>
# <div class="post-item">
# <h3 class="post-title"><a href="/blog/bai-viet-2">Tiêu đề bài viết số 2</a></h3>
# <p class="post-summary">Bài viết số 2 trình bày về cách sử dụng BeautifulSoup hiệu quả.</p>
# </div>
# </div>
# """
# articles_from_mock = parse_articles(mock_html_content)
# if articles_from_mock:
# print("n--- Dữ liệu bài viết giả định (từ hàm parse_articles gốc) ---")
# for i, article in enumerate(articles_from_mock):
# print(f"[{i+1}] Tiêu đề: {article['title']}")
# print(f" URL: {article['url']}")
# print(f" Tóm tắt: {article['summary'][:70]}...")
# print("-" * 20)
else:
print("❌ Không thể tải nội dung HTML từ URL đã cho.")
if __name__ == "__main__":
main()
Giải thích:
target_url: Đây là URL của trangbooks.toscrape.commà chúng ta sẽ crawl làm ví dụ.if __name__ == "__main__":: Đảm bảo rằng hàmmain()chỉ được gọi khi script được chạy trực tiếp.- Logic Parsing TÙY CHỈNH: Bạn có thể thấy, để crawl
books.toscrape.com, chúng ta cần viết lại logic tìm kiếm phần tử khác hoàn toàn so vớiparse_articlesmẫu (được viết cho cấu trúc blog giả định). Đây là điều CẦN THIẾT khi làm việc với các trang web khác nhau. - Xử lý URL tương đối: Với
books.toscrape.com, các URL bài viết con là tương đối (ví dụ:../../a-light-in-the-attic_1000/index.html). Chúng ta cần ghép chúng với base domain để tạo thành URL tuyệt đối. - Độ trễ ngẫu nhiên: Trong các ứng dụng thực tế, bạn sẽ thêm
time.sleep(random.uniform(X, Y))giữa các request hoặc các lần crawl trang để tránh bị chặn IP.
Xử lý lỗi thường gặp
-
Lỗi kết nối / HTTP (
ConnectionError,HTTPError,Timeout):- Nguyên nhân: URL sai, không có mạng, server từ chối kết nối, server phản hồi chậm, hoặc trang web chặn yêu cầu từ script của bạn.
- Cách khắc phục:
- Kiểm tra lại URL, đảm bảo có mạng.
- Tăng
timeouttrongrequests.get(). - Quan trọng: Luôn sử dụng
User-Agenthợp lệ (như đã demo). Nếu vẫn bị chặn, thử cácUser-Agentkhác hoặc cân nhắc sử dụng proxy. - Thêm
time.sleep()giữa các request nếu crawl nhiều trang để tránh bị đánh dấu là bot. - Code fix: Đã có trong hàm
fetch_htmlvớitry-excepttoàn diện.
-
Cấu trúc HTML thay đổi / Selector sai (
AttributeError,IndexError):- Nguyên nhân: Website thường xuyên cập nhật giao diện, làm thay đổi tên class, ID hoặc cấu trúc thẻ HTML. Khi đó, các selector (
class_,id,find,find_all) của bạn sẽ không tìm thấy phần tử.AttributeErrorthường xảy ra khi bạn cố gắng gọi phương thức trên một đối tượngNone(ví dụ:soup.find('non-existent-tag').get_text()). - Cách khắc phục:
- Sử dụng “Inspect Element” (Kiểm tra phần tử) trong trình duyệt (F12) để xem cấu trúc HTML hiện tại của trang web mục tiêu.
- Điều chỉnh các selector trong phần parsing cho phù hợp. Tìm các selector ít thay đổi nhất (ví dụ:
idthường ổn định hơnclass). - Luôn kiểm tra kết quả của
find()trước khi gọi các phương thức tiếp theo (ví dụ:if tag_found: tag_found.get_text()). - Code fix: Đã có trong hàm
parse_articlesmẫu và logic parsing trongmainvới các kiểm traif not element:vàtry-exceptlồng nhau.
- Nguyên nhân: Website thường xuyên cập nhật giao diện, làm thay đổi tên class, ID hoặc cấu trúc thẻ HTML. Khi đó, các selector (
-
Bị chặn IP / Rate Limiting:
- Nguyên nhân: Bạn gửi quá nhiều request trong một thời gian ngắn, khiến server nhận diện bạn là bot và chặn IP của bạn.
- Cách khắc phục:
- Sử dụng
time.sleep()để thêm độ trễ ngẫu nhiên giữa các request (ví dụ:time.sleep(random.uniform(2, 5))). - Thử thay đổi
User-Agentsau một số lượng request nhất định (User-Agent rotation). - Sử dụng proxy xoay vòng (rotating proxies) – kỹ thuật nâng cao hơn để thay đổi IP.
- Code fix: Đã import
randomvàtime, bạn có thể thêmtime.sleep(random.uniform(min_delay, max_delay))trong vòng lặp crawl nếu cần.
- Sử dụng
-
Website sử dụng JavaScript mạnh / CAPTCHA:
- Nguyên nhân: Nội dung bạn muốn crawl được tạo ra bởi JavaScript sau khi trang tải (client-side rendering), hoặc trang web yêu cầu bạn giải CAPTCHA.
- Cách khắc phục:
- Các giải pháp dựa trên
requestsvàBeautifulSoupsẽ không hoạt động hiệu quả với các trang này. - Cần sử dụng các thư viện điều khiển trình duyệt thực như
SeleniumhoặcPlaywrightđể thực thi JavaScript. - Giải quyết CAPTCHA đòi hỏi các dịch vụ bên thứ ba hoặc thuật toán AI phức tạp.
- Lưu ý: Đây là các kỹ thuật nâng cao hơn, nằm ngoài phạm vi của hướng dẫn cơ bản này.
- Các giải pháp dựa trên
Kết luận & Source code toàn văn
Hy vọng với Hướng dẫn crawl dữ liệu bằng Python này, bạn đã có một cái nhìn tổng quan và một công cụ thực chiến để bắt đầu hành trình thu thập dữ liệu của mình. Hãy nhớ luôn crawl một cách có trách nhiệm: tôn trọng robots.txt của website, không gây quá tải cho server, và tuân thủ điều khoản dịch vụ (Terms of Service) của trang web. Việc crawl dữ liệu là một kỹ năng mạnh mẽ, hãy sử dụng nó một cách có đạo đức.
Dưới đây là toàn bộ source code bạn có thể copy và chạy ngay:
import requests
from bs4 import BeautifulSoup
import time
import random # Để tạo độ trễ ngẫu nhiên
def fetch_html(url: str, headers: dict = None) -> str | None:
"""
Tải nội dung HTML từ một URL cho trước.
Sử dụng try-except để xử lý các lỗi kết nối và HTTP.
Args:
url (str): URL của trang web cần tải.
headers (dict, optional): Custom HTTP headers. Mặc định là None.
Returns:
str | None: Nội dung HTML của trang nếu thành công, ngược lại là None.
"""
if headers is None:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
}
try:
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
print(f"✅ Đã tải thành công URL: {url} (Status: {response.status_code})")
return response.text
except requests.exceptions.HTTPError as e:
print(f"❌ Lỗi HTTP khi tải {url}: {e} (Status: {e.response.status_code if e.response else 'N/A'})")
except requests.exceptions.ConnectionError as e:
print(f"❌ Lỗi kết nối khi tải {url}: {e}")
except requests.exceptions.Timeout as e:
print(f"❌ Hết thời gian chờ (Timeout) khi tải {url}: {e}")
except requests.exceptions.RequestException as e:
print(f"❌ Một lỗi request không xác định xảy ra khi tải {url}: {e}")
return None
def parse_articles(html_content: str) -> list[dict]:
"""
Hàm mẫu phân tích nội dung HTML cho cấu trúc blog giả định.
Mục đích minh họa cách xây dựng hàm parser, không áp dụng cho ví dụ books.toscrape.com trong main.
Cấu trúc giả định:
<div class="post-list">
<div class="post-item">
<h3 class="post-title"><a href="/blog/bai-viet-1">Tiêu đề bài viết số 1</a></h3>
<p class="post-summary">Tóm tắt ngắn gọn bài viết 1...</p>
</div>
...
</div>
"""
if not html_content:
print("⚠️ Nội dung HTML rỗng, không thể phân tích.")
return []
soup = BeautifulSoup(html_content, 'html.parser')
articles_data = []
try:
post_list_div = soup.find('div', class_='post-list')
if not post_list_div:
print("❌ Không tìm thấy div 'post-list' trong HTML mẫu. Cấu trúc HTML có thể không khớp.")
return []
article_items = post_list_div.find_all('div', class_='post-item')
if not article_items:
print("❌ Không tìm thấy bài viết nào trong 'post-list' trong HTML mẫu.")
return []
for item in article_items:
title = None
url = None
summary = "N/A"
try:
title_tag = item.find('h3', class_='post-title')
if title_tag:
link_tag = title_tag.find('a', href=True)
if link_tag:
title = link_tag.get_text(strip=True)
url = link_tag.get('href')
else:
title = title_tag.get_text(strip=True)
summary_tag = item.find('p', class_='post-summary')
if summary_tag:
summary = summary_tag.get_text(strip=True)
if title and url:
articles_data.append({
'title': title,
'url': url,
'summary': summary
})
else:
print(f"⚠️ Không thể trích xuất đầy đủ dữ liệu từ một bài viết mẫu. Title: {title}, URL: {url}")
except AttributeError as e:
print(f"❌ Lỗi AttributeError khi phân tích item mẫu: {e}. Item HTML: {item}")
except Exception as e:
print(f"❌ Lỗi không xác định khi xử lý item mẫu: {e}")
except Exception as e:
print(f"❌ Lỗi chung khi phân tích HTML mẫu: {e}")
return articles_data
def main():
"""
Hàm chính để thực hiện quá trình crawl dữ liệu.
"""
# Thay đổi URL này bằng URL bạn muốn crawl
# VÍ DỤ THỰC TẾ: books.toscrape.com
target_url = "http://books.toscrape.com/catalogue/category/books_1/index.html"
print(f"🚀 Bắt đầu crawl dữ liệu từ: {target_url}")
html_content = fetch_html(target_url)
if html_content:
# --- Bắt đầu phần parsing TÙY CHỈNH cho books.toscrape.com ---
# Mỗi website có cấu trúc HTML riêng, nên hàm parsing cũng cần được điều chỉnh.
# Hàm `parse_articles` ở trên chỉ là ví dụ cho một cấu trúc blog giả định.
books_data = []
try:
soup = BeautifulSoup(html_content, 'html.parser')
# Tìm tất cả các item sách, mỗi cuốn nằm trong thẻ <article> có class 'product_pod'
book_items = soup.find_all('article', class_='product_pod')
if not book_items:
print("❌ Không tìm thấy sách nào trên trang. Kiểm tra lại selector 'product_pod'.")
return
for item in book_items:
title = "N/A"
url = "N/A"
price = "N/A"
try:
# Tiêu đề và URL nằm trong h3 > a
title_tag = item.find('h3')
if title_tag:
link_tag = title_tag.find('a', href=True)
if link_tag:
title = link_tag.get('title', strip=True)
relative_url = link_tag.get('href')
base_domain = "http://books.toscrape.com/catalogue/"
url = base_domain + relative_url.replace('../', '') # Xử lý URL tương đối
else:
title = title_tag.get_text(strip=True) # Fallback nếu không có link
# Giá nằm trong p có class 'price_color'
price_tag = item.find('p', class_='price_color')
if price_tag:
price = price_tag.get_text(strip=True)
books_data.append({
'title': title,
'url': url,
'price': price
})
except AttributeError as e:
print(f"❌ Lỗi AttributeError khi phân tích item sách: {e}")
except Exception as e:
print(f"❌ Lỗi không xác định khi xử lý item sách: {e}")
if books_data:
print("n--- Dữ liệu sách đã crawl được ---")
for i, book in enumerate(books_data):
print(f"[{i+1}] Tiêu đề: {book['title']}")
print(f" URL: {book['url']}")
print(f" Giá: {book['price']}")
print("-" * 20)
print(f"n✅ Hoàn thành crawl. Tổng cộng {len(books_data)} cuốn sách được tìm thấy.")
else:
print("⚠️ Không tìm thấy sách nào hoặc có lỗi trong quá trình phân tích.")
except Exception as e:
print(f"❌ Lỗi chung khi phân tích dữ liệu sách: {e}")
# --- Kết thúc phần parsing TÙY CHỈNH ---
# Để minh họa hàm `parse_articles` (hàm mẫu cho cấu trúc blog giả định),
# bạn có thể sử dụng đoạn HTML mẫu dưới đây:
# mock_html_content = """
# <div class="post-list">
# <div class="post-item">
# <h3 class="post-title"><a href="/blog/bai-viet-1">Tiêu đề bài viết số 1</a></h3>
# <p class="post-summary">Đây là tóm tắt của bài viết số 1, nói về Python và automation. Đây là đoạn tóm tắt dài để test.</p>
# </div>
# <div class="post-item">
# <h3 class="post-title"><a href="/blog/bai-viet-2">Tiêu đề bài viết số 2</a></h3>
# <p class="post-summary">Bài viết số 2 trình bày về cách sử dụng BeautifulSoup hiệu quả trong việc parsing HTML.</p>
# </div>
# <div class="post-item">
# <h3 class="post-title"><a href="/blog/bai-viet-3">Tiêu đề bài viết số 3 (không có tóm tắt)</a></h3>
# <!-- Bài viết này cố tình không có thẻ p.post-summary để test lỗi -->
# </div>
# <div class="post-item">
# <h3 class="post-title">Tiêu đề bài viết số 4 (chỉ có tiêu đề, không link)</h3>
# <p class="post-summary">Tóm tắt của bài viết 4.</p>
# </div>
# </div>
# """
# print("nn--- Đang chạy minh họa hàm `parse_articles` với dữ liệu mẫu ---")
# articles_from_mock = parse_articles(mock_html_content)
# if articles_from_mock:
# for i, article in enumerate(articles_from_mock):
# print(f"[{i+1}] Tiêu đề: {article['title']}")
# print(f" URL: {article['url']}")
# print(f" Tóm tắt: {article['summary'][:70]}...") # Giới hạn tóm tắt
# print("-" * 20)
# else:
# print("⚠️ Không tìm thấy bài viết nào từ HTML mẫu hoặc có lỗi khi phân tích.")
else:
print("❌ Không thể tải nội dung HTML từ URL đã cho.")
if __name__ == "__main__":
main()
See more: Hướng dẫn crawl dữ liệu bằng Python.
Discover: Python Trick.
