Splash抓取javaScript动态渲染页面

一、概述

Splash是一个javascript渲染服务。它是一个带有HTTP API的轻量级Web浏览器,使用Twisted和QT5在Python 3中实现。QT反应器用于使服务完全异步,允许通过QT主循环利用webkit并发。
一些Splash功能:

    • 并行处理多个网页
    • 获取HTML源代码或截取屏幕截图
    • 关闭图像或使用Adblock Plus规则使渲染更快
    • 在页面上下文中执行自定义JavaScript
    • 可通过Lua脚本来控制页面的渲染过程
    • 在Splash-Jupyter 笔记本中开发Splash Lua脚本。
    • 以HAR格式获取详细的渲染信息

 

二、Scrapy-Splash的安装

Scrapy-Splash的安装分为两部分,一个是Splash服务的安装,具体通过Docker来安装服务,运行服务会启动一个Splash服务,通过它的接口来实现JavaScript页面的加载;另外一个是Scrapy-Splash的Python库的安装,安装后就可在Scrapy中使用Splash服务了。

环境说明

操作系统:centos 7.6

docker版本:19.03.12

ip地址:192.168.0.10

说明:使用docker安装Splash服务

 

操作系统:windows 10

python版本:3.7.9

ip地址:192.168.0.9

说明:使用Pycharm开发工具,用于本地开发。

 

安装splash服务

通过Docker安装Scrapinghub/splash镜像,然后启动容器,创建splash服务

docker pull scrapinghub/splash
docker run -d --name splash -p 8050:8050 scrapinghub/splash

 

Python包Scrapy-Splash安装

pip3 install scrapy-splash

 

plash Lua脚本

运行splash服务后,通过web页面访问服务的8050端口

http://192.168.0.10:8050/

 

即可看到其web页面,如下图:

 

上面有个输入框,默认是http://google.com,我们可以换成想要渲染的网页如:https://www.baidu.com然后点击Render me按钮开始渲染

但是,等了许久,一直是Initializing...状态。不管它了,可能有bug

 

登录centos系统,使用curl命令测试,访问百度

curl 'http://localhost:8050/render.html?url=https://www.baidu.com/page-with-javascript.html&timeout=10&wait=0.5'

它会返回一段html代码,说明渲染是没有问题的。

 

三、示例页面分析

这里我们可以观察一个典型的供我们练习爬虫技术的网站:quotes.toscrape.com/js/

 说明:这里是一个留意列表,都在<div class="quote">里面。

 

接下来使用scrapy命令来分析一下,打开Pycharm,打开Terminal,输入以下命令:

scrapy shell http://quotes.toscrape.com/js/

输出如下:

...
[s]   view(response)    View response in a browser
>>> 

 

然后输入: response.css('div.quote')

>>> response.css('div.quote')
[]
>>>

代码分析:这里我们爬取了该网页,但我们通过css选择器爬取页面每一条名人名言具体内容时发现没有返回值

我们来看看页面:这是由于每一条名人名言是通过客户端运行一个Js脚本动态生成的。

我们将script脚本打开看看发现这里包含了每一条名人名言的具体信息

注意:在<div class="quote">上面一个标签,也就是<script></script>里面,就可以看到。

 

问题分析

scrapy爬虫框架没有提供页面js渲染服务,所以我们获取不到信息,所以我们需要一个渲染引擎来为我们提供渲染服务---这就是Splash渲染引擎(大侠出场了)

1、Splash渲染引擎简介:

Splash是为Scrapy爬虫框架提供渲染javascript代码的引擎,它有如下功能:(摘自维基百科)

(1)为用户返回渲染好的html页面

(2)并发渲染多个页面

(3)关闭图片加载,加速渲染

(4)执行用户自定义的js代码

(5)执行用户自定义的lua脚步,类似于无界面浏览器phantomjs

2、Splash渲染引擎工作原理:(我们来类比就一清二楚了)

这里我们假定三个小伙伴:(1--懒惰的我 , 2 --提供外卖服务的小哥,3---本人喜欢吃的家味道餐饮点)

