Playwright + Trình duyệt Scraping Không có Scrapeless: Ghi lại và Phát lại một API GraphQL Ẩn
Web Data Collection Specialist
Mở tab Mạng trên rickandmortyapi.com/graphql và theo dõi trong một phút: cuộc gọi kiểm tra sơ đồ GraphiQL được kích hoạt ngay lập tức khi trang tải, và truy vấn bạn nhập vào và tự chạy một lúc sau đó, cả hai đều đến cùng một URL chính xác. Một API REST phân tán hành vi của nó qua các đường dẫn — /characters, /episodes, /locations/1 — vì vậy chỉ cần URL đã cho bạn biết yêu cầu là gì. Một API GraphQL gom tất cả vào một điểm cuối và di chuyển yêu cầu thực vào phần thân POST: một chuỗi query tên các trường bạn muốn, một đối tượng variables cung cấp các đối số, đôi khi là một thẻ operationName xác định đây là cái nào. Đọc lưu lượng đó có nghĩa là đọc phần thân, không phải URL, vì URL đã ngừng mang tín hiệu.
Hướng dẫn này kết nối Playwright với Trình duyệt Lấy dữ liệu Scrapeless qua CDP, điều khiển một sân chơi GraphQL công khai thực sự vào việc kích hoạt một truy vấn, và chặn POST kết quả theo hai cách độc lập — các sự kiện phản hồi của riêng Playwright, và miền CDP Network thô ở dưới chúng — trước khi phát lại yêu cầu chính xác đó với một khách hàng HTTP đơn giản và không có trình duyệt nào. Mọi lệnh bên dưới đều chạy trên mục tiêu trực tiếp.
Một Điểm Cuối, Mọi Hoạt Động
https://rickandmortyapi.graphcdn.app/ là địa chỉ mà sân chơi GraphiQL riêng của API Rick và Morty thực sự gọi, một lớp bên sau bí danh rickandmortyapi.com/graphql thân thiện hơn mà tài liệu của nó quảng cáo; nó trả lời cả yêu cầu đến bí danh đó và yêu cầu đến địa chỉ CDN trực tiếp với dữ liệu giống nhau. Địa chỉ duy nhất đó phục vụ mọi hoạt động mà sân chơi có thể gửi: truy vấn kiểm tra sơ đồ mà nó tự động kích hoạt khi tải để lấp đầy trình khám phá sơ đồ của nó, và bất kỳ truy vấn nào bạn nhập vào và thực hiện. Một bộ lọc mạng viết chỉ dựa trên URL đó (page.route("**/graphcdn.app/**", ...), hoặc một trình lắng nghe CDP chỉ dựa vào tên miền) sẽ chặn cả hai một cách không phân biệt — chính xác là vấn đề mà quy ước phục vụ HTTP của GraphQL tạo ra theo thiết kế: một URL, một phương pháp, mọi hoạt động được phân biệt bởi những gì bên trong yêu cầu thay vì nơi nó được gửi. Tách riêng truy vấn mà thực sự quan trọng có nghĩa là đọc trường operationName trong phần thân POST hoặc chính văn bản query, chứ không phải địa chỉ mà nó đã đến.
Sân chơi tự nó làm nửa còn lại của sự khác biệt trở nên rõ ràng: khác với một điểm cuối REST kích hoạt khi cuộn mà xảy ra ngay khi một trang tải hoặc một người dùng cuộn, trình soạn thảo truy vấn của GraphiQL bắt đầu trống rỗng. Không có gì có ý nghĩa xảy ra cho đến khi bạn nhập một truy vấn và nhấp vào Thực hiện — kỹ thuật ở đây phải thúc đẩy sự tương tác đó, không chỉ chờ đợi nó.
Đ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, và một khóa API Scrapeless từ gói miễn phí tại app.scrapeless.com. Điểm cuối GraphQL mục tiêu tự nó không cần khóa hoặc tài khoản riêng; nó là dữ liệu công khai, không xác thực. Giữ khóa Scrapeless trong một biến môi trường thay vì một chuỗi trong kịch bản của bạn, vì nó di chuyển như tham số truy vấn token trên điểm cuối CDP của Trình duyệt Lấy dữ liệu.
Cài Đặt
bash
pip install playwright
bash
export SCRAPELESS_API_KEY="your_scrapeless_api_key"
Kết Nối Qua CDP
Sử dụng lại cùng một mẫu xây dựng URL mà mọi kịch bản từ Playwright đến Trình duyệt Lấy dữ liệu trong loạt này sử dụng: ba tham số truy vấn trên một điểm cuối WSS.
python
import os
from urllib.parse import urlencode
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=120):
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ả lại một đối tượng Playwright tiêu chuẩn Browser, không yêu cầu cài đặt Chrome cục bộ. Không có gì về hai kỹ thuật chặn bên dưới là cụ thể cho Trình duyệt Lấy dữ liệu — chúng chạy trên bất kỳ Chromium nào có thể tiếp cận CDP — nhưng việc chạy render trên cơ sở hạ tầng của Trình duyệt Lấy dữ liệu có nghĩa là một giao diện GraphQL mà nhận diện khách hàng của nó vẫn cấp nước và kích hoạt các truy vấn của nó như bình thường.
Kích Hoạt Truy Vấn và Bắt Nó Với Một Trình Lắng Nghe Phản Hồi
page.expect_response() liên kết chờ với hành động kích hoạt nó, vì vậy nó hoạt động dù hành động đó là một page.goto() hoặc, như ở đây, một tương tác giao diện người dùng mà bạn tự điều khiển. Nhập một truy vấn thực tế và các biến của nó vào trình soạn thảo của GraphiQL, nhấp vào Thực hiện trong ngữ cảnh expect_response, và đối tượng Response được chặn trả lại chính xác những gì JavaScript của trang gửi đi và nhận lại:
python
import json
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
QUERY = (
"query GetCharacters($page: Int, $name: String) { "
"characters(page: $page, filter: { name: $name }) { "
"info { count pages } "
"results { id name status species } } }"
)
VARIABLES = '{"page": 1, "name": "rick"}'
def scraping_browser_url(proxy_country="US", session_ttl=120):
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()
page.goto("https://rickandmortyapi.com/graphql", wait_until="domcontentloaded")
query_editor = page.locator(".graphiql-query-editor .CodeMirror").first
query_editor.click()
page.keyboard.press("Control+A")
page.keyboard.insert_text(QUERY)
page.locator("button:has-text('Variables')").first.click()
variables_editor = page.locator(".graphiql-editor-tool .CodeMirror").first
variables_editor.click()
page.keyboard.press("Control+A")
page.keyboard.insert_text(VARIABLES)
with page.expect_response(lambda r: "graphcdn.app" in r.url and r.request.method == "POST") as run:
page.locator("button.graphiql-execute-button").click()
resp = run.value
sent = json.loads(resp.request.post_data)
data = resp.json()["data"]["characters"]
print("POST target:", resp.request.url)
print("operationName:", sent["operationName"])
print("variables sent:", sent["variables"])
print("info:", data["info"])
print("first result:", data["results"][0])
print("result count in this page:", len(data["results"]))
browser.close()
Chạy nó trên sân chơi trực tiếp in ra:
text
POST target: https://rickandmortyapi.graphcdn.app/
operationName: GetCharacters
variables sent: {'page': 1, 'name': 'rick'}
info: {'count': 107, 'pages': 6}
first result: {'id': '1', 'name': 'Rick Sanchez', 'status': 'Alive', 'species': 'Human'}
result count in this page: 20
sent["variables"] là cùng một dict Python mà bảng Variables của trình soạn thảo đã giữ — {"page": 1, "name": "rick"} — xác nhận việc chặn đã đọc phần thân yêu cầu thực tế, không phải một giả định về những gì truy vấn có thể chứa. page.keyboard.insert_text() hơn là page.keyboard.type() quan trọng ở đây: CodeMirror, trình soạn thảo mà GraphiQL sử dụng, tự động đóng dấu ngoặc khi bạn nhập chúng từng ký tự một, vì vậy việc mô phỏng các phím bấm riêng lẻ cho một truy vấn đầy { và } tạo ra các dấu đóng thừa và một lỗi cú pháp. insert_text() chèn toàn bộ chuỗi một lần, kiểu như một phép dán, và bỏ qua hoàn toàn logic tự động đóng cho từng phím bấm.
Khớp Yêu Cầu Đúng Trong Miền Mạng CDP Thô
Sự kiện phản hồi của Playwright nằm trên miền Mạng Giao thức DevTools của Chrome, có thể truy cập trực tiếp thông qua CDPSession trong các trường hợp bạn không điều khiển Playwright — một khách hàng CDP trống hoặc một công cụ chỉ phơi bày các sự kiện giao thức. Bởi vì URL điểm cuối chỉ không phân biệt các thao tác, bộ lọc cấp CDP phải kiểm tra postData theo cách mà việc nắm bắt cấp cao hơn ngầm ngụ ý bằng cách khớp với cú nhấp chuột kích hoạt:
python
import json
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
QUERY = (
"query GetCharacters($page: Int, $name: String) { "
"characters(page: $page, filter: { name: $name }) { "
"info { count pages } "
"results { id name status species } } }"
)
VARIABLES = '{"page": 2, "name": "rick"}'
captured = {}
def scraping_browser_url(proxy_country="US", session_ttl=120):
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()
cdp = page.context.new_cdp_session(page)
cdp.send("Network.enable")
def on_request(event):
# The playground also fires a schema-introspection POST to this same
# URL on load. Matching on operationName in the body -- not the URL
# -- is what separates it from the query this script triggers.
request = event["request"]
if "graphcdn.app" in request["url"] and "GetCharacters" in request.get("postData", ""):
captured[event["requestId"]] = None
def on_finished(event):
request_id = event["requestId"]
if request_id in captured and captured[request_id] is None:
body = cdp.send("Network.getResponseBody", {"requestId": request_id})
captured[request_id] = json.loads(body["body"])
cdp.on("Network.requestWillBeSent", on_request)
cdp.on("Network.loadingFinished", on_finished)
page.goto("https://rickandmortyapi.com/graphql", wait_until="domcontentloaded")
query_editor = page.locator(".graphiql-query-editor .CodeMirror").first
query_editor.click()
page.keyboard.press("Control+A")
page.keyboard.insert_text(QUERY)
page.locator("button:has-text('Variables')").first.click()
variables_editor = page.locator(".graphiql-editor-tool .CodeMirror").first
variables_editor.click()
page.keyboard.press("Control+A")
page.keyboard.insert_text(VARIABLES)
page.locator("button.graphiql-execute-button").click()
for _ in range(30):
if captured and all(v is not None for v in captured.values()):
break
page.wait_for_timeout(300)
data = next(iter(captured.values()))["data"]["characters"]
print("requests matched by body content:", len(captured))
print("info:", data["info"])
print("first result:", data["results"][0])
browser.close()
text
requests matched by body content: 1
info: {'count': 107, 'pages': 6}
first result: {'id': '218', 'name': 'Mechanical Rick', 'status': 'unknown', 'species': 'Robot'}
Network.requestWillBeSent được kích hoạt với trường postData của yêu cầu đi ra đã được đính kèm, trước khi có phản hồi — điểm tự nhiên để quyết định xem POST cụ thể này có đáng theo dõi hay không. Network.loadingFinished xác nhận phản hồi khớp đã hoàn tất việc chuyển giao, và chỉ sau đó getResponseBody trả lại các byte. Trang 2 trở về với một kết quả đầu tiên khác so với trang 1, đây là điểm: đường dẫn raw-CDP và đường dẫn lắng nghe phản hồi đang đọc cùng một dây, khớp theo hai cách khác nhau, và cả hai đều dẫn đến dữ liệu thực tế, khác nhau từ cùng một truy vấn trực tiếp.
Những Gì Bạn Nhận Lại
Cả hai đường dẫn nắm bắt đều trả về cùng một hình dạng Character cho truy vấn này, vì cả hai đều đọc cùng một phản hồi cơ sở.
| Trường | Kiểu | Ý nghĩa |
|---|---|---|
info.count |
số nguyên | Tổng số ký tự khớp với bộ lọc trên mọi trang |
info.pages |
số nguyên | Tổng số trang tại kích thước trang hiện tại |
results[].id |
chuỗi | ID ký tự, có thể sử dụng trực tiếp trong một truy vấn character(id: ...) tiếp theo |
results[].name |
chuỗi | Tên ký tự |
results[].status |
chuỗi | "Alive", "Dead", hoặc "unknown" |
results[].species |
chuỗi | Phân loại loài |
Thay thế filter: { name: $name } bằng filter: { status: "Alive" } hoặc bỏ hẳn tham số bộ lọc, và cùng hai kịch bản nắm bắt vẫn hoạt động mà không bị sửa đổi — chỉ có tải variables và info.count kết quả thay đổi, vì kỹ thuật cấp dây không phụ thuộc vào các trường hoặc tham số mà một truy vấn cụ thể sử dụng.
Nhận miễn phí thời gian chạy Trình duyệt Scraping bằng cách đăng ký tại app.scrapeless.com và chạy cả hai kịch bản nắm bắt ở trên chống lại một điểm cuối GraphQL của riêng bạn.
Phát Lại Truy Vấn Mà Không Cần Trình Duyệt
Cả hai sự ngắt trên đã chứng minh cùng một điều: https://rickandmortyapi.graphcdn.app/ chấp nhận một POST JSON thông thường với query, variables, và operationName, không cần xác thực, và trả về cùng dữ liệu Character mà cả hai lắng nghe đã chỉ ra. Khi hình dạng đó đã được biết, không còn cần trình duyệt để hỏi câu hỏi giống hệt — mặc dù yêu cầu phải khai báo một User-Agent thông thường, hoặc cạnh của cổng từ chối ngay lập tức bất kể tải là gì; phần giới hạn bên dưới sẽ đề cập lý do:
python
import json
import urllib.error
import urllib.request
ENDPOINT = "https://rickandmortyapi.graphcdn.app/"
QUERY = (
"query GetCharacters($page: Int, $name: String) { "
"characters(page: $page, filter: { name: $name }) { "
"info { count pages } "
"results { id name status species } } }"
)
payload = json.dumps({
"query": QUERY,
"variables": {"page": 1, "name": "rick"},
"operationName": "GetCharacters",
}).encode("utf-8")
req = urllib.request.Request(
ENDPOINT,
data=payload,
# A default urllib request declares "Python-urllib/x.y" as its User-Agent
# and the gateway's edge rejects that outright -- see "When the Browser
# Stays in the Loop" below for what's actually being checked.
headers={
"Content-Type": "application/json",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
),
},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status != 200:
raise urllib.error.HTTPError(ENDPOINT, resp.status, "unexpected status", resp.headers, None)
body = json.loads(resp.read())
data = body["data"]["characters"]
print("status: 200, no browser process involved")
print("info:", data["info"])
print("first result:", data["results"][0])
print("result count in this page:", len(data["results"]))
text
status: 200, no browser process involved
info: {'count': 107, 'pages': 6}
first result: {'id': '1', 'name': 'Rick Sanchez', 'status': 'Alive', 'species': 'Human'}
result count in this page: 20
Cùng info, cùng kết quả đầu tiên, cùng trang 20 hàng như trong nắm bắt lắng nghe phản hồi — bởi vì đó là yêu cầu chính xác giống nhau, được gửi bởi urllib thay vì bởi cuộc gọi fetch của GraphiQL. Toàn bộ công việc của trình duyệt trong quy trình làm việc này là tiết lộ điểm cuối, hình dạng truy vấn, và định dạng biến; khi những điều này đã được biết, một POST GraphQL chỉ là JSON qua HTTP mang tài liệu yêu cầu dưới dạng chính xác định nghĩa trong tài liệu đặc tả GraphQL, và cách nhanh nhất để hỏi lại câu hỏi giống nhau thường là ngừng hiển thị một trang và hỏi trực tiếp.
Khi Trình Duyệt Ở Trong Vòng Lặp
Không phải tất cả các điểm cuối GraphQL đều hợp tác như vậy, do những lý do cụ thể về cách các cổng GraphQL thường được triển khai. Nhiều cổng yêu cầu một tiêu đề Authorization chứa một mã thông báo mà JavaScript của frontend đính kèm từ bộ nhớ cục bộ hoặc cookie, điều mà bạn không thể tái tạo trừ khi bạn đã nắm bắt nó từ một phiên thực tế — giới hạn tương tự mà các API ẩn có hình dạng REST cũng chia sẻ. Một số đi xa hơn và thực thi Các Truy Vấn Tồn Tại Tự Động, nơi khách hàng gửi một băm SHA-256 của truy vấn thay vì văn bản truy vấn thực tế; một máy chủ chỉ chấp nhận các băm đã đăng ký trước từ chối một yêu cầu phát lại được xây dựng từ một chuỗi truy vấn đơn lẻ, bởi vì băm chưa bao giờ được đăng ký từ khách hàng đó. Trong cả hai trường hợp, bước can thiệp vẫn hoạt động chính xác như được hiển thị ở đây: page.expect_response() và miền Network của CDP đọc bất cứ thứ gì mà trình duyệt thực sự gửi, bao gồm cả tiêu đề xác thực hoặc băm truy vấn đã tồn tại. Chỉ có phần thưởng phát lại trực tiếp không còn áp dụng, vì việc tái tạo những gì trình duyệt đã gắn kèm trở thành phần khó khăn.
Một giới hạn tinh tế hơn đã xuất hiện trong quá trình xác minh bài viết này và đáng được đặt tên trực tiếp: một điểm cuối GraphQL công cộng, không xác thực vẫn có thể nằm sau biện pháp giảm thiểu bot dựa trên dấu vân tay mà không liên quan gì đến truy vấn đó. Một urllib POST thông thường gửi đến điểm cuối ở trên, không kèm theo User-Agent header (mặc định của Python, theo nghĩa đen là chuỗi Python-urllib/3.12), đã trả về HTTP 403 với lỗi Cloudflare 1010: "chủ sở hữu của trang web này đã cấm quyền truy cập của bạn dựa trên chữ ký của trình duyệt của bạn." Điều đó xảy ra mỗi khi, có thể tái hiện được, mặc dù các header giới hạn tỷ lệ chi phí truy vấn của gateway báo cáo vẫn còn ngân sách. Thêm một chuỗi trình duyệt thông thường User-Agent, và không có gì khác về yêu cầu, đã vượt qua cùng kiểm tra trong mọi cuộc gọi tiếp theo. Khối này đã được xác định dựa trên danh tính được khai báo của khách hàng, không phải nội dung của yêu cầu hay tần suất mà nó xuất hiện. Một phiên trình duyệt đám mây cung cấp chữ ký Chromium thực, kiểu mà điểm cuối CDP của Scraping Browser cung cấp, không bao giờ có sự không khớp đó ngay từ đầu.
Kết luận
Một API GraphQL đánh đổi nhiều URL tự mô tả của REST để có một điểm cuối và một body yêu cầu cần được đọc để biết nó đang hỏi gì. page.expect_response() và miền CDP thô Network đều đọc body đó bất kể, được khớp bằng nội dung thay vì địa chỉ, và một khách hàng HTTP thông thường phát lại cùng một JSON khi hình thức đã được xác nhận. Giữ bộ lọc truy vấn được xác định trên operationName hoặc văn bản truy vấn thay vì URL, mong đợi một trình chỉnh sửa truy vấn trống cần đầu vào thực sự trước khi bất kỳ điều gì thú vị xảy ra, và coi lớp giảm thiểu bot của điểm cuối công cộng là một mối quan tâm riêng biệt so với xác thực của nó. Đối với cơ chế CDP mà cả hai con đường xây dựng, giải thích Chrome DevTools Protocol sẽ hướng dẫn những gì giao thức này phơi bày ngoài miền Network.
Đăng ký tại app.scrapeless.com để nhận runtime Scraping Browser miễn phí, hoặc xem trang sản phẩm Scraping Browser và giá cả cho các lần chạy mở rộng.
Tham gia cộng đồng của chúng tôi để so sánh ghi chú với các nhà phát triển khác xây dựng tự động hóa trình duyệt: Discord · Telegram.
Câu hỏi thường gặp
Q: Interception GraphQL là gì trong việc thu thập dữ liệu web?
Đó là việc đọc yêu cầu POST đơn lẻ mà JavaScript của trang hỗ trợ GraphQL gửi để lấy dữ liệu — query và variables trong body yêu cầu đó — thay vì chờ đợi phản hồi để được hiển thị thành HTML và phân tích cú pháp lại.
Q: Tại sao bạn không thể biết hoạt động GraphQL nào đã chạy chỉ từ URL yêu cầu?
Bởi vì một gateway GraphQL thường phục vụ mọi hoạt động từ một điểm cuối cố định. Không giống như API REST, nơi mà các đường dẫn khác nhau tương ứng với các tài nguyên khác nhau, danh tính của một yêu cầu GraphQL sống trong body POST của nó — trường operationName hoặc văn bản query — không phải ở địa chỉ mà nó đã được gửi đến.
Q: Bạn có cần trình duyệt khi đã biết truy vấn, biến và điểm cuối không?
Chỉ khi điểm cuối yêu cầu điều gì mà trình duyệt cung cấp, chẳng hạn như một header ủy quyền hoặc một hash truy vấn được lưu trữ đã đăng ký. Một điểm cuối công cộng chấp nhận một chuỗi truy vấn đầy đủ mà không cần xác thực, giống như cái trong hướng dẫn này, có thể được phát lại bằng một khách hàng HTTP thông thường, như ví dụ phát lại trực tiếp cho thấy.
Q: Sự khác biệt giữa page.expect_response() và miền CDP Network thô ở đây là gì?
page.expect_response() là wrapper cấp cao hơn của Playwright, liên kết với hành động kích hoạt yêu cầu và trả về một đối tượng Response đã được phân tích. Miền CDP Network là giao thức nằm bên dưới nó — Network.requestWillBeSent, Network.loadingFinished, và Network.getResponseBody — hữu ích mà không cần bất kỳ ràng buộc nào của Playwright, hoặc khi một bộ lọc cần kiểm tra body yêu cầu ra trước khi phản hồi tồn tại.
Q: Việc chặn các truy vấn của một công cụ chơi GraphQL công cộng có hợp pháp không?
Đọc phản hồi mà phiên trình duyệt của bạn đã nhận khi truy cập một trang công khai mang lại những cân nhắc khác với việc truy cập dữ liệu đã xác thực hoặc không công khai. Hãy giới hạn bất kỳ quy trình làm việc nào vào 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 robot của mục tiêu, và giữ cho khối lượng yêu cầu trong giới hạn — việc chặn là cách để đọc lưu lượng chính xác, không phải là giấy phép để phớt lờ các quy tắc truy cập.
H: Điều gì sẽ xảy ra với một API GraphQL đã xác thực hoặc chỉ dành cho truy vấn đã duy trì?
Bước chặn vẫn hoạt động — cả hai đường dẫn thu đều đọc bất cứ điều gì mà trình duyệt thực sự gửi, bao gồm tiêu đề xác thực hoặc băm truy vấn duy trì. Bước phát lại trực tiếp mới là thứ bị hỏng, vì một máy chủ chỉ băm từ chối một yêu cầu được tạo từ một chuỗi truy vấn thô chưa bao giờ được đăng ký, và một điểm cuối đã xác thực từ chối một yêu cầu thiếu tiêu đề mà phiên gốc đã mang theo.
H: Tại sao ví dụ phát lại trực tiếp lại thiết lập một tiêu đề User-Agent nếu nó không phải là trình duyệt?
Bởi vì cạnh cổng từ chối yêu cầu mà không có một cái. Một POST urllib đơn giản sử dụng chuỗi User-Agent mặc định của Python trả về lỗi Cloudflare 1010 trong mỗi lần thử, mặc dù các tiêu đề giới hạn tần suất chi phí truy vấn cho thấy ngân sách còn lại — việc chặn được khóa dựa trên danh tính đã khai báo của khách hàng, không phải là truy vấn hoặc tần suất gửi. Một chuỗi User-Agent của trình duyệt thông thường, với không có gì khác về yêu cầu thay đổi, là đủ để vượt qua.
H: Kỹ thuật này có cần Trình duyệt Scraping không có Scrapeless cụ thể, hay nó hoạt động với bất kỳ Chromium nào có thể truy cập CDP?
Cách hoạt động chặn là hành vi CDP chung và hoạt động với bất kỳ Chromium nào có thể truy cập thông qua connect_over_cdp, địa phương hoặc từ xa. Chạy chúng trên Trình duyệt Scraping không có Scrapeless thêm một phiên Chromium đám mây với chữ ký trình duyệt thực, điều này quan trọng đối với một frontend xác định dấu vân tay của khách hàng trước khi cho phép một truy vấn được kích hoạt ngay từ đầu.
H: Điều gì sẽ xảy ra nếu mục tiêu thay đổi sơ đồ hoặc hình dạng truy vấn của mình?
Mã chặn tiếp tục hoạt động miễn là URL điểm cuối vẫn khớp — nó đọc bất kỳ nội dung nào mà trình duyệt gửi bất kể các trường của truy vấn. Một trường được đổi tên hoặc kiểu được cấu trúc lại sẽ làm gãy mã đọc data["characters"]["results"], theo cách tương tự như cách một bộ chọn CSS bị hỏng khi tên lớp thay đổi; một sơ đồ GraphQL thường ổn định hơn đánh dấu, nhưng nó không miễn dịch với thay đổi gây gãy.
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.



