JavaScript 动态渲染页面爬取 —— 基于 Splash

Posted Amo Xiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript 动态渲染页面爬取 —— 基于 Splash相关的知识,希望对你有一定的参考价值。

一、安装 Splash

Splash 是一个 javascript 渲染服务,是一个含有 HTTP API 的轻量级浏览器,它还对接了 Python 中的 Twisted 库和 QT 库。利用它,同样可以爬取动态渲染的页面。利用 Splash,可以实现如下功能:

  1. 异步处理多个网页的渲染过程;
  2. 获取渲染后页面的源代码或截图。
  3. 通过关闭图片渲染或者使用 Adblock 规则的方式加快页面渲染的速度;
  4. 执行特定的 JavaScript 脚本;
  5. 通过 Lua 脚本控制页面的渲染过程;
  6. 获取页面渲染的详细过程并以 HAR(HTTP Archive) 的格式呈现出来。

Splash 建议的安装方式是通过 docker,安装是通过 docker 安装,在这之前请确保已经正确安装好了 docker,官方文档如下:

https://docs.docker.com/desktop/windows/install/

有了 docker,只需要一键启动 Splash 即可,命令如下:

docker run -p 8050:8050 scrapinghub/splash

安装完成之后如下图所示:

这样就证明 Splash 已经在 8050 端口上运行了。这时我们使用 Google 浏览器打开地址:http://localhost:8050 即可看到 Splash 的主页,如下图所示:

Splash 也可以直接安装在远程服务器上,在服务器上以守护态运行 Splash 即可,命令如下:

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

在这里多了一个 -d 参数,它代表将 docker 容器以守护态运行,这样在中断远程服务器连接后不会终止 Splash 服务的运行。docker 基础学习笔记可以参考此文章:https://blog.csdn.net/xw1680/article/details/113360133

二、Splash 的使用

1、基本用法:

在本机 8050 端口上运行 Splash 服务,然后打开 http://localhost:8050/,即可看到 Splash 的 Web 页面,如下图所示:

我们可以更改上方输入框中的 URL 为 https://blog.csdn.net/xw1680?,换完内容后单击 Render me!按钮,开始渲染,结果如下图所示:

渲染结果中包含渲染截图、HAR 加载统计数据和网页的源代码。Splash 渲染了整个网页,包括 CSS、JavaScript 的加载等,最终呈现的页面和在浏览器中看到的是完全一致的。这段过程是由一段脚本控制的:

function main(splash, args)
  assert(splash:go(args.url))
  assert(splash:wait(0.5))
  return 
    html = splash:html(),
    png = splash:png(),
    har = splash:har(),
  
end

脚本是 Lua 语言写的,首先调用 go 方法加载页面,然后调用 wait 方法等待了一定时间,最后返回了页面的源代码、截图和 HAR 信息。通过上面简单例子,我们大体了解了 Splash 是通过 Lua 脚本控制页面的加载过程,加载过程完全模拟浏览器,最后可返回各种格式的结果,如网页源码和截图等。下面,具体来了解一下 Lua 脚本的写法以及相关 API 的用法。

2、入口及返回值:

基本实例:

function main(splash, args)
  	splash:go("https://blog.csdn.net/xw1680?")
  	splash:wait(0.5)
    local title = splash:evaljs("document.title")
    return title=title
end

将这段代码粘贴到下图所示的代码编辑区域,然后单击 Render me! 按钮

返回结果如下图所示:

渲染结果包含网页的标题。通过 evaljs 方法传入了 JavaScript 脚本,而 document.title 返回的就是网页的标题,evaljs 方法执行完毕后将标题赋值给 title 变量,随后将其返回。注意:main 方法是固定的,Splash 默认会调用该方法。main 方法的返回值既可以是字典形式,也可以是字符串形式,最后都会转换为 Splash 的 HTTP 响应,例如:

function main(splash)
	-- 字典形式
	return hello="amoxiang!"
end

-- 字符串形式
function main(splash)
	return 'amoxiang'
end

Splash 支持异步处理,但是并没有显示地指明回调方法,其回调的跳转是在内部完成的。示例如下:

