Author Archives: Phở Code

Scrapy – Cấu hình Scrapy P1

Trong phần này chúng ta sẽ tìm hiểu một số tham số cấu hình Scrapy.

Các cấp độ cấu hình

Scrapy cho phép chúng ta chỉnh sửa các thiết lập ở 4 cấp độ, mức độ càng cao thì độ ưu tiên càng lớn.

Cấp độ đầu tiên là các thiết lập mặc định trong file scrapy/settings/default_settings.py. Và đây là nơi mà chúng ta thường sẽ không để í tới, nếu không muốn nói là đừng đụng vào.

Cấp độ tiếp theo là ở file settings.py trong thư mục của mỗi project khi chúng tạo một project. Thông thường chúng ta sẽ hay chỉnh sửa ở đây tùy vào nhu cầu của mỗi project.

Cấp độ thứ 3 nằm trong phần định nghĩa thuộc tính custom_settings của mỗi lớp Spider được tạo ra.

Cấp độ cuối cùng là thiết lập ở phần command khi chạy các lệnh trên Console như chúng ta đã từng làm trong các phần trước.

Ở đây chúng ta sẽ chủ yếu tìm hiểu một số thiết lập thường dùng.

CONCURRENT_REQUEST

Đây là tham số quy định số lượng request tối đa mà Scrapy được phép gửi cùng một lúc.

Chúng ta có thể dùng lệnh scrapy settings để xem tham số này như sau:

$ scrapy settings --get CONCURRENT_REQUESTS
16

Tham số này được quy định mặc định trong Scrapy là 16. Tuy nhiên tham số này cũng được định nghĩa riêng trong mỗi project ở file settings.py, và thông thường là 32 request. Tuy nhiên tham số này được nằm trong comment trong khi tạo project, do đó Scrapy vẫn thường lấy giá trị 16 mặc định. Chúng ta có thể uncomment dòng CONCURRENT_REQUESTS trong file này của project nào đó để Scrapy sử dụng tham số này cho riêng project đó.

...
CONCURRENT_REQUESTS = 32
...

Chúng ta có thể sửa lại thành bất cứ con số nào mà chúng ta muốn, và phải chạy lệnh scrapy settings trong thư mục của project thì mới thấy tham số này của riêng project đó.

Ngoài ra chúng ta có thể sửa tham số bằng này dòng lệnh như sau:

$ scrapy settings --get CONCURRENT_REQUESTS -s CONCURRENT_REQUESTS=9
19

Lưu ý là việc chỉnh sửa qua dòng lệnh chỉ có tác dụng khi chạy lệnh đó, sau đó mọi thứ sẽ trờ lại như cũ.

Cấu hình logging

LOG_LEVEL

Khi cào dữ liệu từ các spider thì Scrapy thường output ra rất nhiều thứ, nhưng chúng đều có các mức độ từ thấp tới cao, theo thức tự DEBUG > INFO > WARNING > ERROR > CRITICAL, tùy vào loại output đó nằm ở mức độ nào mà nó có được in ra hay không.

Để thiết lập cấp độ Log thì chúng ta thêm dòng sau vào file settings.py:

LOG_ LEVEL = 'INFO'

Lưu ý là các output ở cấp độ cao hơn vẫn được in ra, ví dụ ở đây chúng ta thiết lập là INFO, tức là chỉ có những output thuộc DEBUG là không được in ra, còn lại từ INFO -> CRITICAL đều vẫn được in.

LOG_ENABLED

Tham số này quy định Scrapy có output mọi thứ hay không.

...
LOG_ENABLED = True/False
...

LOG_STDOUT

Tham số này cho biết có đưa những thứ từ các hàm in chuỗi ra không, chẳng hạn như hàm print().

...
LOG_STDOUT = True/False
...

Cấu hình hiệu suất

Việc cấu hình hiệu suất làm việc của Scrapy sẽ được bàn kĩ hơn trong các phần sau, ở đây mình chỉ nói sơ qua. Một tham số có liên quan đến hiệu suất đã được nói ở trên là CONCURRENT_REQUESTS, nhằm quy định số lượng request được gửi đi tại một thời điểm. Tham số này cũng có thể được dùng để bảo vệ server của chúng ta khi chúng ta cào quá nhiều dữ liệu trên nhiều website cùng một lúc.