今天正好天气不好,1呆在宿舍睡了一早上起来,发现肚子饿了,它就想去自己爱吃的家味道餐饮点餐,他在床上大喊一声我要吃大鸡腿,但3并没有返回东西给他,这是后怎么办呢,2出场了,1打来自己了饿了吗APP点了一份荷叶饭,这是外卖小哥收到订单,就为他去3那,拿了他爱吃的荷叶饭给了1,1顿时兴高采烈!

Client----相当于1 /Splash---相当于2 /Web server---相当于3

即:我们将下载请求告诉Splash ,然后Splash帮我们去下载并渲染页面,最后将渲染好的页面返回给我们

 

Splash简要使用说明

render.html端点

Splash为我们提供了多种端点的服务,具体参见http://splash.readthedocs.io/en/stable/api.html#render-html

1、下面我们以render.html端点来体验下:(这里我们使用requests库)

实验:

在Pycharm里,新建一个test.py,代码如下:

import requests

from scrapy.selector import Selector

splash_url = 'http://192.168.0.10:8050/render.html'
args = {'url': 'http://quotes.toscrape.com/js', 'timeout': 10, 'image': 0}
response = requests.get(splash_url, params=args)
sel = Selector(response)
ret = sel.css('div.quote span.text::text').extract()
print(type(ret))

for i in ret:
    print(i)

执行输出:

<class 'list'>
“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”
...

可以发现,这里已经得到留言列表了。

sel.css('div.quote span.text::text') 这里表示使用css选择器,div.quote表:div下的class为:quote 。span.text::text表示:span下的class为:text,并提取text文本信息。相当于jquery里面的text()。这里有点绕,可能有点不太好理解。

它相当于jquery代码

$('div.quote span.text').text()

使用console,测试一下

你看,它真的得到了留言列表。

 

execute端点

2、下面我们来介绍另一个重要的端点:execute端点

execute端点简介:它被用来提供如下服务:当用户想在页面中执行自己定义的Js代码,如:用js代码模拟浏览器进行页面操作(滑动滚动条啊,点击啊等等)

这里:我们将execute看成是一个可以模拟用户行为的浏览器,而用户的行为我们通过lua脚本进行定义:

比如:

打开url页面

等待加载和渲染

执行js代码

获取http响应头部

获取cookies

实验:

使用Pycharm新建一个test1.py,内容如下:

import requests
import json

#编写lua脚本,:访问属性
lua = '''
function main(splash) 
    --打开页面
    splash:go('http:example.com')   
    --等待加载
    splash:wait(0.5)
    --执行js代码
    local title = splash:evaljs('document.title')
    --{中的内容类型python中的键值对}
    return {title = title}
end
'''
splash_url = 'http://192.168.0.10:8050/execute' #定义端点地址
headers = {'content-type':'application/json'}
data = json.dumps({'lua_source':lua}) #做成json对象
response = requests.post(splash_url,headers = headers,data=data) #使用post请求
ret = response.content
ret1 = response.json()

print(ret)
print(ret1)

执行输出:

b'{"title": "Example Domain"}'
{'title': 'Example Domain'}

注意:它是访问http://example.com/,提取title标签对的文字。刚开始,我以为这个网站打不开,没想到,居然可以打开。

 

Splash对象常用属性和方法总结:参考官网http://splash.readthedocs.io/en/stable/scripting-overview.html#和书本

splash:args属性----传入用户参数的表,通过该属性可以访问用户传入的参数,如splash.args.url、splash.args.wait

spalsh.images_enabled属性---用于开启/禁止图片加载,默认值为True

splash:go方法---请求url页面

splash:wait方法---等待渲染的秒数

splash:evaljs方法---在当前页面下,执行一段js代码,并返回最后一句表达式的值

splash:runjs方法---在当前页面下,执行一段js代码

splash:url方法---获取当前页面的url

splash:html方法---获取当前页面的HTML文档

splash:get_cookies---获取cookies信息

 

四、在Scrapy 中使用Splash

在scrapy_splash中定义了一个SplashRequest类,用户只需使用scrapy_splash.SplashRequst来替代scrapy.Request发送请求

该构造器常用参数如下:

url---待爬取的url地址

headers---请求头

cookies---cookies信息

args---传递给splash的参数,如wait\timeout\images\js_source等

