29.2 XML 解析
现在,我们将要看到一个xml解析器的简单实现,称为lxp(估计是lua xml parser的简写) ,它包括了Lua和Expat。Expat是一个开源的C语言写成的XML 1.0 的解析器。它实现 了SAXC,SAX是XML简单的API,是基于事件的API,这意 味着一个SAX解析器读取有一个XML文档,然后反馈给应用程序他所发现的。举个例子, 我们要通知Expat解析这样一个字符串:
<tag cap="5">hi</tag>
它将会产生三个事件:当它读取子字符串 "<tag cap="5">hi</tag>",产生一个读取到开始元素的事件;当它解析 "hi" 时,产生一个读取文本事件(有时也称为字符数据事件);当解析 "end" 时,产生一个读取结束元素的事件。而每个事件,都会调用应用 程序适当的句柄。
这里我们不会涉及到整个 Expat 库,我们只会集中精力关注那些能够阐明和 Lua 相 互作用的新技术的部分。当我们实现了核心功能后,在上面进行扩展将会变得很容易。虽然 Expat 解析 XML 文档时会有很多事件,我们将会关心的仅仅是上面例子提到的三个事件(开始元素,结束元素,文本数据),我们需要调用的 API 是 Expat 众多 API 中 很少的几个。首先,我们需要创建和析构 Expat 解析器的函数:
#include <xmlparse.h> XML_Parser XML_ParserCreate (const char *encoding); void XML_ParserFree (XML_Parser p);
这里函数参数是可选的;在我们的使用中,我们直接选用NULL 作为参数。当我们 有了一个解析器的时候,我们必须注册回调的句柄:
XML_SetElementHandler(XML_Parser p,XML_StartElementHandlerstart, XML_EndElementHandler end); XML_SetCharacterDataHandler(XML_Parser p,XML_CharacterDataHandler hndl);
第一个函数登记了开始元素和结束元素的句柄。第二个函数登记了文本数据(在 XML 语法中的字符数据)的句柄。所有回掉的句柄通过第一个参数接收用户数据。开始 元素的句柄同样接收到标签的名称和它的属性作为参数:
typedef void (*XML_StartElementHandler)(void *uData,const char *name,const char **atts);
这些属性来自于以 '\0' 结束的字符串组成的数组,这些字符串分别对应了一对以属 性名和属性值组成的属性。结束元素的句柄只有一个参数,就是标签名。
typedef void (*XML_EndElementHandler)(void *uData,const char *name)
最终,一个文本句柄仅仅以字符串作为额外的参数。该文本字符串不能是以'\0'结束 的字符串,而是显式指明长度的字符串:
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,他表示这部分是不是XML文档的最后一个部分了。需要注意的是,不是每段文本都需要通过 0来表示结束,我们也可以通过显实的长度来判定。XML_Parse 函数如果发现解析错误就会返回一个 0(expat 也提供了辅助的函数来显示错误信息,但 是因为简单的缘故,我们这里都将之忽略掉)。我们需要Expat 的最后一个函数是允许我 们设置将要传给句柄的用户数据的函数:
void XML_SetUserData (XML_Parser p, void *uData);
好了,现在我们来看一下如何在 Lua 中使用 Expat 库。第一种方法也是最直接的一 种方法:简单的在 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 inputlines assert(p:parse(l)) -- parse the line assert(p:parse("\n")) -- add anewline end assert(p:parse()) --finish document p:close()
现在,让我们把注意力集中到实现中来。首先,考虑如何在 Lua中实现解析器。很自然的会想到使用 userdatum,但是我们将什么内容放在userdata 里呢?至少,我们必须保留实际的 Expat 解析器和一个回调表。我们不能将一个 Lua 表保存在一个 userdatum(或 者在任何的 C 结构中),然而,我们可以创建一个指向表的引用,并将这个引用保存在 userdatum 中。(我们在 26.3.2 节己经说过,一个引用就是 Lua 自动产生的在 registry 中 的一个整数)最后,我们还必须能够将 Lua 的状态保存到一个解析器对象中,因为这些解析器对象就是 Expat回调从我们程序中接受的所有内容,并且这些回调需要调用 Lua。 一个解析器的对象的定义如下:
#include <xmlparse.h> typedef struct lxp_userdata { lua_State *L; XML_Parser *parser; /* associated expat parser */ int tableref; /* tablewith callbacks forthis parser */ } lxp_userdata;
下面是创建解析器对象的函数:
static int lxp_make_parser (lua_State *L) { XML_Parser p; lxp_userdata *xpu; /* (1) createa parser object*/ xpu =(lxp_userdata *)lua_newuserdata(L,sizeof(lxp_userdata)); /* pre-initialize it, in caseof errors */ xpu->tableref = LUA_REFNIL; xpu->parser = NULL; /* set its metatable */ luaL_getmetatable(L, "Expat"); lua_setmetatable(L, -2); /* (2) createthe Expat parser*/ p =xpu->parser = XML_ParserCreate(NULL); if (!p) luaL_error(L, "XML_ParserCreate failed"); /* (3) createand store reference to callback table*/ luaL_checktype(L, 1,LUA_TTABLE); lua_pushvalue(L, 1); /* put table on thestack 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 有四个主要步骤:
第一步遵循共同的模式:首先创建一个userdatum,然后使用 consistent 的值预初始 化 userdatum,最后设置 userdatum 的 metatable。预初始化的原因在于:如果在初始化的时候有任何错误的话,我们必须保证析构器(_gc 元方法)能够发现在可靠状态下发现 userdata 并释放资源。
第二步,函数创建一个 Expat 解析器,将它保存在 userdatum 中,并检测错误。
第三步,保证函数的第一个参数是一个表(回调表),创建一个指向表的引用,并将这个引用保存到新的 userdatum 中。
第四步,初始化 Expat解析器,将 userdatum 设置为将要传递给回调函数的对象,并 设置这些回调函数。注意,对于所有的解析器来说这些回调函数都一样。毕竟,在 C中不可能动态的创建新的函数,取代的方法是,这些固定的C 函数使用回调表来决定每次应该调用哪个 Lua 函数。
下一步是解析方法,负责解析一段 XML 数据。他有两个参数:解析器对象(方法自己)和一个可选的一段 XML 数据。当没有数据调用这个方法时,他通知 Expat 文档己经 解析结束:
static int lxp_parse (lua_State *L) { int status; size_t len; const char *s; lxp_userdata *xpu; /* get and checkfirst argument (shouldbe a parser) */ xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat"); luaL_argcheck(L, xpu, 1, "expat parser expected"); /* get secondargument (a string)*/ s =luaL_optlstring(L, 2, NULL,&len); /* prepare environment for handlers: */ /* put callbacktable at stackindex 3 */ lua_settop(L,2); lua_getref(L, xpu->tableref); xpu->L = L; /*set Lua state*/ /* call Expatto parse string*/ status =XML_Parse(xpu->parser, s, (int)len, s == NULL); /* return errorcode */ lua_pushboolean(L, status); return 1; }
当 lxp_parse调用 XML_Parse的时候,后一个函数将会对在给定的一段 XML数据中找到的所有元素,分别调用这些元素对应的句柄。所以,lxp_parse 会首先为这些句柄准备环境,在调用XML_Parse 的时候有一些细节:记住这个函数的最后一个参数告诉 Expat 给定的文本段是否是最后一段。当我们不带参数调用他时,s 将使用缺省的 NULL,因此这时候最后一个参数将为 true。现在让我们注意力集中到回调函数 f_StartElement、 f_EndElement 和 f_CharData 上,这三个函数有相似的结构:每一个都会针对他的指定事件检查 callback 表是否定义了 Lua 句柄,如果有,预处理参数然后调用这个 Lua 句柄。
我们首先来看 f_CharData句柄,他的代码非常简单。她调用他对应的 Lua 中的句柄 (当存在的时候),带有两个参数:解析器 parser 和字符数据(一个字符串)
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); /* callthe handler */ }
注意,由于当我们创建解析器的时候调用了 XML_SetUserData,所以,所有的 C 句柄都接受 lxp_userdata 数据结构作为第一个参数。还要注意程序是如何使用由 lxp_parse设置的环境的。首先,他假定callback 表在栈中的索引为 3;第二,假定解析器 parser 在栈中索引为 1(parser 的位置肯定是这样的,因为她应该是 lxp_parse的第一个参数)。
f_EndElement 句柄和 f_CharData 类似,也很简单。他也是用两个参数调用相应的Lua 句柄:一个解析器 parser 和一个标签名(也是一个字符串,但现在是以 '\0' 结尾):
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 tagname */ lua_call(L, 2, 0); /* callthe handler */ }
最后一个句柄 f_StartElement 带有三个参数:解析器 parser,标签名,和一个属性列表。这个句柄比上面两个稍微复杂点,因为它需要将属性的标签列表翻译成 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 andfill 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 thehandler */ }
解析器的最后一个方法是 close。当我们关闭一个解析器的时候,我们必须释放解析器对应的所有资源,即 Expat 结构和 callback 表。记住,在解析器创建的过程中如果发生错误,解析器并不拥有这些资源:
static int lxp_close (lua_State *L) { lxp_userdata *xpu; xpu = (lxp_userdata *)luaL_checkudata(L, 1,"Expat"); luaL_argcheck(L, xpu, 1, "expat parserexpected"); /* free (unref)callback table */ luaL_unref(L,LUA_REGISTRYINDEX,xpu->tableref); xpu->tableref = LUA_REFNIL; /* free Expatparser (if thereis one) */ if (xpu->parser)XML_ParserFree(xpu->parser); xpu->parser =NULL; return 0; }
注意我们在关闭解析器的时候,是如何保证它处于一致的(consistent)状态的,当我们对一个己经关闭的解析器或者垃圾收集器己经收集这个解析器之后,再次关闭这个解 析器是没有问题的。实际上,我们使用这个函数作为我们的析构函数。他负责保证每一个解析器自动得释放他所有的资源,即使程序员没有关闭解析器。
最后一步是打开库,将上面各个部分放在一起。这儿我们使用和面向对象的数组例 子(27.3 节)一样的方案:创建一个 metatable,将所有的方法放在这个表内,表的_index 域指向自己。这样,我们还需要一个解析器方法的列表:
static const structluaL_reg lxp_meths[] = { {"parse", lxp_parse}, {"close", lxp_close}, {"_gc", lxp_close}, {NULL, NULL} };
我们也需要一个关于这个库中所有函数的列表。和 OO 库相同的是,这个库只有一 个函数,这个函数负责创建一个新的解析器:
static const structluaL_reg lxp_funcs[ ] = { {"new",lxp_make_parser}, {NULL, NULL} };
最终,open 函数必须要创建 metatable,并通过 _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 (onlylxp.new) */ luaL_openlib (L, "lxp", lxp_funcs, 0); return 1; }