Scrapy – Cào dữ liệu phân trang

Rate this post

Trong phần này chúng ta sẽ tìm hiểu cách cào dữ liệu trên nhiều trang.

Trong các phần trước thì chúng ta chỉ để 1 URL bên trong biến start_urls. Để có thể cào nhiều trang thì chúng ta có thể hard-code tất cả các URL bên trong biến đó, chẳng hạn:

start_urls = (
    'https://phocode.com/scrapy/page0',
    'https://phocode.com/scrapy/page1',
    'https://phocode.com/scrapy/page2',
)

Tất nhiên hard-code là việc nên tránh làm trong phát triển phần mềm, thay vào đó chúng ta có thể lưu và đọc URL từ file:

start_urls = [i.strip() for i in open('phocode_urls.txt').readlines()]

Tuy nhiên cách này cũng không tốt thì vẫn còn hard-code bên trong file. Và thông thường các trang web sẽ hay có hệ thống phân trang, như của Amazon:

Các trang web thương mại điện tử thông thường hiển thị danh sách các sản phẩm trên nhiều trang, khi cào dữ liệu trên các trang này thì chúng ta muốn là phải cào hết. Khi cào dữ liệu trên 1 trang, chúng ta gọi là cào theo chiều dọc (Vertical Crawling), còn cào link qua trang kế tiếp thì gọi là cào theo chiều ngang (Horizontal Crawling).

Đoạn code XPath để lấy link tới trang tiếp theo khá dễ, chúng ta chỉ cần lấy XPath tới nút Next là được, chúng ta dùng chức năng Inspect của Chrome để xem element:

Hình trên là cấu trúc HTML của nút Next vào ngày 17.04.2020, ở thời điểm của bạn có thể trang này đã thay đổi. Tuy vậy thì đoạn XPath để lấy được đường link này như sau:

$ scrapy shell amazon.com/s?k=scrapy 
...
$ response.xpath('//*[contains(@class, "a-last")]//a//@href').extract()
['/scrapy/s?k=scrapy&page=2']

Tất nhiên là có nhiều cách viết khác nhau, miễn sau cúng ta lấy được đoạn URL là thành công. Tương tự chúng ta có thể lấy URL tới từng sản phẩm trên 1 trang bằng đoạn XPath sau:

//*[contains(@data-component-type, "s-product-image")]//a//@href

Cách nhanh nhất ở đây để kiểm tra xem đoạn XPath có đúng hay không là dùng hàm len() trong Python để xem mảng trả về có bao nhiêu phần tử. Hiện tại Amazon hiển thị 16 sản phẩm mỗi trang. Như vậy nếu đoạn code của chúng ta trả về 16 thì nó đúng.

$ result = response.xpath('//*[contains(@data-component-type, "s-product-image")]//a//@href')
$ len(result)
16

Cào dữ liệu 2 chiều bằng Spider

Bây giờ chúng ta sẽ tìm cách cào dữ liệu trên ở cả chiều ngang lẫn chiều dọc. Chúng ta sẽ không cào dữ liệu trên Amazon vì Amazon có hệ thống chống cào dữ liệu liên tục khá khó chịu nếu chúng ta không dùng một vài thủ thuật. Thay vào đó chúng ta sẽ cào trên trang http://quotes.toscrape.com/. ToScrape là trang web chứa dữ liệu mẫu để chúng ta có thể thực hành cào dữ liệu.

Chúng ta tạo 1 file bên trong thư mục spiders có tên manual.py như sau:

# -*- coding: utf-8 -*-
import scrapy
from scrapy import Request
from urllib import parse

class ManuelSpider(scrapy.Spider): 
        name = "manual"
        allowed_domains = ["web"]
        start_urls = ( 
            'http://quotes.toscrape.com/', 
        )

        def parse(self, response):
            next_selectors = response.xpath('//*[contains(@class, "next")]//a//@href')
            for url in next_selectors.extract(): 
                yield Request(parse.urljoin(response.url, url), 
                              callback=self.parse, 
                              dont_filter=True)

Đoạn code trên khá giống với đoạn code trong file basic.py. Ở đây chúng ta định nghĩa lớp ManualSpider với tên spider trong biến namemanual, Phương thức parse() ở đây không return về Item nào mà thay vào đó, chúng ta dùng response.xpath() với đoạn XPath lấy element của nút Next rồi lưu mảng trả về vào biến next_selectors. Sau đó chúng ta lặp qua mảng này, dùng phương thức extract() để lấy URL của nút Next, dùng thêm phương thức urljoin() từ thư viện parse để có được URL hoàn chỉnh rồi truyền vào một đối tượng Request. Đối tượng này sẽ có nhiệm vụ gửi HTTP request, và khi request được phản hồi thành công thì chúng ta gọi lại chính hàm parse(), vì tham số callback được truyền vào chính phương thức parse(), nếu bạn chưa biết callback là gì thì có thể tìm hiểu thêm trên mạng, mục đích ở đây là để Scrapy tiếp tục tìm element của nút Next trong các trang kế tiếp cho đến trang cuối, chúng ta sẽ lấy được tổng cộng 10 URL của trang web này.

