Keywords
đánh giá nodriver, web scraping, mã nguồn mở, benchmark
Content
nodriver là thư viện tự động hóa trình duyệt bằng Python của ultrafunkamsterdam, tác giả đứng sau undetected-chromedriver, và nó được giới thiệu như phiên bản kế nhiệm của dự án đó. Điểm khác biệt về kiến trúc của nó nằm ở chỗ tách hẳn khỏi Selenium và chromedriver: nodriver giao tiếp với Chromium thông qua Chrome DevTools Protocol (CDP), không cần binary WebDriver ở giữa, và API của nó theo kiểu async. Playwright và Puppeteer thuộc một nhóm so sánh khác. Chúng cũng là bộ điều khiển trình duyệt dựa trên protocol, không phải hậu duệ của WebDriver; những khác biệt đáng quan tâm là thiết kế API, cách đóng gói, cách cung cấp trình duyệt và chính sách tương thích.
Bài đánh giá này kiểm tra nodriver 0.50.3 theo góc nhìn gói cài đặt, thay vì thử nó trước các cơ chế phòng thủ thực tế. Tôi đo gói đã cài, khả năng import, bề mặt API, dung lượng đĩa và giấy phép, rồi chỉ khởi chạy trình duyệt với các trang được phục vụ trên 127.0.0.1. Không có mục tiêu trực tiếp, dịch vụ chống bot hay CAPTCHA nào được dùng trong quá trình này. Kết quả chỉ mô tả hành vi gói và mức độ lộ thông tin mặc định của trình duyệt; chúng không đủ để kết luận hiệu quả chống phát hiện.
Ba điểm nổi bật nhất nằm trong phạm vi đó. Thứ nhất, trên Python 3.14 thư viện này không thể import được — chỉ một byte lạc chỗ cũng đủ làm cả gói lỗi ngay trước khi bạn gọi bất kỳ thứ gì. Thứ hai, xét trong nhóm công cụ cùng loại, nó rất gọn: chỉ có ba phụ thuộc trực tiếp đã khai báo cộng thêm một gói phụ thuộc gián tiếp được giải quyết, và tổng dung lượng đo được chỉ khoảng 17 MB trong môi trường thử nghiệm. Thứ ba, trên trang của chính tôi, thuộc tính duy nhất khiến nodriver khác biệt rõ ràng so với Playwright và Puppeteer gốc chỉ là một giá trị boolean — còn chuỗi user-agent ở chế độ headless vẫn ghi HeadlessChrome ở cả ba. Những điều bất ngờ còn lại nằm ở giấy phép.
Lỗi import trên Python 3.14
Bắt đầu với lỗi sẽ đụng vào bạn đầu tiên, vì nó xảy ra trước khi bất kỳ đoạn code nào của bạn chạy. Trên Python 3.14, một lệnh import nodriver bình thường sẽ thất bại ngay:
File ".../nodriver/cdp/network.py", line 1345
#: JSON (±Inf).
^
SyntaxError: Non-UTF-8 code starting with '\xb1' on line 1345, but no encoding declared; see PEP 263
File gây lỗi là cdp/network.py được sinh tự động (phần đầu file ghi # DO NOT EDIT THIS FILE!). Nó chứa một byte không phải UTF-8, 0xb1, nằm trong ký hiệu ± ở chú thích #: JSON (±Inf)., nhưng lại không có khai báo encoding nguồn. Module này được nạp theo chuỗi nodriver/__init__ → cdp/__init__ → network, nên lỗi phân tích cú pháp làm import bị dừng hẳn. Việc rà soát gói không cho thấy file nguồn nào khác không phải UTF-8.
Cần diễn đạt cẩn thận về ranh giới phiên bản. Python 3.14.2 từ chối file này, trong khi Python 3.12.13 import được nó mà không cần chỉnh sửa. Hai file network.py đã cài trong các môi trường đó giống hệt nhau ở cấp byte (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), vì vậy không thể giải thích bằng việc nguồn khác nhau. Bài đánh giá này không khoanh vùng thay đổi cụ thể trong tokenizer của CPython gây ra hiện tượng đó, và Python 3.13 cũng chưa được kiểm tra. Vì thế, kết quả ở đây chỉ phản ánh đúng hai phiên bản đã đo, chứ không khẳng định mọi bản Python trước đó đều hành xử như 3.12.
Từ hai điểm đo đó không thể suy ra gì về Python 3.13.
Đây là một lỗi tái hiện, không phải phát hiện mới. Cùng traceback trên Python 3.14 đã được ghi nhận trong issue #35 của nodriver, kèm bản sửa đề xuất trong pull request #36. Phiên bản 0.50.3 vẫn còn byte lỗi đó. Metadata trên PyPI chỉ liệt kê các classifier đến Python 3.13 và không tuyên bố hỗ trợ 3.14.
Cách khắc phục nhỏ như chính lỗi: dùng phiên bản Python đã được kiểm chứng hoặc mã hóa lại riêng file đó sang UTF-8 như pull request #36 đề xuất. Sau khi đổi mã hóa, package có thể import và introspect trên 3.14 mà không gặp chướng ngại thứ hai nào. Lượt chạy sạch trên 3.12.13 phía dưới xác nhận một đường đi chưa vá; Python 3.13 không được thử trong bài này.
Trên Python 3.12.13, import nodriver thành công mà không cần patch. Mọi con số điều khiển trình duyệt ở phần sau đều xuất phát từ cài đặt đó. Các số đo dung lượng và thời gian import dùng bản đã đổi một byte sang UTF-8 trên 3.14 và được ghi chú rõ tương ứng. Python 3.13 vẫn chưa được kiểm tra ở đây; một kết quả trên 3.12 không thể thay thế cho toàn bộ dải phiên bản trước đó.
Nếu bạn mặc định dùng các interpreter mới — và khá nhiều đội thích nhảy sang Python mới rất nhanh — thì đây là một rào cản có thật, dù sửa được rất đơn giản. Nên biết nó tồn tại để khỏi mất cả buổi vì một SyntaxError trong file mà bạn chưa từng viết.
Thực chất nodriver là gì
Mô tả ngắn gọn kiểu “CDP-native, không dùng webdriver” nghe như tiếp thị cho đến khi bạn xem bên trong wheel có gì. nodriver đóng gói sẵn toàn bộ binding cho DevTools Protocol: package nodriver.cdp chứa 57 module domain protocol — accessibility, dom, network, page, fetch, runtime, target, storage, input, emulation, và còn nhiều module khác nữa. Con số module này chính là cơ chế đằng sau lời khẳng định “đi thẳng qua CDP, không qua Selenium”. Thay vì gọi một executable chromedriver để nói WebDriver và chuyển tiếp hộ bạn, nodriver sinh ra các đối tượng Python cho từng domain CDP rồi tự nói protocol qua WebSocket. cdp/network.py — file có byte lỗi — là một trong 57 module sinh tự động đó, nên lỗi này nằm ngay trong code được sinh ra chứ không phải code viết tay.
Phía trên lớp protocol đó là một mô hình đối tượng dễ dùng hơn. Đối tượng Tab cung cấp 62 method public, và phạm vi truy tìm phần tử rộng hơn nhiều driver khác: tìm text bằng find() và find_all(), CSS bằng select() và select_all(), cùng điểm vào xpath() native. Có XPath, CSS và tìm text trên cùng một đối tượng thực sự tiện; ở một số thư viện khác, bạn vẫn phải quay về evaluate() mới dùng được XPath. Constructor Config mở ra các tham số user_data_dir, headless, browser_executable_path, browser_args, sandbox, lang (mặc định 'en-US'), host, port, expert, cùng **kwargs. Trong quá trình thống kê API, việc tạo Config(headless=True) sinh ra 16 flag khởi chạy Chromium, bao gồm --no-first-run, --no-default-browser-check, --remote-allow-origins=*, và --homepage=about:blank. Tôi không gọi start() trong bước introspect này. Các bài kiểm tra trình duyệt bên dưới là một lần chạy riêng.

Thư viện này có một số API hướng tới chống phát hiện — tôi xác nhận được sự tồn tại của chúng nhưng không thử hành vi của chúng trên bất kỳ mục tiêu nào. Tôi nhắc tới điều đó một lần rồi dừng ở đây, vì tác dụng thực sự của chúng với bất kỳ dịch vụ nào lại chính là thứ tôi chủ động không kiểm tra. Nói trung tính thì: cách đặt tên của nodriver tiết chế hơn một số đối thủ mang nhãn stealth. Câu chuyện của nó thiên về kiến trúc — CDP-native, profile mới cho mỗi lần chạy — hơn là một loạt tên method kiểu detect_and_bypass. Hãy hiểu đây là nhận xét về thiết kế API, không phải lời khẳng định về kết quả.
Những con số đó đến từ việc import package rồi dùng introspection của chính Python — inspect, duyệt module, đếm thuộc tính. Không hề chạm vào website nào. Nếu muốn chạy lại, mọi số liệu đều có ngay trong package; chúng không phải cảm tính.
Nó tự khai báo gì về bản thân

Có một câu hỏi bạn có thể trả lời mà không cần đụng tới bất kỳ hệ thống phòng thủ thực nào: khi nodriver điều khiển trình duyệt, trình duyệt đó sẽ tự khai báo gì với trang nó đang xem? Tôi viết một trang đọc các thứ hiển nhiên — navigator.webdriver, user-agent, platform, languages, số lượng plugin và hardware, hình dạng window.chrome, kết quả từ Permissions API, kích thước cửa sổ và màn hình — rồi phục vụ nó trên 127.0.0.1 và cho bốn stack truy cập: nodriver, Botasaurus, cùng Playwright và Puppeteer gốc làm đối chứng. Cả bốn đều điều khiển cùng một bản Chrome (Chrome for Testing 151.0.7922.10), nên điểm khác biệt nếu có là do thư viện chứ không phải do browser. Chạy headless và headed, mỗi kiểu ba lần. Mọi giá trị bên dưới đều giống nhau trong cả ba lần.
| Stack | Chế độ | navigator.webdriver | Token user-agent | navigator.languages |
|---|---|---|---|---|
| nodriver 0.50.3 | headless | false | HeadlessChrome/151.0.0.0 | ["en-US"] |
| nodriver 0.50.3 | headed | false | Chrome/151.0.0.0 | ["en-US"] |
| Botasaurus 4.0.92 | headless / headed | false | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
| Playwright 1.56.0 | headless / headed | true | HeadlessChrome/151 / Chrome/151 | ["en-US","en"] |
| Puppeteer 24.16.0 | headless / headed | true | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
Điểm khác biệt rõ nhất là biến boolean duy nhất đó. Theo cấu hình khởi chạy mặc định này, nodriver báo navigator.webdriver là false, trong khi Playwright và Puppeteer gốc báo true, cả ở chế độ headless lẫn headed. Bài thử chỉ ra một khác biệt ở cấp cấu hình; nó không chứng minh rằng giá trị đó chỉ đơn giản đến từ việc thiếu binary WebDriver.
Ở cả bốn stack, thuộc tính này vẫn là getter gốc của browser trên Navigator.prototype — function get webdriver() { [native code] } — chứ không phải thuộc tính gắn riêng lên instance hay một hàm đã bị thay thế. Thuộc tính này không bị JavaScript phía trang viết đè sau khi load. Bài thử không bóc tách được launch argument hay đường dẫn nguồn nào tạo ra giá trị khác biệt đó.
Chi tiết thứ hai là chỗ marketing cần được đặt lại cho đúng. Ở chế độ headless, user-agent của nodriver vẫn tự khai là HeadlessChrome/151.0.0.0 — giống hệt Puppeteer gốc, giống hệt Playwright gốc. Chạy headed thì nó thành Chrome/151.0.0.0, cũng y như nhau. Nếu bạn nghĩ một thư viện chống phát hiện sẽ mặc định che chuỗi tự nhận diện nổi tiếng nhất trong tự động hóa trình duyệt, thì không phải vậy. Bạn sẽ phải tự đặt nó.
Gần như mọi thứ khác đều giống nhau ở cả bốn stack, và điều đó đáng nói rõ vì nó thu hẹp câu chuyện lại — mọi thuộc tính dưới đây đều đọc giống hệt trên nodriver, Botasaurus, Playwright và Puppeteer:
| Thuộc tính | Giá trị giống nhau ở cả bốn stack |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| Plugin | năm |
| MIME types | hai |
pdfViewerEnabled | true |
| Số lõi logic | mười hai |
| Bộ nhớ thiết bị báo cáo | 16 GB |
| Điểm chạm | zero |
window.chrome | có mặt, với app/csi/loadTimes và không có runtime |
| Chuỗi renderer WebGL | giống hệt ở cả bốn |
Câu chuyện cũ về việc Permissions API và Notification.permission mâu thuẫn nhau không xuất hiện ở đâu cả — cả bốn đều trả về default và prompt đồng nhất. Tôi cũng rà document và window để tìm dấu vết kiểu cdc_ mà các stack WebDriver cũ thường để lại: không có gì ở cả bốn.
Chỗ duy nhất khiến nodriver trông giống một browser tự động hóa thuần túy hơn là ở hình học cửa sổ. Ở chế độ headless, nodriver báo outerWidth/outerHeight là 0×0 trên màn hình 800×600; Playwright headless báo 1280×720 vì nó tự đặt viewport cho bạn. Puppeteer cũng ở 0×0 như nodriver. Đây là khác biệt về cấu hình mặc định, không phải về khả năng, và bạn hoàn toàn có thể chỉnh nó.
Một điều cuối cùng đáng biết trước khi triển khai: nodriver không đi kèm browser, nên mặc định nó sẽ dùng Chrome đã có sẵn trên máy bạn. Trên máy của tôi, nó tự phát hiện /Applications/Google Chrome.app — Chrome 150.0.7871.187 — và user-agent được khai báo là đúng phiên bản đó, chứ không phải một bản cố định. Phiên bản browser mà đội máy của bạn công bố ra ngoài chính là phiên bản đang được cài trên đội máy đó.
Nói rõ đây là gì và không phải là gì. Đây là bản ghi về việc một stack tự động hóa khai báo gì khi không ai yêu cầu nó giấu giếm — hữu ích nếu bạn ở phía phòng thủ, hoặc nếu bạn muốn biết công cụ của mình đang phát ra điều gì. Nó không phải thước đo xem những thông tin đó có ý nghĩa gì với một dịch vụ cụ thể hay không. Tôi không kiểm tra điều đó, và không dòng nào ở trên nên được hiểu là ám chỉ kết quả.
Nó có lấy được nội dung từ trang không?
Tự khai báo là một chuyện; trả về đúng HTML lại là chuyện khác. Tôi chạy nodriver trên cùng bộ fixture ba lớp nội dung mà phần còn lại của repo benchmark này dùng, để các số liệu khớp với những công cụ khác đã đo ở đây. Trang có ba thứ: A, một liên kết tĩnh với dấu nhận diện là literal nằm trong byte được phục vụ; B, một node được tạo bởi inline script trong lúc parse, với marker và URL được ghép từ các mảnh nên chỉ khi thực thi JavaScript mới thấy được; và C, một node được chèn vào 800 ms sau event load, cũng được ghép theo cách tương tự. Class C là phần khó nhất — một lần đọc ở thời điểm load sẽ không thấy nó.
| Stack | Đọc mặc định | Với một lần chờ rõ ràng |
|---|---|---|
| nodriver 0.50.3 | 2 trên 3 (A + B, bỏ lỡ C) | 3 trên 3 |
| Botasaurus 4.0.92 | 2 trên 3 | 3 trên 3 |
| Playwright 1.56.0 | 2 trên 3 | 3 trên 3 |
| Puppeteer 24.16.0 | 2 trên 3 | 3 trên 3 |
nodriver đi đúng cùng quỹ đạo với các công cụ nặng đô. browser.get() rồi gọi thẳng tab.get_content() là một snapshot ở thời điểm load: nó render JavaScript đúng — class B chứng minh điều đó, vì class B không có trong byte được phục vụ — nhưng nó bỏ lỡ mọi thứ được chèn sau load. Thêm tab.select("#delayed-injected") thì bạn sẽ lấy được cả ba. Cùng một lỗi phổ biến, cùng một cách khắc phục, giống Playwright và Puppeteer. Ổn định qua ba lần lặp và ba lần chạy riêng của toàn bộ bộ test, không hề chập chờn.
Tôi cũng quét độ trễ chèn để xem đọc mặc định sẽ bỏ cuộc ở đâu. nodriver ngừng nhìn thấy class C khi phần tử được chèn sau load từ 100 ms trở lên — đúng cùng ngưỡng với hai control gốc. (Botasaurus là ngoại lệ ở đây, và đó là khác biệt thực sự thú vị giữa hai thư viện anti-detect: get() của nó chặn cho đến khi trang load hoàn chỉnh theo mặc định, nên lần đọc mặc định vẫn bắt được phần chèn ở 300 ms. Nó trả giá khoảng 250 ms cho mỗi lần điều hướng.)
Bản thân cơ chế chờ cũng có một điểm cần tính trước. Trong phép quét này, tab.select() của nodriver rơi vào các bucket polling thô thay vì bám sát sát độ trễ chèn:
| Độ trễ class C | 0 ms | 100 ms | 400 ms | 800 ms | 1500 ms |
|---|---|---|---|---|---|
nodriver select() | 124–152 ms | 1132–1141 | 1128–1129 | 2132–2177 | 2138–2150 |
Puppeteer waitForSelector | 113–129 ms | 203–216 | 512–516 | 911–919 | 1608–1611 |
Trong fixture này, việc chèn sau 100 ms dẫn đến khoảng 1,1 giây cho select(). Vòng lặp đã cài thực hiện await self rồi await self.sleep(0.5) sau một lần miss; chu kỳ kết hợp đo được ở đây rơi vào gần một giây, nhưng await self không được chứng minh là một lệnh ngủ cố định toàn cục. Mọi node chậm được thử đều được tìm thấy. Trình chờ của Puppeteer bám sát các độ trễ này hơn. Nếu có nhiều lần chờ nối tiếp, chênh lệch có thể tích lũy, dù bài này không benchmark một trang sản xuất với 30 selector.
Khởi chạy trình duyệt là chỗ khác mà câu chuyện async và gọn nhẹ gặp thực tế. Việc dựng browser đưa nodriver vào cùng vùng với Botasaurus và Puppeteer, và chậm hơn hẳn Playwright:
| Stack | Thời gian khởi chạy browser, qua các lần chạy |
|---|---|
| nodriver 0.50.3 | 910–1583 ms |
| Botasaurus 4.0.92 | 986–1151 ms |
| Puppeteer 24.16.0 | 969–1008 ms |
| Playwright 1.56.0 | 282–365 ms |
Khi đã chạy, quá trình điều hướng và đọc của nodriver là nhanh nhất trong bốn công cụ ở mức 119–129 ms. Nhẹ tay khi vào số, bình thường khi khởi động.
Cài đặt và dung lượng: phần thực sự tốt
Đây là chỗ nodriver xứng đáng với nhãn “tập trung”, và đó là một факт cài đặt sạch, có thể kiểm chứng, chứ không phải chuyện gì liên quan đến scraping. Một lệnh pip install nodriver sạch sẽ giải quyết các thứ sau:
| Thông tin cài đặt | nodriver 0.50.3 |
|---|---|
| Phụ thuộc runtime trực tiếp đã khai báo | ba — websockets, mss, deprecated |
| Phụ thuộc gián tiếp được giải quyết | wrapt (qua deprecated) |
Tổng site-packages đo được | khoảng 17.2 MB trên 6 thư mục dist-info, bao gồm cả pip của môi trường |
| Phần riêng của nodriver | 3.7 MB |
| numpy / lxml | không có |
| Binary browser khi cài đặt | không kéo xuống gì cả |
Với một nhóm công cụ thường kéo theo cả một stack render, đây là một cách tiếp cận khá nhẹ.
Phân biệt giữa package trực tiếp và package gián tiếp rất quan trọng cho việc bảo trì. Metadata của nodriver yêu cầu mss, websockets, và deprecated; wrapt xuất hiện vì deprecated cần nó. Thư mục dist-info thứ sáu trong môi trường 17.2 MB là pip, vốn đã có sẵn trong virtualenv đó rồi. Vì vậy, “17.2 MB trên sáu distribution” mô tả môi trường đo, còn “bốn package runtime được thêm vào” mô tả thứ mà quá trình cài đặt giải quyết ra. Hai con số này liên quan với nhau nhưng không thể thay thế cho nhau.
Điểm làm cho con số này thật sự có ý nghĩa là phép so sánh: trên cùng máy đó, framework cùng họ Botasaurus nặng 122.3 MB trên 44 package — tức khoảng gấp bảy lần dung lượng đĩa. Đó là khác biệt giữa một CDP driver tập trung và một framework đầy đủ “pin cài sẵn”, và nó có hai mặt. nodriver cho bạn một cây phụ thuộc mỏng, dễ đọc, thật sự có thể audit; Botasaurus cho bạn nhiều thứ hơn ngay từ đầu và đổi lại là thêm gánh nặng đĩa và bề mặt phụ thuộc. Cái nào “tốt hơn” thì không thể nói chung — tùy bạn cần driver hay framework — nhưng nếu bạn ưu tiên một bản cài nhỏ, dễ kiểm tra, nodriver hiếm khi gọn hơn thế cho đúng kiểu việc nó làm.
Dung lượng đĩa nhỏ không đồng nghĩa với import nhẹ. Trên bản Python 3.14 đã vá byte, import nodriver mất khoảng 158 ms (trung vị trên các subprocess import mới, khoảng 151–199 ms). Bản 3.14 chưa vá thì import lỗi, nên không có số đo thời gian cho nó. Cả 57 module domain CDP đều được load ngay từ đầu, và bộ nhớ thường trú sau import là 31.5–31.9 MB trước khi tồn tại bất kỳ process Chrome nào. Một browser được khởi chạy sẽ cộng thêm nhiều hơn và không được tính vào phép đo bộ nhớ này.
Hai lưu ý đi kèm các con số đó. Chúng đến từ một máy duy nhất — macOS arm64 — và số đo dung lượng cũng như thời gian import được thực hiện trên Python 3.14 với bản đã vá một byte, vì bản gốc không thể import trên interpreter đó. Trên một Python được hỗ trợ, bạn không cần bản vá: tôi đã xác nhận import sạch trên 3.12.13, và đó là nơi mọi đo đạc liên quan đến browser được lấy từ. Ngoài ra, bạn vẫn cần một binary Chrome, Chromium, Edge hoặc Brave thật sự khi chạy — nodriver điều khiển browser có sẵn, chứ không ship browser kèm theo, nên chi phí cài đặt đó nằm ngoài 17 MB; và như phần disclosure cho thấy, Chrome nào đang có trên máy bạn thì chính phiên bản đó sẽ là phiên bản mà traffic của bạn khai ra.
Giấy phép mới là quyết định lớn nhất khi muốn dùng

Phần lớn các bài đánh giá một công cụ miễn phí thường coi “nó là open source” là kết thúc câu chuyện giấy phép. Với nodriver, đó mới chỉ là điểm bắt đầu, vì giấy phép của nó là AGPL-3.0 — được xác nhận cả trong LICENSE.txt của wheel lẫn spdx_id trong repo. Đây là giấy phép copyleft mạng rất mạnh, và nó khác đáng kể so với những gì các công cụ lân cận yêu cầu.
Các đối thủ mà người ta thường đem nodriver ra cân nhắc đa phần là giấy phép permissive, nên sự đối lập này rất rõ:
| Công cụ | Giấy phép | Nếu bạn chạy một bản đã chỉnh sửa như dịch vụ mạng |
|---|---|---|
| nodriver | AGPL-3.0 | Mục 13 có thể buộc người vận hành phải cung cấp mã nguồn tương ứng của phiên bản đã sửa cho người dùng từ xa |
| Playwright | Apache-2.0 | không có điều khoản copyleft mạng tương đương |
| Puppeteer | Apache-2.0 | như trên |
| Botasaurus | MIT | như trên |
Phạm vi rất quan trọng. Điều 13 của AGPL áp dụng cho một phiên bản đã chỉnh sửa của chương trình thuộc phạm vi giấy phép khi được dùng cho tương tác mạng từ xa. Bài đánh giá này không quyết định liệu phần code dịch vụ xung quanh có nằm trong tác phẩm được bảo hộ hay không, cũng không xử lý các trường hợp ranh giới như dùng nội bộ hay trong doanh nghiệp. Nếu một sản phẩm hosted có chỉnh sửa nodriver, hãy đọc kỹ giấy phép và kiến trúc với tư vấn pháp lý. Đây là cờ cảnh báo kỹ thuật cho việc triển khai, không phải lời khuyên pháp lý.
Tôi không đánh giá AGPL là tốt hay xấu — copyleft là một lựa chọn hợp lệ và rất nhiều dự án nghiêm túc dùng nó. Tôi chỉ đang nhấn mạnh rằng “nodriver là free và open source” là đúng nhưng chưa đủ. Nghĩa vụ là có thật, khác với mặc định permissive trong mảng này, và nó phải nằm trong quyết định thay vì bị làm phẳng thành “miễn phí”. (Nhân tiện, PyPI không gắn bất kỳ license classifier nào; text AGPL nằm trong wheel và SPDX id nằm trong repo, nên đừng trông vào package index để nó tự làm rõ cho bạn.)
Siêu dữ liệu, tại thời điểm đo
Các con số tại một thời điểm của repo và package, lấy thẳng từ GitHub API và PyPI:
| Thông tin | Giá trị vào ngày 14 tháng 7 năm 2026 |
|---|---|
| Stars | 4,511 |
| Forks | 422 |
| Open issues | 14 |
| Tạo mới | Tháng 2 năm 2024 |
| Lần push gần nhất | Tháng 5 năm 2026 |
| Bản phát hành PyPI mới nhất | 0.50.3 |
| Wheel | pure-Python py3-none-any |
requires-python | >=3.9 |
| Python classifiers | 3.7–3.13 |
Các tín hiệu bảo trì quan sát được khá trái chiều: repo được push vào tháng 5 năm 2026, trong khi package đo gần nhất vẫn chứa lỗi import trên Python 3.14 và bản sửa đề xuất vẫn chưa được phát hành vào thời điểm nghiên cứu. Số sao và số issue mở không thể tự nó quyết định nhịp độ đó có đạt ngưỡng bảo trì của bạn hay không.
Ưu và nhược điểm
Ưu điểm:
- Cài đặt nhỏ, dễ đọc: ba phụ thuộc trực tiếp đã khai báo cộng với
wraptgián tiếp, khoảng 17.2 MB trên sáu thư mụcdist-infotrong môi trường đo (một trong số đó làpip), không có numpy/lxml, và không kéo browser về lúc cài đặt. - CDP-native thật sự: đóng gói sẵn 57 module binding DevTools Protocol riêng và nói protocol trực tiếp, không cần chromedriver/Selenium binary ở giữa.
- Bề mặt tìm phần tử rộng trên
Tab(62 method public) với XPath, CSS và text search native — không cần rơi xuốngevaluate()thô để dùng XPath. - Thiết kế async, mỗi lần chạy có profile mới, và
Configcó đủ các nút chỉnh quen thuộc (headless, executable path, args, lang, port). - Lấy nội dung từ trang tạo bằng JavaScript tốt ngang các công cụ lớn: 2 trên 3 lớp nội dung ở lần đọc mặc định, 3 trên 3 khi thêm một lần chờ rõ ràng — y hệt Playwright và Puppeteer gốc trên cùng fixture, ổn định qua ba lần chạy. Quá trình điều hướng và đọc là nhanh nhất trong bốn công cụ ở mức 119–129 ms.
- Báo
navigator.webdriverlàfalsetheo mặc định trong khi hai control gốc báotrue, mà không patch thuộc tính — descriptor vẫn là getter gốc của browser. - Có bản sắc kiến trúc rõ ràng như phiên bản kế nhiệm CDP-native của undetected-chromedriver.
Nhược điểm:
- Không import được trên Python 3.14 ngay khi cài — một file không phải UTF-8 (
cdp/network.py) gâySyntaxErrorlúc import. Đã tái hiện theo issue #35 đang mở, vẫn chưa được sửa trong 0.50.3. Hãy ghim ≤3.13 (đã xác nhận sạch trên 3.12.13) hoặc tự mã hóa lại file đó. - AGPL-3.0 là yếu tố phải cân nhắc nghiêm túc với bất kỳ ai chạy bản đã chỉnh sửa như dịch vụ mạng — chặt hơn các peer dùng Apache/MIT.
- Dung lượng cài nhỏ không đồng nghĩa import nhỏ: khởi động lạnh khoảng ~158 ms và resident memory ~31.5 MB trước khi browser tồn tại, vì tất cả 57 module CDP đều được nạp ngay.
tab.select()dùng back-off nửa giây khi polling, nên các lần chờ ngắn bị làm tròn lên — chờ 100 ms có thể tốn khoảng 1,1 giây, trong khi waiter của Puppeteer chỉ khoảng 210 ms. Luôn đúng, nhưng tích lũy lại sẽ đáng kể khi có nhiều lần chờ ngắn.- Chế độ headless vẫn tự khai
HeadlessChrometrong user-agent theo mặc định, y hệt các control gốc; cấu hình mặc định không che đi chuỗi tự nhận diện rõ ràng nhất. - Vẫn cần một binary Chrome/Chromium/Edge/Brave thật sự khi chạy; bản cài pip nhẹ mới chỉ là một nửa câu chuyện phụ thuộc, và Chrome nào có trên máy bạn sẽ là phiên bản traffic của bạn tự tiết lộ.
- Hiệu quả trước bất kỳ hệ thống chống bot nào ở đây đều chưa được xác minh — toàn bộ giả định stealth không được kiểm tra theo thiết kế.
Những gì tôi không kiểm tra, và vì vậy không thể kết luận: bộ nhớ theo từng tab, độ trễ round-trip CDP, xử lý profile, throughput ở quy mô lớn, Linux hay Windows, riêng Python 3.13, và — quan trọng nhất — hiệu quả thực tế trước bất kỳ dịch vụ live nào. Mọi browser trong bài này chỉ nói chuyện với fixture trên 127.0.0.1. Tất cả số liệu đều trên một máy duy nhất (macOS arm64); các số điều khiển browser là trên Python 3.12.13 không vá, còn các số dung lượng và import-timing cũ là trên Python 3.14 với bản đã vá một byte.
Tôi cũng không chạy ma trận nâng cấp browser, nên khả năng tương thích với các bản Chrome tương lai vẫn là việc người dùng triển khai phải tự kiểm tra, chứ không phải kết quả của bài đánh giá này.
Phù hợp với ai, và ai nên bỏ qua
nodriver phù hợp nếu bạn muốn một driver Chromium gọn, async, CDP-native, và chấp nhận tự quản browser, cập nhật và runtime. Cây phụ thuộc nhỏ dễ audit hơn, và thiết kế CDP-first hợp với điều khiển ở tầng protocol. Khả năng chạy trong container vẫn chưa được kiểm chứng: bài này không thử image Linux, cài đặt browser, shared library, sandbox hay dọn process.
Hai nhóm nên xem lựa chọn khác. Nếu bạn đang dùng Python 3.14 và không muốn ghim interpreter hay vá file vendored, hãy chờ bản sửa được phát hành — lỗi import hiện là điểm chặn cứng. Và nếu AGPL-3.0 là vấn đề đối với cách bạn định phát hành — ví dụ một dịch vụ hosted với các chỉnh sửa riêng tư — thì chỉ riêng giấy phép đã đủ lý do để cân nhắc một phương án có giấy phép permissive trước khi xây dựng trên nó. Cả hai điều này không phải chê code; đó là những ràng buộc mà bạn nên biết trước thay vì phát hiện trong lúc rà soát tuân thủ.
Cũng nên bỏ qua nó nếu thứ bạn thực sự cần là dữ liệu, chứ không phải một browser để tự điều khiển bằng tay. nodriver đưa cho bạn một tab có thể lập trình và 62 method; biến một trang đã render thành record sạch, có cấu trúc vẫn là việc bạn phải viết code cho. Đó là một công việc khác, và đó chính là ranh giới nơi một API được quản lý sẽ phù hợp hơn.
Các lựa chọn thay thế và Thunderbit nằm ở đâu
Nói thẳng trước: nodriver là miễn phí, theo AGPL và tự host được. Bạn tự chạy browser, tự quản cập nhật, tự chịu runtime và mọi lỗi trong đó. Với một developer muốn đúng mức độ kiểm soát đó, không có dịch vụ managed nào cạnh tranh được về giá với một thư viện mà bạn đã có sẵn.
Trong mã nguồn mở, hãy so sánh theo công việc chứ không theo logo. Nếu bạn đang cân nhắc các driver browser thật, bài so sánh Playwright và Puppeteer của chúng tôi nói về hai “ông lớn” mà nodriver đứng cạnh về mặt hình thái. Scrapling là lựa chọn Python gần nhất trên trục thiên về stealth nếu đó là lý do bạn tìm đến. Nếu bạn cần đầu ra sẵn sàng cho LLM thay vì điều khiển browser thô, Crawl4AI render trang rồi trả về Markdown, còn Scrapy vẫn là framework tiêu chuẩn cho các crawl lớn không cần browser. Nếu bạn đang cân nhắc nhiều công cụ cùng lúc, bài tổng hợp open-source scraper sẽ đặt các nhóm cạnh nhau.
Công khai: bài viết này được xuất bản bởi Thunderbit. Thunderbit là một dịch vụ trích xuất được quản lý, nên phép so sánh ở đây thiên về nhiệm vụ chứ không thiên về kiến trúc. nodriver cho bạn một lớp điều khiển browser mà bạn tự host và tự lập trình; Thunderbit xử lý render và trả lại nội dung trang hoặc record theo schema như một dịch vụ. Dùng nodriver khi điều khiển browser ở mức protocol và tự host là yêu cầu bắt buộc. Hãy cân nhắc một công cụ trích xuất được quản lý khi định dạng đầu ra và việc bàn giao vận hành quan trọng hơn việc sở hữu browser. Các chi tiết biến động về endpoint, credit và giới hạn batch nên xem ở trang giá, không phải trong một benchmark thư viện.
Sự đánh đổi là công việc nằm ở đâu: nodriver giữ việc quản lý browser và bảo trì runtime ở phía bạn, mà không thu phí dịch vụ theo request; một API managed nhận trách nhiệm lớp đó và tính phí cho mỗi lần gọi.
Dùng thử Thunderbit để trích xuất dữ liệu web
Kết luận
Hãy dùng nodriver nếu bạn muốn một driver Chromium nhỏ gọn, async, CDP-native, đã xác nhận chạy được trên phiên bản Python của bạn (trong bài này Python 3.12.13 import sạch), và đã xem xét AGPL-3.0 phù hợp với cách phát hành của mình hay chưa. Cây runtime được giải quyết của nó thêm bốn package, môi trường đo chiếm khoảng 17 MB, và thư viện cung cấp 57 module domain CDP cùng bề mặt Tab 62 method và XPath native. Trên fixture này, nó trả về 2 trên 3 lớp nội dung theo mặc định và 3 trên 3 khi thêm wait, ngang Playwright và Puppeteer gốc.
Tuy nhiên, phải cân nhắc caveat một cách thực tế. Trên Python 3.14, nó không import được cho đến khi bạn sửa một byte không phải UTF-8 — một issue đã được ghi nhận và vẫn đang mở, không phải bí ẩn, nhưng là điểm dừng cứng ngay ngày bạn chạm phải nó. Giấy phép là AGPL-3.0, và đó là một quyết định thật sự cho bất kỳ ai chạy bản đã chỉnh sửa như một dịch vụ, không phải thủ tục hình thức. Bản cài nhỏ không đồng nghĩa với import nhỏ, vì tất cả các module CDP đều được load trước, và cơ chế polling nửa giây của select() khiến các lần chờ ngắn mỗi lần tốn khoảng một giây. Với câu hỏi về disclosure mặc định mà tôi thực sự có thể trả lời, bức tranh còn hẹp hơn lời marketing: chỉ một boolean khác so với Puppeteer gốc, user-agent headless vẫn ghi HeadlessChrome, và mọi thuộc tính khác tôi đo đều giống nhau ở cả bốn stack. Và toàn bộ giả định chống phát hiện — lý do khiến nhiều người tìm đến nodriver ngay từ đầu — là thứ tôi cố ý không kiểm tra. Tôi chỉ kiểm kê thư viện và điều khiển nó trên một trang ở máy của mình thay vì đem nó đối đầu với phòng thủ thực tế, và tôi thà nói điều đó thật thẳng còn hơn đưa cho bạn một lời khẳng định bypass mà tôi không thể bảo chứng. Với những câu hỏi tôi có thể trả lời, nodriver là một driver được xây tốt, gọn bất ngờ, nhưng có hai cạnh sắc — một rào cản theo phiên bản Python và một giấy phép copyleft — mà bạn nên nhìn thấy trước.
Dùng thử Thunderbit để trích xuất dữ liệu web Get Started Free
Câu hỏi thường gặp
Vì sao import nodriver bị lỗi trên Python 3.14?
Vì cdp/network.py chứa một byte ± không phải UTF-8 mà không có khai báo encoding nguồn. Python 3.14.2 từ chối file này và làm dừng luôn import dây chuyền; Python 3.12.13 import được cùng file đó mà không vá. Bài này không khoanh vùng thay đổi cụ thể trong interpreter và cũng không thử Python 3.13. Chuỗi liên quan ở upstream là nodriver issue #35 và pull request #36. Hãy dùng phiên bản bạn đã kiểm tra, hoặc mã hóa lại file sang UTF-8.
“CDP-native, không dùng webdriver” mang lại gì, và chi phí cài đặt là bao nhiêu?
nodriver đóng gói 57 module domain DevTools Protocol và nói CDP qua WebSocket thay vì gọi chromedriver qua Selenium. Tab có 62 method, bao gồm XPath native. Metadata package khai báo ba yêu cầu runtime (websockets, mss, deprecated); khi giải quyết thì thêm wrapt. Môi trường đo dùng khoảng 17.2 MB trên sáu thư mục dist-info, bao gồm cả pip, không có numpy, lxml hay browser binary tải xuống. Hai lưu ý: bản đã vá mất khoảng ~158 ms để import vì cả 57 module CDP đều được load ngay, và vẫn cần một browser họ Chrome riêng biệt.
Giấy phép AGPL-3.0 có quan trọng với dự án của tôi không? Tùy cách bạn phát hành. AGPL-3.0 là giấy phép copyleft theo mạng: nếu bạn chạy một phiên bản đã sửa của nodriver như một dịch vụ cho người khác dùng, bạn có nghĩa vụ cung cấp cho họ mã nguồn đã sửa đó. Với một script cá nhân hoặc công cụ nội bộ không công khai, thường không phải vấn đề. Với một sản phẩm thương mại hosted xây trên nodriver đã vá, đây là câu hỏi cần đưa cho người chịu trách nhiệm compliance — và nó chặt hơn Apache-2.0 cùng MIT trên các công cụ tương đương.
nodriver tự lộ gì về mình — và điều đó có nghĩa là nó vượt qua Cloudflare không?
Phần đầu là số đo được; phần sau thì không, và sự khác nhau này rất quan trọng. Trên một trang tôi phục vụ từ 127.0.0.1, dùng cùng bản Chrome với các control: navigator.webdriver trả về false, trong khi Playwright gốc và Puppeteer gốc đều báo true. Giá trị này được đặt ở thời điểm launch browser, không phải do patch thuộc tính — descriptor vẫn là native getter của Chrome. Ngoài boolean đó, gần như mọi thứ khác đều khớp hệt control: cùng platform string, năm plugin, mười hai lõi, 16 GB bộ nhớ thiết bị báo cáo, cùng hình dạng window.chrome, không có mâu thuẫn Permissions API, và không có dấu vết kiểu cdc_ trên document hay window. Chế độ headless vẫn khai HeadlessChrome/151.0.0.0 trong user-agent, y như cả hai control — thứ đó không được che hộ bạn. Nhưng tất cả những điều đó không nói lên việc nó có hiệu quả trước một dịch vụ anti-bot thực hay không. Tôi không từng trỏ nodriver vào một site live, không từng chạm tới dịch vụ chống bot, và không từng đụng CAPTCHA; đó là phạm vi tôi cố ý loại trừ. Bảng disclosure ở trên chỉ cho biết stack tự khai gì. Nó không cho biết ai đang nghe, hoặc họ sẽ xử lý ra sao.
nodriver có xử lý nội dung render bằng JavaScript đúng không?
Có, với lưu ý quen thuộc về thời điểm đọc. Trên một fixture có ba lớp nội dung, lệnh mặc định browser.get() + tab.get_content() trả về 2 trên 3 — nó thực thi JavaScript đúng (class sync-injected không hề có trong byte được phục vụ nhưng vẫn xuất hiện), nhưng nó đọc ở thời điểm load event nên bỏ lỡ những thứ được chèn sau đó. Thêm tab.select("#delayed-injected") thì trả về 3 trên 3. Kết quả này giống hệt Playwright gốc và Puppeteer gốc trên cùng trang. Chỉ cần lưu ý một quirk: select() dùng polling với back-off nửa giây, nên chờ 100 ms cũng có thể tốn khoảng 1,1 giây.


