添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Splash 是一个 JavaScript 渲染服务,是一个带有 HTTP API 的轻量级浏览器,同时它对接了 Python 中的 Twisted 和 QT 库。利用它,我们同样可以实现动态渲染页面的抓取。

1. 功能介绍

利用 Splash,我们可以实现如下功能:

  • 异步方式处理多个网页渲染过程;
  • 获取渲染后的页面的源代码或截图;
  • 通过关闭图片渲染或者使用 Adblock 规则来加快页面渲染速度;
  • 可执行特定的 JavaScript 脚本;
  • 可通过 Lua 脚本来控制页面渲染过程;
  • 获取渲染的详细过程并通过 HAR(HTTP Archive)格式呈现。
  • 接下来,我们来了解一下它的具体用法。

    2. 准备工作

    在开始之前,请确保已经正确安装好了 Splash 并可以正常运行服务。如果没有安装,可以参考第 1 章。

    3. 实例引入

    首先,通过 Splash 提供的 Web 页面来测试其渲染过程。例如,我们在本机 8050 端口上运行了 Splash 服务,打开 http://localhost:8050/ 即可看到其 Web 页面,如图 7-6 所示。

    图 7-6 Web 页面

    在图 7-6 右侧,呈现的是一个渲染示例。可以看到,上方有一个输入框,默认是 http://google.com ,这里换成百度测试一下,将内容更改为 https://www.baidu.com ,然后点击 Render me 按钮开始渲染,结果如图 7-7 所示。

    图 7-7 运行结果

    可以看到,网页的返回结果呈现了渲染截图、HAR 加载统计数据、网页的源代码。

    通过 HAR 的结果可以看到,Splash 执行了整个网页的渲染过程,包括 CSS、JavaScript 的加载等过程,呈现的页面和我们在浏览器中得到的结果完全一致。

    那么,这个过程由什么来控制呢?重新返回首页,可以看到实际上是有一段脚本,内容如下:

    这个脚本实际上是用 Lua 语言写的脚本。即使不懂这个语言的语法,但从脚本的表面意思,我们也可以大致了解到它首先调用 go() 方法去加载页面,然后调用 wait() 方法等待了一定时间,最后返回了页面的源码、截图和 HAR 信息。

    到这里,我们大体了解了 Splash 是通过 Lua 脚本来控制了页面的加载过程的,加载过程完全模拟浏览器,最后可返回各种格式的结果,如网页源码和截图等。

    接下来,我们就来了解 Lua 脚本的写法以及相关 API 的用法。

    4. Splash Lua 脚本

    Splash 可以通过 Lua 脚本执行一系列渲染操作,这样我们就可以用 Splash 来模拟类似 Chrome、PhantomJS 的操作了。

    首先,我们来了解一下 Splash Lua 脚本的入口和执行方式。

    入口及返回值

    首先,来看一个基本实例:

    我们将代码粘贴到刚才打开的 http://localhost:8050/ 的代码编辑区域,然后点击 Render me!按钮来测试一下。

    我们看到它返回了网页的标题,如图 7-8 所示。这里我们通过 evaljs() 方法传入 JavaScript 脚本,而 document.title 的执行结果就是返回网页标题,执行完毕后将其赋值给一个 title 变量,随后将其返回。

    图 7-8 运行结果

    注意,我们在这里定义的方法名称叫作 main() 。这个名称必须是固定的,Splash 会默认调用这个方法。

    该方法的返回值既可以是字典形式,也可以是字符串形式,最后都会转化为 Splash HTTP Response,例如:

    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-9 所示。

    图 7-9 运行结果

    在脚本内调用的 wait() 方法类似于 Python 中的 sleep() ,其参数为等待的秒数。当 Splash 执行到此方法时,它会转而去处理其他任务,然后在指定的时间过后再回来继续处理。

    这里值得注意的是,Lua 脚本中的字符串拼接和 Python 不同,它使用的是 .. 操作符,而不是 + 。如果有必要,可以简单了解一下 Lua 脚本的语法,详见 http://www.runoob.com/lua/lua-basic-syntax.html

    另外,这里做了加载时的异常检测。 go() 方法会返回加载页面的结果状态,如果页面出现 4xx 或 5xx 状态码, ok 变量就为空,就不会返回加载后的图片。

    5. Splash 对象属性

    我们注意到,前面例子中 main() 方法的第一个参数是 splash ,这个对象非常重要,它类似于 Selenium 中的 WebDriver 对象,我们可以调用它的一些属性和方法来控制加载过程。接下来,先看下它的属性。

    该属性可以获取加载时配置的参数,比如 URL,如果为 GET 请求,它还可以获取 GET 请求参数;如果为 POST 请求,它可以获取表单提交的数据。Splash 也支持使用第二个参数直接作为 args ,例如:

    {
    "error": 400,
    "type": "ScriptError",
    "info": {
    "type": "JS_ERROR",
    "js_error_message": null,
    "source": "[string \"function main(splash, args)\r...\"]",
    "message": "[string \"function main(splash, args)\r...\"]:4: unknown JS error: None",
    "line_number": 4,
    "error": "unknown JS error: None",
    "splash_method": "evaljs"
    },
    "description": "Error happened while executing Lua script"
    }
    {
    "error": 400,
    "type": "ScriptError",
    "info": {
    "error": "network5",
    "type": "LUA_ERROR",
    "line_number": 3,
    "source": "[string \"function main(splash)\r...\"]",
    "message": "Lua error: [string \"function main(splash)\r...\"]:3: network5"
    },
    "description": "Error happened while executing Lua script"
    }

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

    images_enabled

    此属性可以设置图片是否加载,默认情况下是加载的。禁用该属性后,可以节省网络流量并提高网页加载速度。但是需要注意的是,禁用图片加载可能会影响 JavaScript 渲染。因为禁用图片之后,它的外层 DOM 节点的高度会受影响,进而影响 DOM 节点的位置。因此,如果 JavaScript 对图片节点有操作的话,其执行就会受到影响。

    另外值得注意的是,Splash 使用了缓存。如果一开始加载出来了网页图片,然后禁用了图片加载,再重新加载页面,之前加载好的图片可能还会显示出来,这时直接重启 Splash 即可。

    禁用图片加载的示例如下:

  • baseurl :可选参数,默认为空,表示资源加载相对路径。
  • headers :可选参数,默认为空,表示请求头。
  • http_method :可选参数,默认为 GET ,同时支持 POST
  • body :可选参数,默认为空,发 POST 请求时的表单数据,使用的 Content-type application/json
  • formdata :可选参数,默认为空,POST 的时候的表单数据,使用的 Content-type application/x-www-form-urlencoded
  • 该方法的返回结果是结果 ok 和原因 reason 的组合,如果 ok 为空,代表网页加载出现了错误,此时 reason 变量中包含了错误的原因,否则证明页面加载成功。示例如下:

    <html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">{
    "args": {},
    "data": "",
    "files": {},
    "form": {
    "name": "Germey"
    },
    "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en,*",
    "Connection": "close",
    "Content-Length": "11",
    "Content-Type": "application/x-www-form-urlencoded",
    "Host": "httpbin.org",
    "Origin": "null",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) splash Version/9.0 Safari/602.1"
    },
    "json": null,
    "origin": "60.207.237.85",
    "url": "http://httpbin.org/post"
    }
    </pre></body></html>
  • time :等待的秒数。
  • cancel_on_redirect :可选参数,默认为 false ,表示如果发生了重定向就停止等待,并返回重定向结果。
  • cancel_on_error :可选参数,默认为 false ,表示如果发生了加载错误,就停止等待。
  • 返回结果同样是结果 ok 和原因 reason 的组合。

    我们用一个实例感受一下:

    首先,我们声明了一个 JavaScript 定义的方法,然后在页面加载成功后调用了此方法计算出了页面中 div 节点的个数。

    关于 JavaScript 到 Lua 脚本的更多转换细节,可以参考官方文档: https://splash.readthedocs.io/en/stable/scripting-ref.html#splash-jsfunc

    evaljs()

    此方法可以执行 JavaScript 代码并返回最后一条 JavaScript 语句的返回结果,使用方法如下:

    这里我们设置了一个定时任务,0.2 秒的时候获取网页截图,然后等待 1 秒,1.2 秒时再次获取网页截图,访问的页面是淘宝,最后将截图结果返回。运行结果如图 7-11 所示。

    图 7-11 运行结果

    可以发现,第一次截图时网页还没有加载出来,截图为空,第二次网页便加载成功了。

    http_get()

    此方法可以模拟发送 HTTP 的 GET 请求,使用方法如下:

    Splash Response: Object
    html: String (length 355)
    {
    "args": {},
    "headers": {
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en,*",
    "Connection": "close",
    "Host": "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": "http://httpbin.org/get"
    }
    status: 200
    url: "http://httpbin.org/get"
  • headers :可选参数,默认为空,请求头。
  • follow_redirects :可选参数,表示是否启动自动重定向,默认为 true
  • body :可选参数,即表单数据,默认为空。
  • 我们用实例感受一下:

    function main(splash, args)
    local treat = require("treat")
    local json = require("json")
    local response = splash:http_post{"http://httpbin.org/post",
    body=json.encode({name="Germey"}),
    headers={["content-type"]="application/json"}
    }
    return {
    html=treat.as_string(response.body),
    url=response.url,
    status=response.status
    }
    end
    Splash Response: Object
    html: String (length 533)
    {
    "args": {},
    "data": "{\"name\": \"Germey\"}",
    "files": {},
    "form": {},
    "headers": {
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en,*",
    "Connection": "close",
    "Content-Length": "18",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) splash Version/9.0 Safari/602.1"
    },
    "json": {
    "name": "Germey"
    },
    "origin": "60.207.237.85",
    "url": "http://httpbin.org/post"
    }
    status: 200
    url: "http://httpbin.org/post"
    <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": "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://httpbin.org/get"
    }
    </pre></body></html>
    Splash Response: Array[2]
    0: Object
    domain: ".baidu.com"
    expires: "2085-08-21T20:13:23Z"
    httpOnly: false
    name: "BAIDUID"
    path: "/"
    secure: false
    value: "C1263A470B02DEF45593B062451C9722:FG=1"
    1: Object
    domain: ".baidu.com"
    expires: "2085-08-21T20:13:23Z"
    httpOnly: false
    name: "BIDUPSID"
    path: "/"
    secure: false
    value: "C1263A470B02DEF45593B062451C9722"
    <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": "httpbin.org",
    "User-Agent": "Splash"
    },
    "origin": "60.207.237.85",
    "url": "http://httpbin.org/get"
    }
    </pre></body></html>
    <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": "httpbin.org",
    "Site": "Splash",
    "User-Agent": "Splash"
    },
    "origin": "60.207.237.85",
    "url": "http://httpbin.org/get"
    }
    </pre></body></html>

    这里我们首先访问了百度,然后选中了搜索框,随后调用了 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
    Splash Response: Array[10]
    0: "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”"
    1: "“It is our choices, Harry, that show what we truly are, far more than our abilities.”"
    2: “There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”
    3: "“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”"
    4: "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”"
    5: "“Try not to become a man of success. Rather become a man of value.”"
    6: "“It is better to be hated for what you are than to be loved for what you are not.”"
    7: "“I have not failed. I've just found 10,000 ways that won't work.”"
    8: "“A woman is like a tea bag; you never know how strong it is until it's in hot water.”"
    9: "“A day without sunshine is like, you know, night.”"

    这里我们首先选中页面的输入框,输入了文本,然后选中“提交”按钮,调用了 mouse_click() 方法提交查询,然后页面等待三秒,返回截图,结果如图 7-16 所示。

    图 7-16 运行结果

    可以看到,这里我们成功获取了查询后的页面内容,模拟了百度搜索操作。

    前面介绍了 Splash 的常用 API 操作,还有一些 API 在这不再一一介绍,更加详细和权威的说明可以参见官方文档 https://splash.readthedocs.io/en/stable/scripting-ref.html ,此页面介绍了 Splash 对象的所有 API 操作。另外,还有针对页面元素的 API 操作,链接为 https://splash.readthedocs.io/en/stable/scripting-element-object.html

    7. Splash API 调用

    前面说明了 Splash Lua 脚本的用法,但这些脚本是在 Splash 页面中测试运行的,如何才能利用 Splash 渲染页面呢?怎样才能和 Python 程序结合使用并抓取 JavaScript 渲染的页面呢?

    其实 Splash 给我们提供了一些 HTTP API 接口,我们只需要请求这些接口并传递相应的参数即可,下面简要介绍这些接口。

    render.html

    此接口用于获取 JavaScript 渲染的页面的 HTML 代码,接口地址就是 Splash 的运行地址加此接口名称,例如 http://localhost:8050/render.html 。可以用 curl 来测试一下:

    此时得到响应的时间就会相应变长,比如这里会等待 5 秒多钟才能获取淘宝页面的源代码。

    另外,此接口还支持代理设置、图片加载设置、Headers 设置、请求方法设置,具体的用法可以参见官方文档 https://splash.readthedocs.io/en/stable/api.html#render-html

    render.png

    此接口可以获取网页截图,其参数比 render.html 多了几个,比如通过 width height 来控制宽高,它返回的是 PNG 格式的图片二进制数据。示例如下:

    得到的图片如图 7-17 所示。

    图 7-17 运行结果

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

    render.jpeg

    此接口和 render.png 类似,不过它返回的是 JPEG 格式的图片二进制数据。

    另外,此接口比 render.png 多了参数 quality ,它用来设置图片质量。

    render.har

    此接口用于获取页面加载的 HAR 数据,示例如下:

    这样返回的 JSON 结果会包含网页源代码和 HAR 数据。

    此外还有更多参数设置,具体可以参考官方文档: https://splash.readthedocs.io/en/stable/api.html#render-json

    execute

    此接口才是最为强大的接口。前面说了很多 Splash Lua 脚本的操作,用此接口便可实现与 Lua 脚本的对接。

    前面的 render.html 和 render.png 等接口对于一般的 JavaScript 渲染页面是足够了,但是如果要实现一些交互操作的话,它们还是无能为力,这里就需要使用 execute 接口了。

    我们先实现一个最简单的脚本,直接返回数据:

    这里我们用 Python 中的三引号将 Lua 脚本包括起来,然后用 urllib.parse 模块里的 quote() 方法将脚本进行 URL 转码,随后构造了 Splash 请求 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://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)
    {"url": "http://httpbin.org/get", "status": 200, "html": "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Accept-Language\": \"en,*\", \n    \"Connection\": \"close\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) splash Version/9.0 Safari/602.1\"\n  }, \n  \"origin\": \"60.207.237.85\", \n  \"url\": \"http://httpbin.org/get\"\n}\n"}

    可以看到,返回结果是 JSON 形式,我们成功获取了请求的 URL、状态码和网页源代码。

    如此一来,我们之前所说的 Lua 脚本均可以用此方式与 Python 进行对接,所有网页的动态渲染、模拟点击、表单提交、页面滑动、延时等待后的一些结果均可以自由控制,获取页面源码和截图也都不在话下。

    到现在为止,我们可以用 Python 和 Splash 实现 JavaScript 渲染的页面的抓取了。除了 Selenium,本节所说的 Splash 同样可以做到非常强大的渲染功能,同时它也不需要浏览器即可渲染,使用非常方便。

    2022 2048 ADSL API Ajax Bootstrap Bug CDN CQC CSS CSS 反爬虫 CV ChatGPT Cookie Django Eclipse Elasticsearch FTP Git GitHub HTML5 HTTP Hexo Hook IP IT JSON JSP JavaScript K8s LOGO Linux Luma MIUI Markdown Midjourney MongoDB MySQL Mysql NBA Nexior OCR OpenCV PHP PPT PS Pathlib PhantomJS Playwright Python Python 爬虫 Python3 Python3爬虫教程 Pythonic Python爬虫 Python爬虫书 Python爬虫教程 QQ RabbitMQ ReCAPTCHA Redis SAE SSH SVG Scrapy-redis Scrapy分布式 Selenium Session Shell Suno TKE TXT Terminal Ubuntu VS Code Vercel Vs Code Vue Web Webpack Web网页 Windows Winpcap WordPress XPath Youtube acedata aiohttp android ansible api chatgpt cocos2d-x dummy change e6 fitvids git json js逆向 kubernetes