Ngoài ra còn có 2 tham số là CONCURRENT_REQUESTS_PER_DOMAINCONCURRENT_REQUESTS_PER_IP quy định số lượng requets tối đa được gửi đi tại một thời điểm trên mỗi tên miền hoặc địa chỉ IP. Nhưng nếu chúng ta thiết lập cho tham số CONCURRENT_REQUESTS_PER_IP thì Scrapy sẽ không quan tâm đến tham số CONCURRENT_REQUESTS_PER_DOMAIN nữa.

Một tham số khác là CONCURRENT_ITEMS, có chức năng giới hạn số lượng Item có thể trích xuất từ các response tại một thời điểm. Tuy vậy thì tham số này thường không có gì đáng quan tâm, bởi chúng ta thường hay cào 1 Item trên 1 response. Tham số này mặc định là 100, và như vậy là cứ mỗi request được gửi đi – tức là có 1 response được gửi về thì có thể có tối đa 100 Item được trích xuất, nên nếu có 16 request/response được gửi đi/trả về thì chúng ta có thể gặp trường hợp là có tới 1600 Items đang được lưu lên cơ sở dữ liệu (nếu chúng ta có dùng CSDL).

Một tham số khác có ảnh hưởng đến hiệu suất là DOWNLOAD_TIMEOUT, quy định thời gian tối đa có thể đợi trước khi hủy 1 request nếu chưa có response trả về. Tham số này mặc định là 180 giây, tức 3 phút.

Ngoài ra, Scrapy còn có tham số DOWNLOAD_DELAY – quy định thời gian đợi giữa các lần gửi request là random(0, DOWNLOAD_DELAY). Lí do có tham số này là vì có một số website đo tần suất gửi request để biết được có phải là bên gửi là một con bot đang auto hay không (vì một số site không thích bot auto). Tuy nhiên chúng ta có thể tắt tham số này bằng cách thiết lập RANDOMIZE_DOWNLOAD_DELAY=False.

Một tham số khác có thể giúp tăng tốc hiệu suất là DNSCACHE_ENABLED=True/False, giúp lưu lại địa chỉ máy chủ DNS trong bộ nhớ cache để sử dụng cho các request lần sau.

Dừng hoạt động Scrapy

Scrapy có một vài tham số giúp hủy cào dữ liệu khi có một điều kiện nào đó được thỏa mãn. Ví dụ:

Dừng cào dữ liệu sau khi đã cào được 10 item

$ scrapy crawl basic -s CLOSESPIDER_ITEMCOUNT=10

Dừng cào dữ liệu sau khi đã cào được 10 trang.

$ scrapy crawl basic -s CLOSESPIDER_PAGECOUNT=10

Dừng cào dữ liệu sau khi đã hết 10 giây.

$ scrapy crawl basic -s CLOSESPIDER_TIMEOUT=10

Scrapy – Cào dữ liệu file Exel

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

Thực chất mà nói thì để lấy dữ liệu từ file Excel thì chúng ta không cần dùng đến Scrapy, vì ngay chính bản thân Python đã có thư viện riêng để trích xuất dữ liệu từ dạng file này, đó là gói csv.

Bây giờ chúng ta tạo một Spider với tên là sheetReader:

$ scrapy genspider sheetReader example.com
Created spider 'sheetReader' using template 'basic' in module:
    ex.spiders.sheetReader

Chúng ta tạo thêm 1 file CSV có tên human.csv như sau:

Chúng ta sửa nội dung file sheetReader.py như sau:

# -*- coding: utf-8 -*-
import scrapy
import csv

class SheetreaderSpider(scrapy.Spider):
    name = 'sheetReader'
    allowed_domains = ['example.com']
    start_urls = ['http://example.com/']

    def parse(self, response): 
        with open('./spiders/human.csv', 'rU') as f:
            reader = csv.DictReader(f)
            for line in reader:
                print(
                    line["Firstname"], 
                    line["Lastname"],
                    "\t - ",
                    line["Address"],
                    ",",
                    line["City"],
                    line["Zipcode"])

