Nhà biên kịch + Trình duyệt Scraping Không lỗi: Chặn API JSON ẩn
Senior Web Scraping Engineer
Một trang hiển thị "trích dẫn" không gửi đi trích dẫn nào. Yêu cầu https://quotes.toscrape.com/scroll bằng một client HTTP thông thường và nội dung phản hồi ở dạng một <div class="quotes"></div> trống — văn bản thực tế, tác giả và thẻ sẽ xuất hiện sau đó, qua một lời gọi GET /api/quotes?page=N, JavaScript của trang sẽ chạy khi tải và khi cuộn. Đọc cuộc gọi đó trực tiếp, thay vì chờ một trình duyệt để hiển thị nó thành HTML và sau đó phân tích HTML, là việc chặn yêu cầu mạng: kết nối một trình duyệt thực qua Giao thức DevTools của Chrome, lắng nghe lưu lượng mà nó tạo ra, và đọc JSON mà trang đã sản xuất cho chính nó.
Hướng dẫn này kết nối Playwright với Trình duyệt Scraping không có phần tử qua CDP, sau đó chặn điểm cuối ẩn đó theo hai cách — các sự kiện phản hồi của chính Playwright và tên miền CDP Mạng thô bên dưới chúng — trước khi phát lại điểm cuối trực tiếp qua HTTP đơn giản khi hình dạng của nó đã được biết. Mỗi lệnh bên dưới đều chạy chống lại mục tiêu trực tiếp.
Tại Sao Nên Đọc Dữ Liệu Thay Vì DOM
quotes.toscrape.com/scroll là một mục tiêu thực hành scraping công khai được xây dựng đặc biệt để chứng minh cuộn vô hạn, và mã của nó tự mình biện minh cho việc chặn. Lấy trang và tìm kiếm dữ liệu:
bash
curl -s https://quotes.toscrape.com/scroll | grep -o 'class="quote"' | wc -l
Kết quả là số không mỗi lần, vì các trích dẫn không bao giờ có trong HTML mà máy chủ gửi. Một khối jQuery nhỏ gọi $.get('/api/quotes', {page: page}) một lần khi tải và lại một lần nữa mỗi khi vị trí cuộn gần cuối trang, sau đó thêm các hàng trả về vào chứa trống theo cách thủ công. Một trình duyệt chạy JavaScript và chờ đủ lâu sẽ cuối cùng hiển thị 10 trích dẫn mỗi trang trong DOM của nó — nhưng đến khi bạn trở lại đếm các phần tử .quote và lấy văn bản từ các nút <span class="text"> và <small class="author">, bạn đã xây dựng lại, bằng tay, dữ liệu mà trang đã có sẵn dưới dạng JSON sạch sẽ và kiểu hóa: một mảng quotes, mỗi cái với text, một đối tượng author, và một danh sách tags, cộng với cờ has_next cho biết bạn chính xác khi nào nên dừng lại. Đọc phản hồi trực tiếp bỏ qua bước xây dựng lại và cung cấp cho bạn các trường thay vì các nút.
Các Điều Kiện Tiên Quyết
Bạn cần Python 3.9 hoặc mới hơn — playwright 1.59.0 khai báo Requires-Python >=3.9 trên PyPI — gói playwright chính nó, và một khóa API Scrapeless từ gói miễn phí tại app.scrapeless.com. Khóa này được gửi dưới dạng tham số truy vấn token trên điểm cuối CDP của Trình duyệt Scraping, vì vậy hãy giữ nó trong biến môi trường thay vì một giá trị cố định trong kịch bản của bạn. Không cần cài đặt Chrome cục bộ: connect_over_cdp kết nối với một trình duyệt đã tồn tại trong đám mây.
Kết Nối Playwright Với Trình Duyệt Scraping Qua CDP
Cài đặt client và đặt khóa:
bash
pip install playwright
bash
export SCRAPELESS_API_KEY="your_scrapeless_api_key"
Điểm cuối CDP của Trình duyệt Scraping là wss://browser.scrapeless.com/api/v2/browser, có ba tham số truy vấn — token, sessionTTL, và proxyCountry — cùng một bộ phận mà mọi kịch bản Playwright qua Trình duyệt Scraping trong loạt này đều sử dụng:
python
import os
from urllib.parse import urlencode
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=180):
params = urlencode({
"token": API_KEY,
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
chromium.connect_over_cdp(scraping_browser_url()) trả về một đối tượng Browser chuẩn của Playwright. Không có gì trong các mẫu chặn bên dưới là cụ thể cho Trình duyệt Scraping — chúng chạy đối với bất kỳ Chromium nào có thể tiếp cận CDP — nhưng chạy chúng trên Trình Duyệt Scraping có nghĩa là việc hiển thị xảy ra trên cơ sở hạ tầng đã mang egress theo kiểu hộ gia đình và Chromium chống phát hiện, vì vậy các trang tích cực nhận dạng vẫn sẽ hiển thị bình thường trong khi bạn đọc lưu lượng của chúng.
Ghi Lại API Ẩn Với Một Người Lắng Nghe Phản Hồi
Hàm page.expect_response() của Playwright liên kết việc chờ đợi với hành động kích hoạt nó — không có giấc ngủ tùy ý, không kéo chọn một số lượng cho đến khi nó trông ổn định. Liên kết nó quanh page.goto() cho cuộc gọi đầu tiên tới /api/quotes, sau đó quanh cuộn kích hoạt cuộc gọi thứ hai:
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=180):
params = urlencode({
"token": API_KEY,
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url())
page = browser.new_page()
with page.expect_response("**/api/quotes*") as first_page:
page.goto("https://quotes.toscrape.com/scroll", wait_until="domcontentloaded")
data = first_page.value.json()
python
print("Số lượng trích dẫn trong DOM trước khi phản hồi đầu tiên đến:", page.locator(".quote").count())
print(f"đã chặn trang {data['page']} -> {len(data['quotes'])} trích dẫn, has_next={data['has_next']}")
with page.expect_response("**/api/quotes*") as second_page:
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
data = second_page.value.json()
print("Số lượng trích dẫn trong DOM sau khi cuộn ổn định:", page.locator(".quote").count())
print(f"đã chặn trang {data['page']} -> {len(data['quotes'])} trích dẫn, has_next={data['has_next']}")
browser.close()
Chạy nó trên trang web trực tiếp in:
text
Số lượng trích dẫn trong DOM trước khi phản hồi đầu tiên đến: 10
đã chặn trang 1 -> 10 trích dẫn, has_next=True
Số lượng trích dẫn trong DOM sau khi cuộn ổn định: 20
đã chặn trang 2 -> 10 trích dẫn, has_next=True
Mẫu glob "**/api/quotes*" khớp với điểm cuối theo hình dạng URL, đây là mẫu được tài liệu của Playwright cho việc chờ một phản hồi mạng cụ thể thay vì một thời gian chờ cố định. Bởi vì việc chờ được ràng buộc với hành động kích hoạt, data['page'], data['quotes'], và data['has_next'] trở lại dưới dạng các giá trị Python đã định kiểu mà kịch bản của trang web sử dụng — không có .author.name được tái tạo từ một thẻ <small>, không có danh sách thẻ được xây dựng lại từ văn bản của thẻ liên kết.
Đi thấp hơn: Đọc khung thô với miền mạng CDP
Các sự kiện phản hồi của Playwright nằm trên miền mạng của Giao thức DevTools Chrome, và bạn có thể trò chuyện với miền đó trực tiếp thông qua CDPSession. Điều này quan trọng khi bạn không điều khiển Playwright ít nhất là ở mức độ nhất định — một khách hàng CDP bare trong một ngôn ngữ khác, một công cụ chỉ tiết lộ các sự kiện giao thức, hoặc một trường hợp mà bạn muốn tiêu đề phản hồi và thời gian mà một binding cụ thể không bộc lộ — bởi vì Network.responseReceived và Network.getResponseBody hoạt động theo cách tương tự bất kể thư viện khách nào nằm trên chúng:
python
import os, json
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=180):
params = urlencode({
"token": API_KEY,
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
request_ids, bodies = {}, []
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url())
page = browser.new_page()
cdp = page.context.new_cdp_session(page)
cdp.send("Network.enable")
def on_response_received(event):
if "/api/quotes" in event["response"]["url"]:
request_ids[event["requestId"]] = event["response"]["url"]
def on_loading_finished(event):
rid = event["requestId"]
if rid in request_ids:
raw = cdp.send("Network.getResponseBody", {"requestId": rid})
data = json.loads(raw["body"])
bodies.append((request_ids[rid], data["page"], len(data["quotes"]), data["quotes"][0]["author"]["name"]))
cdp.on("Network.responseReceived", on_response_received)
cdp.on("Network.loadingFinished", on_loading_finished)
page.goto("https://quotes.toscrape.com/scroll", wait_until="domcontentloaded")
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
for _ in range(20):
if len(bodies) >= 2:
break
page.wait_for_timeout(300)
for url, page_no, count, author in bodies:
print(f"CDP Network.getResponseBody trên {url}: trang={page_no}, trích dẫn={count}, tác giả đầu tiên={author}")
browser.close()
text
CDP Network.getResponseBody trên https://quotes.toscrape.com/api/quotes?page=1: trang=1, trích dẫn=10, tác giả đầu tiên=Albert Einstein
CDP Network.getResponseBody trên https://quotes.toscrape.com/api/quotes?page=2: trang=2, trích dẫn=10, tác giả đầu tiên=Marilyn Monroe
Network.responseReceived bật lên với tiêu đề và một requestId ngay khi phản hồi bắt đầu; nội dung chính sẽ không có sẵn cho đến khi Network.loadingFinished xác nhận việc chuyển giao đã hoàn tất, đó là lý do tại sao bộ xử lý chia thành hai sự kiện thay vì đọc nội dung từ sự kiện đầu tiên. getResponseBody trả về các byte chính xác mà trình duyệt nhận được, cộng với một cờ base64Encoded cho các tải nhị phân — lớp mà response.json() của Playwright xây dựng dựa trên và ẩn đi khỏi bạn.
Nhận thời gian chạy Scraping Browser miễn phí bằng cách đăng ký tại app.scrapeless.com và chạy cả hai script ở trên chống lại mục tiêu của riêng bạn.
Bỏ qua trình duyệt khi bạn đã biết hình dạng
Cả hai lần chặn ở trên đã chứng minh cùng một điều: /api/quotes?page=N là một GET công khai, không xác thực, trả về quotes, page, và has_next. Khi bạn biết được cấu trúc đó, không cần phải sử dụng trình duyệt nữa — một khách hàng HTTP đơn giản có thể truy cập toàn bộ bộ sưu tập một cách trực tiếp:
python
import json
import urllib.error
import urllib.request
def fetch_page(n):
url = f"https://quotes.toscrape.com/api/quotes?page={n}"
req = urllib.request.Request(url, headers={"User-Agent": "network-interception-demo/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status != 200:
raise urllib.error.HTTPError(url, resp.status, "unexpected status", resp.headers, None)
return json.loads(resp.read())
all_quotes, page = [], 1
while True:
data = fetch_page(page)
all_quotes.extend(data["quotes"])
if not data["has_next"]:
break
page += 1
print("pages fetched:", page)
print("total quotes:", len(all_quotes))
print("first quote author:", all_quotes[0]["author"]["name"])
print("last quote author:", all_quotes[-1]["author"]["name"])
text
pages fetched: 10
total quotes: 100
first quote author: Albert Einstein
last quote author: George R.R. Martin
Mười yêu cầu HTTP đơn giản đã lấy toàn bộ bộ sưu tập 100 câu trích dẫn qua cùng một hợp đồng JSON mà phiên trình duyệt đã xác nhận, mà hoàn toàn không có bất kỳ quy trình trình duyệt nào chạy cả. Đây là lợi ích thực sự của việc chặn: nhiệm vụ duy nhất của trình duyệt ở đây là tiết lộ điểm cuối. Khi bạn đã có nó, cách nhanh nhất để truy cập toàn bộ bộ sưu tập thường là dừng việc hiển thị và gọi điểm cuối trực tiếp, theo tiêu chuẩn Fetch mà cả trình duyệt và khách hàng HTTP đơn giản cuối cùng đều thực hiện.
Khi Bạn Vẫn Cần Trình Duyệt
Không phải tất cả các điểm cuối ẩn đều hợp tác như thế này. Nhiều điểm yêu cầu một cookie phiên mà máy chủ thiết lập trong một lần tải trang trước đó, một mã thông báo CSRF hoặc yêu cầu đã ký kết được tích hợp vào gói JavaScript của trang, hoặc một thân yêu cầu được xây dựng từ trạng thái chỉ tồn tại ở phía khách hàng, và một số gửi qua một khung WebSocket hoặc một POST GraphQL thay vì một GET dạng REST như tiêu chuẩn XMLHttpRequest mô tả. Trong những trường hợp đó, bước phát lại trực tiếp ở phần trước không áp dụng — bạn không thể tái tạo một tiêu đề xác thực mà bạn chưa từng nắm bắt — nhưng bước chặn vẫn áp dụng. page.expect_response() và miền Network của CDP đọc lưu lượng truy cập của một trang bất kể cách nào xác thực nó hoặc hình dạng tải trọng mà nó có, vì chúng quan sát những gì trình duyệt thực sự gửi và nhận thay vì giả định một định dạng yêu cầu cụ thể trước. Trên những trang đó, hãy giữ trình duyệt trong vòng lặp cho mọi trang: để nó tạo phiên, điều hướng, và mà đưa bạn mỗi phản hồi khi nó đến.
Đối với một ví dụ thực tế về cùng một mô hình render rồi đọc mạng được áp dụng cho một trang web đầy đủ thay vì một mục tiêu thực hành, xem hướng dẫn ch scraping TikTok, ghi lại XHR bình luận và bài viết theo cùng một cách trong khi cuộn một nguồn thực tế.
Việc chặn là một bước phát hiện cũng như một bước trích xuất. Lần đầu tiên bạn chạm vào một trang web chưa quen thuộc, hãy mở bảng mạng DevTools của nó, lọc theo Fetch/XHR, và theo dõi những gì xảy ra khi bạn tương tác với trang — việc kiểm tra thủ công đó chính là điều khiến bạn biết điểm cuối nào cần gán cho page.expect_response() trước khi bạn viết kịch bản. Khi điểm cuối đã được biết, mọi thứ ở trên — trình lắng nghe phản hồi, phiên CDP thô, và phát lại trực tiếp — là điều phát hiện đó được mã hóa dưới dạng mã chạy chính nó.
Đăng ký tại app.scrapeless.com để có miễn phí thời gian chạy Scraping Browser, hoặc xem trang sản phẩm Scraping Browser và giá cả cho các phiên chạy quy mô lớn.
Câu Hỏi Thường Gặp
Q: Việc chặn yêu cầu mạng trong việc thu thập dữ liệu web là gì?
Đó là việc đọc lưu lượng XHR/lấy mà JavaScript của trang tự tạo ra — thường là một cuộc gọi API JSON — thay vì chờ đợi lưu lượng đó được hiển thị thành HTML và sau đó phân tích cú pháp mã đánh dấu đã được hiển thị.
Q: Việc chặn các cuộc gọi API của một trang web có hợp pháp không?
Đọc phản hồi mà trình duyệt của bạn đã nhận khi truy cập một trang công khai có những cân nhắc khác với việc truy cập nội dung phía sau xác thực hoặc đạt được dữ liệu không công khai. Hạn chế bất kỳ quy trình công việc nào chỉ với các trang công khai, tôn trọng các điều khoản dịch vụ và chỉ dẫn của robots của mục tiêu, và giữ cho số lượng yêu cầu trong giới hạn — coi việc chặn là cách để đọc lưu lượng truy cập chính xác hơn, không phải là giấy phép để phớt lờ các quy tắc truy cập.
Q: Bạn có còn cần một trình duyệt khi bạn đã biết endpoint ẩn không?
Chỉ khi endpoint yêu cầu một cái gì đó mà trình duyệt cung cấp — một cookie phiên, một mã thông báo đã ký, trạng thái được tính toán bằng JavaScript. Một endpoint công khai, không xác thực như cái trong hướng dẫn này có thể được phát lại bằng một máy khách HTTP thông thường, như ví dụ về phát lại trực tiếp cho thấy.
Q: Sự khác biệt giữa page.expect_response() và lắng nghe miền CDP Network là gì?
page.expect_response() là lớp bọc cấp cao hơn của Playwright: kết nối nó với mẫu URL, kích hoạt hành động, nhận lại một đối tượng Response đã phân tích. Miền Network trong CDP là giao thức bên dưới nó — Network.responseReceived và Network.getResponseBody — hữu ích khi bạn không sử dụng một bọc Playwright nào, hoặc cần chi tiết cấp độ giao thức mà một thư viện khách cụ thể không tiết lộ.
Q: Liệu điều này có hoạt động trên các endpoint trả về thứ gì đó không phải JSON không?
Cả hai mẫu chặn đều đọc bất kỳ byte nào mà phản hồi mang lại — response.text() hoặc response.body() trong Playwright, trường body thô từ Network.getResponseBody trong CDP — vì vậy các đoạn HTML, XML, hoặc bất kỳ dữ liệu nào khác được truyền qua cùng một cách. JSON chỉ đơn giản là trường hợp thông thường cho API nội bộ của một trang.
Q: Liệu page.expect_response() có thể bắt được các yêu cầu được thực hiện trước khi trang bạn đang theo dõi tải không?
Không — nó phải lắng nghe trước khi hành động kích hoạt chạy, đó là lý do nó bọc hành động cụ thể page.goto() hoặc tương tác mà gây ra yêu cầu, thay vì được gắn vào sau đó.
Q: Việc chặn có cần trình duyệt Scrapeless Scraping cụ thể không, hay nó hoạt động với bất kỳ Chromium nào có thể truy cập thông qua CDP?
Kỹ thuật này tự nó là hành vi CDP chung và hoạt động trên bất kỳ Chromium nào bạn có thể truy cập qua connect_over_cdp, cục bộ hoặc từ xa. Chạy trên Scrapeless Scraping Browser thêm tính năng chống phát hiện của Chromium và luồng cư dân, điều này quan trọng khi mục tiêu bạn đang chặn ký hiệu đủ mạnh để một trình duyệt cục bộ bình thường không thể vượt qua việc render để tạo ra lưu lượng truy cập ngay từ đầu.
Q: Điều gì sẽ xảy ra nếu trang web thay đổi endpoint hoặc hình dạng phản hồi của nó?
Mã chặn vẫn hoạt động miễn là mẫu URL vẫn còn khớp; một trường được đổi tên hoặc payload được cấu trúc lại sẽ phá vỡ mã đọc data['quotes'], giống như cách một bộ chọn CSS bị phá vỡ khi tên lớp thay đổi. Không phương pháp nào miễn dịch với việc thiết kế lại — đọc API chỉ có nghĩa là bạn đang theo dõi một hợp đồng JSON thay vì một cấu trúc markup, điều này thường ít thay đổi hơn.
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.



