可使用的lua库

当Splash的 沙盒模式 关闭时,可以支持所有的lua标准库,而当沙盒模式开启(默认开启)时,只有部分标准库可以调用。 您可以参阅 Standard Library 以获取更多信息

splash 默认提供多个非标准模块

  • json : 对json数据进行编码/解码
  • base64 : 对数据进行 Base64 编码/解码
  • treat : 将lua变量进行相应的类型转化,以便适应Splash [2]

与标准模块不同,扩展模块在使用前需要引用,例如

base64 = require("base64")
function main(splash)
    return base64.encode('hello')
end

您可以参阅 自定义lua模块 的内容,来为Splash添加更多lua模块

lua 标准库

当 splash 的沙盒模式开启时(默认),splash支持lua 5.2 版本中的下列标准库:

上述库是预先导入的,您不需要显示的调用 require

注解

目前当沙盒模式开启时,上述库中并不是的所有方法都可以使用。

json

json 库可以将一个数据序列化为json数据,或者将json数据反序列化为lua中的table结构。库中提供了两个方法 json.encodejson.decode

json.encode

将数据编码为JSON格式

原型: result = json.encode(obj)

参数:

  • obj: 将被转化为json格式的对象

返回值: 转化完成后的一个json字符串

目前不支持针对二进制数据的json格式化, json.encode 函数在处理二进制对象的时候,先将其使用Base64方式的加密

json.decode

将json对象反序列化为普通对象

原型: decoded = json.decode(s)

参数:

  • s: 一个json字符串

返回值: 反序列化完成之后产生的Lua对象

示例

json = require("json")

function main(splash)
    local resp = splash:http_get("http:/myapi.example.com/resource.json")
    local decoded = json.decode(resp.content.text)
    return {myfield=decoded.myfield}
end

请注意,不同于 json.encode 函数, json.decode 函数没有针对 二进制对象 的支持。这意味着如果您希望将之前使用 json.encode 得到的json数据还原成最初的二进制对象,您需要自己使用base64的方式进行解码,您可以通过使用 lua中的 base64 模块来完成这个操作

base64

使用base64方式对字符串进行编码/解码, 它提供了两个函数 base64.encodebase64.decode 。如果您需要在json请求或者响应中传递一些二进制数据,那么这些函数将会很有用

base64.encode

将一个字符串使用base64编码

原型: encoded = base64.encode(s)

参数:

返回值: s 通过base64编码后的字符串

base64.decode

将一个字符串通过base64解码

原型: data = base64.decode(s)

参数:

  • s: 需要进行解码的字符串

返回值: 解码后的lua字符串

请注意,base64.decode可能会返回一个非UTF-8格式的字符串,这个字符串在传入splash时可能不太安全 (作为 main 的返回值或者传入其他splash函数)。如果您知道原始数据是UTF-8格式或者ASCII格式会非常好,但是如果您不知道原始数据 的格式或者原始数据根本就不是UTF-8格式的,您可以在 base64.encoded 函数 的返回结果上调用 treat.as_binary

例子:返回一个黑色的 1x1 像素大小的gif图片

treat = require("treat")
base64 = require("base64")

function main(splash)
    local gif_b64 = "AQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw=="
    local gif_bytes = base64.decode(gif_b64)
    return treat.as_binary(gif_bytes, "image/gif")
end

treat

treat.as_binary

将字符串转化为 二进制对象

原型: bytes = treat.as_binary(s, content_type="application/octet-stream")

参数:

  • s: 字符串
  • content_type: s 的content_type值

返回值: 一个 二进制对象

treat.as_binary 返回一个二进制对象, 这个对象不能再在lua中做进一步处理,但是它可以作为main函数的返回值。

treat.as_string

二进制对象 的原始数据中得到一个字符串

原型: s, content_type = treat.as_string(bytes)

参数:

返回值: (s, content_type) 元组,转化后得到的字符串以及它对应的content_type值

treat.as_string 会“解开”一个 二进制对象 并得到一个普通的lua字符串,这个字符串可以被lua程序进一步处理。如果返回的字符串没有使用UTF-8进行解码,那么它仍然可以被lua 做进一步的处理,但是如果将其作为 main 函数的返回值或者作为其他splash函数的参数,将会很不安全,如果您想在Splash中 将其还原成二进制对象,请使用函数 treat.as_binary

treat.as_array

将lua中的table标记为数组(针对编码过后的json数据或者table 到js的转换)

原型: tbl = treat.as_array(tbl)

参数:

  • tbl: lua table

返回值: 与参数相同的table

json 可以表示数组和对象,但是在lua中没法对他们进行区分, 不管是键值对还是数组在lua中都是以table的格式存储。

默认情况下, 从Splash main 函数中返回结果,或者使用 json.encode 函数和 splash.jsfunc 函数时,Lua table将会转化为json

function main(splash)
    -- client gets {"foo": "bar"} JSON object
    return {foo="bar"}
end

使用类似于数组的lua table将会带来意想不到的结果

function main(splash)
    -- client gets {"1": "foo", "2": "bar"} JSON object
    return {"foo", "bar"}
end

treat.as_array 将table标记为 Json数组。

treat = require("treat")

function main(splash)
    local tbl = {"foo", "bar"}
    treat.as_array(tbl)

    -- client gets ["foo", "bar"] JSON object
    return tbl
end

此函数在其中修改其参数, 但是为了方便,它仍然返回原始的table对象,它允许我们简化代码