Đoạn code không thể đơn giản hơn! Chúng ta dùng hàm open() để tạo đối tượng File. Sau đó tạo đối tượng csv.DictReader() và truyền vào tham số là đối tượng File vừa tạo, vậy là chúng ta đã có thể lặp tất cả các dòng có trong file csv, mỗi dòng này thì DictReader sẽ trả về một đối tượng JSON nên chúng ta có thể trích xuất data bằng cách đưa key vào, key ở đây tất nhiên là các ô ở dòng đâu tiên trong file.

Một lưu ý là chúng ta vẫn phải có biến start_urls với ít nhất 1 URL bên trong, vì nếu không có biến này thì Scrapy sẽ không gọi phương thức parse().

Chạy và chúng ta được kết quả sau:

$ scrapy crawl sheetReader
...
John Doe    - 120 jefferson st. , Riverside 08075
Jack McGinnis    - 220 hobo Av. , Phila 09119
John "Da Man" Recipi    - 120 jefferson St. , Riverside 08075
Stephen Tyler    - 7452 Terrace "At the Plaza" road, SomeTown 91234
Blankman    - SomeTown 00298
Joan "the bone", Anne Jet    - 9th at Terrace plc, Desert City 00123

Chúng ta có thể cải tiến đoạn code trên bằng cách truyền tên file giống như tham số thay vì hard-code bên trong file Python. Chúng ta sửa lại file như sau:

# -*- coding: utf-8 -*-
import scrapy
import csv

class SheetreaderSpider(scrapy.Spider):
    name = 'sheetReader'
    allowed_domains = ['example.com']
    start_urls = ['http://example.com/']

    def parse(self, response): 
        with open(getattr(self, 'file', './spiders/human.csv', 'rU')) as f:
            reader = csv.DictReader(f)
            for line in reader: 
            print(
                line["Firstname"], 
                line["Lastname"],
                "\t - ",
                line["Address"],
                ",",
                line["City"],
                line["Zipcode"])

Chúng ta dùng thêm phương thức getattr() để lấy các tham số được truyền vào từ console. Ở đay tham số có tên là file, và nếu tham số này không được truyền vào thì chúng ta dùng file ./spiders/human.csv làm file mặc định để chạy.

Để truyền tên file khi chạy thì chúng ta chỉ cần dùng tham số -a như sau:

$ scrapy crawl sheetReader -a file=hooman.csv
...

Scrapy – Cào dữ liệu từ file JSON

Nếu bạn chưa biết JSON là gì thì có thể hiểu đơn giản nó là một quy tắc lưu trữ dữ liệu theo cú pháp sử dụng cặp dấu ngoặc nhọn {}, cú pháp này bắt nguồn từ Javascript.

Khi chúng ta truy cập một trang web, ngoài việc trang này tải về các trang HTML, CSS, Javascript, file ảnh…v.v thì cũng có nhiều khả năng là chúng cũng tải về các file JSON hoặc các request mà dữ liệu trả về là dạng JSON. Chẳng hạn như khi vào trang vnexpress.net, chúng ta có thể thấy các request đến URL /bid-request trả về data là JSON:

Đây thực chất là data của các mạng quảng cáo có liên kết với VnExpress.

Trong phần này chúng ta sẽ cào data từ file json tại url sau:

https://www.learningcontainer.com/wp-content/uploads/2019/10/Sample-employee-JSON-data.json

File này có nội dung như sau:

{
  "Employees": [
    {
      "userId": "krish",
      "jobTitle": "Developer",
      "firstName": "Krish",
      "lastName": "Lee",
      "employeeCode": "E1",
      "region": "CA",
      "phoneNumber": "123456",
      "emailAddress": "krish.lee@learningcontainer.com"
    },
    {
      "userId": "devid",
      "jobTitle": "Developer",
      "firstName": "Devid",
      "lastName": "Rome",
      "employeeCode": "E2",
      "region": "CA",
      "phoneNumber": "1111111",
      "emailAddress": "devid.rome@learningcontainer.com"
    },
    {
      "userId": "tin",
      "jobTitle": "Program Directory",
      "firstName": "tin",
      "lastName": "jonson",
      "employeeCode": "E3",
      "region": "CA",
      "phoneNumber": "2222222",
      "emailAddress": "tin.jonson@learningcontainer.com"
    }
  ]
}

Bây giờ chúng ta tạo một Spider bằng lệnh spidergen với tên là api:

$ scrapy genspider api web

