XPath so với CSS Selectors: Nên Sử Dụng Cái Nào cho Web Scraping
Advanced Data Extraction Specialist
TL;DR:
- CSS và XPath trả về kết quả giống nhau cho việc trích xuất thông thường: cùng mười hai tiêu đề sách đã trở lại từ
article.product_pod h3 a::attr(title)và//article[@class='product_pod']/h3/a/@titletrên cùng một trang. - XPath là ngôn ngữ duy nhất trong hai ngôn ngữ có thể di chuyển lên cây.
parent::,ancestor::,preceding-sibling::vàfollowing-sibling::mỗi lần khớp 20 nút trên trang đó; CSS không có tương đương cho bất kỳ nút nào trong số đó. - "CSS nhanh hơn" là một sự thật về trình duyệt, không phải sự thật về Python. Trong parsel, việc trích xuất tương tự tốn 487,1 ms bằng CSS so với 309,1 ms bằng XPath qua 2.000 lần lặp, vì cssselect biên dịch mỗi truy vấn CSS thành XPath trước khi thực thi.
a:contains('Sharp')trả về một kết quả trong parsel và némSyntaxErrorvào một trang Chrome thực tế, đó là lý do tại sao một bộ chọn có thể hoạt động trong một trình thu thập dữ liệu và thất bại trong DevTools.- Cả hai ngôn ngữ đều không quan trọng khi đánh dấu không bao giờ đến; một bộ chọn chỉ có thể truy vấn một DOM thực sự được xây dựng.
- Cả hai ngôn ngữ đều đọc các byte mà lớp lấy dữ liệu của bạn đã giải mã, vì vậy một trang được phục vụ mà không có charset có thể trả về
£47.82trong khi trình duyệt hiển thị£47.82. - Chạy bất kỳ cú pháp nào trên các trang đã được hiển thị đầy đủ trên Kế hoạch miễn phí Scrapeless.
Mở trình kiểm tra trên bất kỳ danh sách sản phẩm nào, sao chép bộ chọn mà Chrome cung cấp cho bạn, dán nó vào một trình thu thập dữ liệu Python và nó thường chạy tốt. Những trường hợp thú vị là những trường hợp mà nó không hoạt động — nơi bộ chọn hợp lệ trong một engine và có lỗi cú pháp trong một engine khác, hoặc nơi CSS gọn gàng mà bạn viết hóa ra lại chậm hơn XPath mà bạn đã tránh.
Hai ngôn ngữ này có sự chồng chéo lớn, vì vậy sự so sánh hữu ích nằm ở các rìa: cái mà mỗi ngôn ngữ có thể biểu đạt, cái mà mỗi ngôn ngữ tốn kém, và engine nào thực sự thực thi truy vấn.
Mỗi phép đo bên dưới đến từ một trang công khai được xây dựng cho việc thực hành thu thập dữ liệu — một danh sách danh mục gồm hai mươi cuốn sách trên books.toscrape.com — được phân tích bằng lxml 6.0.2, cssselect 1.3.0, và parsel 1.10.0.
Cùng Một Truy Vấn Trong Cả Hai Ngôn Ngữ
Đối với các mục tiêu phổ biến, hai ngôn ngữ là một sự dịch thẳng từ nhau. Bảng bên dưới kết cặp các truy vấn chọn các tập hợp nút giống hệt nhau trên trang kiểm tra.
| Mục tiêu | CSS | XPath |
|---|---|---|
| Bất kỳ thẻ nào | article |
//article |
| Lớp | .product_pod |
//*[contains(concat(' ',normalize-space(@class),' '),' product_pod ')] |
| ID | #messages |
//*[@id='messages'] |
| Hậu duệ | article h3 a |
//article//h3//a |
| Con trực tiếp | div > span |
//div/span |
| Giá trị thuộc tính | a[title] |
//a[@title] |
| Con thứ N | li:nth-child(2) |
//li[2] |
| Văn bản thuộc tính | a::attr(href) |
//a/@href |
Ba cặp từ bảng đó, chạy trên trang trực tiếp, trả về các tập hợp nút khớp nhau:
python
from parsel import Selector
import requests
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
response = requests.get(url, timeout=30)
response.raise_for_status()
sel = Selector(text=response.content.decode("utf-8"))
pairs = [
("article.product_pod h3 a::attr(href)", "//article[@class='product_pod']/h3/a/@href"),
("p.star-rating::attr(class)", "//p[contains(@class,'star-rating')]/@class"),
("div.image_container img::attr(alt)", "//div[@class='image_container']//img/@alt"),
]
for css_query, xpath_query in pairs:
css_hits = sel.css(css_query).getall()
xpath_hits = sel.xpath(xpath_query).getall()
print(len(css_hits), len(xpath_hits), css_hits == xpath_hits)
Mỗi cặp in 20 20 True. Khi cả hai ngôn ngữ có thể biểu đạt mục tiêu, sự lựa chọn là khả năng đọc, không phải khả năng.
Cái Gì Chỉ XPath Có Thể Biểu Đạt
CSS chọn theo chiều đi xuống. Nó di chuyển từ tổ tiên đến hậu duệ và không bao giờ quay lại, vì vậy một quy tắc có thể nói "đơn giá bên trong thẻ này" nhưng không thể nói "thẻ chứa đơn giá này."
XPath mang theo các trục, và bốn trong số đó không có tương đương CSS nào. Mỗi cái khớp 20 nút trên trang kiểm tra:
python
axes = [
("parent::", "//p[@class='price_color']/parent::div/@class"),
("ancestor::", "//h3/ancestor::article/@class"),
("preceding-sibling::", "//div[@class='product_price']/preceding-sibling::h3/a/@title"),
("following-sibling::", "//h3/following-sibling::div[@class='product_price']//p[@class='price_color']/text()"),
]
for label, query in axes:
hits = sel.xpath(query).getall()
print(f"{label:20s} {len(hits):2d} hits {hits[0]!r}")
text
parent:: 20 hits 'product_price'
ancestor:: 20 hits 'product_pod'
preceding-sibling:: 20 hits 'Sharp Objects'
following-sibling:: 20 hits '£47.82'
Cái cuối cùng là mẫu cần giữ lại. "Tìm tiêu đề, sau đó lấy giá mà theo sau nó" là một mối quan hệ anh chị em, và biểu đạt nó bằng CSS có nghĩa là chọn giá riêng biệt và kết hợp lại hai danh sách bằng chỉ số — điều này âm thầm tạo ra các cặp sai ngay khi một thẻ không có giá.
Tiêu chuẩn W3C Selectors Level 4 định nghĩa ngữ pháp CSS, và việc đi lên không có trong đó theo thiết kế: ngôn ngữ được xây dựng để định dạng, nơi một bộ xử lý giải quyết quy tắc từ trên xuống. Khuyến nghị XPath 1.0 định nghĩa mười ba trục vì nó được xây dựng để định địa chỉ các nút tùy ý trong cây tài liệu.
Sự Phân Chia :contains
Việc khớp văn bản là nơi hai ngôn ngữ phân kỳ theo cách tạo ra các báo cáo lỗi gây nhầm lẫn.
XPath luôn có contains(). CSS đã có một :contains() pseudo-class trong một bản thảo sớm và nó đã bị loại bỏ trước khi tiêu chuẩn ổn định. Vấn đề là cssselect của Python vẫn triển khai nó.
python
print(len(sel.css("a:contains('Sharp')").getall())) # 1
print(len(sel.xpath("//a[contains(., 'Sharp')]").getall())) # 1
Cả hai in 1. Bây giờ hai truy vấn giống nhau bên trong một trang trình duyệt thực tế, được đánh giá thông qua các engine của DOM:
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
JS = """() => {
const out = {};
try { out.css_class = document.querySelectorAll('article.product_pod h3 a').length; }
catch (e) { out.css_class = 'ERR ' + e.name; }
try { out.css_contains = document.querySelectorAll("a:contains('Sharp')").length; }
catch (e) { out.css_contains = 'ERR ' + e.name + ': ' + e.message.slice(0, 60); }
const xp = (q) => document.evaluate(q, document, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;
out.xpath_class = xp("//article[@class='product_pod']/h3/a");
out.xpath_contains = xp("//a[contains(., 'Sharp')]");
out.xpath_parent = xp("//p[@class='price_color']/parent::div");
out.xpath_ancestor = xp("//h3/ancestor::article");
return out;
}"""
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
endpoint = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
"token": os.environ["SCRAPELESS_API_KEY"], "sessionTTL": 300, "proxyCountry": "US",
})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(endpoint)
page = browser.new_page()
page.goto(url, wait_until="domcontentloaded")
for key, value in page.evaluate(JS).items():
print(f"{key:16s} {value}")
browser.close()
text
css_class 20
css_contains ERR SyntaxError: Failed to execute 'querySelectorAll' on 'Document': 'a:conta
xpath_class 20
xpath_contains 1
xpath_parent 20
xpath_ancestor 20
Việc khớp văn bản CSS gặp lỗi. Việc khớp văn bản XPath trả về một nút của nó. Vì vậy, một bộ chọn mà vượt qua các bài kiểm tra trong một trình thu thập dữ liệu Python sẽ gây ra lỗi cú pháp ngay khi ai đó dán nó vào DevTools để kiểm tra và ngược lại với bất kỳ ai xác thực bộ chọn trong bảng điều khiển trước khi gửi chúng.
Hai điều cần lưu ý từ lần chạy đó. XPath của trình duyệt không bị suy giảm: parent:: và ancestor:: mỗi cái đã giải quyết 20 nút thông qua giao diện document.evaluate của DOM. Và :contains là một mở rộng thư viện chứ không phải là một tính năng ngôn ngữ, vì vậy nó chỉ đi xa như thư viện đó.
Nếu một bộ chọn phải hoạt động ở cả hai nơi, //a[contains(., 'Sharp')] là dạng di động.
Cái Nào Thực Sự Nhanh Hơn
Câu trả lời phổ biến là CSS nhanh hơn. Điều này đúng trong trình duyệt, nơi querySelectorAll là một con đường nhanh gốc, và nó là ngược lại trong Python.
Trong lxml không có động cơ CSS. Mỗi truy vấn CSS được dịch sang XPath bởi cssselect và động cơ XPath thực hiện nó, mà tài liệu về bộ chọn Scrapy tuyên bố trực tiếp. Việc viết CSS tiêu tốn bước dịch thuật.
python
import time
N = 2000
start = time.perf_counter()
for _ in range(N):
sel.css("article.product_pod h3 a::attr(title)").getall()
css_ms = (time.perf_counter() - start) * 1000
start = time.perf_counter()
for _ in range(N):
sel.xpath("//article[@class='product_pod']/h3/a/@title").getall()
xpath_ms = (time.perf_counter() - start) * 1000
print(f"CSS {css_ms:8.1f} ms ({css_ms / N * 1000:6.1f} us/call)")
print(f"XPath {xpath_ms:8.1f} ms ({xpath_ms / N * 1000:6.1f} us/call)")
text
CSS 487.1 ms ( 243.5 us/call)
XPath 309.1 ms ( 154.5 us/call)
CSS tốn 1,58 lần thời gian XPath cho đầu ra giống hệt. In ra dịch thuật cho thấy nó đi đâu:
python
from cssselect import GenericTranslator
print(GenericTranslator().css_to_xpath("article.product_pod h3 a"))
text
descendant-or-self::article[@class and contains(concat(' ', normalize-space(@class), ' '), ' product_pod ')]/descendant-or-self::*/h3/descendant-or-self::*/a
//article[@class='product_pod']/h3/a viết tay so sánh một thuộc tính. Dạng được tạo ra chuẩn hóa khoảng trắng và thêm vào danh sách lớp trên mỗi nút ứng cử viên, vì nó phải chính xác cho các phần tử mang theo nhiều lớp. Quy tắc phòng thủ đó là 1,58x.
Đặt điều đó vào tỷ lệ trước khi tối ưu hóa cho nó: khoảng cách gần như là 89 microgiây cho mỗi lần gọi trên một trang 50 KB. Một yêu cầu HTTP đơn lẻ tốn hàng nghìn lần nhiều hơn. Ngôn ngữ bộ chọn không phải là nơi thời gian của trình thu thập dữ liệu đi, và khả năng đọc thường là giao dịch tốt hơn — con số chỉ quan trọng trong một vòng lặp phân tích nóng qua HTML đã được lưu trữ.
Cái Bẫy Cả Hai Ngôn Ngữ Chia Sẻ
Một bộ chọn trả lại bất cứ điều gì mà lớp lấy dữ liệu của bạn giải mã. Khi một máy chủ gửi text/html mà không có charset, các yêu cầu lùi lại về ISO-8859-1 theo các quy tắc loại truyền thông của tài liệu đặc tả ngữ nghĩa HTTP, trong khi các byte trên đường truyền là UTF-8:
python
import requests
from parsel import Selector
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
response = requests.get(url, timeout=30)
response.raise_for_status()
print(response.headers.get("content-type"))
print(response.encoding)
print(response.apparent_encoding)
print(Selector(text=response.text).css("p.price_color::text").get())
print(Selector(text=response.content.decode("utf-8")).css("p.price_color::text").get())
text
text/html
ISO-8859-1
utf-8
'£47.82'
'£47.82'
Bộ chọn đã đúng cả hai lần. Giải mã response.content một cách rõ ràng, thay vì tin tưởng response.text, chính là điều làm cho giá trị được trích xuất có thể sử dụng — và không có sự thay đổi nào của ngôn ngữ bộ chọn có thể sửa chữa nó.
Kiểm tra một bộ chọn trên một trang được hiển thị ở phía client cần một trình duyệt thực. Kế hoạch miễn phí Scrapeless bao gồm đủ phiên để thử cả hai cú pháp trên một DOM trực tiếp.
Nơi Không Có Ngôn Ngữ Nào Giúp Đỡ
Cả hai ngôn ngữ truy vấn một DOM. Không có ngôn ngữ nào xây dựng một cái.
Khi một danh sách được hiển thị ở phía client, HTML mà một khách hàng HTTP thông thường nhận được chứa shell chứ không phải bản ghi, vì vậy một bộ chọn chính xác trả lại không có nút và trông giống như một lỗi của bộ chọn. Cách sửa chữa nằm ở phía trên của bộ chọn: hãy hiển thị trang trước, sau đó truy vấn nó. Trình duyệt thu thập dữ liệu Scrapeless cung cấp một trình duyệt đám mây qua CDP, vì vậy trang mà trình duyệt biên soạn chính là trang mà bộ chọn của bạn chạy — đó chính xác là cách mà các số liệu trong trình duyệt ở trên được ghi lại.
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
cdp_endpoint = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
"token": os.environ["SCRAPELESS_API_KEY"],
"sessionTTL": 300,
"proxyCountry": "US",
})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp_endpoint)
page = browser.new_page()
page.goto(url, wait_until="domcontentloaded")
titles = page.eval_on_selector_all(
"article.product_pod h3 a", "els => els.map(e => e.title)"
)
print(len(titles), titles[0])
browser.close()
text
20 Sharp Objects
Khi DOM đã tồn tại, câu hỏi của bộ chọn lại trở thành một câu hỏi về kiểu dáng. Để tìm hiểu về lớp lựa chọn tự nó, hướng dẫn parsel của chúng tôi bao gồm cả hai phương ngữ trên một cây dựa trên lxml, và giá cả liệt kê những gì một phiên hiển thị tốn.
Chọn CSS Khi Nào, Chọn XPath Khi Nào
| Tình Huống | Chọn |
|---|---|
| Lớp, id, thuộc tính hoặc phù hợp với hướng kế thừa | CSS |
| Bộ chọn được chia sẻ với các đồng đội phía trước | CSS |
Quy tắc cũng phải chạy trong DevTools hoặc querySelectorAll |
CSS, hoặc XPath di động |
| Chọn một container theo những gì nó chứa | XPath |
| Ghép một nhãn với giá trị theo sau nó | XPath |
| Phù hợp với văn bản hiển thị | XPath |
| Đi lên một tổ tiên | XPath |
| Phân tích XML, RSS, hoặc sơ đồ trang web | XPath |
| Một vòng lặp nóng qua HTML đã lưu trong lxml | XPath |
Chọn CSS khi mục tiêu có thể truy cập từ bên dưới và truy vấn sẽ được đọc bởi những người viết bảng kiểu. Nó ngắn hơn, và trên trang thử nghiệm, nó đã chọn mọi trường thông thường mà không bị mất mát.
Chọn XPath khi mối quan hệ bạn cần là cấu trúc hơn là phân cấp — lên cây, ngang anh chị em, hoặc khóa trên văn bản. Nó cũng là mặc định trung thực bên trong lxml và parsel, nơi nó là thứ thực sự thực hiện.
Hầu hết các trình trích xuất (scrapers) hoạt động trộn hỗn hợp hai loại theo từng trường thay vì chọn một bên, và parsel chấp nhận cả hai theo cùng một cây DOM, vì vậy quyết định là theo từng bộ chọn thay vì theo từng dự án.
Kết luận
CSS và XPath trả lời cùng một câu hỏi cho các trường hợp thông thường, và hai mươi tiêu đề đã trở lại giống hệt nhau từ cả hai. Những khác biệt quan trọng hơn hẹp hơn so với khung thông thường mà chúng ta thường thấy: XPath có thể đi lên và so khớp văn bản, CSS thì không; CSS nhanh hơn trong trình duyệt và chậm hơn trong lxml, nơi mà nó biên dịch sang XPath trước; và :contains hoạt động trên một engine trong khi không hoạt động trên engine khác, đó là nguồn gốc của hầu hết sự nhầm lẫn "hoạt động trên trình trích xuất của tôi, không hoạt động trên bảng điều khiển".
Chọn theo bộ chọn, giữ hình thức có thể di chuyển khi một truy vấn cần chạy ở cả hai nơi, và giải mã phản hồi trước khi đổ lỗi cho bất kỳ ngôn ngữ nào vì một ký tự bị sai lệch.
Sẵn sàng để kiểm tra các bộ chọn chống lại các trang mà render trước khi bạn truy vấn chúng chưa? Bắt đầu với gói miễn phí Scrapeless và chỉ định bất kỳ cú pháp nào vào một DOM trực tiếp.
Câu hỏi thường gặp
Q: XPath có nhanh hơn bộ chọn CSS không?
Điều đó phụ thuộc vào engine. Trong lxml và parsel, XPath nhanh hơn vì CSS được biên dịch thành XPath trước khi thực thi — khoảng cách đo được ở đây là 309.1 ms so với 487.1 ms trên 2,000 lần lặp lại cùng một lần trích xuất. Trong trình duyệt, querySelectorAll là một đường dẫn nhanh gốc và CSS thắng. Dù sao đi nữa, sự khác biệt là trong micro giây mỗi lần gọi, thấp hơn nhiều so với chi phí của yêu cầu HTTP.
Q: Có thể bộ chọn CSS so khớp văn bản phần tử không?
Không trong trình duyệt. document.querySelectorAll("a:contains('x')") ném ra một SyntaxError, vì :contains() đã bị loại bỏ trước khi đặc tả Selectors được ổn định. Thư viện cssselect của Python vẫn triển khai nó, vì vậy truy vấn giống nhau hoạt động trong parsel. Đối với một bộ chọn có hành vi tương tự ở cả hai, sử dụng //a[contains(., 'x')].
Q: Có thể bộ chọn CSS chọn một phần tử cha không?
Không. CSS chỉ chọn hướng xuống, vì vậy không có parent:: hoặc ancestor:: tương đương. XPath xử lý cả hai, và trên trang kiểm tra //h3/ancestor::article đã so khớp tất cả 20 thẻ. Lớp giả :has() cho phép bạn lọc một phần tử theo các phần tử con của nó, điều này bao phủ một phần cùng ý định, nhưng nó vẫn trả về phần tử bên ngoài thay vì đi lên từ một phần tử bên trong.
Q: Tại sao bộ chọn của tôi hoạt động trong DevTools nhưng không trả lại gì trong Python?
Có hai nguyên nhân thông thường. Trang web render nội dung của nó bằng JavaScript, vì vậy HTML mà client HTTP của bạn nhận được không bao giờ chứa các node mà trình duyệt sau này xây dựng — bộ chọn là đúng và DOM không có ở đó. Hoặc bộ chọn sử dụng một tiện ích mở rộng chỉ dành cho trình duyệt hoặc thư viện. So sánh thân phản hồi thô với những gì trình kiểm tra hiển thị trước khi thay đổi bộ chọn.
Q: Tôi nên sử dụng CSS hay XPath trong Scrapy?
Cả hai, theo từng trường. Scrapy phơi bày response.css() và response.xpath() qua cùng một bộ chọn parsel, và chúng có thể được kết hợp với nhau. Sử dụng CSS cho các sự so khớp lớp và thuộc tính đơn giản, và chuyển sang XPath cho việc so khớp văn bản hoặc đi lên thay vì uốn cong một quy tắc CSS cho vừa.
Q: Bộ chọn XPath có hoạt động trên tất cả các trình duyệt không?
Có, thông qua document.evaluate thay vì querySelectorAll. Đây là một giao diện DOM riêng biệt, và các trục được hỗ trợ hoàn toàn — parent:: và ancestor:: mỗi cái đã trả về 20 node trong lần chạy ở trên. Tiện ích $x() trong Chrome DevTools bọc cùng một giao diện cho việc sử dụng trong bảng điều khiể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.



