PixelRAG + Scrapeless: Điều gì xảy ra khi ghép ảnh chụp màn hình từ xa?
Lead Scraping Automation Engineer
TL;DR:
- Visual RAG đánh chỉ số các trang dưới dạng các ô chụp màn hình thay vì văn bản đã phân tích, do đó hình dạng ô không chỉ mang tính thẩm mỹ — nó quyết định cái mà mô hình nhúng thực sự thấy.
- Bộ tạo hình của PixelRAG thiết lập một vùng chụp có kích thước 875 x 8192 với
Emulation.setDeviceMetricsOverride, sau đó cắt mỗi ảnh chụp màn hình thành 875 pixel rộng. - Đối với một trình duyệt đám mây mà ghi đè báo cáo thành công và không bao giờ áp dụng chiều rộng yêu cầu. Qua các phiên, chiều rộng bố cục thu được đã trở lại ở bất kỳ đâu từ 945 đến 5120 — đôi khi không thay đổi, thường thì rộng hơn, không khi nào là 875.
- Chỗ cắt được tôn trọng một cách chính xác, điều này làm cho nó trở nên nguy hiểm: bạn có được một ô có kích thước chính xác chứa phần bên trái của một trang được bố trí rộng hơn nhiều.
- Trên một lần chụp thực tế, một ô rộng 875 của một bố cục rộng 1920 giữ một cột sản phẩm trong khi trang bố trí bốn, với thẻ bị cắt ngang qua nút của nó.
- Đường nhanh
rawFilePathđược chấp nhận mà không có lỗi đối với một trình duyệt từ xa, trả về dữ liệu nội tuyến thay thế, và không ghi bất kỳ tệp cục bộ nào — vì vậy một pipeline đọc tệp đó không nhận được gì trong khi mỗi cuộc gọi đều có vẻ thành công. - So sánh chiều rộng bạn yêu cầu với
innerWidthsau khi ghi đè trước khi bạn tin tưởng vào một ô đơn lẻ. - Bắt đầu với gói miễn phí Scrapeless và đo đạc hình học chụp của riêng bạn.
Trình duyệt Scraping Scrapeless là một trình duyệt đám mây mà bạn điều khiển qua CDP WebSocket, và mọi đo đạc dưới đây đều được thực hiện với nó thông qua các lệnh CDP giống như những gì một trình tạo kết xuất cục bộ sẽ sử dụng.
Phương pháp lấy thông tin được gia tăng bằng hình ảnh nhanh chóng bỏ qua việc phân tích HTML. Thay vì chuyển đổi một trang thành văn bản và nhúng các đoạn, nó tạo hình trang thành hình ảnh, nhúng các hình ảnh, và cho phép một mô hình thị giác đọc câu trả lời từ các pixel. Bảng, biểu đồ và bố cục nhiều cột vẫn tồn tại, vì không có gì bị nén thành một dòng văn bản.
Điều đó chỉ hoạt động nếu bức tranh là chính xác. Một trình tạo hình ô đưa ra một lời hứa cụ thể — trang được bố trí ở một chiều rộng cố định, sau đó cắt thành các ô có chiều cao cố định — và một chỉ số hình ảnh chỉ tốt như lời hứa đó. PixelRAG là ví dụ rõ ràng nhất hiện nay, và bộ tạo hình của nó đáng để bạn chú ý vì giả định mà nó đưa ra là giả định mà mọi pipeline chụp màn hình đều tuân theo. Bài viết này đo đạc điều sẽ xảy ra với giả định đó trên một trình duyệt mà bạn không khởi động.
Cách Hệ Thống Ô Được Kỳ Vọng Làm Việc
Bộ tạo hình nhanh của PixelRAG nói raw CDP qua một WebSocket thay vì thông qua một thư viện driver. Nó thiết lập hai hằng số ở đầu render/src/pixelrag_render/backends/fast_cdp.py:
python
VIEWPORT_WIDTH = 875
TILE_HEIGHT = 8192
Sau đó, mỗi lần cho một worker, nó áp dụng chúng như vùng chụp:
python
await conn.cdp("Emulation.setDeviceMetricsOverride", {
"width": VIEWPORT_WIDTH,
"height": tile_height,
"deviceScaleFactor": 1,
"mobile": False,
})
Và cho mỗi ô, nó chụp một hình chữ nhật đã cắt với cùng một chiều rộng, bước xuống trang:
python
await conn.cdp("Page.captureScreenshot", {
"clip": {"x": 0, "y": t * tile_height, "width": VIEWPORT_WIDTH,
"height": clip_h, "scale": 1},
})
Logic là chuẩn và hợp lý. Emulation.setDeviceMetricsOverride là cuộc gọi CDP mà các khái niệm của спецификация viewport CSS tương ứng với, và đó là những gì các thư viện driver gọi bên dưới khi bạn thiết lập một viewport. Bố trí trang ở 875, cắt nó thành các dải cao 8192, nhúng các dải. Trên một trình duyệt mà bạn khởi động, đó chính xác là những gì xảy ra.
Điều Kiện Tiên Quyết
- Python 3.10 hoặc mới hơn và gói
websockets. - Một khóa API Scrapeless từ bảng điều khiển, xuất dưới dạng
SCRAPELESS_KEY. - Không có trình duyệt cục bộ. Mọi lệnh gọi bên dưới đều tới trình duyệt đám mây qua một kết nối WebSocket.
bash
pip install "websockets==15.0.1"
export SCRAPELESS_KEY="your_api_key_here"
Đo Lường Ghi Đè Thay Vì Tin Cậy Nó
Ghi đè trả về một đối tượng kết quả mà không có lỗi, đó là toàn bộ vấn đề — không có gì trong phản hồi gợi ý rằng yêu cầu không được tôn trọng. Đọc chiều rộng bố cục lại từ trang thay thế.
python
import asyncio, json, os
import websockets
CDP = ("wss://browser.scrapeless.com/api/v2/browser"
f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
WIDTH, HEIGHT = 875, 8192
async def call(ws, state, method, params=None, session=None):
state["id"] += 1
msg = {"id": state["id"], "method": method}
if params:
msg["params"] = params
if session:
msg["sessionId"] = session
await ws.send(json.dumps(msg))
while True:
reply = json.loads(await ws.recv())
if reply.get("id") == state["id"]:
return reply
async def one_session(run):
async with websockets.connect(CDP, max_size=50 * 1024 * 1024) as ws:
state = {"id": 0}
target = await call(ws, state, "Target.createTarget", {"url": "about:blank"})
tid = target["result"]["targetId"]
attached = await call(ws, state, "Target.attachToTarget",
{"targetId": tid, "flatten": True})
sid = attached["result"]["sessionId"]
await call(ws, state, "Page.enable", session=sid)
await call(ws, state, "Page.navigate",
{"url": "https://books.toscrape.com/"}, session=sid)
await asyncio.sleep(2.5)
async def inner_width():
r = await call(ws, state, "Runtime.evaluate",
{"expression": "innerWidth", "returnByValue": True},
session=sid)
return r["result"]["result"]["value"]
before = await inner_width()
result = await call(ws, state, "Emulation.setDeviceMetricsOverride",
{"width": WIDTH, "height": HEIGHT,
"deviceScaleFactor": 1, "mobile": False}, session=sid)
await asyncio.sleep(1)
after = await inner_width()
print(f"run{run}: error={result.get('error')} before={before} "
f"after={after} requested={WIDTH} applied={after == WIDTH}")
await call(ws, state, "Target.closeTarget", {"targetId": tid})
return after
async def main():
widths = [await one_session(run) for run in range(1, 4)]
print(f"distinct widths: {sorted(set(widths))} · requested {WIDTH} ever applied: "
f"{WIDTH in widths}")
asyncio.run(main())
text
run1: error=None before=945 after=1280 requested=875 applied=False
run2: error=None before=945 after=1600 requested=875 applied=False
run3: error=None before=945 after=945 requested=875 applied=False
distinct widths: [945, 1280, 1600] · requested 875 ever applied: False
Chạy lại và cột after giữ các số khác nhau. Sự không ổn định đó là kết quả thay vì tiếng ồn xung quanh nó, vì vậy dòng đáng đánh dấu là dòng cuối cùng: chiều rộng yêu cầu không bao giờ là chiều rộng được áp dụng.
Ba điều đáng tách biệt ở đây. Cuộc gọi không thất bại. Chiều rộng mà nó rơi vào khác nhau từ phiên này sang phiên khác — qua các lần chạy riêng biệt, giá trị đã trở lại là 945, 1240, 1280, 1400, 1440, 1600, 1680, 1920, 2560 và 5120. Và trong không phiên nào trong số đó là chiều rộng 875 mà đã được yêu cầu.
Lưu ý lần chạy số 3 ở trên: đôi khi chiều rộng không di chuyển một chút nào, và 945 trước đó trở thành 945 sau đó. Vì vậy, ghi đè không đáng tin cậy là một no-op và cũng không phải là một thay đổi. Tính chất duy nhất đã giữ trong mọi phiên là tính hữu ích — số bạn yêu cầu không phải là số bạn nhận được.
Việc đọc thực tiễn là trình duyệt từ xa sở hữu cửa sổ của chính nó, và việc ghi đè việc định dạng lại diễn ra đối với cửa sổ đó thay vì đối với các số của bạn. Tại địa phương, bạn sở hữu quá trình, vì vậy cuộc gọi tương tự sẽ hành xử.
Những gì mà Gạch Ngói Thực Sự Chứa Đựng
Bây giờ hãy chụp một gạch ngói theo cách mà một trình tạo gạch ngói thực hiện, với một clip rõ ràng ở chiều rộng mà bạn nghĩ bạn đã đặt.
python
import asyncio, base64, json, os, pathlib, struct
import websockets
CDP = ("wss://browser.scrapeless.com/api/v2/browser"
f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
WIDTH = 875
async def call(ws, state, method, params=None, session=None):
state["id"] += 1
msg = {"id": state["id"], "method": method}
if params:
msg["params"] = params
if session:
msg["sessionId"] = session
await ws.send(json.dumps(msg))
while True:
reply = json.loads(await ws.recv())
if reply.get("id") == state["id"]:
return reply
async def main():
async with websockets.connect(CDP, max_size=100 * 1024 * 1024) as ws:
state = {"id": 0}
tid = (await call(ws, state, "Target.createTarget",
{"url": "about:blank"}))["result"]["targetId"]
sid = (await call(ws, state, "Target.attachToTarget",
{"targetId": tid, "flatten": True}))["result"]["sessionId"]
await call(ws, state, "Page.enable", session=sid)
await call(ws, state, "Page.navigate",
{"url": "https://books.toscrape.com/"}, session=sid)
await asyncio.sleep(2.5)
await call(ws, state, "Emulation.setDeviceMetricsOverride",
{"width": WIDTH, "height": 8192,
"deviceScaleFactor": 1, "mobile": False}, session=sid)
await asyncio.sleep(1)
layout = await call(ws, state, "Runtime.evaluate",
{"expression": "innerWidth", "returnByValue": True},
session=sid)
shot = await call(ws, state, "Page.captureScreenshot",
{"format": "png", "captureBeyondViewport": True,
"clip": {"x": 0, "y": 0, "width": WIDTH,
"height": 1200, "scale": 1}}, session=sid)
image = base64.b64decode(shot["result"]["data"])
png_w, png_h = struct.unpack(">II", image[16:24])
print(f"layout width: {layout['result']['result']['value']}")
print(f"tile size: {png_w}x{png_h} (requested {WIDTH}x1200)")
pathlib.Path("tile.png").write_bytes(image)
await call(ws, state, "Target.closeTarget", {"targetId": tid})
asyncio.run(main())
text
layout width: 1920
tile size: 875x1200 (requested 875x1200)
Chiều rộng và chiều cao được in ra ở đây được đọc trực tiếp từ tiêu đề hình ảnh thay vì được giả định, vì đặc tả PNG đặt cả hai trong khối đầu tiên của tệp. Gạch ngói có kích thước chính xác như đã yêu cầu. Trang phía sau nó được định dạng rộng hơn đáng kể, và chiều rộng mà nó hạ cánh thay đổi theo từng phiên, vì vậy phần trăm của trang mà bạn chụp lại cũng không cố định.
Giá trị tile.png được lưu từ lần chạy này cho thấy điều đó tốn bao nhiêu. Với chiều rộng bố trí là 1920, gạch ngói giữ một cột sản phẩm trong khi trang định dạng bốn cột, thẻ hiển thị bị cắt qua nút "Thêm vào giỏ", và văn bản tiêu đề chạy ra ngoài cạnh bên phải. Chưa đầy một nửa chiều rộng của trang nằm ngoài hình ảnh.
Một bộ thu thập văn bản sẽ không quan tâm, vì nó đọc DOM và DOM là hoàn chỉnh. Một chỉ mục trực quan rất quan tâm: gạch ngói là đầu vào, và khoảng một nửa trang không bao giờ tới được mô hình nhúng. Việc truy xuất giảm sút mà không có một lỗi nào trong đường ống, đó là lý do tại sao việc so sánh chiều rộng yêu cầu với innerWidth là đáng thực hiện một lần cho mỗi môi trường.
Có muốn kiểm tra hình học chụp của riêng bạn với trình duyệt đám mây không? Tạo một tài khoản Scrapeless miễn phí và chạy hai khối trên.
Đường Dẫn Chụp Thô Biến Mất
Mẹo thông lượng của PixelRAG là bỏ qua mã hóa PNG trong trình duyệt. Nó yêu cầu Chrome đổ các pixel thô trực tiếp vào bộ nhớ chia sẻ với tham số rawFilePath, sau đó nén chúng trong một nhóm quá trình riêng biệt. Kho lưu trữ cung cấp một tệp vá Chromium chính xác cho điều này, và công nhân nén đọc lại tệp với open(raw_path, "rb").
Đối với một trình duyệt từ xa, tham số đó được chấp nhận và lặng lẽ bị bỏ qua.
python
import asyncio, json, os, pathlib
import websockets
CDP = ("wss://browser.scrapeless.com/api/v2/browser"
f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
RAW = pathlib.Path("/dev/shm/tile_probe.raw")
async def call(ws, state, method, params=None, session=None):
state["id"] += 1
msg = {"id": state["id"], "method": method}
if params:
msg["params"] = params
if session:
msg["sessionId"] = session
await ws.send(json.dumps(msg))
while True:
reply = json.loads(await ws.recv())
if reply.get("id") == state["id"]:
return reply
async def main():
RAW.unlink(missing_ok=True)
async with websockets.connect(CDP, max_size=100 * 1024 * 1024) as ws:
state = {"id": 0}
tid = (await call(ws, state, "Target.createTarget",
{"url": "about:blank"}))["result"]["targetId"]
sid = (await call(ws, state, "Target.attachToTarget",
{"targetId": tid, "flatten": True}))["result"]["sessionId"]
await call(ws, state, "Page.enable", session=sid)
await call(ws, state, "Page.navigate",
{"url": "https://books.toscrape.com/"}, session=sid)
await asyncio.sleep(2.5)
shot = await call(ws, state, "Page.captureScreenshot",
{"fromSurface": True, "optimizeForSpeed": True,
"rawFilePath": str(RAW),
"clip": {"x": 0, "y": 0, "width": 875,
"height": 1000, "scale": 1}}, session=sid)
print("error:", shot.get("error"))
print("inline data returned:", len(shot["result"].get("data", "")))
print("local file exists:", RAW.exists())
await call(ws, state, "Target.closeTarget", {"targetId": tid})
asyncio.run(main())
text
error: None
inline data returned: 245576
local file exists: False
Không có lỗi, dữ liệu hình ảnh thực trên phản hồi, và không có gì trên đĩa. Đường dẫn là điều phát hiện: /dev/shm đề cập đến máy tính chạy trình duyệt, và đó không phải là máy tính chạy mã của bạn. Một công nhân mở tệp dự kiến tạo ra FileNotFoundError cho mỗi gạch ngói, và nếu những thất bại đó được đếm thay vì được tạo ra, lần chạy này báo cáo các gạch ngói đã được ghi trong khi thư mục xuất vẫn trống.
Đây là một trong hai phát hiện sắc bén hơn, bởi vì nó không phải là một sự suy giảm chất lượng. Đó là một đường ống không sản xuất gì và nói rằng nó đã hoạt động.
Phải Làm Gì Thay Thế
Không tìm thấy nào chứng minh chống lại việc kết xuất từ xa. Chúng lập luận chống lại việc mang các giả định địa phương vào nó.
Đọc lại hình học thay vì thiết lập nó. innerWidth sau khi ghi đè là số mà toán học gạch ngói của bạn nên sử dụng, không phải là số bạn đã truyền. Nếu các gạch ngói của bạn phải có chiều rộng cố định cho một mô hình đã được đào tạo tại chiều rộng đó, hãy tỷ lệ hoặc đệm hình ảnh đã chụp sau đó thay vì dựa vào trình duyệt để định dạng theo số của bạn.
Lấy hình ảnh mã hóa thay vì con đường thô. Mất rawFilePath tốn thời gian mã hóa PNG hoặc JPEG và trả lại các byte cho quá trình của bạn, đó là nơi duy nhất chúng hữu ích khi trình duyệt là từ xa.
Xem xét hình học gạch ngói như một điều cần khẳng định. Một kiểm tra so sánh chiều rộng yêu cầu với chiều rộng đo, chạy một lần khi môi trường thay đổi, bắt cả hai điều này trước khi một chỉ mục được xây dựng. Không cái nào xuất hiện như một lỗi, và cả hai thay đổi những gì mà mô hình thấy.
Kết Luận
Gạch ngói ảnh chụp giả định rằng trình tạo tôn trọng cửa sổ xem mà bạn đã đặt, và trên một trình duyệt bạn đã khởi chạy, giả định đó là an toàn. Trên một trình duyệt đám mây, việc ghi đè thành công và chiều rộng bố trí hạ cánh ở một nơi khác — được đo ở đây là sáu giá trị khác nhau qua sáu phiên, không cái nào trong số đó là 875 yêu cầu — trong khi clip ảnh chụp được tôn trọng tới từng pixel. Kết quả là một gạch ngói có kích thước chính xác của khu vực sai.
Con đường chụp thô thất bại sạch sẽ và nghiêm trọng hơn: được chấp nhận, bị bỏ qua, không tệp, không lỗi. Kiểm tra số lượng gạch ngói và chiều rộng đo trước khi tin tưởng vào một chỉ mục trực quan, và đọc lại hình học thay vì giả định.
Thông tin nền tảng sâu hơn về giao thức có trong Chrome DevTools Protocol là gì, kết nối tự nó được đề cập trong hướng dẫn về Playwright và Trình duyệt Scraping, chi tiết kế hoạch nằm trên trang giá Scrapeless, và các tham số phiên nằm trong tài liệu Scrapeless.
Câu hỏi thường gặp
Q: Điều này có nghĩa là PixelRAG không thể sử dụng trình duyệt đám mây?
Không hề — điều này có nghĩa là trình kết xuất của nó giả định một cái địa phương vào hôm nay. Logic gạch, mô hình nhúng và chỉ mục không bị ảnh hưởng; điều cần thay đổi là bước chụp, mà nên đọc lại chiều rộng bố cục và sử dụng phản hồi hình ảnh mã hóa thay vì đường dẫn tệp thô.
Q: Tại sao sự ghi đè lại thay đổi chiều rộng nếu nó không áp dụng giá trị của tôi?
Bởi vì nó kích hoạt một sự bố trí lại, chỉ chống lại cửa sổ của chính trình duyệt thay vì các chỉ số bạn yêu cầu. Chiều rộng trước cuộc gọi là 945 trong mọi phiên đo lường và một cái gì đó khác sau đó, vì vậy cuộc gọi có ảnh hưởng — đó là điểm đến mà bạn không thể chọn.
Q: Tham số clip có đáng tin cậy không?
Có, và đó là điều làm cho sự không khớp dễ bị bỏ lỡ. Mỗi lần chụp đều trở lại với đúng kích thước pixel đã yêu cầu. Kích thước đầu ra chính xác không chứng tỏ rằng trang bên dưới được bố trí với kích thước đó.
Q: Một thư viện trình điều khiển như Playwright có thể tránh điều này không?
Không, vì trình điều khiển phát hành cùng một lệnh CDP bên dưới. Một phép đo trước đó trên điểm cuối này phát hiện rằng bộ đặt viewport của Playwright để lại các kích thước báo cáo hoàn toàn không thay đổi, vì vậy triệu chứng khác nhau một chút theo từng khách hàng trong khi nguyên nhân là giống nhau. Chỉ tiêu WebDriver BiDi là nỗ lực theo tiêu chuẩn để đưa việc điều khiển trình duyệt từ xa vào một hợp đồng định nghĩa, nơi mà hành vi như thế này cuối cùng sẽ được xác định.
Q: Làm thế nào để tôi kiểm tra nhanh chóng thiết lập của mình?
Đặt viewport, sau đó đánh giá innerWidth và so sánh nó với những gì bạn đã yêu cầu. Một dòng, một chuyến đi, và nó sẽ ngay lập tức cho bạn biết liệu toán học gạch của bạn đang hoạt động trên một số thực hay một số hy vọng.
Q: Một crop có chiều rộng cố định thực sự có làm hỏng việc lấy dữ liệu không?
Nó thay đổi phân phối đầu vào. Một mô hình nhúng được điều chỉnh trên các ảnh chụp màn hình của các trang được bố trí ở một chiều rộng nhất định sẽ nhận được các crop của các bố cục rộng hơn, với nội dung bị thiếu ở bên phải. Miếng gạch vẫn nhúng và vẫn trả về các hàng xóm, vì vậy không có gì nổi bật như bị hỏng — các kết quả chỉ đơn giản là trở nên tồi tệ hơn trên các trang mà nội dung hữu ích ngồi ngoài crop.
Q: sessionTTL nên là bao nhiêu cho một phiên chạy gạch?
Đủ dài để bao phủ toàn bộ trang, vì gạch là tuần tự và một trang cao cần nhiều lần chụp trong một phiên. Đo kích thước nó với trang bị chậm nhất mà bạn mong đợi thay vì một cái trung bình, và đóng mục tiêu khi phiên chạy kết thúc.
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.