function main(splash, args)
  	local example_urls = "www.baidu.com","www.jd.com","www.taobao.com"
	local urls = args.urls or example_urls
  	local results = 
    for index, url in ipairs(urls) do
    	-- .. 这里就算不懂lua语法,也大概能猜到是拼接的意思
        local ok, reason = splash:go("http://" .. url)
        -- ok表示状态
        if ok then
        		--wait()方法类似于Python中的sleep方法, 2为等待的秒数
        		--splash执行到这里时,会转而处理其他任务,在等待参数指定的时间后再回来继续处理。
      			splash:wait(2)
      			results[url] = splash:png()
      	end
    end
  	return results
end

运行代码后返回 3 个网站的页面截图,如下图所示:


3、splash 对象的属性:

main() 方法中的第一个参数是 splash,类似于 Selenium 中的 WebDriver 对象,我们可以调用它的一些属性和方法来控制加载过程。

args 属性: 获取页面加载时配置的参数,例如请求的地址(URL)。对于 GET 请求,args 属性还可以用于获取 GET 请求的参数;对于 POST 请求,args 属性还可以用于获取表单提交的数据。此外,Splash 支持将 main() 方法的第二个参数直接设置为 args,例如:

function main(splash, args)
	-- local url = splash.args.url
	local url = args.url
end

js_enabled 属性: Splash 执行 JavaScript 代码的开关,默认是 true,对其进行设置 true 或 false 可以控制是否执行 JavaScript 代码:

function main(splash, args)
  	splash:go("https://blog.csdn.net/xw1680?")
  	splash.js_enabled = false -- 禁止执行 js
  	splash:wait(0.5)
    local title = splash:evaljs("document.title")
    return title=title
end

当我们禁止执行 JavaScript 代码后,重新调用 evaljs 方法执行了 JavaScript 代码,此时运行这段代码,会抛出异常,如下图所示:

一般不设置此属性,默认开启。

resource_timeout 属性:用于设置页面加载的超时时间,单位为秒。如果设置为 0 或 nil(类似 Python 中的 None),代表不检测超时。示例代码如下:

function main(splash)
    splash.resource_timeout = 0.1 --设置超时时间为0.1秒 在0.1秒内没有响应,即抛出异常
  	splash:go("https://blog.csdn.net/xw1680?")
    return splash:png()
end

此属性适合在页面加载速度较慢的情况下设置。如果超过某个时间后页面依然无响应,则直接抛出异常并忽略。

images_enabled 属性: 用于设置是否加载图片,默认是加载。禁用该属性可以节省网络流量并提高网页加载速度。但是:禁用图片加载可能会影响 JavaScript 渲染。因为禁用图片后,外层 DOM 节点的高度会受影响,进而影响 DOM 节点的位置。因此,如果 JavaScript 对图片节点有操作的话,其执行就会受到影响。另外:Splash 使用了缓存。如果一开始加载出来了网页图片,然后禁用了图片加载, 再重新加载页面,之前加载好的图片可能还会显示出来,这时直接重启 Splash 即可。禁用 images_enabled 属性示例代码如下:

function main(splash)
    splash.images_enabled = false
  	splash:go("https://www.jd.com/")
    return png=splash:png()
end

这样返回的页面截图不会带有任何图片,加载速度也会快很多,如下图所示:

splash.plugins_enabled 属性: 可以控制浏览器插件(如 Flash 插件) 是否开启。默认情况下,此属性是 false,表示不开启。可以使用如下代码控制其开启和关闭 plugins_enabled:

splash.plugins_enabled = true/false

scroll_position 属性: 控制页面上下滚动或左右滚动,是一个比较常用的属性。示例如下:

function main(splash)
  	splash:go("https://www.jd.com/")
    splash.scroll_position = y=600
    return png=splash:png()
end

控制页面向下滚动 600 像素值,运行结果如下图所示:

如果要让页面左右滚动,可以传入 x 参数,代码如下:

splash.scroll_position = x=100, y=200

4、splash 对象的方法:

go 方法: 用来请求某个链接,可以模拟 GET 和 POST 请求,同时支持传入请求头、表单等数据,用法如下:

