Crawl dữ liệu và MongoDB
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Phân biệt được các lớp quy tắc khi lấy dữ liệu từ web: robots.txt, điều khoản dịch vụ (Terms of Service), bản quyền và dữ liệu cá nhân, và hiểu vì sao robots.txt cho phép vẫn chưa đủ.
- Đọc được robots.txt theo RFC 9309 bằng
urllib.robotparser, kể cả cách xử lý khi tệp không có hoặc server lỗi. - Viết được một crawler nhỏ, lịch sự bằng
requests(cótimeout,User-Agenttrung thực, giãn cách giữa các yêu cầu) và bóc phần chữ có ích bằng BeautifulSoup. - Lưu trang vào MongoDB dưới dạng document, dùng chỉ mục duy nhất (unique index) trên
urlvà upsert để chạy lại không sinh bản trùng.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Functions, Strings, and File I/O, in Python(Not translated yet) (Learned)
Trực giác
Phần tiêu đề “Trực giác”Bạn muốn làm một chatbot trả lời câu hỏi về tài liệu hướng dẫn của một thư viện mã nguồn mở. Tài liệu có vài trăm trang trên web. Chép tay từng trang thì mất cả tuần, nên bạn viết một chương trình tự tải trang, bóc phần chữ, rồi cất lại. Việc tự động đi qua các trang web và tải về gọi là crawl (web crawling). Việc lấy dữ liệu ra khỏi một trang đã tải gọi là bóc tách (web scraping). Hai từ hay dùng lẫn, nhưng crawl nhấn vào việc đi từ trang này sang trang khác, còn scrape nhấn vào việc lấy dữ liệu.
Chương trình của bạn giống một vị khách vào cửa hàng. Ở cửa có tấm biển “Khu vực dành cho nhân viên”: đó là robots.txt. Biển không khóa được ai, nhưng khách lịch sự đọc biển và làm theo. Và biển không cấm khu nào cũng không có nghĩa bạn được chụp ảnh hết hàng hóa mang về bán. Khách lịch sự còn không chen vào cùng lúc với cả trăm người: một chương trình gửi hàng trăm yêu cầu mỗi giây có thể làm sập một site nhỏ.
Tải xong rồi cất ở đâu? Vài trăm trang, mỗi trang có địa chỉ, tiêu đề, nội dung, giờ lấy; có trang thêm tác giả, có trang không. Tuần sau chạy lại, bạn muốn cập nhật trang đã đổi chứ không muốn mỗi trang có hai bản. Một cơ sở dữ liệu tài liệu (document database) như MongoDB hợp với việc này: mỗi trang là một tờ phiếu, phiếu này có thêm ô “tác giả” cũng không sao.
Bài toán của bài này là: tải và bóc chữ từ các trang web một cách hợp lệ, lịch sự, rồi lưu chúng vào MongoDB sao cho mỗi trang đúng một bản, có đủ thông tin để sau này chatbot dẫn được nguồn.
Lý thuyết
Phần tiêu đề “Lý thuyết”Trước dòng code đầu tiên: bạn có được lấy không?
Phần tiêu đề “Trước dòng code đầu tiên: bạn có được lấy không?”Hỏi câu này trước, vì một crawler viết đúng kỹ thuật vẫn có thể làm điều sai. Có nhiều lớp quy tắc, mỗi lớp trả lời một câu hỏi khác nhau:
| Lớp | Trả lời câu hỏi | Cách kiểm |
|---|---|---|
| robots.txt | Crawler được đi vào đường dẫn nào? | Đọc /robots.txt của site (mục dưới) |
| Điều khoản dịch vụ | Site có cho phép thu thập tự động không? | Đọc trang điều khoản. Nhiều site cấm crawl |
| Bản quyền | Bạn được dùng nội dung vào việc gì? | Xem giấy phép của nội dung. Đọc được không có nghĩa là được sao chép |
| Dữ liệu cá nhân | Có tên, email, số điện thoại, ảnh của người thật không? | Tránh thu thập ngay từ đầu |
| Gánh nặng cho server | Crawler có làm site chậm đi không? | Giãn cách yêu cầu, tải ít, dừng khi site báo quá tải |
Pháp luật mỗi nước một khác, và bài này không phải lời khuyên pháp lý. Hai ví dụ để thấy chuyện này có thật:
- Ở Mỹ, trong vụ hiQ Labs kiện LinkedIn, năm 2022 tòa phúc thẩm khu vực 9 cho rằng việc tự động lấy trang công khai, không cần đăng nhập, không vi phạm đạo luật chống xâm nhập máy tính CFAA. Phán quyết đó chỉ nói về CFAA. Cũng trong năm 2022, tòa sơ thẩm cho rằng điều khoản cấm scraping trong thỏa thuận người dùng của LinkedIn có hiệu lực, và vụ kiện khép lại bằng một phán quyết đồng thuận buộc hiQ ngừng scraping LinkedIn. Vụ này không có nghĩa crawl luôn hợp pháp.
- Ở Việt Nam, Luật Bảo vệ dữ liệu cá nhân (Luật số 91/2025/QH15) có hiệu lực từ 01/01/2026. Muốn biết chi tiết, hãy đọc văn bản gốc, và hỏi chuyên gia khi dùng dữ liệu vào việc thương mại.
Cách an toàn nhất là chọn nguồn cho phép rõ ràng: API chính thức, dữ liệu mở có giấy phép, tài liệu có giấy phép tự do. Bỏ dữ liệu cá nhân ra khỏi những gì bạn lưu.
robots.txt theo RFC 9309
Phần tiêu đề “robots.txt theo RFC 9309”robots.txt là một tệp văn bản ở gốc site, ví dụ https://example.com/robots.txt. Quy ước này có từ năm 1994 và được chuẩn hóa thành giao thức loại trừ robot (Robots Exclusion Protocol) trong RFC 9309 (Koster và cộng sự, 2022). Một tệp gồm các nhóm (group):
User-agent: *Disallow: /admin/Disallow: /search
User-agent: BadBotDisallow: /- Mỗi nhóm bắt đầu bằng một hay vài dòng
User-agent, rồi tới các dòngAllowvàDisallow. Crawler tìm nhóm có tên khớp với tên của nó (không phân biệt hoa thường). Không có thì dùng nhóm*. Không có cả hai thì không có quy tắc nào. Ở ví dụ trên,BadBotbị cấm cả site, mọi crawler khác chỉ bị cấm/admin/và/search. - Quy tắc khớp theo tiền tố (prefix) của đường dẫn, và phân biệt hoa thường.
Disallow: /searchcấm cả/search?q=ailẫn/searching-tips, nhưng không cấm/Search. - Khi nhiều quy tắc cùng khớp, quy tắc dài nhất (cụ thể nhất) thắng. Một
Allowvà mộtDisallowdài bằng nhau thìAllowthắng. Không quy tắc nào khớp thì được phép. RFC 9309 còn có ký tự đại diện*và$; bài này không dùng tới.
Tải robots.txt không phải lúc nào cũng được. RFC 9309 nói rõ phải làm gì:
Khi tải /robots.txt | Nghĩa | Crawler làm gì |
|---|---|---|
| Thành công (2xx) | Có quy tắc | Làm theo quy tắc |
| Mã 4xx (404, 403…) | Không lấy được tệp | Được truy cập tự do |
| Mã 5xx, hoặc không kết nối được | Không với tới được | Coi như cấm hết |
Riêng mã 429 (gửi quá nhiều yêu cầu) thì khác: RFC xếp nó vào nhóm 4xx, nhưng site đang bảo bạn chậm lại. Ô crawler của bài xử lý nó như 5xx: tạm coi như cấm hết. Crawler được giữ lại bản đã tải để khỏi tải lại mỗi lần, nhưng không nên dùng nó quá 24 giờ.
Python có sẵn urllib.robotparser: parse(lines) đọc các dòng có sẵn, can_fetch(agent, url) trả True hay False. Hàm crawl_delay(agent) đọc dòng Crawl-delay mà nhiều site vẫn ghi, dù RFC 9309 không định nghĩa dòng này. Hàm read() tự tải tệp, nhưng nó gửi User-Agent mặc định của Python chứ không phải tên crawler của bạn, và coi mã 401, 403 là cấm hết, khác với RFC. Vì vậy ô crawler của bài tự tải robots.txt bằng requests.
Từ các bản vá năm 2026 (đã thử 3.13.14 và 3.14.5), robotparser chọn quy tắc dài nhất như RFC và hiểu *, $. Các bản trước đó (đã thử 3.12.10 và 3.14.2, cũng là bản Python của ô chạy trên trang) xét quy tắc theo thứ tự trong tệp, quy tắc khớp đầu tiên thắng. Ô robots.txt ở mục Ví dụ code không có quy tắc chồng nhau, nên bản nào cũng cho cùng kết quả.
Tải trang lịch sự với requests
Phần tiêu đề “Tải trang lịch sự với requests”Thư viện requests tải một trang bằng requests.get(url, headers=..., timeout=...), trả về một đối tượng có status_code (mã trạng thái HTTP), content (nội dung trang dạng byte) và text (nội dung đã giải mã thành chuỗi). Bốn thói quen nên có từ dòng đầu tiên, và ô crawler ở mục Ví dụ code làm đủ cả bốn:
- Luôn đặt
timeout. Không đặt thìrequestschờ mãi, và chương trình có thể treo vô hạn khi server không trả lời.timeout=10là số giây tối đa chờ kết nối, hay chờ giữa hai lần nhận dữ liệu; nó không phải giới hạn tổng thời gian tải. Quá hạn thìrequestsnémrequests.Timeout. Khi crawl thật, hãy bắt lỗi này (hayrequests.RequestExceptionnói chung) để một trang chậm không làm dừng cả chương trình. - Khai báo
User-Agenttrung thực. Header này cho site biết ai đang gọi. Hãy ghi tên dự án và cách liên hệ, nhưatlas-tutorial-bot/0.1 (+https://...), đừng giả làm trình duyệt. Chủ site thấy bot của bạn gây phiền thì còn biết liên hệ ai, thay vì chặn hẳn. - Kiểm tra mã trạng thái. Trang 404 hay trang “bạn bị chặn” vẫn có HTML. Chỉ lưu khi mã là 200, hoặc gọi
r.raise_for_status()để ném lỗi với mã 4xx, 5xx. Mã 429 (gửi quá nhiều yêu cầu) và 503 (server quá tải) nghĩa là site đang bảo bạn chậm lại: hãy dừng. - Giãn cách giữa các yêu cầu bằng
time.sleep. Không có con số chuẩn chung. Bài này gợi ý 1 giây giữa hai yêu cầu, và chờ lâu hơn nếu robots.txt cóCrawl-delaylớn hơn. Đừng chạy nhiều luồng cùng lúc vào một site nhỏ.
Bóc chữ bằng BeautifulSoup
Phần tiêu đề “Bóc chữ bằng BeautifulSoup”HTML là cây các thẻ (tag): <title>, <p>, <a href="...">. BeautifulSoup đọc HTML thành cây đó để bạn tìm thẻ. Gói cài tên là beautifulsoup4, nhưng khi import thì viết bs4.
BeautifulSoup(html, "html.parser")dùng parser có sẵn trong Python. Đưa vàor.content(byte) thì BeautifulSoup tự đọc mã hóa từ thẻ<meta charset>của trang.- Tìm thẻ.
soup.find("title")trả thẻ đầu tiên khớp, hoặcNonenếu không có.soup.find_all("a")vàsoup.select("article p")(bộ chọn CSS, CSS selector) trả danh sách mọi thẻ khớp. - Lấy dữ liệu.
tag.get_text(strip=True)lấy chữ bên trong thẻ, bỏ khoảng trắng thừa ở hai đầu. Thẻ có thẻ con thì dùngget_text(" ", strip=True)để giữ dấu cách giữa các mảnh.tag.get("href")lấy một thuộc tính, vàurljoin(url_của_trang, href)ghép đường dẫn tương đối nhưembeddingthành địa chỉ đầy đủ.
Một trang thật có menu, chân trang, quảng cáo, script. Chọn đúng vùng nội dung (thường là <article> hay <main>) thì dữ liệu sạch hơn nhiều so với lấy chữ của cả trang.
Lưu vào MongoDB
Phần tiêu đề “Lưu vào MongoDB”MongoDB lưu dữ liệu thành các document: các cặp tên trường và giá trị, có thể lồng document con và mảng. Đây là “document” theo nghĩa cơ sở dữ liệu, không phải văn bản Word. Các document nhóm lại thành collection, các collection nằm trong một database. Từ Python, bạn dùng thư viện pymongo, và một document chỉ là một dict.
- Schema linh hoạt. Các document trong cùng collection không cần cùng tập trường. Trang có tác giả thì thêm trường
author, trang không có thì thôi. Khác với bảng SQL, nơi mọi dòng phải có cùng các cột. Linh hoạt không có nghĩa là tùy tiện: hãy thống nhất tên trường, nhưurl,title,text,fetched_at, để còn truy vấn được. _id. Mỗi document có một trường_idduy nhất. Bạn không đặt thì driver tự sinh mộtObjectId.- Ghi và đọc.
insert_one(doc)thêm một document,insert_many([doc1, doc2])thêm nhiều.find(bộ_lọc)trả một con trỏ (cursor) để duyệt dần các document khớp,find_one(bộ_lọc)trả document đầu tiên hoặcNone. Bộ lọc cũng là dict:{"url": "https://example.com/a"}.
Chạy crawler lần hai thì sao? Có hai cách giữ mỗi url đúng một document:
- Chỉ mục duy nhất.
pages.create_index("url", unique=True)làm cho hai document không thể có cùngurl. Ghi trùng thì server từ chối:insert_onenémDuplicateKeyError,insert_manynémBulkWriteError. Đây là lưới an toàn: kể cả code có lỗi, dữ liệu vẫn không trùng. - Upsert (ghép của update và insert).
update_one({"url": url}, {"$set": page}, upsert=True)tìm document cóurlđó: có rồi thì cập nhật các trường trong$set, chưa có thì thêm mới. Không báo lỗi, chạy lại bao nhiêu lần cũng được. Kết quả cómatched_count(số document khớp bộ lọc) vàupserted_id(khácNonekhi vừa thêm mới).
Dùng cả hai: upsert để ghi, chỉ mục duy nhất để chặn khi lỡ tay dùng insert_one. Bộ lọc của upsert phải là trường có chỉ mục duy nhất. Lọc sai trường thì mỗi lần chạy lại sinh một document mới.
Chuỗi kết nối (connection string) cho pymongo biết server ở đâu, như mongodb://localhost:27017. Với dịch vụ MongoDB trên cloud, chuỗi này chứa cả tên người dùng và mật khẩu, nên code của bài đọc nó từ biến môi trường MONGODB_URI. Cách đặt biến môi trường giống cách đặt API key ở bài Sử dụng API văn bản.
Dữ liệu crawl là nguyên liệu cho bài RAG và chatbot hiện đại, nên ngay từ lúc crawl hãy lưu url (để chatbot dẫn nguồn), fetched_at (để biết trang nào cần crawl lại) và text đã bỏ menu, script, vì chữ rác lọt vào kho thì cũng lọt vào câu trả lời. Cắt văn bản thành đoạn và tạo embedding nằm ở bài RAG, còn làm sạch và khử trùng lặp dữ liệu quy mô lớn nằm ở bài Chuẩn bị dữ liệu pre-train.
Giới hạn
Phần tiêu đề “Giới hạn”- Trang dựng bằng JavaScript.
requestschỉ tải HTML server gửi về, không chạy JavaScript. Trang hiện nội dung bằng JavaScript sẽ trả về HTML gần như rỗng. Khi đó, hãy tìm API của site trước khi nghĩ tới công cụ điều khiển trình duyệt. - Crawler của bài không tự đi theo liên kết. Nó tải một danh sách đường dẫn cho sẵn. Crawler thật cần hàng đợi các liên kết, nhớ trang đã thăm, giới hạn số trang và độ sâu.
- Dữ liệu chưa sạch. Trùng nội dung giữa các trang, lỗi mã hóa chữ, trang lỗi lọt vào: xem mục Lỗi hay gặp và bài Chuẩn bị dữ liệu pre-train.
Minh họa
Phần tiêu đề “Minh họa”Ô dưới đây đưa một trang HTML nhỏ, viết sẵn trong code, qua BeautifulSoup. Hãy nhìn HTML ở đầu ô rồi so với kết quả mẫu: cái gì còn lại, cái gì biến mất.
Run on your machine
Bóc chữ từ một trang HTML cho sẵn
- Install
pip install beautifulsoup4
# Bóc tiêu đề, nội dung chính và liên kết từ một trang HTML cho sẵn.from urllib.parse import urljoin
from bs4 import BeautifulSoup
URL = "https://example.com/blog/rag" # địa chỉ giả định, không tải gìHTML = """<html><head><title>RAG là gì? | Blog</title></head><body> <nav><a href="/">Trang chủ</a> <a href="/blog/">Blog</a></nav> <article> <h1>RAG là gì?</h1> <p class="meta">Đăng ngày 01/10/2026</p> <p>RAG tìm đoạn văn liên quan rồi ghép vào prompt.</p> <p>Nhờ vậy mô hình trả lời dựa trên nguồn có thật.</p> <a href="embedding">Đọc tiếp: embedding</a> </article> <footer>© 2026 Blog</footer> <script>trackVisit();</script></body></html>"""
soup = BeautifulSoup(HTML, "html.parser") # parser có sẵn trong Python
# find trả thẻ đầu tiên khớp, hoặc None nếu không cótitle = soup.find("title")print("Tiêu đề:", title.get_text(strip=True) if title else None)print("Thẻ h2:", soup.find("h2"))
# Chỉ lấy phần bài viết: bỏ menu, chân trang, script và dòng ngày đăngarticle = soup.find("article")paragraphs = [p.get_text(" ", strip=True) for p in article.select("p:not(.meta)")]print("Nội dung:", " ".join(paragraphs))
# Liên kết tương đối thành tuyệt đối, để crawler đi tiếp đượcfor a in soup.find_all("a"): print("Liên kết:", urljoin(URL, a.get("href")))Sample output
From one real run.
Tiêu đề: RAG là gì? | BlogThẻ h2: NoneNội dung: RAG tìm đoạn văn liên quan rồi ghép vào prompt. Nhờ vậy mô hình trả lời dựa trên nguồn có thật.Liên kết: https://example.com/Liên kết: https://example.com/blog/Liên kết: https://example.com/blog/embedding- Thẻ h2: None. Trang không có thẻ
<h2>, nênfindtrảNone. Nếu viết thẳngsoup.find("h2").get_text(), chương trình dừng vớiAttributeError. Vì vậy dòng tiêu đề kiểm traif titletrước. - Nội dung chỉ còn hai đoạn văn trong
<article>. Menu, chân trang, script và dòng ngày đăng (lớpmeta, bị loại bằng bộ chọnp:not(.meta)) đều không lọt vào. - Liên kết
embeddinglà đường dẫn tương đối. Ghép với địa chỉ tranghttps://example.com/blog/rag,urljointhay phần cuốiragvà chohttps://example.com/blog/embedding.
Kết quả này, cộng url và thời điểm lấy, thành một document như sau khi lưu vào MongoDB. MongoDB tự thêm _id, và fetched_at được lưu dưới dạng kiểu ngày giờ của MongoDB, không phải chuỗi.
{ "url": "https://example.com/blog/rag", "title": "RAG là gì? | Blog", "text": "RAG tìm đoạn văn liên quan rồi ghép vào prompt. Nhờ vậy mô hình trả lời dựa trên nguồn có thật.", "fetched_at": "2026-10-24T08:00:00Z"}Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 10/2026, với Python 3.14, requests 2.34, beautifulsoup4 4.15 và pymongo 4.19. Phần MongoDB chạy thử với MongoDB 8.3 (image mongo:8).
Ô đầu tiên chạy ngay trên trang, vì urllib.robotparser có sẵn trong Python và ô không cần mạng. Hai ô sau chạy trên máy bạn: một ô cần mạng, một ô cần server MongoDB.
Đọc robots.txt không cần mạng
Phần tiêu đề “Đọc robots.txt không cần mạng”Ô này đưa một robots.txt viết sẵn cho RobotFileParser.parse, rồi hỏi can_fetch với sáu đường dẫn.
Đọc robots.txt bằng urllib.robotparser
# Đọc một robots.txt cho sẵn rồi hỏi: có được tải đường dẫn này không?from urllib.robotparser import RobotFileParser
ROBOTS = """User-agent: *Disallow: /admin/Disallow: /searchCrawl-delay: 2
User-agent: BadBotDisallow: /"""
robots = RobotFileParser()robots.parse(ROBOTS.splitlines()) # phân tích chuỗi, không cần mạng
agent = "atlas-tutorial-bot/0.1"paths = ["/", "/blog/rag", "/admin/users", "/Admin/users", "/search?q=ai", "/searching-tips"]for path in paths: ok = robots.can_fetch(agent, "https://example.com" + path) print(f"{path:<16} {ok}")
print("crawl delay:", robots.crawl_delay(agent))# BadBot có nhóm riêng, cấm cả siteprint("BadBot /blog/rag:", robots.can_fetch("BadBot", "https://example.com/blog/rag"))The first run downloads Python, about 10 MB.
/và/blog/raglàTruevì không quy tắc nào khớp./admin/userslàFalsenhưng/Admin/userslàTrue: so khớp đường dẫn phân biệt hoa thường./search?q=aivà/searching-tipsđềuFalse:Disallow: /searchkhớp theo tiền tố, nên chặn cả trang mẹo tìm kiếm mà có lẽ chủ site không định chặn.crawl delay: 2lâu hơn mức 1 giây bài gợi ý, nên crawler nên chờ 2 giây.BadBotcó nhóm riêngDisallow: /, nên bị cấm cả/blog/rag.
Một crawler nhỏ và lịch sự
Phần tiêu đề “Một crawler nhỏ và lịch sự”Ô này ghép mọi thứ ở mục Lý thuyết. Hàm load_robots tải robots.txt bằng requests (để gửi đúng User-Agent của bạn) và xử lý mã trạng thái theo bảng của RFC 9309: 2xx thì đọc quy tắc (giải mã UTF-8 như RFC yêu cầu), 4xx thì cho phép hết, 5xx, 429 hay lỗi mạng thì cấm hết. Sau đó, với mỗi đường dẫn: hỏi robots.txt, tải với timeout, dừng nếu gặp 429 hay 503, chỉ giữ trang có mã 200, rồi chờ.
Run on your machine
Crawler lịch sự: robots.txt, User-Agent, giãn cách
- Install
pip install requests beautifulsoup4
# Tải vài trang một cách lịch sự: hỏi robots.txt, khai tên, giãn cách.import timefrom datetime import datetime, timezonefrom urllib.robotparser import RobotFileParser
import requestsfrom bs4 import BeautifulSoup
SITE = "https://example.com" # đổi thành site bạn được phép crawlPATHS = ["/", "/trang-khong-co/"]# Khai thật bạn là ai và liên hệ ở đâu, đừng giả làm trình duyệtHEADERS = {"User-Agent": "atlas-tutorial-bot/0.1 (+https://example.com/)"}DELAY = 1.0 # giây giữa hai yêu cầu: gợi ý của bài, không phải chuẩn
def load_robots(): """Tải robots.txt, xử lý mã trạng thái theo RFC 9309.""" robots = RobotFileParser() try: r = requests.get(SITE + "/robots.txt", headers=HEADERS, timeout=10) status = r.status_code except requests.RequestException: status = 503 # không với tới được: xử lý như lỗi server if 200 <= status < 300: # RFC 9309: robots.txt phải mã hóa UTF-8 robots.parse(r.content.decode("utf-8", "replace").splitlines()) elif 400 <= status < 500 and status != 429: robots.parse(["User-agent: *", "Allow: /"]) # không lấy được tệp else: robots.parse(["User-agent: *", "Disallow: /"]) # 5xx, 429: cấm hết return robots
robots = load_robots()agent = HEADERS["User-Agent"]delay = max(DELAY, robots.crawl_delay(agent) or 0)pages = []for path in PATHS: url = SITE + path if not robots.can_fetch(agent, url): print("robots.txt cấm:", url) continue r = requests.get(url, headers=HEADERS, timeout=10) # luôn có timeout print(r.status_code, url) if r.status_code in (429, 503): print("Site đang quá tải hoặc bảo chậm lại: dừng.") break if r.status_code == 200: soup = BeautifulSoup(r.content, "html.parser") title = soup.find("title") pages.append({ "url": url, "title": title.get_text(strip=True) if title else "", "text": soup.get_text(" ", strip=True), "fetched_at": datetime.now(timezone.utc), }) time.sleep(delay)
print("Số trang lấy được:", len(pages))SITE để là example.com, một tên miền dành riêng cho ví dụ trong tài liệu. Đổi nó thành site bạn đã kiểm tra điều khoản và được phép crawl. Mỗi đường dẫn in một dòng gồm mã trạng thái và địa chỉ. Với một site có trang / nhưng không có /trang-khong-co/, bạn sẽ thấy một dòng 200, một dòng 404, và số trang lấy được là 1: trang 404 không được lưu. Giữa hai yêu cầu, chương trình chờ ít nhất 1 giây.
Ô này lấy chữ của cả trang bằng soup.get_text(" ", strip=True) cho gọn, nên tiêu đề, menu và chân trang cũng lọt vào text. Với site thật, hãy chọn vùng nội dung như ở mục Minh họa.
Lưu vào MongoDB, không trùng url
Phần tiêu đề “Lưu vào MongoDB, không trùng url”Ô cuối đọc MONGODB_URI từ biến môi trường, tạo chỉ mục duy nhất trên url, rồi thử cả hai cách ghi. serverSelectionTimeoutMS=5000 làm chương trình báo lỗi sau 5 giây nếu không tìm thấy server, thay vì chờ 30 giây theo mặc định. Dòng pages.drop() xóa collection pages của database atlas_demo mỗi lần chạy, để kết quả lần nào cũng như nhau. Đừng trỏ MONGODB_URI vào database có dữ liệu thật của bạn mà không đọc kỹ dòng này.
Để thử trên máy, cách nhanh nhất là chạy MongoDB bằng Docker (bài Docker và Cloud giải thích Docker; ở đây chỉ cần một dòng lệnh), rồi đặt MONGODB_URI thành mongodb://localhost:27017:
docker run -d --name atlas-mongo -p 127.0.0.1:27017:27017 mongo:8Run on your machine
Lưu trang vào MongoDB: chỉ mục duy nhất và upsert
- Install
pip install pymongo- Environment variables
MONGODB_URI
# Lưu các trang đã crawl vào MongoDB, mỗi url đúng một document.import osfrom datetime import datetime, timezone
from pymongo import MongoClientfrom pymongo.errors import DuplicateKeyError
# Chuỗi kết nối có thể chứa mật khẩu: đọc từ biến môi trườngclient = MongoClient(os.environ["MONGODB_URI"], serverSelectionTimeoutMS=5000)pages = client["atlas_demo"]["pages"]pages.drop() # xóa collection demo, để chạy lại vẫn ra cùng kết quả
# Chỉ mục duy nhất: hai document không thể có cùng urlpages.create_index("url", unique=True)
now = datetime.now(timezone.utc)pages.insert_many([ {"url": "https://example.com/a", "title": "Trang A", "text": "Nội dung A.", "fetched_at": now}, {"url": "https://example.com/b", "title": "Trang B", "text": "Nội dung B.", "fetched_at": now, "lang": "vi"},])
# Chèn lại trang A bằng insert_one: chỉ mục chặn lạitry: pages.insert_one({"url": "https://example.com/a", "title": "A"})except DuplicateKeyError: print("Trùng url, không chèn thêm.")
# Upsert: url đã có thì cập nhật, chưa có thì thêm mớifor page in [ {"url": "https://example.com/a", "title": "Trang A", "text": "Nội dung A, bản mới.", "fetched_at": now}, {"url": "https://example.com/c", "title": "Trang C", "text": "Nội dung C.", "fetched_at": now},]: result = pages.update_one({"url": page["url"]}, {"$set": page}, upsert=True) print(page["url"], "khớp:", result.matched_count, "thêm mới:", result.upserted_id is not None)
print("Số document:", pages.count_documents({}))# find trả con trỏ; tham số thứ hai chọn trường cần lấyfor doc in pages.find({}, {"_id": 0, "url": 1, "text": 1}).sort("url"): print(doc)print(pages.find_one({"lang": "vi"}, {"_id": 0, "title": 1}))Sample output
From one real run.
Trùng url, không chèn thêm.https://example.com/a khớp: 1 thêm mới: Falsehttps://example.com/c khớp: 0 thêm mới: TrueSố document: 3{'url': 'https://example.com/a', 'text': 'Nội dung A, bản mới.'}{'url': 'https://example.com/b', 'text': 'Nội dung B.'}{'url': 'https://example.com/c', 'text': 'Nội dung C.'}{'title': 'Trang B'}Trang B có thêm trường lang mà trang A không có, và MongoDB vẫn nhận. Lần insert_one chèn lại trang A bị chỉ mục chặn, còn upsert cập nhật A (khớp 1, không thêm mới) và thêm C (khớp 0), nên collection còn đúng 3 document, mỗi url một bản. Tham số thứ hai của find và find_one chọn trường cần lấy, nên các dòng cuối chỉ có url, text hay title.
Trong một chương trình thật, bạn ghép hai ô: crawler tạo danh sách pages, rồi gọi upsert cho từng trang.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Trước khi crawl, hỏi qua mọi lớp quy tắc: robots.txt, điều khoản dịch vụ, bản quyền, dữ liệu cá nhân, gánh nặng cho server. robots.txt cho phép là cần nhưng chưa đủ, và nó không phải cơ chế bảo mật.
- robots.txt (RFC 9309) chọn nhóm theo tên crawler hoặc
*, khớp theo tiền tố, phân biệt hoa thường, quy tắc dài nhất thắng. Tải tệp gặp 4xx thì được truy cập tự do, 5xx thì coi như cấm hết. - Crawler lịch sự đặt
timeout, khaiUser-Agenttrung thực, chỉ lưu trang có mã 200, giãn cách và dừng khi gặp 429 hay 503. Với BeautifulSoup, nhớ rằngfindcó thể trảNone, và hãy chọn đúng vùng nội dung. - MongoDB lưu mỗi trang thành một document: chỉ mục duy nhất trên
urlchặn bản trùng, upsert cập nhật hoặc thêm mới. Lưuurlvàfetched_atđể chatbot dẫn được nguồn, và đọc chuỗi kết nối từMONGODB_URI.
Đọc thêm:
- Koster, M., Illyes, G., Zeller, H., Sassman, L. (2022). RFC 9309: Robots Exclusion Protocol. IETF. Mục 2 (cách so khớp quy tắc, cách xử lý mã trạng thái) ngắn và dễ đọc.
- Python Software Foundation (2026). urllib.robotparser: Parser for robots.txt. Tài liệu Python 3.14 của
RobotFileParser,can_fetch,crawl_delay. - Reitz, K. và cộng sự (2026). Requests: Quickstart, các mục Custom Headers, Response Status Codes và Timeouts. Và Richardson, L. (2026). Beautiful Soup Documentation, các mục về parser,
find,find_all,selectvàget_text. - MongoDB (2026). PyMongo: Insert Documents, Find Documents, Update Documents và Indexes. Tài liệu chính thức của các hàm trong ô code cuối, kể cả upsert và chỉ mục duy nhất.
- Newby, T. G., Feder, D., Yoo-Scott, J. (21/04/2022). hiQ Labs Scrapes By Again: The Ninth Circuit Reaffirms that Data-Scraping Does Not Violate the CFAA. Fenwick & West. Bài bình luận của một hãng luật về phán quyết năm 2022, kể cả những rủi ro mà phán quyết không xóa đi (bản quyền, điều khoản dịch vụ). Về kết cục của vụ kiện cuối năm 2022: Dull, K. R., Jacobson, J. B. (19/12/2022). LinkedIn’s Data Scraping Battle with hiQ Labs Ends with Proposed Judgment. The National Law Review.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About