File api.py được tạo ra trong thư mục spiders, chúng ta sửa lại như sau:

# -*- coding: utf-8 -*-
import scrapy
import json

class ApiSpider(scrapy.Spider):
    name = 'api'
    allowed_domains = ['web']
    start_urls = ['https://www.learningcontainer.com/wp-content/uploads/2019/10/Sample-employee-JSON-data.json']

    def parse(self, response):
        js = json.loads(response.body)
        employees = js["Employees"]
        for employee in employees:
            print(employee["firstName"], 
                  employee["lastName"], 
                  " - ", 
                  employee["jobTitle"])

Dữ liệu JSON có thể nói là loại cấu trúc dữ liệu dễ cào nhất nếu so sánh với HTML, vì chúng ta không cần phải viết XPath hay CSS. Thêm vào đó Python có thư viện json , chúng ta chỉ cần gọi phương thức loads() và đưa vào chuỗi JSON là mọi thứ đã hoàn thành. Việc còn lại là chỉ cần đưa vào key để lấy data mà chúng ta cần.

Chạy đoạn code trên chúng ta được kết quả như sau:

$ scrapy crawl api
...
DEBUG: Crawled (200) <GET https://www.learningcontainer.com/...data.json> (referer: None)
Krish Lee - Developer
Devid Rome - Developer
tin jonson - Program Directory
...

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

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.

Scrapy – Tạo project Scrapy P2

Trong phần này chúng ta tiếp tục code thêm một vài thứ cho project được tạo trong phần trước.

Lưu dữ liệu vào file

Trong phần trước, khi chúng ta chạy lệnh scrapy crawl basic, Scrapy sẽ cào data của trang example.com và trả về một đối tượng json. Lần này chúng ta chạy lại lệnh đó nhưng có thể quy định Scrapy lưu output vào file như sau:

D:/ex>scrapy crawl basic -o items.json
[
    {"title": ["Example Domain"]}
]

D:/ex>scrapy crawl basic -o items.jl
{"title": ["Example Domain"]}

D:/ex>scrapy crawl basic -o items.csv
<?xml version="1.0" encoding="utf-8"?>
<items>
<item><title><value>Example Domain</value></title></item>
</items>

D:/ex>scrapy crawl basic -o items.xml
title
Example Domain

Chúng ta truyền vào tham số -o và tên của file mà chúng ta muốn lưu. Lưu ý tên file phải có đuôi định dạng, nếu không Scrapy sẽ báo lỗi. Scrap có thể lưu rất nhiều loại định dạng, ở đây chúng dùng 4 loại định dạng là JSON, JSON Line, CSV và XML. Bạn có thể tìm hiểu thêm về các định dạng này trên mạng. Sau khi chạy xong bạn sẽ thấy có các file với tên mà chúng ta truyền vào được tạo ra và có nội dung như trên.

Hiện tại Scrapy không có tính năng lưu dữ liệu thẳng vào database. Tuy nhiên chúng ta sẽ tìm hiểu cách lưu vào database trong các bài sau.

Ngoài ra chúng ta còn có thể yêu cầu Scrapy upload thẳng data lên các dịch vụ đám mây thông qua FTP hay S3 như ví dụ sau:

D:/ex> scrapy crawl basic -o "ftp://user:pass@ftp.scrapybook.com/items.json"
D:/ex> scrapy crawl basic -o <b>"</b>s3://aws_key:aws_secret@scrapybook/items.json"

Lưu ý là bạn phải đặt thông tin đăng nhập các tài khoản S3 hay hosting vào thì đoạn code trên mới chạy.

ItemLoader và Processor

Trong Scrapy có một lớp tên là ItemLoader, có chức năng giúp chúng ta quản lý các đoạn code dễ dàng hơn và xử lý dữ liệu đơn giản hơn.

Chúng ta sửa lại phương thức parse() như sau:

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

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_urls = ( 'http://example.com', )

    def parse(self, response):
        ld = ItemLoader(item=ExItem(), response=response)
        ld.add_xpath('title', '//div//h1/text()')
        return ld.load_item()

Trong đoạn code trên thì chúng ta khởi tạo một đối tượng từ class ItemLoader và truyền các tham số về ExItem() và XPath vào đối tượng này bằng phương thức add_xpath(), sau đó return bằng phương thức load_item(). Khi bạn chạy scrapy crawl basic thì kết quả vẫn như cũ không có gì thay đổi.