ok, reason = spalsh:gourl, baseurl=nil,headers=nil,http_method="GET",body=nil,formdata=nil
-- 参数说明:
--1.url:请求的URL
--2.baseurl:可选参数, 默认为空, 表示资源加载的相对路径。
--3.headers:可选参数, 默认为空, 表示请求头
--4.http_method:可选参数, 默认为GET,同时支持 POST
--5.body:http_method 为POST时的表单数据,使用的 Content-type 为 application/json, 是可选参数,默认为空。
--6.formdata:http_method 为POST时的表单数据,使用的Content-type为application/x-www-form-urlencoded
--是可选参数, 默认为空。

该方法的返回值是 ok 变量和 reason 变量的组合,如果 ok 为空,代表页面加载出现了错误,reason 中包含错误的原因,否则代表页面加载成功。示例代码如下:

function main(splash)
    local ok, reason = splash:go"http://www.httpbin.org/post", http_method="POST", body="name=AmoXiang"
  	if ok then  
  		return splash.html()
    end
end

这里模拟了 POST 请求,并传入了 POST 的表单数据,如果页面加载成功,则返回页面的源代码。运行结果如下图所示:

可以看到,成功实现了 POST 请求并发送了表单数据。

wait() 方法: 用于控制页面等待时间,语法如下:

ok, reason = splash:waittime,cancel_on_redirect=false,cancel_on_error=true
--参数说明如下:
--1.time:等待的时间,单位为秒。
--2.cancel_on_redirect:表示如果发生了重定向就停止等待,并返回重定向结果。可选参数,默认为false,
--3.cancel_on_error:如果页面加载错误就停止等待, 可选参数,默认为false。

其返回值同样是 ok 变量和 reason 变量的组合。示例代码如下:

function main(splash)
    splash:go"https://blog.csdn.net/xw1680"
  	splash:wait(2)
  	return html=splash.html()
end

执行上面的代码,可以访问笔者博客主页并等待2秒,随后返回页面源代码,如下图所示:

jsfunc 方法: 可以直接调用 JavaScript 定义的方法,但是需要用双中括号把调用的方法包起来,相当于实现了 JavaScript 方法到 Lua 脚本的转换。示例代码如下:

function main(splash, args)
    local get_span_count = splash:jsfunc([[function ()
    		let body = document.body;
    		const spanList = body.getElementsByTagName("span");
    		return spanList.length;
  	]])
    splash:go"https://blog.csdn.net/xw1680"
  	return ("There are % s SPAN"):format(get_span_count())
end

首先,声明了一个 JavaScript 定义的方法,然后在页面加载成功后调用此方法计算出页面中 span 节点的个数。关于 JavaScript 到 Lua 脚本的更多转换细节,参考官方文档: https://splash.readthedocs.io/en/stable/scripting-ref.html#splash-jsfunc

evaljs(): 此方法用于执行 JavaScript 代码并返回最后一条 JavaScript 语句的返回结果,其用法如下:

result = splash:evaljs(js)

例如,可以用下面的代码获取页面标题:

 local title = splash:evaljs("document.title")

runjs() 方法: 可以执行 JavaScript 代码,它的功能与 evaljs() 方法的功能类似,但是更偏向于执行某些动作或声明某些方法。例如:

function main(splash, args)
	splash:go"https://blog.csdn.net/xw1680"
	splash:runjs("person = function() return 'AmoXiang'")
	local result = splash:evaljs("person()")
	return result
end

这里先用 runjs 方法声明了一个 JavaScript 方法 foo,然后通过 evaljs 方法调用 foo 方法得到的结果,如下图所示:

html 方法:用于获取页面的源代码,是一个非常简单且常用的方法,示例代码如下:

function main(splash, args)
	splash:go("https://www.httpbin.org/get")
	return splash:html()
end

运行结果如下:

png 方法: 此方法用于获取 PNG 格式的页面截图,示例如下:

function main(splash, args)
	splash:go("https://blog.csdn.net/xw1680/")
	return splash:png()
end

jpeg 方法: 获取 JPEG 格式的页面截图,示例如下:

function main(splash, args)
	splash:go("https://blog.csdn.net/xw1680/")
	return splash:jpeg()
end

har 方法: 获取页面加载过程的描述信息,示例如下:

function main(splash, args)
	splash:go("https://blog.csdn.net/xw1680/")
	return splash:har()
end

运行结果如下图所示:

这张图里显示了博客页面加载过程中的每个请求记录的详情。

url 方法: 获取当前正在访问的 URL,示例如下:

function main(splash, args)
	splash:go("https://blog.csdn.net/xw1680/")
	return splash:url()
end

运行结果如下图所示:

set_user_agent 方法: 设置浏览器的 User-Agent,示例如下:

function main(splash)
  splash:set_user_agent('Splash')
  splash:go("http://httpbin.org/get")
  return splash:html()
end

这里我们将浏览器的 User-Agent 属性设置为了 Splash, 运行结果如下图所示:

可以看到,设置的 User-Agent 属性值生效了。

select() 方法: 选中符合条件的第一个节点,如果有多个节点符合条件只会返回一个,其参数是 css 选择器。示例代码如下:

function main(splash)
  splash:go("https://www.baidu.com/")
  input = splash:select("#kw")
  input:send_text('AmoXiang')
  splash:wait(3)
  return splash:png()
end

先访问百度官网,然后用 select 方法选中搜索框,随后调用 send_text 方法填写了文本,最后返回网页截图。运行结果如下图所示:

select_all() 方法: 选中所有符合条件的节点,其参数是 CSS 选择器。示例:

function main(splash)
  local treat = require('treat')
  assert(splash:go("http://quotes.toscrape.com/"))
  assert(splash:wait(0.5))
  local texts = splash:select_all('.quote .text')
  local results = 
  for index, text in ipairs(texts) do
    results[index] = text.node.innerHTML
  end
  return treat.as_array(results)
end

运行结果如下:

可以发现,成功获取了10个节点的正文内容。

mouse_click() 方法: 可以模拟鼠标点击操作,传入的参数为坐标值 x 和 y。也可以直接选中某个节点直接调用此方法,示例如下:

function main(splash)
  splash:go("https://www.baidu.com")
  input = splash:select("#kw")
  input:send_text('Splash')
  splash:wait(3)
  submit = splash:select('#su')
  submit:mouse_click()
  splash:wait(5)
  return splash:png()
end

首先选中页面的输入框,输入文本 Splash,然后选中提交按钮,调用了 mouse_click() 方法提交查询,之后等待5秒,就会返回页面截图,如下图所示:

可以看到,成功获取了查询后的页面内容,模拟了百度搜索操作。至此,splash 对象的常用方法介绍完毕,还有一些方法这里就不逐一进行介绍了,如:获取 cookie 等,更加详细和权威的说明可以参见官方文档:https://splash.readthedocs.io/en/stable/scripting-ref.html,此页面介绍了 splash 对象的所有方法。另外,还有针对页面元素的方法,见官方文档:
https://splash.readthedocs.io/en/stable/scripting-element-object.html

5、调用 Splash 提供的 API:

前面简单介绍了 Splash Lua 脚本的用法,但这些脚本是在 Splash 页面里运行测试的,如何才能利用 Splash 渲染页面?Splash 怎样才能和 Python 程序结合使用并爬取 JavaScript 渲染的页面?其实,Splash 给我们提供了一些 HTTP API,我们只需要请求这些 API 并传递相应的参数即可获取页面渲染后的结果,下面介绍开始学习 API。

render.html: 获取 JavaScript 渲染的页面的 HTML 代码,API 地址是 Splash 的运行地址加上此 API 的名称,例如:http://localhost:8050/render.html,用 curl 工具测试一下,如下图所示:

此处给 API 传递了一个 url 参数,以指定渲染的 URL,返回结果即为页面渲染后的源代码。用 Python 实现的代码如下:

# -*- coding: utf-8 -*-
# @Time    : 2021/12/15 22:41
# @Author  : AmoXiang
# @FileName: demo1.py
# @Software: PyCharm
# @Blog    :https://blog.csdn.net/xw1680?

import requests

url = "http://localhost:8050/render.html?url=https://www.baidu.com"
response = requests.get(url)
print(response.text)

这样就可以成功输出百度页面渲染后的源代码了。此 API 还有其他参数,例如:wait,用来指定等待秒数。如果要确保页面完全加载出来,就可以设置此参数,例如:

