Lấy dữ liệu cuộn vô hạn: Ngừng đoán xem phải cuộn bao nhiêu lần
Advanced Data Extraction Specialist
Tóm tắt TL;DR:
- Một trang cuộn vô hạn không gửi gần như gì trong HTML của nó. Bản demo được sử dụng ở đây phục vụ 2,671 bytes chứa không có phần tử nào; từng mục sẽ đến sau đó.
- Số lượng cuộn cố định là sự thất bại mà mọi người đều gửi. Năm lần cuộn đã trả về 60 trên 100 mục - không có lỗi, không có cảnh báo, một lần thoát sạch với 40% dữ liệu bị thiếu.
- Cuộn cho đến khi số lượng mục không còn tăng nữa. Điều đó đã trả về tất cả 100.
- Số lần cuộn không phải là thuộc tính của trang web. Vòng lặp giống nhau cần 11 hành động cuộn tại chỗ và 3 trên một trình duyệt đám mây, đều đạt được 100 cả hai lần.
- Trang đã thực hiện 10 cuộc gọi đến
/api/quotes?page=N. Đọc trực tiếp điểm cuối đó trả về cùng 100 mục và báo cáohas_next, vì vậy vòng lặp biết khi nào nó đã hoàn thành. - Kế hoạch miễn phí Scrapeless bao gồm việc chạy trình duyệt đám mây trong hướng dẫn này.
Cuộn vô hạn làm hỏng những công cụ thu thập dữ liệu một cách lặng lẽ. Yêu cầu thành công, các bộ chọn khớp, kịch bản thoát với số không - và tập dữ liệu thì thiếu. Không có gì trong quá trình này cho bạn biết bạn đã bỏ lỡ bao nhiêu, vì trang không bao giờ hứa hẹn có bao nhiêu thứ.
Hướng dẫn này đo điều đó trên một trang demo trực tiếp. Nó xây dựng vòng lặp mà hầu hết các hướng dẫn khác gửi, cho thấy chính xác những gì nó bỏ qua, thay thế nó bằng một cái có điều kiện dừng thực sự và sau đó tìm yêu cầu mà trang đã thực hiện từ đầu.
Những gì máy chủ thực sự gửi
Lấy trang mà không có trình duyệt và sẽ không có gì để phân tích:
python
def served_html() -> tuple[int, int]:
request = urllib.request.Request(SCROLL_URL, headers={"User-Agent": UA})
with urllib.request.urlopen(request, timeout=45) as response:
html = response.read().decode("utf-8", "replace")
return len(html), html.count('class="quote"')
text
bytes của trang cuộn : 2671
phần tử quote trong html : 0
2,671 bytes và không có một mục nào. Đánh dấu là một lớp vỏ; nội dung được thu thập bởi kịch bản sau khi trang tải, thường là khi một phần tử chỉ báo gần dưới cùng vào tầm nhìn - cơ chế định nghĩa trong đặc tả Intersection Observer. Một bộ chọn CSS không thể thất bại dễ dàng hơn thế này - nó không khớp với gì cả vì không có gì ở đó cho đến khi đó.
Cài đặt
bash
pip install playwright
playwright install chromium
Lệnh thứ hai tải về nhị phân trình duyệt; gói pip một mình sẽ không khởi động được. Chạy xác minh đã sử dụng Playwright 1.59.0.
Vòng lặp mà mọi người gửi
Công thức tiêu chuẩn là cuộn một số lần cố định và sau đó đọc trang. page.mouse.wheel() gửi một sự kiện cuộn thực sự loại định nghĩa trong đặc tả UI Events, đó là những gì trình lắng nghe của trang đang chờ đợi:
python
def scroll_fixed(page, times: int, pause: float) -> int:
for _ in range(times):
page.mouse.wheel(0, 20000)
time.sleep(pause)
return page.locator("div.quote").count()
Năm lần cuộn trên trang trực tiếp:
text
quotes sau 5 lần cuộn : 60
thời gian đã trôi qua : 5.1
Sáu mươi mục. Trang giữ một trăm. Kịch bản không phát ra gì, bộ chọn hoạt động, và lần chạy trông có vẻ thành công từ bên ngoài - điều này làm cho đây là phiên bản đắt tiền của lỗi. Chọn năm vì nó đã hoạt động một lần, và mọi lần chạy sau đều kế thừa một sự thiếu hụt lặng lẽ 40%.
Việc tăng số lượng cũng không phải là một cách sửa chữa. Nó đổi sự thu thập thiếu cho những cuộn lãng phí, và nó vẫn mã hóa một sự đoán về một trang có thể thay đổi kích thước lô hàng bất cứ khi nào nó muốn.
Cuộn cho đến khi dừng tăng trưởng
Điều kiện mà bạn thực sự muốn là có thể quan sát: tiếp tục cuộn trong khi số lượng mục vẫn đang tăng, và dừng lại khi nó giữ ổn định.
python
def scroll_until_stable(page, pause: float, no_growth_limit: int = 2) -> tuple[int, int]:
seen = page.locator("div.quote").count()
scrolls = 0
stable = 0
while stable < no_growth_limit:
page.mouse.wheel(0, 20000)
time.sleep(pause)
scrolls += 1
count = page.locator("div.quote").count()
if count == seen:
stable += 1
else:
stable = 0
seen = count
return seen, scrolls
no_growth_limit quyết định bạn cần bao nhiêu bằng chứng trước khi tin rằng trang đã hoàn thành. Một lần đọc bằng phẳng không phải là chứng cứ - một lô hàng vẫn đang trong quá trình chuyển phát trông giống hệt như cuối danh sách. Yêu cầu hai lần đọc bằng phẳng liên tiếp tốn một cuộn thêm và loại bỏ sự không rõ ràng đó.
text
quotes đã thu thập : 100
hành động cuộn đã thực hiện : 11
thời gian đã trôi qua : 11.1
các cuộc gọi API mà trang đã thực hiện : 10
url api đầu tiên : https://quotes.toscrape.com/api/quotes?page=1
Tất cả 100, và vòng lặp đã phát hiện số lượng thay vì giả định nó. Lưu ý rằng con số thời gian trôi qua chủ yếu là khoảng dừng mà vòng lặp này chọn để chờ - mười một lần cuộn mỗi lần một giây. Đây là thuộc tính của vòng lặp, không phải của trang web.
Số lần cuộn không phải là thuộc tính của trang web
Chạy chức năng giống hệt trên một trình duyệt đám mây thay vì Chromium cục bộ:
text
số câu trích dẫn thu thập được : 100
số hành động cuộn được thực hiện : 3
Chức năng giống nhau, cùng trang, cùng 100 mục — 3 hành động cuộn thay vì 11. Chiều cao viewport và tốc độ mỗi lô đến quyết định mức độ một sự kiện cuộn tiến lên trang, và không có yếu tố nào nằm trong tầm kiểm soát của bạn. Kích thước viewport mà việc cuộn được đo lường là những gì Module View CSSOM chỉ định. Bất kỳ số lượng cuộn nào được mã cứng đều được hiệu chỉnh theo kích thước cửa sổ của một máy.
Lần chạy đó sử dụng Trình duyệt cào Scrapeless, mà Playwright kết nối qua Giao thức DevTools của Chrome. Chỉ có dòng kết nối thay đổi:
python
endpoint = (
"wss://browser.scrapeless.com/api/v2/browser"
f"?token={os.environ['SCRAPELESS_API_KEY']}&sessionTTL=180&proxyCountry=ANY"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(endpoint, timeout=90000)
page = browser.new_page()
page.goto(SCROLL_URL, wait_until="domcontentloaded")
cloud_total, cloud_scrolls = scroll_until_stable(page, pause=1.0)
connect_over_cdp thay thế chromium.launch() và giao tiếp qua Giao thức DevTools của Chrome qua một socket thay vì đến một quy trình cục bộ, vì vậy vòng lặp cuộn, bộ chọn và việc trích xuất vẫn như cũ. Giữ khóa trong môi trường với tên SCRAPELESS_API_KEY; tài liệu Giới thiệu về Trình duyệt Cào nêu rõ các tham số phiên còn lại.
Bắt đầu mất một phút — tạo một tài khoản Scrapeless miễn phí và gói miễn phí bao gồm lần chạy này.
Xem Những Gì Trang Yêu Cầu
Vòng lặp cuộn là cách để yêu cầu trang lấy dữ liệu. Lắng nghe các yêu cầu cho thấy nó lấy gì:
python
calls: list[str] = []
context = browser.new_context(user_agent=UA)
context.on("request", lambda r: calls.append(r.url) if "/api/quotes" in r.url else None)
Mười cuộc gọi, cuộc gọi đầu tiên là https://quotes.toscrape.com/api/quotes?page=1. Trang đang phân trang một điểm cuối JSON và hiển thị kết quả; việc cuộn chỉ là tác nhân kích hoạt.
Điểm cuối đó có thể được đọc trực tiếp, và nó trả lời câu hỏi mà vòng lặp cuộn phải suy luận:
python
def read_api() -> tuple[int, int]:
quotes = 0
page = 1
while True:
request = urllib.request.Request(API_URL.format(page=page), headers={"User-Agent": UA})
with urllib.request.urlopen(request, timeout=45) as response:
payload = json.loads(response.read().decode())
quotes += len(payload["quotes"])
if not payload["has_next"]:
return quotes, page
page += 1
text
số câu trích dẫn thu thập được : 100
số trang api được yêu cầu : 10
thời gian đã trôi qua : 3.8
Cùng 100 mục, với has_next như một điều kiện kết thúc rõ ràng thay vì "số lượng dừng không thay đổi." Phản hồi đã được cấu trúc sẵn, vì vậy không có bộ chọn nào nằm giữa bạn và các trường — điều này cũng có nghĩa là không có bộ chọn nào có thể bị hỏng khi kiểu đánh dấu được thiết kế lại.
Điều này đáng được kiểm tra trước khi viết bất kỳ vòng lặp cuộn nào. Nó không phải lúc nào cũng tồn tại: nhiều trang hiển thị từ phía máy chủ khi cuộn, ký các yêu cầu của họ hoặc trả về các đoạn HTML thay vì JSON. Khi nó tồn tại, nó là mục tiêu bền bỉ hơn, và trình duyệt vẫn là cái cho bạn thấy nó có ở đó. Đối với các hình thức phân trang khác — nút tiếp theo, các trang đánh số, tải thêm — hướng dẫn phân trang đầy đủ bao gồm từng loại.
Chạy Nó
bash
export SCRAPELESS_API_KEY="your-api-key"
python3 scroll_demo.py
Đầu ra hoàn chỉnh từ lần chạy xác minh:
text
playwright 1.59.0
--- những gì máy chủ thực sự gửi ---
số byte trang cuộn : 2671
số phần tử trích dẫn trong html: 0
--- số lượng cuộn cố định ---
số câu trích dẫn sau 5 lần cuộn : 60
thời gian đã trôi qua : 5.1
--- cuộn cho đến khi số lượng dừng tăng ---
số câu trích dẫn thu thập được : 100
số hành động cuộn được thực hiện : 11
thời gian đã trôi qua : 11.1
số cuộc gọi api mà trang đã thực hiện : 10
địa chỉ url api đầu tiên : https://quotes.toscrape.com/api/quotes?page=1
--- vòng lặp giống hệt trên Trình duyệt Cào ---
số câu trích dẫn thu thập được : 100
số hành động cuộn được thực hiện : 3
--- đọc cùng một api trực tiếp ---
số câu trích dẫn thu thập được : 100
số trang api được yêu cầu : 10
thời gian đã trôi qua : 3.8
tỷ lệ thời gian trình duyệt/api : 3x
Tỷ lệ ở dòng cuối cùng so sánh đồng hồ tường của vòng lặp trình duyệt với các lần đọc trực tiếp. Hầu hết phần bên trình duyệt là khoảng dừng một giây có chủ đích giữa các lần cuộn, vì vậy hãy hiểu rằng đó là chi phí của việc truy vấn một trang vì điều kiện mà nó không bao giờ báo cáo - không phải là một thước đo của chính trình duyệt.
## Khắc phục sự cố
**Số lượng không bao giờ dừng lại.** Một số trang lặp lại nội dung của chúng. Giới hạn vòng lặp với số lượng cuộn tối đa cùng với kiểm tra sự ổn định, và coi việc chạm vào giới hạn đó như một tín hiệu để kiểm tra trang thay vì như một lần chạy hoàn tất.
**Không có mục nào ngay cả sau khi cuộn.** Có thể hộp chứa cuộn thay vì cửa sổ. Cuộn chính nó với `page.locator(...).hover()` theo sau bởi `page.mouse.wheel(...)`, hoặc gọi `scroll_into_view_if_needed()` trên mục cuối cùng.
**Số lượng tăng lên, sau đó trang trở nên trống rỗng.** Danh sách dài thường được ảo hóa, vì vậy các hàng bị xóa khỏi DOM khi chúng rời khỏi vùng nhìn. Thu thập các mục khi bạn đi thay vì đọc tất cả vào cuối.
**Nó hoạt động ở chế độ có đầu và không có đầu.** Kích thước viewport khác nhau giữa hai chế độ, điều này thay đổi khoảng cách mà một sự kiện cuộn di chuyển và liệu bộ tải có vào xem được hay không. Đặt một viewport rõ ràng trên ngữ cảnh.
**`playwright._impl._errors.Error: Executable doesn't exist`.** Tệp nhị phân trình duyệt chưa bao giờ được tải xuống. Chạy `playwright install chromium`.
## Kết luận
Cuộn vô hạn biến “tôi đã lấy mọi thứ chưa?” thành một câu hỏi mà trình thu thập dữ liệu của bạn phải tự trả lời, và một số lượng cuộn cố định trả lời bằng cách đoán. Các phép đo ở đây cho thấy điều đó tốn kém như thế nào: 60 mục out of 100 từ năm lần cuộn, và số lượng cuộn thay đổi từ 11 xuống 3 chỉ bằng cách chuyển sang một trình duyệt khác.
Lặp lại trên điều kiện quan sát được - số lượng mục ổn định trong các lần đọc liên tiếp - thay vì một con số bạn chọn. Và trước khi viết vòng lặp, hãy theo dõi các yêu cầu của trang: khi nội dung đến dưới dạng JSON với cờ `has_next`, trang đã cho bạn biết khi nào bạn đã hoàn tất.
Sẵn sàng thử chưa? <a href="https://app.scrapeless.com/passport/login?utm_source=website&utm_medium=blog&utm_campaign=scrapingbrowser&utm_term=infinite-scroll-web-scraping"><strong>Bắt đầu với gói miễn phí của Scrapeless</strong></a> và xem <a href="https://www.scrapeless.com/vi/pricing?utm_source=website&utm_medium=blog&utm_campaign=scrapingbrowser&utm_term=infinite-scroll-web-scraping"><strong>giá hiện tại</strong></a> cho các khối lượng cao hơn.
## Câu hỏi thường gặp
**Q: Tôi nên cuộn bao nhiêu lần?**
Đừng chọn một con số. Các phép đo ở trên sử dụng một vòng lặp so với hai trình duyệt và cần 11 lần cuộn ở một và 3 ở cái kia cho cùng 100 mục, vì chiều cao viewport và thời gian lô quyết định khoảng cách mà mỗi sự kiện cuộn đi xa. Lặp lại trong khi số lượng mục vẫn đang tăng và dừng lại sau khi nó giữ ổn định hai lần.
**Q: Làm thế nào để tôi biết trang đã tải xong mọi thứ chưa?**
Hai lần đọc liên tiếp với số lượng mục không thay đổi là tín hiệu thực tế, vì một lần đọc phẳng đơn lẻ là không thể phân biệt với một lô vẫn đang bay. Nếu yêu cầu cơ bản của trang tiết lộ một cờ như `has_next`, đó là một câu trả lời chắc chắn hơn là một suy luận.
**Q: Tôi có cần một trình duyệt cho cuộn vô hạn không?**
Thường thì không. Trang demo ở đây tải nội dung của nó từ một điểm cuối JSON mà có thể được đọc trực tiếp cho tất cả 100 mục. Trình duyệt vẫn là cách bạn phát hiện ra điểm cuối đó - gắn một trình lắng nghe yêu cầu, cuộn một lần và đọc các URL. Các trang được phân phối phía máy chủ khi cuộn hoặc ký các yêu cầu của chúng cần trình duyệt.
**Q: Tại sao trình thu thập dữ liệu của tôi trả về ít mục hơn số lượng trang hiển thị?**
Hoặc là vòng lặp dừng lại sớm, hoặc danh sách được ảo hóa và các hàng đã bị xóa khỏi DOM khi chúng cuộn ra khỏi tầm nhìn. In số lượng mục sau mỗi lần cuộn: một số lượng tăng lên rồi giảm xuống cho thấy sự ảo hóa, và một số lượng vẫn tăng khi vòng lặp kết thúc cho thấy vòng lặp dừng lại quá sớm.
**Q: Liệu `wait_until="networkidle"` có giải quyết được điều này không?**
Không. Nó chờ cho tải ban đầu ổn định, điều đó xảy ra trước khi bất kỳ cuộn nào kích hoạt việc lấy dữ liệu. Các lô đến là phản hồi cho các sự kiện cuộn, vì vậy trang có thể ở trạng thái nhàn rỗi và gần như trống rỗng cùng một lúc - đó chính là trạng thái 2.671 byte được đo vào lúc bắt đầu.
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.



