Cách xây dựng một quy trình thu thập dữ liệu web với Pandas và Scrapeless
Advanced Data Extraction Specialist
TL; DR:
- API thu thập dữ liệu Scrapeless Universal trả về HTML đã được rendered cho một trang công khai, và hàm
read_htmlcủa pandas chuyển đổi các bảng trên trang đó thành DataFrames trong một cuộc gọi duy nhất. - Hướng dẫn này xây dựng công việc theo năm giai đoạn rõ ràng: lấy dữ liệu, khám phá, trích xuất, biến đổi, và lưu trữ, mỗi giai đoạn chỉ là vài dòng Python.
- Trong pandas 3.x,
read_htmlcần một bộ bao quanhio.StringIOcho một chuỗi HTML, vì một chuỗi trần giờ đây được đọc như một đường dẫn tệp và báo lỗi. - Việc làm sạch thuộc về sau khi trích xuất: đổi tên các cột, ép kiểu số bằng
pd.to_numeric, và thêm các cột tính toán trước khi bất kỳ điều gì được ghi vào đĩa. - Parquet bảo tồn các kiểu cột nhưng không tự động nhỏ hơn CSV; trong một mẫu 75 hàng, chi phí chân trang của nó làm cho nó trở thành tệp lớn hơn, và lợi thế kích thước xuất hiện với khối lượng.
- Bắt đầu từ gói miễn phí Scrapeless và nhắm giai đoạn lấy dữ liệu vào nguồn của riêng bạn.
Hầu hết các hướng dẫn về pandas bắt đầu với một CSV đã tồn tại. Công việc thực tế hiếm khi bắt đầu ở đó. Các số liệu bạn muốn thường nằm trong một trang HTML, được bọc trong điều hướng, kiểu dáng, và đánh dấu mà một requests.get thông thường thường không thể lấy được một cách sạch sẽ. Khoảng cách giữa "có một bảng trên trang đó" và "có một DataFrame được định kiểu trong bộ nhớ" là nơi tồn tại một pipeline thu thập dữ liệu.
Bài viết này khép lại khoảng cách đó với hai công cụ. API thu thập dữ liệu Scrapeless Universal xử lý việc truy xuất và trả về trang dưới dạng HTML. Pandas xử lý cấu trúc: nó đọc các bảng, làm sạch chúng, và ghi lại các tệp được định kiểu mà bạn có thể phân tích. Ví dụ mục tiêu là một hộp cát thu thập công khai với một bảng phân trang các mùa giải đội NHL, vì vậy mọi con số bên dưới đều đến từ một cuộc chạy thực tế trên một trang thực tế.
Pipeline Tóm tắt
Pipeline có năm giai đoạn, và mỗi giai đoạn truyền một đối tượng đơn giản, được định nghĩa rõ ràng cho giai đoạn tiếp theo:
fetch (chuỗi HTML) → discover (bảng nào) → extract (DataFrame) → transform (DataFrame đã được định kiểu, sạch sẽ) → store (CSV + Parquet)
Việc giữ tách biệt các giai đoạn mang lại lợi ích ngay lần đầu tiên một trang thay đổi. Khi bố cục thay đổi, chỉ giai đoạn khám phá di chuyển. Khi một cột bắt đầu xuất hiện dưới dạng văn bản, chỉ giai đoạn biến đổi thay đổi. Các giai đoạn lấy dữ liệu và lưu trữ giữ nguyên.
Giai đoạn 1: Lấy Trang dưới dạng HTML Sạch
Giai đoạn lấy dữ liệu gửi một yêu cầu đến Scrapeless và trả về trang dưới dạng một chuỗi HTML. Yêu cầu lấy một actor và một đối tượng input; actor unlocker.webunlocker truy xuất trang, và khóa API đi qua tiêu đề x-api-token.
Pipeline này cần ba gói: chính pandas, một trình phân tích cho read_html, và một động cơ Parquet cho giai đoạn lưu trữ. Cài đặt tất cả ba gói cùng một lúc.
bash
pip install pandas pyarrow lxml
python
import io
import json
import os
import urllib.request
import pandas as pd
API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
def fetch_html(url: str) -> str:
payload = json.dumps(
{"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
).encode()
request = urllib.request.Request(
API_URL,
data=payload,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=90) as response:
return json.loads(response.read())["data"]
html = fetch_html("https://www.scrapethissite.com/pages/forms/")
tables = pd.read_html(io.StringIO(html))
df = tables[0]
print(f"tables trên trang: {len(tables)} | hình dạng: {df.shape}")
print("các cột thô:", list(df.columns))
Nội dung phản hồi là một bao bì JSON với mã đánh dấu đã được rendered dưới khóa data, vì vậy json.loads(response.read())["data"] là toàn bộ trang dưới dạng một chuỗi. js_render được đặt là False ở đây có chủ ý, phần Khi Nào Bạn Cần Một Trang Được Rendered giải thích.
Thiết lập khóa một lần trong shell của bạn trước khi chạy bất kỳ điều gì. Sử dụng khóa thực tại thời gian chạy và giữ cho placeholder ra khỏi nguồn của bạn.
bash
export SCRAPELESS_API_KEY="sk_your_key_here"
Giai đoạn 2: Khám Phá Bảng
Khám phá trả lời một câu hỏi: bảng nào trên trang chứa dữ liệu. Chạy đoạn mã ở trên sẽ in ra câu trả lời cho mục tiêu này.
text
tables trên trang: 1 | hình dạng: (25, 9)
các cột thô: ['Tên Đội', 'Năm', 'Thắng', 'Thua', 'Thua thêm', 'Tỷ lệ Thắng', 'Bàn Thắng (GF)', 'Bàn Thua (GA)', '+ / -']
read_html quét HTML và trả về một danh sách với một DataFrame cho mỗi phần tử <table> mà nó tìm thấy, theo cùng một mô hình bảng như đặc tả dữ liệu dạng bảng HTML. Trang này có một bảng đơn, vì vậy tables[0] là bảng mà bạn muốn. Trên một trang có nhiều bảng, hãy in hình dạng và các hàng đầu tiên của mỗi bảng, chọn chỉ số phù hợp với các cột của bạn, và mã hóa cứng chỉ số đó. Các tên cột thô được chuyển thẳng từ các ô <th>, đó là lý do tại sao chúng vẫn mang theo khoảng trắng và dấu câu.
Giai đoạn 3: Đọc vào một DataFrame
Việc trích xuất đã được thực hiện. Đó là mục đích của read_html: nó biến toàn bộ bảng thành một DataFrame mà không cần vòng lặp thủ công qua các hàng và ô. Tài liệu tham khảo pandas read_html ghi lại yêu cầu về io.StringIO mà hầu hết các lần thử đầu tiên trên pandas 3.x gặp phải. Một chuỗi HTML trần được diễn giải như một tên tệp; việc bọc nó trong io.StringIO(html) cho phép pandas phân tích chuỗi đó.
Với 25 hàng và 9 cột trong tay, khung thô có thể sử dụng nhưng chưa sạch sẽ. Các tên cột thì vụng về, và mỗi giá trị vẫn là kiểu mà bộ phân tích HTML suy diễn. Cả hai vấn đề đều thuộc về giai đoạn tiếp theo.
Giai đoạn 4: Biến đổi và định kiểu dữ liệu
Giai đoạn biến đổi thực hiện ba công việc theo thứ tự: đổi tên các cột thành cái gì đó bạn có thể gõ, ép giá trị thành số, và thêm các cột phát sinh mà phân tích của bạn cần.
python
raw.columns = ["team", "year", "wins", "losses", "ot_losses", "win_pct", "goals_for", "goals_against", "goal_diff"]
numeric = ["year", "wins", "losses", "ot_losses", "win_pct", "goals_for", "goals_against", "goal_diff"]
raw[numeric] = raw[numeric].apply(pd.to_numeric, errors="coerce")
raw["games"] = raw["wins"] + raw["losses"] + raw["ot_losses"].fillna(0)
raw["winning_season"] = raw["win_pct"] >= 0.5
clean = raw.dropna(subset=["team", "wins"]).reset_index(drop=True)
pd.to_numeric với errors="coerce" là công cụ chủ lực. Nó chuyển đổi các số sạch và biến bất cứ thứ gì không thể phân tích thành NaN thay vì làm hỏng toàn bộ cột, điều này quan trọng ở đây vì các mùa giải cũ để trống ô thua trong hiệp phụ. fillna(0) sau đó coi những ô trống đó là số không khi tính toán số trận đã chơi, và dropna loại bỏ bất kỳ hàng nào thiếu tên đội hoặc số lượng chiến thắng. Kết quả là một khung mà mỗi cột số thực sự là số, và các cột phát sinh games và winning_season đã sẵn sàng để nhóm và lọc.
Giai đoạn 5: Lưu dưới dạng CSV và Parquet
Lưu trữ ghi khung sạch hai lần, vì hai định dạng đáp ứng nhu cầu khác nhau.
python
clean.to_csv("teams.csv", index=False)
clean.to_parquet("teams.parquet", index=False)
CSV là định dạng di động và có thể đọc bởi bất cứ thứ gì, từ bảng tính đến một dòng lệnh shell, và nó tuân theo định dạng giá trị phân cách bằng dấu phẩy được triển khai rộng rãi. Chi phí của nó là các kiểu mất đi ngay khi bạn ghi; mỗi cột trở thành văn bản khi đọc trở lại. Parquet giữ nguyên lược đồ. Khi bạn đọc lại teams.parquet, wins vẫn là một số nguyên và win_pct vẫn là một số thực, mà không cần phải ép kiểu lại, vì định dạng tệp Apache Parquet lưu trữ loại của mỗi cột bên cạnh các giá trị của nó.
Parquet không phải luôn là tệp nhỏ hơn, bất kể truyền thuyết nói gì. Trên mẫu 75 hàng này, CSV có kích thước 4,379 byte và tệp Parquet có kích thước 8,999 byte, vì siêu dữ liệu và chân tệp của Parquet là chi phí cố định mà một tập dữ liệu nhỏ không thể phân bổ. Lưu trữ kết quả nhỏ dưới dạng CSV nếu kích thước là điều bạn quan tâm. Sử dụng Parquet khi số lượng hàng tăng lên hàng chục nghìn và việc đọc theo cột với kiểu dữ liệu trở nên quan trọng hơn lượng byte.
Quy trình hoàn chỉnh
Đặt năm giai đoạn vào một tập lệnh và thêm phân trang, quy trình sẽ lấy ba trang, xây dựng một khung 75 hàng, làm sạch nó và ghi cả hai tệp.
python
import io
import json
import os
import urllib.request
import pandas as pd
API_URL = "https://api.scrapeless.com/api/v2/unlocker/request"
BASE = "https://www.scrapethissite.com/pages/forms/"
PAGES = 3 # giới hạn: ba trang của bảng công khai trong sandbox
def fetch_html(url: str) -> str:
"""Giai đoạn 1 - lấy HTML đã được render qua Scrapeless Universal Scraping API."""
payload = json.dumps(
{"actor": "unlocker.webunlocker", "input": {"url": url, "js_render": False, "headless": False}}
).encode()
request = urllib.request.Request(
API_URL,
data=payload,
headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"], "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=90) as response:
return json.loads(response.read())["data"]
Giai đoạn 1-3 - lấy từng trang, phát hiện bảng duy nhất, đọc thẳng vào DataFrame
frames = []
for page in range(1, PAGES + 1):
html = fetch_html(f"{BASE}?page_num={page}")
frames.append(pd.read_html(io.StringIO(html))[0])
raw = pd.concat(frames, ignore_index=True)
print(f"đã lấy trang: {PAGES} | số hàng đã phân tích: {len(raw)}")
Giai đoạn 4 - biến đổi: làm sạch tên cột, ép kiểu số, thêm cột derived
raw.columns = ["đội", "năm", "chiến thắng", "thua", "thua thêm giờ", "tỉ lệ thắng", "bàn thắng", "bàn thua", "chênh lệch bàn thắng"]
numeric = ["năm", "chiến thắng", "thua", "thua thêm giờ", "tỉ lệ thắng", "bàn thắng", "bàn thua", "chênh lệch bàn thắng"]
raw[numeric] = raw[numeric].apply(pd.to_numeric, errors="coerce")
raw["trận đấu"] = raw["chiến thắng"] + raw["thua"] + raw["thua thêm giờ"].fillna(0)
raw["mùa giải thắng"] = raw["tỉ lệ thắng"] >= 0.5
clean = raw.dropna(subset=["đội", "chiến thắng"]).reset_index(drop=True)
print(f"số hàng sau khi làm sạch: {len(clean)} | mùa giải thắng: {int(clean['mùa giải thắng'].sum())}")
best = clean.sort_values("chiến thắng", ascending=False).iloc[0]
print(f"mùa giải tốt nhất: {best['đội']} {int(best['năm'])} ({int(best['chiến thắng'])} chiến thắng)")
Giai đoạn 5 - lưu trữ khung dưới dạng CSV để di động và Parquet để đọc cột kiểu
clean.to_csv("teams.csv", index=False)
clean.to_parquet("teams.parquet", index=False)
print(f"byte csv: {os.path.getsize('teams.csv')} | byte parquet: {os.path.getsize('teams.parquet')}")
Chạy in ra một dấu vết ngắn gọn của từng giai đoạn:
text
đã lấy trang: 3 | số hàng đã phân tích: 75
số hàng sau khi làm sạch: 75 | mùa giải thắng: 26
mùa giải tốt nhất: Pittsburgh Penguins 1992 (56 chiến thắng)
byte csv: 4379 | byte parquet: 8999
Tham số truy vấn page_num điều khiển phân trang, và PAGES giới hạn chạy đến ba trang để ví dụ vẫn nhỏ và lịch sự. Tăng hằng số đó để mở rộng phạm vi, và giữ nó ở một nơi để giới hạn là một quyết định, không phải một tai nạn.
Chỉ định đường ống này ở một nguồn bạn quan tâm bằng cách hoán đổi URL BASE và tên cột, và hình dạng năm giai đoạn được giữ nguyên không thay đổi. Nếu bạn muốn có bối cảnh rộng hơn về cách mỗi giai đoạn này phù hợp, hướng dẫn về ETL pipeline là gì đi qua các bước trích xuất, biến đổi và tải dưới dạng hướng dẫn tổng quát.
Khi Bạn Cần Một Trang Đã Rendered
Đường ống này đặt js_render thành False, và đó là một lựa chọn cố ý, không phải một mặc định để bỏ qua. Bảng sandbox có mặt trong HTML mà máy chủ gửi, vì vậy không có gì cho trình duyệt để render, và việc bỏ qua rendering làm cho mỗi lần lấy nhanh hơn. Nhiều trang là khác nhau: bảng bạn muốn được chèn bởi JavaScript sau khi HTML ban đầu được tải, và một lần lấy không render trả về một shell trống. Khi read_html không tìm thấy bảng nào trên một trang mà bạn có thể thấy trong trình duyệt, hãy đặt js_render thành True để Scrapeless trả về trang sau khi các kịch bản của nó chạy. Quyết định theo từng nguồn thay vì bật rendering ở khắp nơi, vì rendering một trang không cần thiết chỉ làm tăng độ trễ.
Trước khi bạn mở rộng bất kỳ điều gì trong số này, hãy đọc robots.txt và điều khoản của mục tiêu. Giao thức loại trừ Robots cho bạn biết các đường dẫn mà một trang web yêu cầu các khách hàng tự động không được can thiệp, và tôn trọng điều này giúp một đường ống dữ liệu ở bên đúng của các trang web mà nó phụ thuộc. Giữ cho khối lượng được giới hạn và mục tiêu công khai, như ví dụ này đã làm.
Sẵn sàng chạy điều này với một nguồn thực tế? Tạo một tài khoản Scrapeless miễn phí và thay đổi URL trong giai đoạn lấy dữ liệu.
Kết luận
Một đường ống scraping là năm giai đoạn nhỏ, mỗi giai đoạn đảm nhiệm một công việc. Scrapeless lấy trang, read_html trích xuất bảng, to_numeric và một vài gán biến làm sạch nó, và hai cuộc gọi to_ lưu trữ nó. Bởi vì các giai đoạn là tách biệt, đường ống có thể tồn tại qua những thay đổi: một bố cục mới ảnh hưởng đến việc phát hiện, một loại cột mới ảnh hưởng đến việc biến đổi, và phần còn lại giữ nguyên. Bắt đầu từ kịch bản làm việc ở trên, hoán đổi vào mục tiêu riêng của bạn và phát triển giai đoạn biến đổi theo nhu cầu dữ liệu của bạn.
Bắt đầu với gói miễn phí Scrapeless để chạy giai đoạn lấy dữ liệu đối với các trang của riêng bạn và kiểm tra giá của Scrapeless khi bạn xác định một công việc thường xuyên.
Câu Hỏi Thường Gặp
H: Tại sao pandas.read_html lại thất bại trên một chuỗi HTML trong pandas 3.x?
Một tham số chuỗi đơn giản được xử lý như một đường dẫn tệp hoặc URL, vì vậy pandas cố gắng mở nó và phát sinh lỗi. Bọc mã trong io.StringIO(html) và truyền nó thay thế; read_html sau đó phân tích chuỗi trong bộ nhớ và trả về một danh sách các DataFrame.
H: Tôi có cần BeautifulSoup hoặc lxml để sử dụng read_html không?
read_html cần một trình phân tích HTML được cài đặt, và nó sử dụng lxml hoặc html5lib ở bên trong, vì vậy hãy cài đặt một trong số đó cùng với pandas. Bạn không cần viết mã phân tích riêng cho việc trích xuất bảng; read_html điều khiển trình phân tích và trả về DataFrames.
Q: Khi nào tôi nên đặt js_render thành True?
Hãy đặt nó thành True khi dữ liệu được thêm vào trang bởi JavaScript sau khi HTML ban đầu đã tải, điều này dẫn đến việc read_html không tìm thấy bảng nào trên một trang rõ ràng có bảng trong trình duyệt. Để nó là False khi bảng đã có trong HTML của máy chủ, vì việc render một trang không cần thiết chỉ làm tăng độ trễ.
Q: Tôi nên lưu trữ dữ liệu đã lấy như CSV hay Parquet?
Chọn CSV khi bạn muốn một tệp di động, dễ đọc và số lượng dòng là nhỏ; chọn Parquet khi bạn muốn bảo tồn kiểu cột và tập dữ liệu đủ lớn đến mức các kiểu đọc theo cột là cần thiết. Với các mẫu nhỏ, Parquet có thể là tệp lớn hơn do chi phí cố định của footer, vì vậy kích thước đơn thuần ủng hộ CSV cho đến khi dữ liệu tăng lên.
Q: Tôi xử lý một trang có nhiều bảng như thế nào?
read_html trả về mỗi bảng như một DataFrame riêng lẻ trong một danh sách, vì vậy hãy in hình dạng và vài hàng đầu tiên của từng phần tử để xác định cái bạn muốn, sau đó chỉ mục nó trực tiếp. Khi bạn biết vị trí là ổn định, hãy mã cứng chỉ mục đó trong giai đoạn trích xuất.
Q: Làm thế nào để tôi giữ việc lấy dữ liệu lịch sự khi tôi thêm nhiều trang hơn?
Giữ trang bị ràng buộc trong một hằng số duy nhất, như PAGES làm ở đây, để việc mở rộng phạm vi là một chỉnh sửa có chủ đích thay vì một vòng lặp không giới hạn. Đọc robots.txt và các điều khoản của trang trước, và chỉ thu thập dữ liệu công khai với khối lượng mà mục tiêu có thể hấp thụ.
Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.