Tuy nhiên khi dùng ItemLoader, chúng ta có thể sử dụng được các tiện ích của một nhóm class khác được gọi là Processors. Đây là các class làm các công việc hậu xử lý dữ liệu (data post-processing), chẳng hạn như tách chuỗi, nối chuỗi, parse từ kiểu này sang kiểu khác…v.v Một số lớp tiêu biểu là Join – nối chuỗi, MapCompose – chuẩn hóa và format chuỗi. Ví dụ:

Đầu tiên chúng ta sửa class ExItem trong file items.py ở thư mục trước như sau:

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy
from scrapy.item import Item, Field

class ExItem(scrapy.Item):
    Ex1 = Field()
    Ex2 = Field()
    Ex3 = Field()

Ở đây chúng ta chỉ thay đổi các thuộc tính. Tiếp theo chúng ta sửa file basic.py như sau:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, Join
from ex.items import ExItem

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_urls = ( 'http://example.com', )

    def parse(self, response):
        ld = ItemLoader(item=ExItem(), response=response) 
        ld.add_xpath('Ex1', '//p', Join())		
        ld.add_xpath('Ex2', '//p[1]/text()', MapCompose(lambda i: i.replace('.', ',')))
        ld.add_xpath('Exe', '//p[1]/text()', MapCompose(str.strip))
        return ld.load_item()

Trong đoạn code trên, chúng ta truyền thêm vào phương thức add_xpath() các đối tượng Processor. Với thuộc tính Ex1, chúng ta truyền vào đối tượng Join(), đối tượng này sẽ gộp 2 element <p> lại thành 1 khi trả về. Ở thuộc tính Ex1, chúng ta truyền vào MapCompose(...), bên trong là một hàm được viết theo biểu thức lambda, có tác dụng đổi tất cả kí tự dấu chấm thành dấu phẩy. Còn ở thuộc tính Ex3 thì cũng là một đối tượng MapCompose() với tham số là str.strip, tham số này sẽ bảo MapCompose làm công việc loại bỏ kí tự trắng thừa ở trước, sau và giữa các kí tự.

