Để thực hành viết code theo tài liệu này cũng như để làm các bài tập, sử dụng Google Colab notebook sau đây: https://colab.research.google.com/drive/1h9NBNNQ1yA_s7d7IZhoy2IiX4DAgtz88?usp=sharing
API (Application Programming Interface - giao diện lập trình ứng dụng) hiểu đơn giản là một tập hợp các định nghĩa và giao thức cho phép các ứng dụng phần mềm giao tiếp và tương tác với nhau. API định nghĩa các quy tắc và giao thức giúp các phần mềm có thể yêu cầu và trao đổi dữ liệu với nhau. Nếu con người tương tác với phần mềm thông qua giao diện người dùng (UI - User Interface) thì một phần mềm tương tác với một phần mềm khác thông qua một giao diện đặc biệt gọi là API.
API giống như một hợp đồng (contract). Nó định nghĩa những gì mà các ứng dụng, phần mềm khác có thể hỏi nó, hỏi như thế nào, và kết quả nhận được sẽ là gì.
Ví dụ:
Library/SDK APIs: method .upper()
của string trong Python (vd: "abc".upper()) là một API.
Chính vì các ngôn ngữ lập trình cũng bao gồm rất nhiều APIs như này,
code của ta viết ra có thể chạy được trên các hệ điều hành khác nhau,
cho dù cách triển khai cụ thể bên trong của từng API cho mỗi hệ điều
hành có thể khác nhau. Ví dụ code sau sẽ chạy đúng trên cả Windows lẫn
MacOS cho dù hệ thống file và cách quản lý file của mỗi hệ điều hành là
khác nhau
## /Users/tuankuro211/Library/CloudStorage/OneDrive-Personal/Projects/datatute/Course/data_engineering_boosterWeb-based APIs: Web API của Github: https://docs.github.com/en/rest
OS/Platform APIs: POSIX (https://en.wikipedia.org/wiki/POSIX)
Tuy nhiên, ngày nay khi nói đến API thì người ta hầu như thường là nói đến web-based API. Trong bài học này chúng ta cũng sẽ tập trung nói về web-based API.
Ta thường gặp một số loại web API phổ biến sau: REST APIs, SOAP APIs, GraphQL APIs, gRPC APIs, WebSockets APIs, … Trong số những loại này thì phổ biến nhất là REST APIs (Representational State Transfer), thường được sử dụng khi dữ liệu không quá lớn và mô hình dữ liệu không quá phức tạp, giúp các ứng dụng trao đổi dũ liệu linh hoạt với nhiều định đạng dữ liệu. SOAP APIs (Simple Object Access Protocol) là một công nghệ cũ hơn và thường thấy trong các hệ thống cũ. SOAP có cấu trúc chặt chẽ và sử dụng định dạng dữ liệu XML. GraphQL APIs (Graph Query Language) phức tạp hơn và nhìn chung phù hợp khi ta có mô hình dữ liệu phức tạp, gồm nhiều nguồn dữ liệu liên quan tới nhau.
Trong bài học này, ta sẽ tập trung vào những nội dung kiến thức quan trọng để hiểu và sử dụng tốt REST APIs. API mà tuân thủ theo kiến trúc của REST thì được gọi là một RESTful API.
Tuỳ vào bối cảnh mà API client có thể là những thứ khác nhau (có thể là một công cụ như Postman, hay một service trong đó có code để gửi API request, …), tuy nhiên về cơ bản nó là thứ mà gửi các API request tới API server (thường việc gửi request này được kích hoạt do người dùng thực hiện một hành động nào đó, hoặc do một sự kiện - event - nào đó đã xảy ra), sau đó nhận và xử lý các kết quả được API trả về.
Một API request thường gồm những phần chính sau đây:
Endpoint: Mọi API request đều được điều hướng tới một endpoint nào đó. Một endpoint là một URL giúp truy cập tới một tài nguyên cụ thể nào đó. e
Method: Mọi API request đều phải có một method. Method định nghĩa hành động mà client muốn thực hiện trên tài nguyên mà endpoint truy cập được. Với REST APIs, ta sử dụng các HTTP method như GET, POST, PUT, PATCH, và DELETE. Các method này giúp thực hiện các hành động như truy vấn dữ liệu, tạo, cập. nhật, xoá dữ liệu.
Parameter: Parameter là các tham số được truyền vào API endpoint để cung cấp các chỉ dẫn giúp API xử lý request. Các parameter có thể được đưa vào trong URL của API request, hoặc đưa vào query string, hoặc ở phần body của request. Ví dụ ta có một API endpoint để truy cập dữ liệu thông tin khách hàng, endpoint này có thể cho phép ta truyền vào một parameter là số điện thoại của khách hàng để truy vấn và trả ra dữ liệu của đúng chính xác khách hàng đó.
Request header: API request header là các cặp key-value cung cấp thêm thông tin về API request. Ví dụ: Content-Type header cung cấp thông tin về định dạng của dữ liệu ở phần body của request, còn Authorization header thì cung cấp thông tin về xác thực như API key hoặc OAuth token.
Request body: Phần body của request bao gồm dữ liệu mà sẽ cần sử dụng để thực hiện các hành động như tạo mới, cập nhật hoặc xoá tài nguyên. Ví dụ nếu ta muốn tạo mới một bản ghi khách hàng thông qua API endpoint, phần request body có thể sẽ gồm các dữ liệu như tên, số điện thoại, tuổi, địa chỉ khách hàng. Tài liệu mô tả API thường sẽ nêu rõ dữ liệu này cần phải có định dạng như thế nào (JSON, XML, …).
API server là nơi tiếp nhận và xử lý các API request, bao gồm xác thực quyền truy cập tài nguyên của người gửi request, kiểm tra dữ liệu được gửi trong request, lấy dữ liệu hoặc chỉnh sửa dữ liệu từ database, và gửi lại một phản hồi (response) cho API client.
Database ở server không phải là một thành phần của API, tuy nhiên API sẽ không thể hoạt động được nếu thiếu nó. Database lưu trữ và tổ chức dữ liệu để API Server có thể lấy dữ liệu ra hoặc chỉnh sửa dữ liệu một cách hiệu quả. API Server đóng vai trò trung gian, đứng giữa API client và database.
Sau khi xử lý request thì API server sẽ trả ra response cho client. Nội dung của response này sẽ khác nhau tuỳ thuộc vào loại API request và tuỳ vào thiết kế của API, nhưng thường bao gồm những thành phần sau:
Status code: API status code là các mã HTTP status code mà được trả ra bởi API để thể hiện trạng thái của API request. Các mã code này cung cấp thông tin cho client về kết quả thực hiện API request, từ đó client biết cần phải làm gì tiếp theo. Một số mã code thường gặp có thể kể đến như 200 OK (server đã lấy ra được dữ liệu thành công), 201 Created (server đã tạo resource mới thành công), 404 Not Found (server không thể tìm thấy resource được yêu cầu), …
Các HTTP status code có thể được chia thành các nhóm với ý nghĩa như sau:
1xx – Informational: Request đã được server nhận, tiếp tục xử lý
2xx – Success: Hành động được yêu cầu đã được chấp nhận và thực hiện thành công bởi server
3xx – Redirection: Cần thực hiện thêm các hành động khác để hoàn thành request
4xx – Client Error: Request chứa syntax không chính xác hoặc không thể thực hiện được
5xx – Server Error: Server không thực hiện thành công một request hợp lệ
Xem tất cả các HTTP status codes tại đây: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
Response headers: Các HTTP response header rất giống với các request header, chỉ khác ở chỗ là chúng cung cấp thêm thông tin về response trả ra từ server.
Body: Body của response chứa dữ liệu hoặc nội dung được trả ra bởi API server. Phần này thường bao gồm các data object mà được yêu cầu, metadata, error message (nếu request thất bại). Ví dụ ta thực hiện một GET request để truy vấn thông tin khách hàng, thì phần body của response được trả ra có thể bao gồm thông tin khách hàng dưỡi dạng JSON, timestamp, nguồn dữ liệu, …
| Khái niệm | Mô tả | Ví dụ |
|---|---|---|
| URI (Uniform Resource Identifier) | Một identifier cho một tài nguyên (resource), có thể là URL, URN hoặc cả 2. URI xác định tài nguyên ở đây là gì. | |
| URL (Uniform Resource Locator) | Một loại URI cụ thể mà giúp ta tìm kiếm một tài nguyên trên mạng và cách truy cập. | https://www.example.com/page.html |
| URN (Uniform Resource Name) | Một loại URI mà cung cấp một tên duy nhất cho một tài nguyên, nhưng không nêu ra địa điểm và phương thức truy cập của nó. URN xác định tài nguyên đó là gì và là duy nhất. | urn:isbn:978-3-16-148410-0 (mã ISBN cho một quyển sách) |
Tổng kết lại:
URI (Uniform Resource Identifier): Đây là khái niệm rộng nhất. Hiểu đơn giản thì đây là một chuỗi ký tự mà xác định một tài nguyên (resource), có thể xác định bằng vị trí (URL) hoặc bằng tên (URN).
URL: URL là một tập con của URI. Một URL là một identifier giúp xác định một tài nguyên và cung cấp chỉ dẫn để truy cập tài nguyên đó.
URN: URN là identifier được tạo ra bởi các đơn vị, tổ chức làm về tiêu chuẩn và có thể áp dụng cho bất kỳ thứ gì cần có một định danh tiêu chuẩn (a standard identifier) trong hoạt động của con người, chứ không chỉ giới hạn ở hệ thống máy tính và phần mềm. URN xác định một tên gọi cố định cho tài nguyên và tên gọi này không phụ thuộc vào vị trí của tài nguyên đó. URN không có thông tin về vị trí hoặc cách truy xuất thông tin từ tài nguyên.
Các HTTP method giúp API client thực hiện các hành động CRUD (Create, Read, Update, Delete) đối với các tài nguyên theo những tiêu chuẩn nhất định. Các HTTP method phổ biến bao gồm:
| Method | Mô tả | Ví dụ |
|---|---|---|
| GET - Truy vấn dữ liệu | Mục đích: Lấy dữ liệu từ server mà không thay đổi trạng thái của tài nguyên Idempotent: Có. (Gửi GET request nhiều lần thì vẫn trả ra cùng một kết quả) Cacheable: Có (Response trả ra thường được cache). |
GET /api/products trả ra danh sách các product GET /api/products/123 trả ra dữ liệu chi tiết về một product với ID là 123 |
| POST - Tạo mới dữ liệu | Mục đích: Gửi dữ liệu tới server để tạo tài nguyên mới. Thông thường thì server sẽ phản hồi lại bằng cách trả ra response với một status nhất định nào đó hoặc response chứa ID của tài nguyên mới. Idempotent: Không. (Gửi POST request nhiều lần có thể sẽ tạo ra nhiều tài nguyên) Cacheable: Không (thường thì mặc định response trả ra từ POST request không được cache). |
POST /api/products có thể tạo mới một sản phẩm trong database |
| PUT - Thay thế dữ liệu hiện có | Mục đích: Cập nhật một tài nguyên có sẵn bằng cách thay thế nó bằng một tài nguyên khác. Thông thường khi gửi PUT request ta phải gửi toàn bộ data của tài nguyên mới mà sẽ được dùng để thay thế tài nguyên cũ. Idempotent: Có. (Gửi nhiều PUT requests giống nhau thì vẫn sẽ nhận được cùng một kết quả) Cacheable: Không (giống như POST, mặc định PUT request không được cache). |
PUT /api/products/123 sẽ thay thế toàn bộ các trường dữ liệu của product với ID 123 bằng dữ liệu mới được gửi kèm với request |
| PATCH - Cập nhật một số trường dữ liệu | Mục đích: Cập nhật một phần dữ liệu của tài nguyên hiện có bằng cách cập nhật chỉ một số trường dữ liệu nhất định. Idempotent: Có. Cacheable: Không. |
PATCH /api/products/123 đi kèm với dữ liệu là { “price”: 19.99 } sẽ cập nhật trường price của sản phẩm có ID 123 thành 19.99 |
| DELETE - Xoá dữ liệu | Mục đích: Xoá một số tài nguyên khỏi server. Idempotent: Có. Cacheable: Không. |
DELETE /api/products/123 sẽ xoá sản phẩm có ID 123 khỏi database |
Đối với người mới bắt đầu thì việc đọc tài liệu của bên cung cấp API có thể sẽ hơi rối (tuỳ vào mức độ chi tiết của tài liệu). Để làm việc với API dễ dàng hơn, ta cần lưu ý một số điểm sau:
Các bước thực hiện chính:
Tìm hiểu về công nghệ và tiêu chuẩn được sử dụng để tạo API (SOAP, REST, …) sau đó tìm và đọc sơ lược tài liệu của bên cung cấp API, trong đó cần lưu ý kĩ các nội dung sau đây:
URL gốc (base URL) là gì và có những endpoint nào, và những endpoint nào ta sẽ cần sử dụng
Có bộ SDK (Software Development Kit) hay không. SDK là một bộ các công cụ giúp cho ta sử dụng API được dễ dàng, tiện lợi và an toàn hơn.
Xác thực (authentication) và uỷ quyền truy cập (authorisation): Ta cần xác định các cách xác thực (authentication) mà nhà cung cấp API cho phép (API key, OAuth 2.0, base64-encoding string, …) và phương thức nào phù hợp với nhu cầu và các quy định liên quan của công việc mà ta đang làm. Bên cạnh việc xác thực thì ta cần xác định được xem user/hệ thống mà ta sử dụng để gọi API có quyền truy cập những gì và có đủ quyền truy cập cần thiết hay không. Với một số API khi ta thực hiện xác thực (authentication) thì sẽ phải cung cấp giá trị cho tham số SCOPE để xác định ta sẽ được truy cập những gì thông qua API (authorisation).
Đối với các endpoint cần sử dụng thì headers cần dùng là gì (headers cung cấp metadata về request) và có phải gửi dữ liệu tới server hay không (request payload) và nếu có thì định dạng của payload là gì. Định dạng của dữ liệu trả ra từ endpoint là gì (JSON/Avro/Protobuf, …), kiểm tra schema của dữ liệu trả ra (dữ liệu trả ra gồm những trường dữ liệu nào).
Limits và quotas: Các khái niệm này thường dùng để đề cập đến các giới hạn đối với việc sử dụng API, mà thường là rate limit, tức là số lượng request tối đa được cho phép gửi/nhận đến hệ thống trong một khoảng thời gian (một giây, một phút, hay một giờ, …). Ví dụ về quotas của Google Drive API: https://developers.google.com/drive/api/guides/limits. Nếu có batch request ta nên sử dụng. Ta nên cân nhắc sử dụng parallelism một cách cẩn trọng và hợp lý.
Pagination: Pagination tức là việc khi kết quả trả ra từ API quá lớn thì nó sẽ được chia thành các trang (page) để client không phải nhận toàn bộ lượng dữ liệu lớn đó một lúc. Pagination cũng bảo vệ Server khỏi những truy vấn dữ liệu quá nặng, từ đó giúp Server phản hồi các API request nhanh và ổn định hơn.
Kiểm tra cách mà bên cung cấp API cập nhật các phiên bản API, từ đó có một chiến lược về versioning phù hợp để đảm bảo các job ETL không bị lỗi.
Lựa chọn phương thức xác thực (authentication) phù hợp
Xác thực và gửi một số request đơn giản để thử nghiệm API endpoint. Ta có thể sử dụng luôn Python hoặc sử dụng những phần mềm/nền tảng hỗ trợ cho việc xây dựng và sử dụng API như Postman hay curl chẳng hạn. Ngoài những điểm đã tìm hiểu ở bên trên, ta lưu ý thêm về:
Xử lý các lỗi thường gặp phải khi gọi API và viết retry logic (Ví dụ như khi gặp rate limits hoặc các network error, …)
Xác định hướng xử lý sự thay đổi về schema (schema evolution), thường là liên quan tới việc một số trường dữ liệu được thêm mới vào hoặc bị xoá bớt đi trong response từ API.
ETL pipeline design: Gửi API request để thực hiện incremental load (dùng timestamp, cursor, …), tạo và thiết lập nơi lưu trữ kết quả trả về từ API request, tạo một idempotent pipeline (khi chạy lại pipeline để gửi API request thì không làm sai dữ liệu và không tạo ra trùng lặp dữ liệu), …
Monitoring: Log phản hồi nhận được từ API, log các API request thất bại, …
Viết code hoàn chỉnh
Viết tài liệu (documentation)
Trong ví dụ này, ta tạo 1 phần mềm nho nhỏ dùng để download tất cả các file trong 1 link google drive
Bước 1: Tạo 1 Google Cloud account và 1 Project (có rất nhiều hướng dẫn trên mạng)
Bước 2: Kích hoạt Google Drive API.
Hướng dẫn: https://developers.google.com/drive/api/quickstart/python
Truy cập link: https://console.cloud.google.com/flows/enableapi?apiid=drive.googleapis.com
Bước 3: Create a service account
tải file về và để trong folder của chương trình google drive file downloader. Trong trường hợp này tôi đặt tên là “datatute1-service-account-key.json”
Bước 4:
Xem thông tin reference về Google Drive API tại đây:
https://developers.google.com/drive/api/reference/rest/v3
https://developers.google.com/drive/api/guides/manage-downloads
google-auth: https://googleapis.dev/python/google-auth/latest/index.html
Link gg drive chứa các file cần download: https://drive.google.com/drive/folders/1Eji7_eRLA4hSi0IP4qzPp8xZoA80FUAt?usp=sharing
file gg_drive_downloader.py
import requests
import os
import re
from google.oauth2 import service_account
from google.auth.transport.requests import AuthorizedSession
def main():
"""
Hàm main thực hiện các bước sau:
1. Lấy giá trị folder_id từ URL của folder google drive
2. Authenticate sử dụng Service Account và trả ra một authorized session
3. Tạo danh sách các files cần download
4. Download files
"""
# URL của google drive folder chứa các file cần download
FOLDER_URL = (
"https://drive.google.com/drive/u/2/folders/1Eji7_eRLA4hSi0IP4qzPp8xZoA80FUAt"
)
# Đường dẫn tới file service account key (trong trường hợp này file được đặt trong cùng folder với file gg_drive_downloader.py)
SERVICE_ACCOUNT_FILE = "datatute1-service-account-key.json"
# Các scope truy cập Google Drive API (chỉ cần quyền read để download file)
SCOPES = ["https://www.googleapis.com/auth/drive.readonly"]
# Base URL của API
API_BASE_URL = "https://www.googleapis.com/drive/v3"
# Lấy giá trị folder_id từ URL của folder google drive
folder_id = extract_folder_id(FOLDER_URL)
# Authenticate sử dụng Service Account và trả ra một authorized session.
session = authenticate_service_account(SERVICE_ACCOUNT_FILE, SCOPES)
# Tạo danh sách các files cần download
files = get_list_of_files(API_BASE_URL, session, folder_id)
# Download files
download_files(API_BASE_URL, session, "download", files)
def extract_folder_id(folder_url: str) -> str:
"""
Dùng REGEX để trích xuất lấy giá trị folder id từ URL của folder google drive.
Args:
folder_url (str): URL của folder google drive.
Returns:
str: folder id.
"""
pattern = r"[^/]+$"
match = re.search(pattern, folder_url)
if match:
folder_id = match.group()
return folder_id
def authenticate_service_account(
service_account_file: str, scopes: list
) -> AuthorizedSession:
"""
Authenticate sử dụng Service Account và trả về một authorized session.
Args:
service_account_file (str): Đường dẫn tới file service account key.
scopes (list): Các scope truy cập Google Drive API cần được authenticate
Returns:
AuthorizedSession: một authorized session object, được dùng để gửi requests tới Google Drive API.
"""
credentials = service_account.Credentials.from_service_account_file(
service_account_file, scopes=scopes
)
session = AuthorizedSession(credentials)
return session
def get_list_of_files(
api_base_url: str, session: AuthorizedSession, folder_id: str
) -> list:
"""
Tạo list gồm id và tên của các file nằm trong folder google drive.
Args:
api_base_url (str): Base URL của Google Drive API.
folder_id (str): id của folder google drive
session (AuthorizedSession): một authorized session object, được dùng để gửi requests tới Google Drive API.
Returns:
list: list gồm các dict, mỗi dict gồm id và tên của các file nằm trong folder google drive
"""
url = f"{api_base_url}/files"
params = {
"q": f"'{folder_id}' in parents and trashed=false",
"fields": "files(id, name)",
}
response = session.get(url, params=params)
# Raise error cho các status từ 400 - 599
# Đây chỉ là một cách đơn giản để xử lý các lỗi gặp phải khi invoke API endpoint
# Trong thực tế, có những trường hợp ta sẽ muốn bắt và xử lý riêng từng loại status code
# và có logic để retry phù hợp, sử dụng try...except... trong python
# (tuỳ loại status code mà ta gửi lại request tới endpoint sau 1 khoảng thời gian nhất định)
response.raise_for_status()
file_list = response.json().get("files", [])
return file_list
def download_files(
api_base_url: str,
session: AuthorizedSession,
output_folder_name: str,
file_list: list,
) -> None:
"""
Downloads files từ folder google drive thông qua Google Drive API.
Args:
api_base_url (str): Base URL của Google Drive API.
output_folder_name (str): của của folder mà sẽ chứa các file được download về
file_list (list): list gồm các dict, mỗi dict gồm id và tên của các file nằm trong folder google drive
Returns:
None
"""
for file in file_list:
file_id = file["id"]
file_name = file["name"]
url = f"{api_base_url}/files/{file_id}?alt=media"
# Tạo folder chứa file download nếu chưa tồn tại
os.makedirs(output_folder_name, exist_ok=True)
file_path = os.path.join(output_folder_name, file_name)
response = session.get(url)
response.raise_for_status()
# Lưu file vào folder
with open(file_path, "wb") as file:
file.write(response.content)
main()Sau khi chạy chương trình ta thấy các file đã được download về:
Ta có thể hiểu một cách đơn giản web crawling là nói đến việc tìm kiếm các website (URLs, metadata) và download các trang web.
Web scraping là nói đến việc trích xuất những dữ liệu cụ thể từ các trang web, thường là biến đổi nội dung của các trang web (parse HTML, XML, hay JSON response từ trang web) thành dữ liệu có cấu trúc.
| Web crawling | Web scraping |
|---|---|
| Liên quan tới việc tìm kiếm, download và lưu trữ nội dung của một lượng lớn các trang web | Liên quan tới việc trích xuất những dữ liệu cụ thể nào đó từ trang web với một cấu trúc nhất định. |
| Thường được thực hiện ở quy mô lớn | Có thể được triển khai ở cả quy mô nhỏ lẫn lớn |
| Cung cấp thông tin tổng quan | Cung cấp thông tin cụ thể |
| Ví dụ: Googlebot | Ví dụ: Trích xuất thông tin cụ thể về tên sản phẩm, giá tiền từ các sàn thương mại điện tử khác thành, biến đổi thành dữ liệu có cấu trúc và thực hiện phân tích, so sánh giá |
Một trong những lí do khiến Python trở thành một sự lựa chọn phổ biến cho web scraping đấy là nó có những thư viện được xây dựng để hỗ trợ tốt cho công việc này. Dưới đây là một số thư viện phổ biến:
BeautifulSoup: Đây là một thư viện được dùng phổ biến để parse file HTML (phân tích cú pháp HTML).
Selenium: Đây là thư viện được dùng cho yếu để test các ứng dụng web nhưng cũng được dùng cho web scraping.
Requests (HTTP for Humans): Đây là thư viện giúp thực hiện các loại HTTP requests khác nhau như GET, POST, …
lxml: Đây là thư viện tương tự như BeautifulSoup để xử lý file XML và HTML.
Bước 1: Download nội dung từ các trang web
Ở bước này, web scraper tải xuống nội dung từ các trang web được yêu cầu.
Bước 2: Trích xuất và biến đổi dữ liệu
Ở bước này, web scraper sẽ parse nội dung HTML đã được tải xuống, trích xuất dữ liệu từ HTML. Sau đó, dữ liệu được biến đổi về định dạng phù hợp.
Bước 3: Lưu trữ dữ liệu
Ở bước này, web scraper sẽ lưu trữ dữ liệu đã được trích xuất ở bước trên vào database hoặc dưới một định dạng phù hợp như JSON, CSV, …
Trước khi thực hiện web scraping thì ta cần kiểm tra một số thông tin sau của trang web để hiểu được cấu trúc và độ lớn của trang web.
Kiểm tra file “robots.txt”
File này cho ta biết là bên sở hữu website họ muốn cho ta (và các crawl bot khác trên mạng) crawl phần dữ liệu nào trên website của họ.
Ví dụ ta muốn kiểm tra file robots.txt của website “dantri.com.vn” thì ta truy cập địa chỉ sau: https://www.dantri.com.vn/robots.txt và kết quả trả ra sẽ như dưới đây:
User-agent: *
Allow: /
Sitemap: https://dantri.com.vn/sitemaps/category-sitemap.xml
Sitemap: https://dantri.com.vn/sitemaps/articles.xml
Sitemap: https://dantri.com.vn/sitemaps/Videos.xml
Sitemap: https://dantri.com.vn/google-news-sitemap.xml
Sitemap: https://dantri.com.vn/sitemaps/collection-sitemap.xml
Disallow: /wp-json/v1/newspaper
Disallow: /print-*.htm
Disallow: /news-*.htm
Disallow: */amp/video/*
Disallow: /*.tag
Disallow: /*/trang-1*.htm
Disallow: /*/trang-2*.htm
Disallow: /*/trang-3*.htm
Disallow: /*/trang-4*.htm
Disallow: /*/trang-5*.htm
Disallow: /*/trang-6*.htm
Disallow: /*/trang-7*.htm
Disallow: /*/trang-8*.htm
Disallow: /*/trang-9*.htm
Disallow: /tim-kiem/*betgg8.net*
Disallow: /wp-json/v1/newspaper
Disallow: /tim-kiem/*✔*
Disallow: /tim-kiem/*đŸ’•*
Disallow: /tim-kiem/*â¡*
Disallow: /tim-kiem/*đŸ–*
Disallow: /tim-kiem/*â¤*
Disallow: /tim-kiem/*â½*
Disallow: /tim-kiem/*casino666.vip*
Disallow: /tim-kiem/*â*
Disallow: /tim-kiem/*tg@macaoai*
Disallow: /tim-kiem/*(*
Disallow: /tim-kiem/*â— *
Disallow: /tim-kiem/*/xodpqbox/*
Disallow: /tim-kiem/*ă€*
Disallow: /tim-kiem/*[*
Disallow: /tim-kiem/*sbs866.com*
Disallow: /tim-kiem/*jj789.co*
Disallow: /tim-kiem/*đŸ…*
Disallow: /tim-kiem/*đŸ¥•*
Disallow: /tim-kiem/*đŸŒ*
Disallow: /tim-kiem/*đŸŒˆ*
Disallow: /tim-kiem/*đŸ¥‘*
Disallow: /tim-kiem/*đŸ”*
Disallow: /tim-kiem/*đŸ¥¤*
Disallow: /tim-kiem/*香*
Disallow: /tim-kiem/*港*
Disallow: /tim-kiem/*安心*
Disallow: /tim-kiem/*(@*
Disallow: /tim-kiem/*đŸ¥*
Disallow: /tim-kiem/*đŸ‘ˆ*
Disallow: /tim-kiem/*å…¥*
Disallow: /tim-kiem/*황*
Disallow: /tim-kiem/*â–³*
Disallow: /tim-kiem/*́§€ë…¸*
Disallow: /tim-kiem/*★*
Disallow: /tim-kiem/*â—€*
Disallow: /tim-kiem/*â˜*
Disallow: /tim-kiem/*â—*
Disallow: /tim-kiem/*æ—*
Disallow: /tim-kiem/*â–£*
Disallow: /tim-kiem/*â™*
Disallow: /tim-kiem/*↖*
Disallow: /tim-kiem/*↘*
Disallow: /tim-kiem/*́¹´*
Disallow: /tim-kiem/*â–§*
Disallow: /tim-kiem/*≫*
Disallow: /tim-kiem/*́¤*
Disallow: /tim-kiem/*́´*
Disallow: /tim-kiem/*è‘£*
Disallow: /tim-kiem/*â—*
Disallow: /tim-kiem/*â—*
Disallow: /tim-kiem/*↕*
Disallow: /tim-kiem/*金*
Disallow: /tim-kiem/*å¹£*
Disallow: /tim-kiem/*usdt*
Disallow: /tim-kiem/*%20*
Disallow: /tim-kiem/*.htm?pi=*
Disallow: /*%F0%9F%8E%83*
Disallow: /*人*
Disallow: /*casino*
Disallow: /tim-kiem.htm?s=*
Kiểm tra file Sitemap
Website cung cấp sitemap để giúp ta biết cần phải crawl dữ liệu từ phần nào của website, tránh việc phải crawl tất cả các page sẽ làm tăng lưu lượng truy cập vào website một cách không cần thiết.
Xem tiêu chuẩn cho file sitemap tại đây: http://www.sitemaps.org/protocol.html.
Với website Dân Trí, như ta đã thấy ở file “robot.txt” bên trên, website này cung cấp cho ta một số sitemap, ví dụ như sitemap của các bài viết trên Dân Trí theo thời gian: https://dantri.com.vn/sitemaps/articles.xml . Ta thấy nội dung sitemap này như sau (dưới đây chỉ là một phần nội dung:
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<sitemapindex xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://dantri.com.vn/sitemaps/Article-2025-09-01.xml</loc>
</sitemap>
<sitemap>
<loc>https://dantri.com.vn/sitemaps/Article-2025-08-01.xml</loc>
</sitemap>
<sitemap>
<loc>https://dantri.com.vn/sitemaps/Article-2025-07-01.xml</loc>
</sitemap>
<sitemap>
<loc>https://dantri.com.vn/sitemaps/Article-2025-06-01.xml</loc>
...
Vậy ta thấy trong sitemap này lại có dẫn link tới các sitemap khác của từng tháng một. Ta truy cập thử sitemap của tháng 9/2025: https://dantri.com.vn/sitemaps/Article-2025-09-01.xml
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<urlset xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://dantri.com.vn/noi-vu/nhu-cau-thue-nha-o-cong-vu-tang-45-lan-de-xuat-mo-rong-doi-tuong-thue-20250929222207368.htm</loc>
<lastmod>2025-09-30T07:00:00.000+07:00</lastmod>
</url>
<url>
<loc>https://dantri.com.vn/lao-dong-viec-lam/chi-tiet-truong-hop-mua-thuoc-ngoai-benh-vien-duoc-bao-hiem-y-te-thanh-toan-20250927183546592.htm</loc>
<lastmod>2025-09-30T07:00:00.000+07:00</lastmod>
</url>
<url>
<loc>https://dantri.com.vn/the-gioi/trieu-tien-tuyen-bo-khong-bao-gio-tu-bo-chuong-trinh-hat-nhan-20250930064432008.htm</loc>
<lastmod>2025-09-30T06:59:31.177+07:00</lastmod>
</url>
...
Ta thấy trong sitemap này có đường link dẫn tới tất cả các bài viết trong tháng 9/2025 trên trang Dân Trí, và có thông tin về thời gian thay đổi thông tin gần nhất của từng bài viết (“<lastmod>”).
Những thông tin này rất hữu ích vì chúng giúp cho các crawler (ví dụ các crawler của các search engine như Google, Bing chẳng hạn) nhận biết được và có thể crawl sitemap từ một thời điểm nhất định nào đó.
Kiểm tra độ lớn của website
Độ lớn của website ảnh hưởng tới cách ta thực hiện crawl dữ liệu từ nó (ví dụ website lớn quá mà ta crawl tuần tự từng page một chẳng hạn thì sẽ không hiệu quả).
Ta có thể kiểm tra size của website dantri.com.vn bằng cách sử dụng
Google như sau. Ta gõ site:dantri.com.vn và xem có bao
nhiêu kết quả trả ra. Ở đây ta thấy có hơn 3 triệu kết quả trả ra. Như
vậy đây là một website lớn.
Kiểm tra công nghệ được dùng để tạo website
Công nghệ được dùng để xây website cũng có thể ảnh hưởng tới cách ta crawl dữ liệu. Trong Python có một thư viện tên là “builtwith” giúp ta kiểm tra công nghệ được dùng để xây một website nào đó.
from Wappalyzer import Wappalyzer, WebPage
url = "https://dantri.com.vn"
try:
wappalyzer = Wappalyzer.latest()
webpage = WebPage.new_from_url(url)
technologies = wappalyzer.analyze(webpage)
print(technologies)
except Exception as e:
print(f"An error occurred: {e}")Ta thấy kết quả trả ra các công nghệ được dùng để xây dantri.com.vn là
{'Microsoft ASP.NET', 'Windows Server', 'Nginx', 'IIS'}
Kiểm tra source code của các page
Từ trình duyệt web, ta có thể bấm chuột phải vào một page, chọn “View Page Source”, ta sẽ xem được dữ liệu của page đó dưới dạng HTML, tuy nhiên nhìn code HTML với một trang web có nhiều thành phần thì cũng sẽ khó nhìn.
Ta có thể dùng một cách khác đó là chuột phải vào page và bấm “Inspect” hoặc “Inspect Page Source” (với Google Chrome, các trình duyệt khác cũng sẽ có những cách tương tự). Với cách này, ta sẽ nhìn thấy code HTML được format dễ nhìn hơn và khi ta chọn một element nào thì nó sẽ được highlight tương ứng trên page, rất trực quan.
Dưới đây là các kiến thức về HTML cần thiết cho web scraping. Để tìm hiểu thêm về HTML, tham khảo:
w3schools.com/html/
freeCodeCamp’s Learn HTML - Full tutorials for beginners 2022: https://www.youtube.com/watch?v=kUMe1FH4CHE
Một trang HTML cơ bản trông như sau:
Mỗi một HTML element (phần tử) thì được xác định bởi một start tag,
rồi đến nội dung (content), và kết thúc bằng một end tag. Ví dụ:
<tagname>Content goes here...</tagname>
Ví dụ một số HTML elements:
<h1>My First Heading</h1>
<p>My first paragraph.</p>
| Start tag | Element content | End tag |
|---|---|---|
| <h1> | My First Heading | </h1> |
| <p> | My first paragraph. | </p> |
| <br> | none | none |
HTML element có thể được nested (tức là một element có thể bao gồm một hoặc nhiều element khác).
Ví dụ như element <html> được gọi là root element
và nó định nghĩa toàn bộ trang HTML.
Với trường hợp này thì start tag là <html> và end
tag </html>. Giữa 2 tag này ta thấy có một
<body> element, định nghĩa phần thân của tài liệu
HTML. Phần này thì có start tag là <body> và end tag
là </body>. Giữa 2 tag này ta lại thấy các element
khác, ví dụ như: <h1> và <p> ,
…
Các HTML element mà không có nội dung thì gọi là các element rỗng. Ví
dụ: Tag <br> thể hiện xuống dòng (line break) là một
element rỗng (nó không có end tag):
Các HTML tags không phân biệt giữa viết hoa và viết thường, tuy nhiên theo thông lệ thì ta nên dùng viết thường (lower case).
Tất cả các HTML element đều có thể có attribute. Các attribute cung cấp thêm thông tin về element.
Attribute luôn được khai báo ở start tag và thường có dạng cặp giá
trị name/value. Ví dụ: name="value".
id: Attribute này dùng để chỉ định một id duy nhất cho một HTML element. Vì vậy nên ta không thể có 2 element mà cùng chung id trong một tài liệu HTML được.
Attribute id dùng để trỏ đến một style nhất định strong
style sheet. Attribute này cũng được dùng bởi JavaScript để truy cập và
thao tác với element có id đó.
Ví dụ:
class: Attribute class được dùng để
chỉ định một class cho một HTML element. Attribute này thường trỏ tới
tên class trong style sheet. Nó cũng được dùng bởi JavaScript để truy
cập và thao tác với các element mà thuộc class đó.
Trong ví dụ dưới đây, ta có 3 element <div> với
attribute class là “city”. Vì vậy cả 3
<div> element này sẽ đều được format giống nhau, theo
style definition .city.
<!DOCTYPE html>
<html>
<head>
<style>
.city {
background-color: tomato;
color: white;
border: 2px solid black;
margin: 20px;
padding: 20px;
}
</style>
</head>
<body>
<div class="city">
<h2>London</h2>
<p>London is the capital of England.</p>
</div>
<div class="city">
<h2>Paris</h2>
<p>Paris is the capital of France.</p>
</div>
<div class="city">
<h2>Tokyo</h2>
<p>Tokyo is the capital of Japan.</p>
</div>
</body>
</html>href: Tag <a> định nghĩa một
hyperlink. Attribute href thì sẽ chỉ định URL của trang web
mà link sẽ dẫn tới (Vd:
<a href="https://www.examples.com"Visit Example website</a>
src: Tag <img> được dùng để
nhúng (embed) một hình ảnh vào một trang HTML. Attribute
src chỉ định đường dẫn đến hình ảnh đó. Ví dụ:
alt: Attribute alt là bắt buộc khi
sử dụng tag <img> và được dùng để chỉ định một đoạn
text để thay thế cho hình ảnh, trong trường hợp hình ảnh không được hiển
thị do một lí do nào đó.
title: Attribute title định nghĩa
một số thông tin bổ sung thêm cho một element. Giá trị được gắn với
attribute này sẽ được thể hiện dưới dạng tooltip khi ta di chuột vào
element.
Khi một web page được tải thì trình duyệt tạo ra một Document Object Model của page đó. HTML DOM model được xây dựng dưới dạng một cây các đối tượng. Mỗi HTML element là một nút (node) trong cây đó.
Giả sử ta có HTML page sau:
<html>
<head>
<title>Sample Website</title>
</head>
<body>
<h1>Welcome to My Website</h1>
<div class="content">
<p id="p1">This is the second paragraph with a <a href="https://example.com">link</a>.</p>
</div>
</body>
</html>DOM của page sẽ như sau:
Trong Python, các thư viện như Beautiful Soup hay XML có các parser giúp chuyển nội dung HTML thành các Python object tương tự như một cây DOM. Các object này giúp ta trích xuất dữ liệu từ các phần khác nhau của tài liệu HTML một cách dễ dàng.
Ta có thể tìm kiếm HTML element bằng id, tag name, class name, CSS selector, hoặc bằng HTML object collection
Tìm HTML elements bằng id
Finding HTML elements by tag name
Finding HTML elements by class name
Finding HTML elements by CSS selectors
Finding HTML elements by HTML object collections
Vẫn với code HTML như trên
from bs4 import BeautifulSoup
# Sample HTML content
html_doc = """
<html>
<head>
<title>Sample Website</title>
</head>
<body>
<h1>Welcome to My Website</h1>
<div class="content">
<p id="p1">This is the second paragraph with a <a href="https://example.com">link</a>.</p>
</div>
</body>
</html>
"""
# Parse the HTML document
soup = BeautifulSoup(html_doc, "html.parser")Ta sẽ dùng thư viện BeautifulSoup để parse code HTML này và tạo một BeautifulSoup object. Object này giống như một DOM tree, giúp ta tìm kiếm các element được dễ dàng hơn.
from bs4 import BeautifulSoup
# Sample HTML content
html_doc = """
<html>
<head>
<title>Sample Website</title>
</head>
<body>
<h1>Welcome to My Website</h1>
<div class="content">
<p id="p1">This is the second paragraph with a <a href="https://example.com">link</a>.</p>
</div>
</body>
</html>
"""
# Parse the HTML document
soup = BeautifulSoup(html_doc, "html.parser")
print(type(soup))## <class 'bs4.BeautifulSoup'>
Ta sử dụng object “soup” để tìm kiếm các element:
# Demonstrate DOM-like tree as nested Python dict
tree_structure = {
"html": {
"head": {
"title": soup.title.string
},
"body": {
"h1": soup.h1.string,
"div": {
"p": {
"text": soup.find("p", {"id": "p1"}).contents[0],
"a": {
"href": soup.find("a")["href"],
"text": soup.find("a").string
}
}
}
}
}
}
print(tree_structure)## {'html': {'head': {'title': 'Sample Website'}, 'body': {'h1': 'Welcome to My Website', 'div': {'p': {'text': 'This is the second paragraph with a ', 'a': {'href': 'https://example.com', 'text': 'link'}}}}}}
Trong phần này, ta sẽ cùng nhau làm một dự án nho nhỏ để scrape dữ liệu từ một trang web tên là vjshop.vn. Đây là một đơn vị bán máy ảnh, máy quay và các phụ kiện cho chúng. Giả sử ta quan tâm tới các phụ kiện cho action camera và ta muốn scrape dữ liệu để theo dõi giá của các phụ kiện cho action camera, từ đó có thể mua kịp thời khi giá giảm.
Ta cần lấy tên và giá tiền của các phụ kiện action camera.
Để cho đơn giản và tập trung vào nội dung chính là web scraping, trong ví dụ này ta sẽ chỉ cùng nhau scrape dữ liệu và print out dữ liệu để kiểm chứng là dữ liệu đã được scrape thành công. Việc của các bạn học viên là đọc hiểu code ví dụ và mở rộng nó, ví dụ như lấy thêm các attribute (cột) khác, thực hiện thêm bước lưu trữ dữ liệu vào database, v.v.
File https://www.vjshop.vn/robots.txt
User-agent: *
Disallow: /may-anh-cu
Disallow: /ong-kinh-cu
Disallow: /apple-cu
Disallow: /phu-kien-cu
Disallow: /flycam-phu-kien
Disallow: /hightech-cu
Disallow: /gimbal-cu
Allow: /
Sitemap: https://vjshop.vn/sitemap.xml
Trang mà ta cần scrape dữ liệu là trang chứa thông tin phụ kiện cho action camera: https://vjshop.vn/camera-hanh-dong-phu-kien.
Trang này không nằm trong danh sách Disallow của file robots.txt -> Như vậy ta có thể hiểu rằng chủ website cho phép ta scrape dữ liệu từ trang thông tin phụ kiện cho action camera.
Khi bấm chuột phải vào page, chọn “Inspect”, ta thấy dữ liệu tên và giá bán của từng sản phẩm được lưu ở các tag HTML như ảnh dưới đây
Tuy nhiên, khi sử dụng thư viện “requests” để lấy nội dung HTML từ
page này, ta lại không thấy có các tag và dữ liệu như trong ảnh. Đây là
vì trang web này sử dụng Next.js (một React framework) và ta có thể tìm
thấy dữ liệu của page ở tag <script> . Dữ liệu này có
dạng json, cụ thể như sau:
<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {
...
"products": [
{...},
...
]
}
}
}Ta thực hiện viết code để scrape dữ liệu như sau (chạy thử code bằng cách truy cập google collab notebook ở phần Hướng dẫn thực hành):
from bs4 import BeautifulSoup
import requests
import pandas as pd
import json
from requests.models import Response
BASE_URL = "https://vjshop.vn/camera-hanh-dong-phu-kien"
HEADERS = {
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.4079.1285 Mobile Safari/537.36"
}
def parse_html(response: Response) -> BeautifulSoup:
"""
Parses the HTML content of a given response using BeautifulSoup.
Args:
response (Response): The requests library Response object.
Returns:
BeautifulSoup: A BeautifulSoup object representing the parsed HTML.
"""
soup = BeautifulSoup(response.text, "html.parser")
return soup
def extract_product_json_data(soup: BeautifulSoup) -> dict:
"""
Extracts product json data from a given BeautifulSoup object.
Website vjshop uses Next.js which embeds the data in JSON inside the HTML source,
like this:
<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {
...
"products": [
{...},
...
]
}
}
}
Args:
soup (BeautifulSoup): The BeautifulSoup object representing the parsed HTML.
Returns:
dict: A dictionary containing the extracted product information.
"""
# Find the tag "script" which has the json data
script_tag_data = soup.find("script", id="__NEXT_DATA__")
# Convert to python dict to work with json data more easily
json_data = json.loads(script_tag_data.contents[0])
return json_data
def check_invalid_page(json_data: dict) -> bool:
"""
Checks if the json data indicates an invalid page.
An invalid page is a page where there is no product shown.
Args:
json_data (dict): The dictionary containing the json data.
Returns:
bool: True if the page is considered invalid, False otherwise.
"""
if len(json_data["props"]["pageProps"]["products"]) == 0: # an empty list
print("Invalid page")
return True
else:
print("Valid page. Proceed to extract information...")
return False
def extract_product_attributes(json_data: dict) -> list:
"""
Extracts product attributes from json data.
Args:
json_data (dict): The dictionary containing the json data.
Returns:
list: A Python list containing dictionaries. Each dictionary stores
the attributes of one product.
"""
# Extract product attributes as python dict and append to a product list
product_list = []
for product in json_data["props"]["pageProps"]["products"]:
info_dict = {
"id": product["core"]["id"],
"name": product["core"]["name"],
"original_price": product["prices"]["default"].get("price", None),
"discounted_price": product["prices"].get("sale", {}).get("price", None)
}
product_list.append(info_dict)
print(f"Number of products on page: {len(product_list)}")
return product_list
def create_product_dataframe(product_list: list[dict]) -> pd.DataFrame:
"""
Creates a DataFrame from a list of product dictionaries.
Args:
product_list (list): A list of dictionaries, where each dictionary represents a product.
Returns:
pd.DataFrame: A DataFrame containing the product information.
"""
return pd.DataFrame(product_list)
def run_scraper(base_url: str, headers: str) -> None:
page_number = 1
total_product_list = []
while True:
try:
# Send a GET request to the specified URL with the current page number
response = requests.get(f"{base_url}?page={page_number}", headers=headers)
response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
print(f"Request successful! Status code = {response.status_code}")
# Parse HTML response
soup = parse_html(response)
# Extract the json product data
json_data = extract_product_json_data(soup)
# Check if the page is valid
if check_invalid_page(json_data):
break
# Extract product attributes
product_list = extract_product_attributes(json_data)
# Extend the total product list with the product list on the current page
total_product_list.extend(product_list)
page_number +=1
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
break
except requests.exceptions.RequestException as e:
print("A request error occurred:", e)
break
# Create the product dataframe
df = create_product_dataframe(total_product_list)
return df
df = run_scraper(BASE_URL, HEADERS)
df