Scrapy với những website load bằng Javascripts

1. Đặt vấn đề

Trước đây, khi Javascripts còn chưa phổ biến, việc lấy data từ 1 page chỉ đơn thuần là bóc tách HTML, CSS, Xpath. Nhưng ngày nay, khi Javascripts đã có mặt khắp mọi nơi, thì việc lấy data đã trở lên khó khăn hơn. Với nhưng page load dữ liệu thông qua Javascripts thì rất có thể lần requests đầu tiên từ Scrapy ta chỉ nhận được một mẩu html như vậy thôi (Ví dụ lấy từ website: https://www.fahasa.com/)

<html><body>Loading ...<script type="text/javascript" src="/aes.js" ></script><script>function toNumbers(d){var e=[];d.replace(/(..)/g,function(d){e.push(parseInt(d,16))});return e}function toHex(){for(var d=[],d=1==arguments.length&&arguments[0].constructor==Array?arguments[0]:arguments,e="",f=0;f<d.length;f++)e+=(16>d[f]?"0":"")+d[f].toString(16);return e.toLowerCase()}var a=toNumbers("252313760fcdd2a2fbce3f180640e131"),b=toNumbers("44e293b06bc49b9a1d617bb9e5ca1d3f"),c=toNumbers("99ca7eeac4b2e81c7f91853709373c9f");document.cookie="BPC2="+toHex(slowAES.decrypt(c,2,a,b))+"; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/";document.cookie="BPC2Referrer="+document.referrer+"; expires=Thu, 31-Dec-37 23:55:55 GMT; path=/";location.href="https://www.fahasa.com/?attempt=1";</script></body></html>

Như ví dụ trên, khi client gửi requests lên website https://www.fahasa.com/ thì sẽ nhận được một đoạn mã HTML kèm Javascripts như trên. Client cần execute được đoạn mã Javascripts để generate ra được cookie. Từ giá trị cookie nhận được sẽ đc truyền vào các requests tiếp theo để đc accept và lấy data về. Vấn đề đặt ra là làm sao excute được đoạn mã Javascripts kia???

2. Giải pháp

Để giải quyết vấn đề nêu trên, ta cần phải excute được đoạn mã Javascripts và một công cụ tuyệt vời đó là Splash. Scrapy sẽ không gửi requests trực tiếp mà thông qua Splash, khi nhận được dữ liệu trả về từ server, Splash sẽ excute những đoạn mã script trong dữ liệu nhận được sau đó mới trả về cho Scrapy.

a. Cài đặt

Đầu tiên thì phải có Docker cái đã. Sau đó chỉ cần chạy:

$ sudo docker pull scrapinghub/splash

$ sudo docker run -p 8050:8050 scrapinghub/splash

là ta đã có Splash để dùng rồi. Giờ cài thêm thư viện(module) Splash

$ pip install scrapy scrapy-splash

Và thêm config trong file settings.py như sau:

# ...
SPLASH_URL = 'http://localhost:8050'
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
COOKIES_ENABLED = True # Nếu cần dùng Cookie
SPLASH_COOKIES_DEBUG = False
SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}
DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 400,
}
# ...

Xem thêm về cách config tại đây

b. Sử dụng

Chúng ta sẽ sử dụng SplashRequest để thay thế cho Request của Scrapy

# -*- coding: utf-8 -*-
import scrapy
from scrapy_splash import SplashRequest

script = """
function main(splash)
    splash:init_cookies(splash.args.cookies)
    local url = splash.args.url
    assert(splash:go(url))
    assert(splash:wait(5))
    return {
        cookies = splash:get_cookies(),
        html = splash:html()
    }
end
"""

script2 = """
function main(splash)
    splash:init_cookies(splash.args.cookies)
    local url = splash.args.url
    assert(splash:go(url))
    assert(splash:wait(0.5))
    return {
        cookies = splash:get_cookies(),
        html = splash:html()
    }
end
"""


class FahasaSpider(scrapy.Spider):
    name = 'fahasa'
    allowed_domains = ['fahasa.com']
    start_urls = [
        "https://www.fahasa.com/sach-trong-nuoc/van-hoc-trong-nuoc.html"
        ]

    def start_requests(self):
        for url in self.start_urls:
            yield SplashRequest(url, self.parse, endpoint='execute',
                                args={'lua_source': script})

    def parse(self, response):
        # Get the next page and yield Request
        next_selector = response.xpath('//*[@title="Next"]/@href')
        for url in next_selector.extract():
            yield SplashRequest(url, endpoint='execute',
                                args={'lua_source': script2})

        # Get URL in page and yield Request
        url_selector = response.xpath(
            '//*[@class="product-name p-name-list"]/a/@href')
        for url in url_selector.extract():
            yield SplashRequest(url, callback=self.parse_item,
                                endpoint='execute',
                                args={'lua_source': script2})

    def parse_item(self, response):
        """
        Handle crawl logic here
        """
        pass

Bởi vì Fahasa có cơ chế dùng JS để gen cookies, nên mình phải chờ để cookies được gen ra, và send cookie đó cho các request khác sử dụng. Mình sẽ cố gắng giải thích workflow của đoạn code trên. Đầu tiên, chúng ta có list các url để crawl tại start_urls. Bình thường thì scrapy sẽ parse trực tiếp từ các url trong này, nhưng vì ở đây có 1 bước dùng JS đầu tiên nên ta không thể làm vậy được, mà phải đi qua function start_requests, dùng SplashRequest chờ 5s và update cookies đã được gen cho các request sắp tới, bằng script

script = """
function main(splash)
    splash:init_cookies(splash.args.cookies)
    local url = splash.args.url
    assert(splash:go(url))
    assert(splash:wait(5))
    return {
        cookies = splash:get_cookies(),
        html = splash:html()
    }
end
"""

Sau khi có cookies và HTML đã được render, gửi trực tiếp tới parse để lấy link các page tiếp theo và các link sách trong từng page. Vẫn tiếp tục sử dụng cookies từ response object trước, script gần giống bên trên, chỉ là giảm thời gian chờ đi vì ta đã có cookies, không cần thiết phải chờ Fahasa chạy JS để gen nữa.

script2 = """
function main(splash)
    splash:init_cookies(splash.args.cookies)
    local url = splash.args.url
    assert(splash:go(url))
    assert(splash:wait(0.5))
    
    return {
        cookies = splash:get_cookies(),
        html = splash:html()
    }
end
"""

Đoạn script trên được viết bằng LUA, các bạn có thể tìm hiểu thêm về LUA và cú pháp của nó.

3. Kết luận

Bài viết của mình xin dừng lại ở đây, để tìm hiểu sau hơn các bạn có thể tham khảo tại các địa chỉ sau: Documents của Splash http://splash.readthedocs.io/en/stable/ Cách cài đặt Docker: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-16-04