[
    {
        "Ex1": ["<p>This domain is for use in illustrative examples in documents. 
               You may use this\n    domain in literature without prior coordination or 
               asking for permission.</p> 
               <p><a href=\"https://www.iana.org/domains/example\">
               More information...</a></p>"], 
        "Ex2": ["This domain is for use in illustrative examples in documents, 
               You may use this\n    domain in literature without prior coordination or 
               asking for permission,"], 
        "Ex3": ["This domain is for use in illustrative examples in documents. 
               You may use this\n    domain in literature without prior coordination or 
               asking for permission."]
    }
]

Bạn có thể tìm hiểu thêm về ItemLoader và processor tại đây:

  • http://doc.scrapy.org/en/latest/topics/loaders.html

Contracts

Contracts có tác dụng giống như Unit Test. Nếu bạn không biết Unit Test là gì thì có thể hiểu đơn giản rằng đây là tính năng cho phép chúng ta kiểm tra xem code có còn thực hiện đúng những gì nó làm hay không sau một thời gian dài không được đụng tới.

Contracts được tạo ra bằng cách đặt các luật trong phần comment sử dụng 3 dấu nháy đôi, các luật này được bắt đầu bằng kí tự @. Để ví dụ thì chúng ta sửa file basic.py như sau:

# -*- coding: utf-8 -*-
import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose, Join
from ex.items import ExItem

class BasicSpider(scrapy.Spider):    
    name = "basic"
    allowed_domains = ["web"]
    start_urls = ( 'http://example.com', )

    def parse(self, response):
        """
        @url http://example.com
        @returns items 1
        @scrapes title
        """
        ld = ItemLoader(item=ExItem(), response=response) 
        ld.add_xpath('title', '//div/h1/text()') 
        return ld.load_item()

Ý nghĩa ở đây là: Spider này sẽ cào dữ liệu ở trang web được quy định trong @url, và trả về 1 items được quy định trong @return, tên thuộc tính được trả về phải là title trong @scrapes. Phần định nghĩa contract phải được đặt phía sau tên phương thức.

Để chạy contracts thì chúng ta dùng lệnh scrapy check <tên_spider>:

D:/ex> scrapy check basic
-----------------------------------------------------------------------
Ran 2 contracts in 0.784s
OK

Nếu bạn ra được OK thì tức là code vẫn hoạt động bình thường. Nếu sai thì Scrapy sẽ in ra FAILED.

Scrapy – Tạo project Scrapy P1

Thay vì chỉ chạy code thông qua console như trong các phần trước, thì phần này chúng ta sẽ tạo một project Scrapy.

Chúng ta tạo một project Scrapy bằng cách chạy lệnh scrapy startproject <tên_project>.

D:> scrapy startproject ex
New Scrapy project 'ex', using template directory 'd:...', created in:
    D:\ex

You can start your first spider with:
    cd ex
    scrapy genspider example example.com

Ở đây chúng ta tạo một project tên là ex ở ổ đĩa D:>, Scrapy sẽ tạo một thư mục mang tên ex trong ổ đĩa D:>.

D:> cd ex
D:/ex> tree
D:.
|___ex
    |___spiders
    |    |___ __pycache__
    |____ __pycache__

Đoạn code bên trên sẽ cho chúng ta biết những thư mục con và file đã được Scrapy tạo ra bên trong thư muc ex.

Trong đó có 3 file đáng lưu ý là items.py,pipelines.pysettings.py. Ngoài ra còn có thư mục spiders nhưng không chứa gì ngoài file __init__.py. Trong phần này chúng ta sẽ chủ yếu làm việc với items.py và những gì trong thư mục spiders.

Định nghĩa Item

Mở file items.py ra. Chúng ta sẽ thấy Scrapy đã tạo một ít code bên trong, mà trong đó là đoạn import scrapy và phần định nghĩa class BdsItem trống rỗng và một số comment.

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy

class ExItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass

Chúng ta sẽ sửa lại class này bằng cách thêm vào một thuộc tính như sau:

import scrapy
from scrapy.item import Item, Field

class ExItem(scrapy.Item):
    title = Field() 

Chúng ta định nghĩa một thuộc tính tên là title thuộc lớp Field, được khởi tạo bằng cách gọi Field().

Spider

Spider có thể coi là các đoạn code làm công việc cào dữ liệu, làm tất cả các công đoạn trong tiến trình UR2IM. Thông thường chúng ta viết 1 spider cho 1 trang trong website hoặc một phần của trang nếu trang đó quá lớn.

Chúng ta tạo một spider bằng lệnh scrapy genspider <tên_spider>. Ví dụ:

D:>ex scrapy genspider basic web
Created spidder 'basic' using template 'basic' in module:
  bds.spiders.basic

Đoạn code trên sẽ tạo ra một file có tên basic.py trong thư mục ex/spiders.

# -*- coding: utf-8 -*-
import scrapy


class BasicSpider(scrapy.Spider):
    name = 'basic'
    allowed_domains = ['web']
    start_urls = ['http://web/']

    def parse(self, response):
        pass

Bên trong file này có định nghĩa lớp BasicSpider, lớp này dùng mẫu “Basic”, và bị giới hạn chỉ cào dữ liệu trên các websites. Scrapy 1.8.0 có tất cả 4 mẫu Spider là basic, crawl, csvfeedxmlfeed. Bạn có thể xem danh sách các mẫu bằng cách chạy lệnh scrapy genspider -l. Chúng ta sẽ tìm hiểu thêm về các loại mẫu sau. Lớp BasicSpider này thừa kế từ lớp Spider từ gói scrapy.Spider, có một số thuộc tính và phương thức parse(), phương thức này nhận vào 2 tham số, selfresponse, và tham số response phải truyền vào ở đây chính là đối tượng response mà chúng ta sử dụng trong Scrapy shell.

Bây giờ chúng ta sửa lại file này như sau:

# -*- coding: utf-8 -*-
import scrapy

class BasicSpider(scrapy.Spider): 
    name = "basic" 
    allowed_domains = ["web"] 
    start_urls = ( 'https://example.com', )

    def parse(self, response): 
        self.log("Title: %s" % response.xpath( '//div//h1/text()').extract()) 

Đầu tiên chúng ta sửa lại mảng start_urls bằng cách đưa vào đường link dẫn tới trang http://example.com

Bên trong phương thức parse(). Chúng ta gọi phương thức log(), phương thức này có tác dụng in chuỗi ra màn hình (console) giống như print(). Bên trong phương thức này chúng ta truyền chuỗi và gọi đoạn code lấy giá trị của element <h1> theo XPath như trong các bài trước.

Bây giờ chung ta có thể chạy đoạn code spider này với lệnh scrapy crawl như sau:

D:/ex>scrapy crawl basic
...
...
INFO: Scrapy 1.8.0 started (bot: bds)
...
INFO: Spider opened
...
DEBGUG: Crawled (200) <GET http://example.com> (referer: None)
DEBUG: Title: ['Example Domain']
...
INFO: Spider closed (finished)

Scrapy sẽ khởi chạy và gọi phương thức parse() của lớp BasicSpider. Màn hình sẽ in ra rất nhiều dòng output, chúng ta chưa cần quan tâm về chúng. Tuy nhiên nếu bạn tìm thấy các đoạn INFO và DEBUG giống như trên thì Spider của bạn đã chạy thành công.

Chúng ta có thể chạy Spider bằng lệnh khác là scrapy parse như sau:

D:/ex/scrapy parse --spider=basic http://example.com

Trong đoạn code trên, chúng ta truyền vào tham số --spider là basic, để Scrapy biết là nó sẽ chạy Spider bên trong file basic.py, tham số tiếp theo là URL mà nó sẽ cào dữ liệu.

Lưu lại dữ liệu vào Item

Chúng ta sửa lại file basic.py như sau:

# -*- coding: utf-8 -*-
import scrapy
from ex.items import ExItem

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_urls = ( 'https://example.com', )

    def parse(self, response):
        item = ExItem()
        item["Title"] = response.xpath( '//div//h1/text()').extract()
        return item

Ở đây chúng ta import lớp ExItem được định nghĩa trong file items.py trước đó. Sau đó chúng ta khởi tạo đối tượng item từ lớp này và gán giá trị cho thuộc tính Title là những gì được trả về từ việc gọi response().xpath().extract(). Cuối cùng là return đối tượng này thay vì gọi phương thức response.log() như trước.

Chúng ta chạy lại lệnh scrapy crawl basic như lúc trước:

D:/ex>scrapy crawl basic
...
DEBUG: Scraped from <200 https://example.com>
{'Title': ['Example Domain']}
...

Ngoài những dòng output khác, thì những dòng log đã biến mất, thay vào đó là Scrapy in ra một object data chứa key là 'Title' với giá trị là mảng ['Example Domain']

Scrapy – Tiến trình cào dữ liệu UR2IM

Quá trình khai quật dữ liệu của bạn có thể khác nhau tùy vào từng bước nhưng nhìn chung sẽ tuân theo một tiến trình có tên UR2IM. Hình mô tả:

URL

Đầu tiên là URL, chúng ta cần phải biết chúng ta muốn cào dữ liệu ở trang web nào, chẳng hạn https://phocode.com. Rồi vô bên trong trang web đó sẽ lại có nhiều URL dẫn đến các trang con khác, chẳng hạn https://phocode.com/scrapy…v.v Và để bắt đầu quá trình cào dữ liệu trang web. Chúng ta dùng lệnh scrapy shell <url>:

$ scrapy shell https://phocode.com
...
...
[s] View(response) View response in a browser

Một số trang web sẽ không trả lời vì chúng ta gửi thiếu một header là user-agent. Để giải quyết vấn đề này thì chúng ta có thể gửi thêm thông tin về user-agent thông qua tham số -s. Ví dụ:

$ scrapy shell -s USER_AGENT="Mozilla/5.0" https://phocode.com

Chúng ta sẽ tìm hiểu thêm về user-agent sau.

Ngoài ra chúng ta có thể bật tính năng debug tương tác để dễ dàng tìm lỗi trong quá trình cào dữ liệu sau này bằng tham số --pdb như sau:

$ scrapy shell --pdb https://phocode.com

Request và Response

Sau khi đã chạy lệnh scrapy shell thì chúng ta sẽ được trao quyền chạy các lệnh Python như bình thường và có thể dùng các biến toàn cục của Scrapy.

Khi chúng ta gửi lệnh scrapy shell thì Scrapy đã thực hiện việc gửi một “request” GET đến trang web và nhận được thông tin trả lời, mã trả lời có thể là các mã HTTP quen thuộc như 200 (thành công), 301 (redirect), 404 (lỗi không tìm thấy)…. Và sau đó chúng ta có thể truy xuất tới dữ liệu trả về trong biến response của Scrapy, biến này có một trường tên là body và có kiểu là một mảng kí tự, chẳng hạn chúng ta có thể xem 50 phần tử đầu tiên trong mảng này bằng cách gọi response.body[:50]:

Và đây cũng chính là 50 kí tự đầu tiên khi bạn xem mã nguồn HTML của blog Phở Code

Item

Bây giờ chúng ta sẽ thử cào dữ liệu, và dùng trang https://phocode.com/python/scrapy/scrapy-gioi-thieu/ để thử nghiệm 🙂

$ scrapy shell https://phocode.com/python/scrapy/scrapy-gioi-thieu/

Trên trang web có rất nhiều thứ, nhưng đa phần chỉ là những thứ liên quan tới giao diện như logo, mục lục, links… Cào dữ liệu thì chúng ta quan tâm đến những thứ như… dữ liệu, chẳng hạn chúng ta có thể quan tâm tới việc lấy tiêu đề trang, ở đây là chuỗi “Scrapy – Giới thiệu”, chúng ta sẽ lấy XPath của tiêu đề. Dùng Console của Chrome, chúng ta copy được đoạn XPath như sau:

//*[@id="post-9277"]/header/h1

Bạn có thể thấy là Chrome trả về cho chúng ta XPath dùng attribute id với một id rất cụ thể là post-9277, đây là id do website tự tạo ra, nên không tốt nếu dùng nó làm XPath (xem bài trước để hiểu hơn).

Chúng ta sẽ đơn giản hóa nó một chút, thử lấy element <h1> với cú pháp //h1. Tuy nhiên làm thế cũng không tốt vì Scrapy sẽ trả về 2 element h1 là tên bài viết và tên trang web. Chúng ta có thể sửa lại là //header/h1 và đây là XPath đúng để lấy ra duy nhất tên của bài viết.

Bây giờ chúng ta chạy XPath đó với Scrapy xem được cái gì

$ response.xpath('//header/h1/text()').extract()
['Scrapy - Giới thiệu']

Chúng ta lấy được một mảng, có 1 phần tử là chuỗi mang giá trị là “Scrapy – Giới thiệu”. Bạn có thể thấy hàm text() được gọi phía sau h1, có tác dụng chỉ lấy phần nội dung của element này và extract() là để lấy giá trị của text. Nếu không có hàm text() thì chúng ta sẽ nhận được tất cả các attribute còn lại. Nếu không có hàm extract() thì chúng ta sẽ nhận được element thay vì chỉ phần text. Bạn có thể thử bỏ 1 trong 2 hoặc cả 2 để xem kết quả trả về là gì.

Nếu không có text() hay extract() thì chúng ta sẽ nhận được giá trị trả về là một đối tượng Selector từ phương thức xpath().

[<Selector xpath="//header/h1" data='...'>]

Đối tượng này chứa nội dung bên trong là đoạn code HTML của thứ mà chúng ta muốn lấy. Scrapy còn có một phương thức khác cũng có thể dùng để lấy Selector là css(). Ví dụ:

$ response.css('.entry-title')
[<Selector xpath="descendant-or-self::...">]

Và chúng ta cũng có thể dùng extract() để lấy dữ liệu cần lấy bên trong ra như khi dùng với xpath(). Và chúng ta cũng còn có thể gọi chúng nối tiếp nhau, để có thể dễ dàng lấy được dữ liệu mà chúng ta muốn. Ví dụ:

$ response.css('.entry-title').xpath('text()').extract()
['Scrapy - Giới thiệu']

Một ghi chú ở đây đó là khi chúng ta tìm element thông qua CSS thì Scrapy sẽ tự động dịch đoạn CSS đó sang code XPath luôn, chỉ là CSS thì thông thường dễ viết hơn XPath. Do đó chúng ta có thể viết css()xpath() nối tiếp nhau.

Tuy nhiên trong serie này chúng ta sẽ làm việc với XPath là chủ yếu.