Tùy biến Nâng cao
Mục lục
1. Cấu Trúc SiteConfig và Sitemap Động (DynamicSitemapResolver)
Cấu hình của từng trang báo được lưu trữ tại bảng từ điển SITES_CONFIG trong vnstock_news.config.sites. Mỗi cấu hình bao gồm các thông tin:
name: Tên hiển thị của báo (ví dụ:"Báo CafeF").domain: Tên miền chính (ví dụ:"cafef.vn").rss: Danh sách đường dẫn RSS Feed (urls: [...]).sitemap_url: Đường dẫn tới Sitemap XML chính.sitemap: Cấu hình Sitemap động khi trang báo chia sitemap theo thời gian (pattern_typedạng"monthly"hoặc"incremental").config: Danh sách bộ chọn CSS/XPath để bóc tách tiêu đề, sapo, nội dung, thời gian xuất bản, tác giả và chuyên mục.
Xử lý Sitemap Động
Một số trang báo (như Báo Pháp luật TP.HCM - PLO, Thời báo Kinh tế Sài Gòn) phân chia sitemap theo từng tháng hoặc dạng lũy tiến.
Lớp DynamicSitemapResolver trong vnstock_news hỗ trợ tự động xác định đường dẫn Sitemap theo thời gian:
from vnstock_news.config.sitemap_resolver import DynamicSitemapResolver
from vnstock_news.config.sites import SITES_CONFIG
# 1. Khởi tạo resolver ở chế độ ghi log
resolver = DynamicSitemapResolver(debug=True)
# 2. Lấy cấu hình của báo có sitemap động (ví dụ: PLO)
plo_config = SITES_CONFIG["plo"]
# 3. Tính toán URL sitemap phù hợp với thời điểm hiện tại (năm - tháng)
resolved_url = resolver.get_sitemap_url("plo", plo_config)
print(f"Sitemap URL được tạo: {resolved_url}")
# Kết quả trả về dạng: https://plo.vn/sitemaps/news-2026-8.xmlnews-2026-08.xml) hoặc theo ngày, bạn có thể thiết lập thuộc tính sitemap với pattern_type: "monthly" hoặc "incremental" cùng chuỗi định dạng format tương ứng.
2. Thiết Lập Bộ Chọn CSS Dự Phòng
Khi cấu hình giao diện bài viết giữa các chuyên mục không đồng nhất, vnstock_news hỗ trợ truyền danh sách nhiều bộ chọn (CSS Selectors) theo thứ tự ưu tiên. Thư viện sẽ lần lượt thử nghiệm các bộ chọn trong danh sách cho đến khi tìm thấy dữ liệu.
Các bộ chọn chuẩn trong config
Dictionary config hỗ trợ các khóa sau:
title_selector: Tiêu đề bài viết.short_desc_selector: Đoạn tóm tắt / Sapo.content_selector: Nội dung chính bài viết.publish_time_selector: Thời gian xuất bản.author_selector: Tên tác giả.category_selector: Chuyên mục.tags_selector: Từ khóa / Tags.image_url_selector: Ảnh đại diện bài viết.
Cấu hình bộ chọn dự phòng và thuộc tính thẻ
Một bộ chọn có thể cấu hình tìm kiếm theo thẻ HTML (tag), tên lớp (class) hoặc lấy trực tiếp thuộc tính thẻ (attr như content, datetime, src):
custom_advanced_selectors = {
# Tiêu đề: Thử class "detail-title" trước, nếu không có chuyển sang thẻ meta
"title_selector": [
{"tag": "h1", "class": "detail-title"},
{"tag": "meta", "attr": "property", "value": "og:title", "extract": "content"}
],
# Sapo bài viết (phần tóm tắt ở đầu)
"short_desc_selector": [
{"tag": "h2", "class": "sapo-detail"},
{"tag": "div", "class": "sapo"}
],
# Nội dung bài viết
"content_selector": {"tag": "div", "class": "detail-content"},
# Thời gian xuất bản: Lấy thuộc tính datetime trong thẻ <time> hoặc content của meta
"publish_time_selector": [
{"tag": "time", "attr": "datetime"},
{"tag": "meta", "attr": "property", "value": "article:published_time", "extract": "content"},
{"tag": "span", "class": "post-time"}
],
# Tác giả
"author_selector": {"tag": "span", "class": "author-name"}
}3. Trích Xuất Dữ Liệu Cấu Trúc JSON-LD và JavaScript DataLayer
Một số trang báo hiện đại nhúng thông tin bài viết trong các thẻ script dạng JSON-LD Schema (<script type="application/ld+json">) hoặc biến JavaScript toàn cục.
Cơ chế trích xuất dữ liệu cấu trúc
Khi bộ chọn HTML không tìm thấy dữ liệu, vnstock_news sẽ tự động quét các thẻ <script> có định dạng JSON-LD:
{
"@context": "https://schema.org",
"@type": "NewsArticle",
"headline": "VN-Index tăng điểm phiên đầu tuần",
"datePublished": "2026-08-07T09:30:00+07:00",
"author": {
"@type": "Person",
"name": "Phương Nam"
},
"articleBody": "Nội dung chi tiết bài viết..."
}Thư viện sẽ trích xuất thông tin từ các trường @type == NewsArticle hoặc Article, lấy headline cho tiêu đề, datePublished cho thời gian xuất bản và articleBody cho nội dung. Cơ chế này giúp thu thập dữ liệu ổn định kể cả khi giao diện bài viết thay đổi.
4. Tự Định Nghĩa Trang Báo Mới (custom_config)
Bạn có thể tự định nghĩa đối tượng custom_config để thu thập dữ liệu từ các trang web chưa có sẵn trong cấu hình mặc định.
custom_config, bạn không truyền tham số site_name (để site_name=None). Nếu truyền đồng thời cả site_name và custom_config, thư viện sẽ ưu tiên sử dụng cấu hình mặc định của site_name.
Ví dụ: Thu thập trang web mới bằng AsyncBatchCrawler
import asyncio
import pandas as pd
from vnstock_news import AsyncBatchCrawler
async def crawl_custom_unsupported_site():
# 1. Định nghĩa cấu hình cho trang báo mới
custom_site_profile = {
"site_name": "Trang tin tức custom",
"domain": "thanhnien.vn",
"config": {
"title_selector": {"tag": "h1", "class": "detail-title"},
"short_desc_selector": {"tag": "h2", "class": "detail-sapo"},
"content_selector": {"tag": "div", "class": "detail-cmain"},
"publish_time_selector": {"tag": "div", "class": "detail-time"},
"author_selector": {"tag": "div", "class": "author-name"}
}
}
# 2. Khởi tạo AsyncBatchCrawler với custom_config (không truyền site_name)
crawler = AsyncBatchCrawler(
custom_config=custom_site_profile,
max_concurrency=3,
debug=False
)
# 3. Chỉ định nguồn thu thập (RSS Feed hoặc Sitemap XML)
sources = ["https://thanhnien.vn/rss/home.rss"]
# 4. Thực thi thu thập dữ liệu
df = await crawler.fetch_articles_async(
sources=sources,
top_n=5
)
if not df.empty:
print(f"Thu thập thành công {len(df)} bài viết từ nguồn tùy chỉnh:")
print(df[["title", "publish_time", "author"]].head())
else:
print("Không lấy được dữ liệu.")
if __name__ == "__main__":
asyncio.run(crawl_custom_unsupported_site())5. Phân Tích Xu Hướng Từ Khóa Với TrendingAnalyzer
Lớp TrendingAnalyzer hỗ trợ thống kê tần suất xuất hiện của các cụm từ (n-gram) trong nội dung bài viết để phân tích xu hướng tin tức.
TrendingAnalyzer cần được nhập từ module tương ứng: from vnstock_news.trending.analyzer import TrendingAnalyzer.
Các phương thức chính của TrendingAnalyzer
update_trends(text, ngram_range=[2, 3, 4]): Cập nhật tần suất xuất hiện của các cụm từ trong đoạn văn bản.get_top_trends(top_n=20): Trả về danh sách các cụm từ xuất hiện nhiều nhất cùng tần suất.reset_trends(): Đặt lại bộ đếm tần suất.
Ví dụ phân tích cụm từ xu hướng
import asyncio
from vnstock_news.trending.analyzer import TrendingAnalyzer
from vnstock_news import AsyncBatchCrawler, SITES_CONFIG
async def analyze_market_trending_phrases():
# 1. Tải bài viết từ CafeF
site = "cafef"
sitemap_url = SITES_CONFIG[site]["sitemap_url"]
crawler = AsyncBatchCrawler(site_name=site, max_concurrency=3)
df = await crawler.fetch_articles_async(sources=[sitemap_url], top_n=30)
if df.empty:
print("Không có dữ liệu bài viết để phân tích.")
return
# 2. Khởi tạo TrendingAnalyzer
analyzer = TrendingAnalyzer(min_token_length=3)
# 3. Cập nhật cụm từ (2, 3, 4 từ) từ tiêu đề và tóm tắt
for _, row in df.iterrows():
title = str(row.get("title", ""))
desc = str(row.get("short_description", ""))
full_text = f"{title} {desc}"
analyzer.update_trends(full_text, ngram_range=[2, 3, 4])
# 4. Lấy top 15 cụm từ xuất hiện nhiều nhất
top_phrases = analyzer.get_top_trends(top_n=15)
print("Danh sách cụm từ phổ biến:")
for rank, (phrase, count) in enumerate(top_phrases.items(), 1):
print(f" {rank:2d}. Cụm từ: '{phrase:25s}' - Số lần: {count:3d}")
if __name__ == "__main__":
asyncio.run(analyze_market_trending_phrases())6. Quy Trình Thu Thập Đa Trang và Kiểm Tra Chất Lượng Dữ Liệu
Đoạn mã dưới đây hỗ trợ kiểm tra khả năng bóc tách dữ liệu trên nhiều trang báo khác nhau, đồng thời kiểm tra tính đầy đủ của các trường thông tin chính (title, content, publish_time, author):
import asyncio
import pandas as pd
import warnings
from vnstock_news import AsyncBatchCrawler, SITES_CONFIG, SUPPORTED_SITES
warnings.filterwarnings("ignore")
async def run_multi_site_pipeline_and_health_check():
health_reports = []
collected_datasets = []
for site in SUPPORTED_SITES[:5]: # Chạy thử nghiệm trên 5 trang đầu tiên
print(f"Đang kiểm tra trang: [{site.upper()}] ...")
cfg = SITES_CONFIG[site]
crawler = AsyncBatchCrawler(site_name=site, max_concurrency=2)
sources = []
if "rss" in cfg and cfg["rss"].get("urls"):
sources = [cfg["rss"]["urls"][0]]
elif "sitemap_url" in cfg:
sources = [cfg["sitemap_url"]]
if not sources:
health_reports.append({"site": site, "status": "FAIL (No Source)"})
continue
try:
df = await crawler.fetch_articles_async(sources=sources, top_n=3)
if df.empty:
health_reports.append({"site": site, "status": "EMPTY"})
else:
df["source_site"] = site
collected_datasets.append(df)
row = df.iloc[0]
def _is_valid(val):
if pd.isna(val) or val is None: return False
if isinstance(val, str) and not val.strip(): return False
return True
report = {
"site": site,
"status": "SUCCESS",
"title": "✅" if _is_valid(row.get("title")) else "❌",
"content": "✅" if _is_valid(row.get("content")) else "❌",
"publish_time": "✅" if _is_valid(row.get("publish_time")) else "❌",
"author": "✅" if _is_valid(row.get("author")) else "❌"
}
health_reports.append(report)
except Exception as e:
health_reports.append({"site": site, "status": "ERROR", "error": str(e)})
# Hiển thị kết quả kiểm tra
df_health = pd.DataFrame(health_reports)
print("\nKết quả kiểm tra dữ liệu:")
print(df_health.to_string(index=False))
if __name__ == "__main__":
asyncio.run(run_multi_site_pipeline_and_health_check())
Thảo luận