cache_args--针对参数重复调用或数据量大大情况,让Splash缓存该参数

endpoint---Splash服务端点

splash_url---Splash服务器地址,默认为None

实验:https://github.com/scrapy-plugins/scrapy-splash(这里有很多使用例子供大家学习)

 

新建项目

打开Pycharm,并打开Terminal,执行以下命令

scrapy startproject dynamic_page
cd dynamic_page
scrapy genspider quotes quotes.toscrape.com

 

在scrapy.cfg同级目录,创建bin.py,用于启动Scrapy项目,内容如下:

#在项目根目录下新建:bin.py
from scrapy.cmdline import execute
# 第三个参数是:爬虫程序名
execute(['scrapy', 'crawl', 'quotes',"--nolog"])

 

创建好的项目树形目录如下:

./
├── bin.py
├── dynamic_page
│   ├── __init__.py
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── quotes.py
└── scrapy.cfg

 

修改settIngs.py

改写settIngs.py文件这里小伙伴们可参考github(https://github.com/scrapy-plugins/scrapy-splash)---上面有详细的说明

在最后添加如下内容:

# Splash服务器地址
SPLASH_URL = 'http://192.168.0.10:8050'
# 开启两个下载中间件,并调整HttpCompressionMiddlewares的次序
DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}
# 设置去重过滤器
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
# 用来支持cache_args(可选)
SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}
DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'
ITEM_PIPELINES = {
   'dynamic_page.pipelines.DynamicPagePipeline': 100,
}

注意:请根据实际情况,修改Splash服务器地址,其他的不需要改动。

 

修改文件quotes.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy_splash import SplashRequest #重新定义了请求
from dynamic_page.items import DynamicPageItem


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    allowed_domains = ['quotes.toscrape.com']
    start_urls = ['http://quotes.toscrape.com/js/']

    def start_requests(self):  # 重新定义起始爬取点
        for url in self.start_urls:
            yield SplashRequest(url, args={'timeout': 8, 'images': 0})

    def parse(self, response):
        authors = response.css('div.quote small.author::text').extract()  # 选中名人并返回一个列表
        quotes = response.css('div.quote span.text::text').extract()  # 选中名言并返回一个列表

        # 创建item字段对象,用来存储信息
        item = DynamicPageItem()
        item['authors'] = authors
        item['quotes'] = quotes

        ##使用zip()函数--小伙伴们自行百度菜鸟教程即可
        # 构造了一个元祖再进行遍历,再次使用zip结合dict构造器做成了列表,由于yield ,所以我们使用生成器解析返回
        yield from (dict(zip(['author', 'quote'], item)) for item in zip(authors, quotes))
        next_url = response.css('ul.pager li.next a::attr(href)').extract_first()
        if next_url:
            complete_url = response.urljoin(next_url)  # 构造了翻页的绝对url地址
            yield SplashRequest(complete_url, args={'timeout': 8, 'images': 0})
View Code

 

修改items.py

# -*- 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 DynamicPageItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 与itcast.py定义的一一对应
    authors = scrapy.Field()
    quotes = scrapy.Field()
View Code

 

修改pipelines.py

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

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import json

class DynamicPagePipeline(object):
    def __init__(self):

        #python3保存文件 必须需要'wb' 保存为json格式
        self.f = open("dynamicpage_pipline.json",'wb')

    def process_item(self, item, spider):
        # 读取item中的数据 并换行处理
        content = json.dumps(dict(item), ensure_ascii=False) + ',\n'
        self.f.write(content.encode('utf=8'))

        return item

    def close_spider(self,spider):
        #关闭文件
        self.f.close()
View Code

 

执行bin.py,等待1分钟,就会生成文件dynamicpage_pipline.json。

打开json文件,内容如下:

{"author": "Albert Einstein", "quote": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”"},
{"author": "J.K. Rowling", "quote": "“It is our choices, Harry, that show what we truly are, far more than our abilities.”"},
...

 

 

 

本文参考链接:

https://www.cnblogs.com/zhangxinqi/p/9279014.html

https://www.cnblogs.com/518894-lu/p/9067208.html

posted @ 2020-09-07 13:22  肖祥  阅读(1918)  评论(0编辑  收藏  举报