Daily Archives: 09/04/2020

Scrapy – Ngôn ngữ XPath

Nếu bạn “xuất thân” là một coder truyền thống, thì nhiều khả năng là bạn chưa biết gì về XPath, trích xuất dữ liệu từ một website là công việc đòi hỏi các kĩ năng so sánh chuỗi, tìm các thẻ, xử lí các kí tự…v.v thậm chí là xử lý một cây HTML để lấy cho được cái mà bạn cần. Nhưng thực tế thì bạn không cần làm những việc phức tạp đó. Bạn có thể làm tất cả mọi thứ đó với một ngôn ngữ tên là XPath. 

Bạn có thể vào Console của Google Chrome và thử tính năng $x của XPath. Chẳng hạn bây giờ bạn vào trang example.com và gõ đoạn code $x('//h1')  vào console và xem kết quả:

Console sẽ trả về cho bạn một mảng trong Javascript chứa các phần tử HTML h1 tương ứng đã được lấy ra.

Một số cú pháp XPath hữu dụng

Cấu trúc HTML luôn luôn bắt đầu với <html>, chúng ta có thể kết hợp tên phần tử với dấu slash / để lấy phần tử mong muốn. Ví dụ (khi chạy trên trang example.com):

$x('/html') 
Kết quả: [ html ]
$x('/html/body') Kết quả: [ body ]
$x('/html/body/div') Kết quả: [ div ]
$x('/html/body/div/h1') Kết quả: [ h1 ]
$x('/html/body/div/p') Kết quả: [ p, p ] $x('/html/body/div/p[1]') Kết quả: [ p ] $x('/html/body/div/p[2]') Kết quả: [ p ]

$x('/html/head/title')
Kết quả: [ title ]

Chú ý là bên trong element <div> có 2 element <p>, do đó cú pháp html/body/div/p trả về mảng chứa 2 phần tử <p>. Chúng ta có thể dùng có pháp p[1]p[2] để lấy ra chỉ duy nhất element <p> tương ứng.

Với những trang web lớn thông thường thì để trích xuất một element có thể phải viết đoạn XPath rất dài. Tuy nhiên chúng ta có thể chèn 2 dấu // ở đầu đoạn code để lấy tất cả các phần tử tương ứng mà không cần biết cấu trúc của chúng. Ví dụ. //p lấy tất cả element <p>, //a thì sẽ lấy tất cả element <a>.

$x('//p')
Kết quả: [ p, p ]

$x('//a')
Kết quả: [ a ]

2 dấu /X/ cũng có thể được viết ở các element phía sau. Ví dụ //div//a sẽ lấy tất cả element <a> mà nằm trong element <div>.Lưu ý là //div/a (1 dấu / trước a)  sẽ trả về mảng rỗng. Bởi vì trong trang example.com thì không có element <a> nào nằm ngay trong element <div>.

$x('//div//a')
Kết quả: [ a ]

$x('//div/a')
Kết quả: [ ]

Chúng ta cũng có thể lấy thuộc tính của element. Ví dụ:

$x('//a/@href')
Kết quả: [ href ]

Chúng ta có thể dùng dấu * để lấy tất cả các element con trong một element. Ví dụ:

$x('//div/*') 
Kết quả: [h1, p, p]

Chúng ta có thể trích xuất những element chỉ mang theo nó những attribute nhất định. Chẳng hạn @class, @id... hay những attribute nào mang một giá trị nào đó. Ví dụ //a[@href] sẽ trả về những element <a> có attribute href bên trong. Hoặc //a[@href="https://www.iana.org/domains/example"] sẽ trả về chỉ những element <a> có attribute href có giá trị là đường dẫn như trên.

$x('//a[@href="https://www.iana.org/domains/example"]')
Kết quả: [ a ]

Một kiểu cú pháp nữa là tìm các đường link có chứa một chuỗi nào đó, ví dụ:

Tìm element <a> href chứa có chuỗi iana
$x('//a[contains(@href, "iana")]')

Tìm element <a> có href bắt đầu với http://www.
$x('//a[starts-with(@href, "http://www.")]')

Tìm element <a> có href không chứa chuỗi abc
$x('//a[not(contains(@href, "abc"))]')

not,starts-with,contains()… được gọi là các hàm. ngoài 3 hàm đó thì XPath còn rất nhiều hàm khác. Nhưng chúng ta sẽ tìm hiểu thêm sau.

Dùng XPath với Scrapy

Chúng ta mở console lên và gõ scrapy shell <đường_dẫn_trang_web> để bắt đầu trích xuất dữ liệu từ trang đó. Ví dụ:

>>> scrapy shell example.com

Từ đây chúng ta có thể sử dụng nhiều loại biến toàn cục mà Scrapy cung cấp để truy xuất dữ liệu. Một trong số đó là biến response thuộc lớp HtmlResponse, lớp này có phương thức xpath() dùng để lấy dữ liệu theo cú pháp XPath. Một số ví dụ:

>>> response.xpath('/html').extract()
['<html>...</html>']
>>> response.xpath('/html/body/div/h1').extract()
['<h1>Example Domain</h1>']
>>> response.xpath('/html/body/div/p').extract()
['<p>This domain is...</p>']
>>> response.xpath('//html/head/title').extract()
['<title>Example Domain</title>']
>>> response.xpath('//a').extract()
['<a href="https://www.iana.org/domains/examples">More information...</a>']
>>> response.xpath('//a/@href').extract() 
['http://www.iana.org/domains/example'] 
>>> response.xpath('//a/text()').extract() 
['More information…']

Bạn có thể test thử code XPath trong Chrome Console, sau đó chạy thử trong Scrapy.

Dùng Chrome để lấy XPath

Chúng ta có thể vào Console của Chrome để copy đoạn XPath như trong hình dưới đây:

Xử lý những thay đổi của website

Các trang web đôi khi hay thay đổi giao diện dù ít hay nhiều, và điều đó sẽ làm cho các đoạn XPath của chúng ta bị sai. Do đó chúng ta cần liên tục cập nhật lại XPath cho đúng, thông thường việc này cũng không tốn nhiều thời gian. Tuy nhiên có một số tip mà chúng ta nên để ý để tránh kéo dài thời gian thay đổi:

  • Không hard-code số thứ tự trong mảng. Ví dụ nếu chúng ta lấy XPath từ Chrome thì thường là Chrome sẽ trả về rất nhiều chỉ số mảng như:
//*[@id="myid"]/div/div/div[1]/div[2]/div/div[1]/div[1]/a/img
  • Và chỉ cần trang web thêm một thẻ <div> vào đầu là chúng ta phải cập nhật lại hết các chỉ số trên. Để giải quyết việc này thì chúng ta nên lấy XPath càng gần với thẻ <img> cuối càng tốt. Ví dụ:
//div[@class="thumbnail"]/a/img
  • Tên class hướng dữ liệu sẽ có ích hơn các class hướng giao diện. Chẳng hạn như nhắm đến class thumbnail sẽ tốt hơn là nhắm đến class green,  nhưng chúng vẫn kém hiệu quả hơn class như container. Hai class đầu tiên chủ yếu dùng để tùy chỉnh giao diện web, trong khi cái cuối cùng thì lại có í nghĩa về mặt thiết kế hơn, và do đó ít bị thay đổi hơn.
  • ID thường là thứ đáng tin cậy nhất nên tốt nhất là thường xuyên sử dụng chúng để trích xuất dữ liệu. Ví dụ:
//*[@id="more_info"]//text()

Lưu ý là tránh các id do website tự tạo ra. Chẳng hạn //[@id=”order-F498232″] vì chúng không cố định.