Lưu ý là ở đây chúng ta dùng câu lệnh yield chứ không phải return. Nếu bạn chưa biết thì yield có tác dụng giống như return, chỉ khác là yield không dừng phương thức lại như return. Chúng ta dùng yield là bởi vì nếu có nhiều nút Next (mà thực chất thì chỉ có 1) thì chúng ta vẫn muốn gửi Request tới tất cả nút Next đó, trong khi phương thức parse() thì luôn yêu cầu phải trả về một đối tượng Request hoặc Item. Chạy scrapycrawl manual để xem kết quả:

D:ex> scrapy crawl manual
...
DEBUG: Crawled (200) <GET http://quote.toscrape.com/> (referer: None)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/2> (referer: http://quote.toscrape.com/)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/3> (referer: http://quote.toscrape.com/page/2)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/4> (referer: http://quote.toscrape.com/page/3)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/5> (referer: http://quote.toscrape.com/page/4)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/6> (referer: http://quote.toscrape.com/page/5)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/7> (referer: http://quote.toscrape.com/page/6)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/8> (referer: http://quote.toscrape.com/page/7)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/9> (referer: http://quote.toscrape.com/page/8)
DEBUG: Crawled (200) <GET http://quote.toscrape.com/page/10> (referer: http://quote.toscrape.com/page/9)
INFO: Closing spider (finished)
....

Một lưu ý nữa là tham số dont_filter=True, tham số này mang nghĩa là vẫn cào dữ liệu trên các URL trùng nhau. Mặc định tham số này là False, và nếu không gán bằng True thì có thể xảy ra trường hợp Scrapy không gửi Request.

Đó là phần cào theo chiều ngang. Bây giờ chúng ta sửa file manual.py tiếp như sau:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.loader import ItemLoader
from ex.items import ExItem
from scrapy import Request
from urllib import parse

class ManuelSpider(scrapy.Spider): 
    name = "manual"
    allowed_domains = ["web"]
    start_urls = ( 
        'http://quotes.toscrape.com/', 
    )
    total_scraped = 0

    def parse(self, response):
        next_selectors = response.xpath('//*[contains(@class, "next")]//a//@href')
        for url in next_selectors.extract(): 
            yield Request(parse.urljoin(response.url, url), 
                          callback=self.parse, 
                          dont_filter=True) 

        item_selectors = response.xpath('//*[contains(@class, "quote")]//span//a//@href') 
        for url in item_selectors.extract():
            yield Request(parse.urljoin(response.url, url), 
                          callback=self.getItem, 
                          dont_filter=True)

    def getItem(self, response): 
        ld = ItemLoader(item=ExItem(), response=response) 
        ld.add_xpath('author', '//*[contains(@class, "author-title")]//text()')
        self.total_scraped = self.total_scraped + 1
        print('Total scrapes:', self.total_scraped)
        return ld.load_item()

Đoạn code trên chúng ta thêm phần lấy XPath cho đường link đến trang chứa thông tin về Author, và sau mỗi làn yield Request thì sẽ gọi phương thức callback ở đây là getItem(), trong phương thức này chúng ta lấy đoạn text tên tác giả (dựa theo class author-title).

Vì số lượng output request và XPath cũng tương đối lớn nên để kiểm tra thì ở đây chúng ta có biến total_scraped dùng để đếm số lượng author-title đã được cào. Trang quotes.toscrape này có tổng cộng 10 trang với 10 author-title trên mỗi trang, nên tổng số lượng author-title có thể cào được là 100. Nếu sau khi chạy scrapy crawl manual mà ra được 100 thì đoạn code chạy thành công.

Khi cào dữ liệu trên các trang web lớn, chẳng hạn như Amazon, số lượng trang có thể lên đến hàng nghìn, nhưng nếu bạn không muốn cào tất cả thì có thể dùng tham số -s CLOSESPIDER_ITEMCOUNT như sau:

D:\ex> scrapy crawl manual -s CLOSESPIDER_ITEMCOUNT=20
...
Total scrapes: 20

Trong đó chúng ta truyền vào 20 và scrapy sẽ tự động dừng cào khi đã cào được 20 Item.

0 0 votes
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments