Splash的使用
Splash 是一个 JavaScript 渲染服务,是一个含有 HTTP API 的轻量级浏览器,它还对接了 Python 中的 Twisted 库和 QT 库。利用它,同样可以爬取动态渲染的页面。
功能介绍
利用 Splash,可以实现如下功能:
-
异步处理多个网页的染过程;
-
获取染后页面的源代码或截图;
-
通过关闭图片染或者使用 Adblock 规则的方式加快页面染的速度;
-
执行特定的 JavaScript 脚本;
-
通过 Lua 脚本控制页面的染过程;
-
获取页面谊染的详细过程并以 HAR(HTTP Archive)的格式呈现出来。
接下来,我们一起了解 Splash 的具体用法。
准备工作
请确保 Splash 已经正确安装好并可以在本地 8050 端口上正常运行。安装方法可以参考 https://setup.scrape.center/splash 。
实例引入
首先,利用 Splash 提供的 Web 页面来测试其煊染过程。例如,在本机 8050 端口上运行 Splash 服务,然后打开 http://localhost:8050/ ,即可看到 Splash 的 Web 页面,如图 7-9 所示。
在图 7-9 中,右侧呈现的是一个煊染示例,可以看到其上方有一个输入框,默认显示文字是 http://google.com ,我们将其换成 https://www.baidu.com 测试一下,换完内容后单击 Render me!按钮,开始煊染,结果如图 7-10 所示。
图7-9 Splash 的 Web 页面
图7-10 渲染结果
渲染结果中包含煊染截图、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 语言写的。即使不懂 Lua 语言的语法,也能大致看懂脚本的表面意思,首先调用 go 方法加载页面,然后调用 wait 方法等待了一定时间,最后返回了页面的源代码、截图和 HAR 信息。
至此,我们大体了解了 Splash 是通过 Lua 脚本控制页面的加载过程,加载过程完全模拟浏览器,最后可返回各种格式的结果,如网页源码和截图等。
接下来,我们就了解一下 Lua 脚本的写法以及相关 API 的用法。
Splash Lua脚本
Splash 能够通过 Lua 脚本执行一系列煊染操作,因此我们可以用它模拟 Chrome、PhantomJS。
先了解一下 Splash Lua 脚本的入口和执行方式。
入口及返回值
来看一个基本实例:
function main(splash, args)
splash:go("http://www.baidu.com")
splash:wait(0.5)
local title = splash:evaljs("document.title")
return {title=title}
end
将这段代码粘贴到图 7-9 中的代码编辑区域,然后单击 Render me!按钮,返回结果如图 7-11 所示。
图7-11 运行结果
可以看到,染结果中包含网页的标题。这里我们通过 evaljs 方法传入了 JavaScript 脚本,而 document.title 返回的就是网页的标题,evaljs 方法执行完毕后将标题赋值给 title 变量,随后将其返回。
注意,我们在这里定义的方法叫 main。这个名称是固定的,Splash 会默认调用这个方法。main 方法的返回值既可以是字典形式,也可以是字符串形式,最后都会转化为 Splash 的 HTTP 响应,例如:
function main(splash)
return {hello="world!"}
end
返回的是字典形式的内容。下面的代码:
function main(splash)
return 'hello'
end
返回的是字符串形式的内容。
异常处理
Splash 支持异步处理,但是并没有显式地指明回调方法,其回调的跳转是在内部完成的。示例如下:
function main(splash, args)
local example_urls = {"www.baidu.com", "www.taobao.com", "www.zhihu.com"}
local urls = args.urls or example_urls
local results = {}
for index, url in ipairs(urls) do
local ok, reason = splash:go("http://" .. url)
if ok then
splash:wait(2)
results[url] = splash:png()
end
end
return results
end
运行这段代码后的返回结果是代码中 3 个网站的页面截图,如图 7-12 所示。
图 7-12 运行结果
splash对象的属性
能够注意到,前面例子中 main 方法的第一个参数是 splash,这个对象非常重要,类似于 Selenium 中的 WebDriver对象,我们可以调用它的一些属性和方法来控制加载过程。接下来,先看 splash 的属性。
args属性
该属性用于获取页面加载时配置的参数,例如请求 URL。对于 GET 请求,args 属性还可以用于获取 GET 请求的参数;对于 POST 请求,args 属性还可以用于获取表单提交的数据。此外,Splash 支持 main 方法的第二个参数直接设置为 args,例如:
function main(splash, args)
local url = args.url
end
这里的第二个参数 args 就相当于 splash.args 属性,以上代码等价于:
function main(splash)
local url = splash.args.url
end
js_enabled属性
这个属性是 Splash 执行 JavaScript 代码的开关,将其设置为 true 或 false 可以控制是否执行 JavaScript 代码,默认取 true。例如:
function main(splash, args)
splash:go("https://www.baidu.com")
splash.js_enabled = false
local title = splash:evaljs("document.title")
return {title=title}
end
这里我们将 js_enabled 设置为 false,代表禁止执行 JavaScript 代码,然后重新调用 evaljs 方法执行了 JavaScript 代码,此时运行这段代码,就会抛出异常:
{
"error": 400,
"type": "ScriptError",
"info": {"type": "JS ERROR",
"js_error_message": null,
"source": "[string \"function main(splash, args)...\\r\\n\"]",
"message": "[string \"function main(splash, args)...\\r\\n\"]4: unknown JS error: None",
"line_number": 4,
"error": "unknown JS error: None",
"splash_method": "evaljs"
},
"description": "Error happened while executing Lua script"
}
不过,我们一般不设置此属性,默认开启。
resource_timeout属性
此属性用于设置页面加载的超时时间,单位是秒。如果设置为 0 或 nil(类似 Python 中的 None),代表不检测超时。示例如下:
function main(splash)
splash.resource_timeout = 0.1
assert(splash:go('https://www.taobao.com'))
return splash:png()
end
这里将超时时间设置为 0.1 秒。这意味着如果在 0.1 秒内没有得到响应,就抛出异常:
{
"error": 400,
"type": "ScriptError",
"info": {
"error": "networks",
"type": "LUA_ERROR",
"line_number": 3,
"source": "[string \"function main(splash)...\\r\\n\"]",
"message": "Lua error: [string \"function main(splash)...\\r\\n\"]3: networks"
},
"description": "Error happened while executing lua script"
}
此属性适合在页面加载速度较慢的情况下设置。如果超过某个时间后页面依然无响应,则直接抛出异常并忽略。
images_enabled属性
此属性用于设置是否加载图片,默认是加载。禁用该属性可以节省网络流量并提高页面的加载速度,但是需要注意,这可能会影响 JavaScript 渲染。因为禁用该属性之后,它的外层 DOM 节点的高度会受影响,进而影响 DOM 节点的位置。当 JavaScript 对图片节点执行操作时,就会受到影响。
另外有一点值得注意,Splash 会使用缓存。这意味着即使禁用 images_enabled 属性,一开始加载出来的网页图片也会在重新加载页面后显示出来,这种情况下直接重启 Splash 即可。
禁用 images_enabled 属性的示例如下:
function main(splash, args)
splash.images_enabled = false
assert(splash:go('https://www.jd.com'))
return {png=splash:png()}
end
这样返回的页面截图不会带有任何图片,加载速度也会快很多。
plugins_enabled属性
此属性用于控制是否开启浏览器插件(如 Flash 插件),默认取 false,表示不开启。可以使用如下代码开启/关闭 plugins_enabled:
splash.plugins_enabled = true/false
scroll_position属性
此属性可以控制页面上下滚动或左右滚动,是一个比较常用的属性。示例如下:
function main(splash, args)
assert(splash:go('https://www.taobao.com'))
splash.scroll_position = {y=400}
return {png=splash:png()}
end
这样可以控制页面向下滚动 400 像素值,运行结果如图 7-13 所示。
图7-13 设置 scroll_position 属性后的运行结果
如果要让页面左右滚动,可以传入 x 参数,代码如下:
splash.scroll_position = {x=100, y=200}
Splash对象的方法
除了前面介绍的属性,splash 对象还有如下方法。
go方法
该方法用于请求某个链接,可以模拟 GET 请求和 POST 请求,同时支持传入请求头、表单等数据,其用法如下:
ok,reason = splash:go{url,baseurl=nil,headers=nil,http_method="GET",body=nil,formdata=nil}
对其中各参数的说明如下。
-
url:请求 URL。 -
baseurl:资源加载的相对路径,是可选参数,默认为空。 -
headers:请求头,是可选参数,默认为空。 -
http_method:请求方法,是可选参数,默认为 GET,同时支持 POST。 -
body:http_method为 POST 时的表单数据,使用的 Content-type 为application/json,是可选参数,默认为空。 -
formdata:http_method 为 POST 时的表单数据,使用的 Content-type 为application/x-www-form-urlencoded,是可选参数,默认为空。
该方法的返回值是 ok 变量和 reason 变量的组合,如果 ok 为空,代表页面加载出现了错误,reason 中包含错误的原因,否则代表页面加载成功。示例如下:
function main(splash, args)
local ok, reason = splash:go("http://www.httpbin.org/post", http_method="POST", body="name=Germey")
if ok then
return splash:html()
end
end
这里我们模拟了一个 POST 请求,并传入了表单数据,如果页面加载成功,就返回页面的源代码。
运行结果如下:
可以看到,成功实现了 POST 请求并发送了表单数据。
wait方法
此方法用于控制页面等待时间,其用法如下:
ok, reason = splash:wait{time, cancel_on_redirect=false, cancel_on_error=true}
对其中各参数的说明如下。
-
time: 等待的时间,单位为秒。 -
cancel_on_redirect: 如果发生了重定向就停止等待,并返回重定向结果,是可选参数,默认 为false。 -
cancel_on_error: 如果页面加载错误就停止等待,是可选参数,默认认为false。
其返回值同样是 ok 变量和 reason 变量的组合。
我们用一个实例感受一下:
function main(splash)
splash:go("https://www.taobao.com")
splash:wait(2)
return {html=splash:html()}
end
执行如上代码,可以访问淘宝页面并等待 2 秒,随后返回页面源代码。
jsfunc方法
此方法用于直接调用 JavaScript 定义的方法,但是需要用双中括号把调用的方法包起来,相当于实现了从 JavaScript 方法到 Lua 脚本的转换。示例如下:
function main(splash, args)
local get_div_count = splash:jsfunc([[
(function () {
var body = document.body;
var divs = body.getElementsByTagName('div');
return divs.length;
})
]])
splash:go("https://www.baidu.com")
return ("There are %s DIVs"):format(get_div_count())
end
这段代码的运行结果如下:
There are 21 DIVs
这里我们先声明了一个 JavaScript 定义的方法 get_div_count,然后在页面加载成功后调用此方法计算出了页面中 div 节点的个数。
关于从 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://www.baidu.com")
splash:runjs("foo = function() {return 'bar'}")
local result = splash:evaljs("foo()")
return result
end
这里我们先用 runjs 方法声明了一个 JavaScript 方法 foo,然后通过 evaljs 方法调用 foo 得到的结果。
运行结果如下:
bar
可以看到,这里我们成功模拟了发送 POST 请求,并发送了表单数据。
html方法
此方法用于获取页面的源代码,是一个非常简单且常用的方法,示例如下:
function main(splash, args)
splash:go("https://www.httpbin.org/get")
return splash:html()
end
运行结果如下:
<html>
<head></head>
<body>
<pre style="word-wrap: break-word; white-space: pre-wrap;">
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en,*",
"Connection": "close",
"Host": "www.httpbin.org",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) Splash Version/9.0 Safari/602.1"
},
"origin": "60.207.237.85",
"url": "https://www.httpbin.org/get"
}
</pre>
</body>
</html>
png方法
此方法用于获取 PNG 格式的页面截图,示例如下:
function main(splash, args)
splash:go("https://www.taobao.com")
return splash:png()
end
jpeg方法
此方法用于获取 JPEG 格式的页面截图,示例如下:
function main(splash, args)
splash:go("https://www.taobao.com")
return splash:jpeg()
end
har方法
此方法用于获取页面加载过程的描述信息,示例如下:
function main(splash, args)
splash:go("https://www.baidu.com")
return splash:har()
end
运行结果如图 7-14 所示。
图7-14 har 方法的运行结果
这张图里显示了百度页面加载过程中的每个请求记录的详情。
url方法
此方法用于获取当前正在访问的 URL,示例如下:
function main(splash, args)
splash:go("https://www.baidu.com")
return splash:url()
end
运行结果如下:
https://www.baidu.com/
set_user_agent方法
此方法用于设置浏览器的 User-Agent,示例如下:
function main(splash)
splash:set_user_agent('Splash')
splash:go("http://www.httpbin.org/get")
return splash:html()
end
这里我们将浏览器的 User-Agent 属性设置为了 Splash,运行结果如下:
<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">{"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en,*",
"Connection": "close",
"Host": "www.httpbin.org",
"User-Agent": "Splash"
},
"origin": "60.207.237.85",
"url": "http://www.httpbin.org/get"
}
</pre></body></html>
可以看到,我们设置的 User-Agent 属性值生效了。
select方法
该方法用于选中符合条件的第一个节点,如果有多个节点符合条件,则只返回一个,其参数是 CSS 选择器。示例如下:
function main(splash)
splash:go("https://www.baidu.com/")
input = splash:select("#kw")
input:send_text('Splash')
splash:wait(3)
return splash:png()
end
这里我们首先访问百度官网,然后用 select 方法选中搜索框,随后调用 send_text 方法填写了文本,最后返回网页截图。运行结果如图 7-15 所示。
图7-15 运行结果
可以看到,我们成功填写了输入框。
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
这里我们通过 CSS 选择器选中了节点的正文内容,然后遍历所有节点,获取了其中的文本。
运行结果如下:
可以发现,我们成功获取了 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 秒,就会返回页面截图,如图 7-16 所示。
图7-16 mouse_click 方法的运行结果
可以看到,我们成功获取了查询后的页面内容,模拟了百度的搜索操作。
至此,Splash 对象的常用方法介绍完毕,还有一些方法这里不一一介绍了,更加详细和权威的说明可以参见官方文档 https://splash.readthedocs.io/en/stable/scripting-ref.html ,此页面介绍了 splash 对象的所有方法。另外,还有针对页面元素的方法,见官方文档 https://splash.readthedocs.io/en/stable/scripting element-object.html 。
调用 Splash 提供的 API
前面我们介绍了 Splash Lua 脚本的用法,但这些脚本是在 Splash 页面里测试运行的,如何才能利用 Splash 渲染页面?Splash 怎样才能和 Python 程序结合使用并爬取 JavaScript 渲染的页面?
其实,Splash 给我们提供了一些 HTTP API,我们只需要请求这些 API 并传递相应的参数即可获取页面染后的结果,下面我们学习这些 API。
render.html
此 API 用于获取 JavaScript 渲染的页面的 HTML 代码,API 地址是 Splash 的运行地址加上此 API 的名称,例如 http://localhost:8050/render.html ,我们可以用 curl 工具测试一下:
curl http://localhost:8050/render.html?url=https://www.baidu.com
我们给此 API 传递了一个 url 参数,以指定渲染的 URL,返回结果即为页面渲染后的源代码。
用 Python 实现的代码如下:
import requests
url = 'http://localhost:8050/render.html?url=https://www.baidu.com'
response = requests.get(url)
print(response.text)
这样就可以成功输出百度页面渲染后的源代码了。
此 API 还有其他参数,例如 wait,用来指定等待秒数。如果要求页面完全加载出来,就可以设置此参数,例如:
import requests
url = 'http://localhost:8050/render.html?url=https://www.taobao.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
此 API 用于获取页面截图,其参数比 render.html 要多几个,例如 width 和 height 用来控制截图的宽和高,返回值是 PNG 格式图片的二进制数据。示例如下:
curl http://localhost:8050/render.png?url=https://www.taobao.com&wait=5&width=1000&height=700
这里我们通过设置 width 和 height 参数,将页面截图的大小缩放为 1000×700 像素。
如果用 Python 实现,可以将其返回的二进制数据保存为 PNG 格式的图片,代码如下:
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('taobao.png', 'wb') as f:
f.write(response.content)
得到的图片如图 7-17 所示。
图7-17 用 Python 实现的运行结果
这样我们就成功获取了京东首页染完成后的页面截图,详细的参数设置可以参考官网文档 https://splash.readthedocs.io/en/stable/api.html#render-png 。
render.jpeg
此 API 和 render.png 类似,不过它返回的是 JPEG 格式图片的二进制数据。
另外,此 API 比 render.png 多一个参数 quality,该参数可以设置图片质量。
render.har
此 API 用于获取页面加载的 HAR 数据,示例如下:
curl http://localhost:8050/render.har?url=https://www.jd.com&wait=5
运行结果非常多,是一个 JSON 格式的数据,里面包含页面加载过程中的 HAR 数据,如图 7-18 所示。
图7-18 render.har 方法的运行结果
render.json
此 API 包含前面介绍的所有 render 相关的功能,返回值是 JSON 格式的数据,示例如下:
curl http://localhost:8050/render.json?url=https://www.httpbin.org
运行结果如下:
{"title": "httpbin(1): HTTP Client Testing Service", "url": "https://www.httpbin.org/", "requestedUrl": "https://www.httpbin.org/", "geometry": [0, 0, 1024, 768]}
可以看到,这里返回了 JSON 格式的请求数据。
我们可以通过传入不同的参数控制返回结果。例如,传入 html=1,返回结果会增加页面源代码;传入 png=1,返回结果会增加 PNG 格式的页面截图;传入 har=1,返回结果会增加页面的 HAR 数据。例如:
curl http://localhost:8050/render.json?url=https://www.httpbin.org&html=1&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 了。
先实现一个最简单的脚本,直接返回数据:
function main(splash))
return 'hello'
end
然后将此脚本转化为 URL 编码后的字符串,拼接在 execute 后面,示例如下:
curl http://localhost:8050/execute?lua_source=function+main%28splash%29%0A++return+%27hello%27%0Aend
运行结果如下:
hello
这里我们通过 lua_source 参数传递了转码后的 Lua 脚本,通过 execute 获取了脚本最终的执行结果。
我们更加关心的是如何用 Python 实现上述过程,如果用 Python 实现,那么代码如下:
import requests
from urllib.parse import import quote
lua = '''
function main(splash)
return 'hello'
end
'''
url = 'http://localhost:8050/execute?lua_source=' + quote(lua)
response = requests.get(url)
print(response.text)
运行结果如下:
hello
这里我们用 Python 中的三引号将 Lua 脚本括了起来,然后用 urllib.parse 模块里的 quote 方法对脚本进行 URL 转码,之后构造了请求 URL,并将其作为 lua_source 参数传递,这样运行结果就会显示 Lua 脚本执行后的结果。
我们再通过实例看下:
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 {
html=treat.as_string(response.body),
url=response.url,
status=response.status
}
end
'''
url = 'http://localhost:8050/execute?lua_source=' + quote(lua)
response = requests.get(url)
print(response.text)
运行结果如下:
可以看到,返回结果是 JSON 形式的,我们成功获取了请求 URL、状态码和页面源代码。
如此一来,之前所讲的 Lua 脚本就都可以用此方式与 Python 对接了,所有网页的动态染、模拟点击、表单提交、页面滑动、延时等待后的结果均可以自由控制获取细节,获取页面源代码和截图也都不在话下。
到现在为止,我们可以利用 Python 和 Splash 爬取 JavaScript 渲染的页面了。除了 Selenium,Splash 同样可以实现非常强大的疽染功能,同时它不需要浏览器便可疽染,使用起来非常方便。
负载均衡配置
用 Splash 爬取页面时,如果爬取的数据量非常大,任务非常多,那么只用一个 Splash 服务就会使压力非常大,此时可以考虑搭建一个负载均衡器把压力分散到多个服务器上,相当于多台机器、多个服务共同参与任务的处理,可以减小单个 Splash 服务的压力。
由于篇幅原因,请移步 https://setup.scrape.center/splash-loadbalance 查看具体的配置方法。