第一版是针对 Lua 5.0 编写的。虽然在很大程度上仍然适用于后续版本,但有一些差异。
第四版针对 Lua 5.3,可在 亚马逊 和其他书店购买。
购买本书,您还可以帮助支持 Lua 项目


29.2 – XML 解析器

现在,我们将了解 lxp 的简化实现,它是 Lua 和 Expat 之间的绑定。Expat 是用 C 编写的开源 XML 1.0 解析器。它实现了 SAX,即 XML 的简单 API。SAX 是基于事件的 API。这意味着 SAX 解析器读取 XML 文档,并在读取过程中通过回调向应用程序报告它找到的内容。例如,如果我们指示 Expat 解析类似这样的字符串

    <tag cap="5">hi</tag>
它将生成三个事件:一个 start-element 事件,当它读取子字符串 "<tag cap="5">" 时;一个 text 事件(也称为 character data 事件),当它读取 "hi" 时;一个 end-element 事件,当它读取 "</tag>" 时。这些事件中的每一个都会在应用程序中调用一个适当的 回调处理程序

这里我们不会介绍整个 Expat 库。我们只关注那些说明了与 Lua 交互的新技术的那些部分。在实现此核心功能后,稍后很容易添加花里胡哨的东西。尽管 Expat 处理了十几个不同的事件,但我们只考虑在前一个示例中看到的三个事件(开始元素、结束元素和文本)。我们在此示例中需要的 Expat API 部分很小。首先,我们需要创建和销毁 Expat 解析器的函数

    #include <xmlparse.h>
    
    XML_Parser XML_ParserCreate (const char *encoding);
    void XML_ParserFree (XML_Parser p);
参数 encoding 是可选的;我们将在我们的绑定中使用 NULL

在拥有解析器后,我们必须注册其回调处理程序

    XML_SetElementHandler(XML_Parser p,
                          XML_StartElementHandler start,
                          XML_EndElementHandler end);
    
    XML_SetCharacterDataHandler(XML_Parser p,
                                XML_CharacterDataHandler hndl);
第一个函数注册开始元素和结束元素的处理程序。第二个函数注册文本的处理程序(在 XML 术语中称为 character data)。

所有回调处理程序都将一些用户数据作为其第一个参数接收。开始元素处理程序还接收标签名称及其属性

    typedef void (*XML_StartElementHandler)(void *uData,
                                            const char *name,
                                            const char **atts);
属性以 NULL 终止的字符串数组形式出现,其中连续两个字符串包含一个属性名称及其值。结束元素处理程序只有一个额外的参数,即标签名称
    typedef void (*XML_EndElementHandler)(void *uData,
                                          const char *name);
最后,文本处理程序只接收文本作为额外参数。此文本字符串不是以空值终止的;相反,它具有显式长度
    typedef void
    (*XML_CharacterDataHandler)(void *uData,
                                const char *s,
                                int len);

要向 Expat 馈送文本,我们使用以下函数

    int XML_Parse (XML_Parser p,
                   const char *s, int len, int isFinal);
Expat 通过连续调用 XML_Parse 以片段形式接收要解析的文档。XML_Parse 的最后一个参数 isFinal 告知 Expat 该片段是否是文档的最后一个片段。请注意,每一段文本不需要以零终止;相反,我们提供一个显式长度。如果 XML_Parse 函数检测到解析错误,则返回零。(Expat 提供辅助函数来检索错误信息,但为了简单起见,我们在这里将忽略它们。)

我们从 Expat 需要的最后一个函数允许我们设置将传递给处理程序的用户数据

    void XML_SetUserData (XML_Parser p, void *uData);

现在让我们看看如何在 Lua 中使用此库。第一种方法是直接方法:只需将所有这些函数导出到 Lua。更好的方法是将功能调整为 Lua。例如,因为 Lua 是无类型的,所以我们不需要不同的函数来设置每种类型的回调。更好的是,我们可以完全避免回调注册函数。相反,当我们创建一个解析器时,我们提供一个回调表,其中包含所有回调处理程序,每个处理程序都有一个适当的键。例如,如果我们只想打印文档的布局,我们可以使用以下回调表

    local count = 0
    
    callbacks = {
      StartElement = function (parser, tagname)
        io.write("+ ", string.rep("  ", count), tagname, "\n")
        count = count + 1
      end,
    
      EndElement = function (parser, tagname)
        count = count - 1
        io.write("- ", string.rep("  ", count), tagname, "\n")
      end,
    }
使用输入 "<to> <yes/> </to>",这些处理程序将打印
    + to
    +   yes
    -   yes
    - to
使用此 API,我们不需要函数来操作回调。我们直接在回调表中操作它们。因此,整个 API 只需要三个函数:一个用于创建解析器,一个用于解析一段文本,一个用于关闭解析器。(实际上,我们将最后两个函数实现为解析器对象的函数。)API 的典型用法可以如下所示
    p = lxp.new(callbacks)     -- create new parser
    for l in io.lines() do     -- iterate over input lines
      assert(p:parse(l))               -- parse the line
      assert(p:parse("\n"))            -- add a newline
    end
    assert(p:parse())        -- finish document
    p:close()


现在让我们把注意力转向实现。第一个决定是如何在 Lua 中表示解析器。使用用户数据是很自然的,但我们需要在其中放入什么?至少,我们必须保留实际的 Expat 解析器和回调表。我们不能将 Lua 表存储在用户数据中(或任何 C 结构中);但是,我们可以创建一个对表的引用,并将引用存储在用户数据中。(请从 第 27.3.2 节 中记住,引用是注册表中 Lua 生成的整数键。)最后,我们必须能够将 Lua 状态存储到解析器对象中,因为这些解析器对象是我们程序从 Expat 回调接收的所有内容,并且回调需要调用 Lua。因此,解析器对象的定义如下

    #include <xmlparse.h>
    
    typedef struct lxp_userdata {
      lua_State *L;
      XML_Parser *parser;          /* associated expat parser */
      int tableref;   /* table with callbacks for this parser */
    } lxp_userdata;

下一步是创建解析器对象的函数。如下所示

    static int lxp_make_parser (lua_State *L) {
      XML_Parser p;
      lxp_userdata *xpu;
    
      /* (1) create a parser object */
      xpu = (lxp_userdata *)lua_newuserdata(L,
                                       sizeof(lxp_userdata));
    
      /* pre-initialize it, in case of errors */
      xpu->tableref = LUA_REFNIL;
      xpu->parser = NULL;
    
      /* set its metatable */
      luaL_getmetatable(L, "Expat");
      lua_setmetatable(L, -2);
    
      /* (2) create the Expat parser */
      p = xpu->parser = XML_ParserCreate(NULL);
      if (!p)
        luaL_error(L, "XML_ParserCreate failed");
    
      /* (3) create and store reference to callback table */
      luaL_checktype(L, 1, LUA_TTABLE);
      lua_pushvalue(L, 1);  /* put table on the stack top */
      xpu->tableref = luaL_ref(L, LUA_REGISTRYINDEX);
    
      /* (4) configure Expat parser */
      XML_SetUserData(p, xpu);
      XML_SetElementHandler(p, f_StartElement, f_EndElement);
      XML_SetCharacterDataHandler(p, f_CharData);
      return 1;
    }
lxp_make_parser 函数有四个主要步骤

下一步是 parse 方法,它解析一段 XML 数据。它得到两个参数:解析器对象(方法的 self)和一段可选的 XML 数据。当在没有任何数据的情况下调用它时,它会通知 Expat 文档没有更多部分

    static int lxp_parse (lua_State *L) {
      int status;
      size_t len;
      const char *s;
      lxp_userdata *xpu;
    
      /* get and check first argument (should be a parser) */
      xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
      luaL_argcheck(L, xpu, 1, "expat parser expected");
    
      /* get second argument (a string) */
      s = luaL_optlstring(L, 2, NULL, &len);
    
      /* prepare environment for handlers: */
      /* put callback table at stack index 3 */
      lua_settop(L, 2);
      lua_getref(L, xpu->tableref);
      xpu->L = L;  /* set Lua state */
    
      /* call Expat to parse string */
      status = XML_Parse(xpu->parser, s, (int)len, s == NULL);
    
      /* return error code */
      lua_pushboolean(L, status);
      return 1;
    }
lxp_parse 调用 XML_Parse 时,后一个函数将为它在给定文档片段中找到的每个相关元素调用处理程序。因此,lxp_parse 首先为这些处理程序准备一个环境。在对 XML_Parse 的调用中还有另一个细节:请记住,此函数的最后一个参数告诉 Expat 给定的文本片段是否是最后一个。当我们调用 parse 而没有参数时,s 将为 NULL,因此最后一个参数将为 true。

现在让我们将注意力转向回调函数 f_StartElementf_EndElementf_CharData。这三个函数都有类似的结构:每个函数都检查回调表是否为其特定事件定义了 Lua 处理程序,如果定义了,则准备参数,然后调用该 Lua 处理程序。

让我们首先看看 f_CharData 处理程序。它的代码非常简单。它只使用两个参数(解析器和字符数据(一个字符串))调用 Lua 中的相应处理程序(如果存在)

    static void f_CharData (void *ud, const char *s, int len) {
      lxp_userdata *xpu = (lxp_userdata *)ud;
      lua_State *L = xpu->L;
    
      /* get handler */
      lua_pushstring(L, "CharacterData");
      lua_gettable(L, 3);
      if (lua_isnil(L, -1)) {  /* no handler? */
        lua_pop(L, 1);
        return;
      }
    
      lua_pushvalue(L, 1);  /* push the parser (`self') */
      lua_pushlstring(L, s, len);  /* push Char data */
      lua_call(L, 2, 0);  /* call the handler */
    }