treat = require("treat")
function main(splash)
    -- client gets ["foo", "bar"] JSON object
    return treat.as_array({"foo", "bar"})
end

注解

针对table,每有什么方法可以检测它具体的类型,因为 {} 符号本身可以表示多种含义,它既可以作为json数组, 也可以作为json对象。当table的具体类型不确定的时候就容易产生一个错误的输出:即使某些数据总是一个数组,当数组为空 时,它可能会被当做一个对象, 为了避免这种错误,Splash提供了一个工具函数 treat.as_array

添加您自己的模块

Splash 提供了一个通过HTTP API等方式在脚本中使用自定义Lua模块的功能(存储在服务端)。这个功能允许:

  1. 重用代码,而不是通过网络一遍又一遍的发送
  2. 使用第三方lua模块
  3. 实现需要不安全代码的功能,并能够在沙盒中安全的使用它

注解

您可以访问 http://lua-users.org/wiki/ModulesTutorial 来 学习关于lua模块的知识, 请爱上最新的编写模块的方法,因为它会让您的模块在沙盒模式下更好的工作,这里有一个很好的lua 模块样式指南: http://hisham.hm/2014/01/02/how-to-write-lua-modules-in-a-post-module-world/

配置

请您进行如下配置,以便使用lua的自定义模块

  1. 配置lua模块的路径,让后在对应目录下添加您自己的模块
  2. 告诉Splash,在沙盒中可以使用哪些自定义模块
  3. 在lua脚本中使用 require 来导入一个库

您可以在启动Splash的时候加上参数 --lua-package-path 来指定lua库的路径。 --lua-package-path 的值应该是 以分号分隔的多个路径的字符串,每一个路径应该有一个 ? 它被用来替代模块名称

$ python3 -m splash.server --lua-package-path "/etc/splash/lua_modules/?.lua;/home/myuser/splash-modules/?.lua"

注解

如果您使用docker来安装splash,请参考 文件共享 这部分内容来获取与设置文件路径相关的内容

注解

--lua-package-path 中的值会被添加到 Lua的 package.path

当您使用 Lua 沙盒的时候 (默认情况下开启) .在脚本中 require 会 收到一定的限制, 它只能加载在白名单中的模块。白名单在默认情况下是空的,也就是说在默认情况下您不能加载任何东西,为了让 您的模块可用,您可以在启动Splash的时候设置 --lua-sandbox-allowed-modules 选项。它应该包含以分号为分隔符的字符串, 其中每个子串代表允许被加载的模块的名称

$ python3 -m splash.server --lua-sandbox-allowed-modules "foo;bar" --lua-package-path "/etc/splash/lua_modules/?.lua"

设置完成之后,您就可以通过 require 来加载您的自定义模块

local foo = require("foo")
function main(splash)
    return {result=foo.myfunc()}
end

模块的编写

一个基础的模块看起来像这样:

-- mymodule.lua
local mymodule = {}

function mymodule.hello(name)
    return "Hello, " .. name
end

return mymodule

在脚本中可以这样来使用

local mymodule = require("mymodule")

function main(splash)
    return mymodule.hello("world!")
end

在许多情况下,模块可能会使用 splash 对象,这里给出一些办法来编写这类模块。最简单的办法就是让 函数允许传入 splash 对象作为参数。

-- utils.lua
local utils = {}

-- wait until `condition` function returns true
function utils.wait_for(splash, condition)
    while not condition() do
        splash:wait(0.05)
    end
end

return utils

用法:

local utils = require("utils")

function main(splash)
    splash:go(splash.args.url)

    -- wait until <h1> element is loaded
    utils.wait_for(splash, function()
       return splash:evaljs("document.querySelector('h1') != null")
    end)

    return splash:html()
end

另一个编写此类模块的方法是,往一个 splash 对象中添加对应的方法。您可以通过向 Splash 类来添加方法。 这种方法在Ruby中被称之为 “打开类”,而在python中被叫做补丁。

-- wait_for.lua

-- Sandbox is not enforced in custom modules, so we can import
-- internal Splash class and change it - add a method.
local Splash = require("splash")

function Splash:wait_for(condition)
    while not condition() do
        self:wait(0.05)
    end
end

-- no need to return anything

用法:

require("wait_for")

function main(splash)
    splash:go(splash.args.url)

    -- wait until <h1> element is loaded
    splash:wait_for(function()
       return splash:evaljs("document.querySelector('h1') != null")
    end)

    return splash:html()
end

具体使用哪种风格,取决于您的个人喜好,函数的方式更加明确,更加容易组合。使用补丁的方式可以使代码更加紧凑,无论您使用哪种 办法来向 splash 对象中添加函数,都可以直接使用 require

如前面的例子所示。 标准的lua模块和函数的沙盒限制不适用于自定义的Lua模块。换句话说自定义的lua模块不受限制,您可以发挥 出lua的全部功效,这将令我们可以使用第三发模块,利用lua来写出更加高级的功能。但是在使用的时候需要特别小心。 例如让我们来使用 os 模块

-- evil.lua
local os = require("os")
local evil = {}

function evil.sleep()
    -- Don't do this! It blocks the event loop and has a startup cost.
    -- splash:wait is there for a reason.
    os.execute("sleep 2")
end

function evil.touch(filename)
    -- another bad idea
    os.execute("touch " .. filename)
end

-- todo: rm -rf /

return evil
[2]这段话的原文是 “fine-tune the way Splash works with your Lua varaibles and returns the result” 这里为了理解方便采用意译