# -*- coding: utf-8 -*-
# @Time    : 2021/12/15 22:41
# @Author  : AmoXiang
# @FileName: demo1.py
# @Software: PyCharm
# @Blog    :https://blog.csdn.net/xw1680?

import requests

url = "http://localhost:8050/render.html?url=https://www.jd.com&wait=5"
response = requests.get(url)
print(response.text)

增加等待时间后,得到响应的时间会相应变长,如这里我们等待大约5秒钟才能获取 JavaScript 渲染后的京东页面源代码。另外,此 API 还支持代理设置、图片加载设置、请求头设置和请求方法设置,具体的用法可以参见官方文档:https://splash.readthedocs.io/en/stable/api.html#render-html

render.png: 用于获取页面截图,其参数比 render.html 要多几个,例如 width 和 height 用来控制截图的宽和高,返回值是 PNG 格式图片的二进制数据。示例如下:

# -*- coding: utf-8 -*-
# @Time    : 2021/12/15 22:51
# @Author  : AmoXiang
# @FileName: demo2.py
# @Software: PyCharm
# @Blog    :https://blog.csdn.net/xw1680?

import requests

url = "http://localhost:8050/render.png?url=https://www.jd.com&wait=5&width=1000&height=700"

response = requests.get(url)
with open("jd.png", "wb") as file:
    file.write(response.content)

得到的图片如下图所示:

这样就成功获取了京东首页渲染完成后的页面截图,详细的参数设置可以参考官网文档:https://splash.readthedocs.io/en/stable/api.html#render-png

render.jpeg: 此 API 和 render.png 类似,不过它返回的是 JPEG 格式图片的二进制数据。另外,此 API 比 render.png 多一个参数 quality,该参数可以设置图片质量。https://splash.readthedocs.io/en/stable/api.html#render-jpeg

render.har: 此 API 用于获取页面加载的 HAR 数据,运行结果非常多,是一个 JSON 格式的数据,里面包含页面加载过程中的 HAR 数据。如下图所示:

render.json: 此 API 包含前面介绍的所有 render 相关的 API 的功能,返回值是 JSON 格式的数据,我们可以通过传入不同的参数控制返回结果,例如:传入 html=1,返回结果会增加页面源代码;传入 png=1,返回结果会增加 PNG 格式的页面截图;传入 har=1,返回结果会增加页面的 HAR 数据。参考官方文档:https://splash.readthedocs.io/en/stable/api.html#render-json

execute: 此 API 才是最为强大的 API。之前介绍了很多关于 Splash Lua 脚本的操作,用此 API 即可实现与 Lua 脚本的对接。要爬取一般的 JavaScript 渲染页面,使用前面的 render.html 和 render.png 等 API 就足够了,但如果要实现一些交互操作,这些 API 还是心有余而力不足,就需要使用 execute 了。实现一个最简单的脚本:

# -*- coding: utf-8 -*-
# @Time    : 2021/12/15 22:59
# @Author  : AmoXiang
# @FileName: demo3.py
# @Software: PyCharm
# @Blog    :https://blog.csdn.net/xw1680?

import requests
from urllib.parse import quote

lua = """
function main(splash)
    return 'hello splash'
end
"""

url = "http://localhost:8050/execute?lua_source=" + quote(lua)
response = requests.get(url)
print(response.text)

运行结果如下:

实例:

# -*- coding: utf-8 -*-
# @Time    : 2021/12/15 23:15
# @Author  : AmoXiang
# @FileName: demo4.py
# @Software: PyCharm
# @Blog    :https://blog.csdn.net/xw1680?


import requests
from urllib.parse import quote

lua = """
function main(splash, args)
    local treat = require("treat")
    local response = splash:http_get("http://www.httpbin.org/get")
    return 

以上是关于JavaScript 动态渲染页面爬取 —— 基于 Splash的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript 动态渲染页面爬取 —— 基于 Selenium

JavaScript 动态渲染页面爬取 —— 基于 Selenium

利用scrapy-splash爬取JS生成的动态页面

BOOK动态渲染页面爬取--Selenium库

Class 17 - 2 动态渲染页面爬取 — Splash

Python技能树共建动态渲染页面爬取