请注意,由于我们在创建解析器时调用了 XML_SetUserData,所有这些 C 处理程序都将 lxp_userdata 结构作为其第一个参数接收。还要注意它如何使用由 lxp_parse 设置的环境。首先,它假设回调表位于堆栈索引 3。其次,它假设解析器本身位于堆栈索引 1(它必须在那里,因为它应该是 lxp_parse 的第一个参数)。

f_EndElement 处理程序也很简单,与 f_CharData 非常相似。它还使用两个参数调用其相应的 Lua 处理程序:解析器和标记名称(仍然是字符串,但现在以 null 结尾)。

    static void f_EndElement (void *ud, const char *name) {
      lxp_userdata *xpu = (lxp_userdata *)ud;
      lua_State *L = xpu->L;
    
      lua_pushstring(L, "EndElement");
      lua_gettable(L, 3);
      if (lua_isnil(L, -1)) {  /* no handler? */
        lua_pop(L, 1);
        return;
      }
    
      lua_pushvalue(L, 1);  /* push the parser (`self') */
      lua_pushstring(L, name);  /* push tag name */
      lua_call(L, 2, 0);  /* call the handler */
    }

最后一个处理程序 f_StartElement 使用三个参数调用 Lua:解析器、标记名称和属性列表。此处理程序比其他处理程序复杂一些,因为它需要将标记的属性列表转换为 Lua。我们将使用一个非常自然的转换。例如,类似这样的开始标记

    <to method="post" priority="high">
生成以下属性表
    { method = "post", priority = "high" }
f_StartElement 的实现如下
    static void f_StartElement (void *ud,
                                const char *name,
                                const char **atts) {
      lxp_userdata *xpu = (lxp_userdata *)ud;
      lua_State *L = xpu->L;
    
      lua_pushstring(L, "StartElement");
      lua_gettable(L, 3);
      if (lua_isnil(L, -1)) {  /* no handler? */
        lua_pop(L, 1);
        return;
      }
    
      lua_pushvalue(L, 1);  /* push the parser (`self') */
      lua_pushstring(L, name);  /* push tag name */
    
      /* create and fill the attribute table */
      lua_newtable(L);
      while (*atts) {
        lua_pushstring(L, *atts++);
        lua_pushstring(L, *atts++);
        lua_settable(L, -3);
      }
    
      lua_call(L, 3, 0);  /* call the handler */
    }

解析器的最后一个方法是 close。当我们关闭解析器时,我们必须释放其所有资源,即 Expat 结构和回调表。请记住,由于创建期间偶尔会出现错误,解析器可能没有这些资源。

    static int lxp_close (lua_State *L) {
      lxp_userdata *xpu;
    
      xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
      luaL_argcheck(L, xpu, 1, "expat parser expected");
    
      /* free (unref) callback table */
      luaL_unref(L, LUA_REGISTRYINDEX, xpu->tableref);
      xpu->tableref = LUA_REFNIL;
    
      /* free Expat parser (if there is one) */
      if (xpu->parser)
        XML_ParserFree(xpu->parser);
      xpu->parser = NULL;
      return 0;
    }
请注意,我们在关闭解析器时如何保持其处于一致状态,因此如果我们尝试再次关闭它或当垃圾回收器完成它时,就不会出现问题。实际上,我们将使用此函数作为完成器。这确保了每个解析器最终都会释放其资源,即使程序员没有关闭它。

最后一步是打开库,将所有这些部分放在一起。我们将在面向对象数组示例(第 28.3 节)中使用与这里相同的方案:我们将创建一个元表,将所有方法放入其中,并使其 __index 字段指向自身。为此,我们需要一个包含解析器方法的列表

    static const struct luaL_reg lxp_meths[] = {
      {"parse", lxp_parse},
      {"close", lxp_close},
      {"__gc", lxp_close},
      {NULL, NULL}
    };
我们还需要一个包含此库的函数的列表。与面向对象库常见的一样,此库有一个函数,用于创建新解析器
    static const struct luaL_reg lxp_funcs[] = {
      {"new", lxp_make_parser},
      {NULL, NULL}
    };
最后,open 函数必须创建元表,使其通过 __index 指向自身,并注册方法和函数
    int luaopen_lxp (lua_State *L) {
      /* create metatable */
      luaL_newmetatable(L, "Expat");
    
      /* metatable.__index = metatable */
      lua_pushliteral(L, "__index");
      lua_pushvalue(L, -2);
      lua_rawset(L, -3);
    
      /* register methods */
      luaL_openlib (L, NULL, lxp_meths, 0);
    
      /* register functions (only lxp.new) */
      luaL_openlib (L, "lxp", lxp_funcs, 0);
      return 1;
    }