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 - 1io.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;
}

Lua_第28章 资源管理(下)相关推荐

  1. python第一章(下)

    python系列 python第一章(上) python第二章 python第三章(上) python第三章(下) 字符串 python系列 一.字符串 定义方式 拼接 索引与切片 切片 设置取子串顺 ...

  2. 【STM32F407的DSP教程】第28章 FFT和IFFT的Matlab实现(幅频响应和相频响应)

    完整版教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=94547 第28章       FFT和IFFT的Matlab实现(幅 ...

  3. 第28章 Spring框架内的JNDI支持

    第28章 Spring框架内的JNDI支持 本章内容 JNDI简单回顾 Spring框架内JNDI访问的基石--JndiTemplate JNDI对象的依赖注入--JndiObjectFactoryB ...

  4. 《Kotlin从小白到大牛》第28章:项目实战1:开发PetStore宠物商店项目

    第28章 项目实战1:开发PetStore宠物商店项目 前面学习的Kotlin知识只有通过项目贯穿起来,才能将书本中知识变成自己的.通过项目实战读者能够了解软件开发流程,了解所学知识在实际项目中使用的 ...

  5. CDISC的ADaMIG (V1.2) 中英文对照【4】_第四章(下)实施问题,标准解决方案和示例

    本AdaMIG (v1.2)来自CDISC官网以下链接: https://www.cdisc.org/standards/foundational/adam/adam-implementation-g ...

  6. 【STM32H7】第28章 ThreadX GUIX滚轮控件实现参数调节

    最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=98429 第28章       ThreadX GUIX滚轮控件实现参数调节 ...

  7. 第7章 Linux下的文件编程(一)

    很久没有发文章了,这次把Linux系统下的文件编程整理了一下,太久不写的话,人会变懒的所以还是得坚持哈. Linux下的文件编程 第7章 Linux下的文件编程(一) 7.1 概述 7.1.1 Lin ...

  8. 【RL-TCPnet网络教程】第28章 RL-TCPnet之DNS应用

    第28章      RL-TCPnet之DNS应用 本章节为大家讲解RL-TCPnet的DNS应用,学习本章节前,务必要优先学习第27章的DNS基础知识.有了这些基础知识之后,再搞本章节会有事半功倍的 ...

  9. 第二章课下测试补交博客

    第二章课下测试补交博客 转载于:https://www.cnblogs.com/WYjingheng/p/8017802.html

  10. mysql linux 安装_mysql-5.7.28 在Linux下的安装教程图解

    2.上传tar包到服务器到 /usr/local/src 3.卸载系统自动的Mariadb rpm -qa | grep mariadb rpm -e --nodeps mariadb-libs-5. ...

最新文章

  1. MF+Matrix Factorization+矩阵分解
  2. 去掉 Idea 中注入 Mapper 警告的方法
  3. 大创idea2018-03-30
  4. 海洋CMS仿7KB影视电影在线播放网站模板
  5. gin mongodb restful api设计: 动态的patch接口
  6. CSS demo:flaot amp; clear float
  7. 后台异常引起前端提示跨域出错
  8. TDD测试驱动开发案例【水货】
  9. exfat默认配置大小_如何分配U盘exFAT格式单元大小保证速度和空间呢
  10. 打印插件Lodop响应慢、卡顿问题分析与解决方案以及常见问题
  11. Linux服务器间如何进行文件同步
  12. 使用webpack打包nodejs 后台端环境|NodeJs 打包后台代码
  13. 浅谈《守望先锋》中的 ECS 构架
  14. 1 什么是末端柔顺控制?
  15. #includecstring
  16. 关于单边账的解释及解决(收单行业)
  17. Linux:chmod命令
  18. 主流的企业级虚拟化解决方案
  19. 骑砍2 游戏文件修改漫谈
  20. 阅读Hierarchical Graph Representation Learning with Differentiable Pooling(NeurIPS 2018)

热门文章

  1. 从西直门立交桥谈IT架构与重构
  2. TC397 MCMCAN
  3. 服务器显示日志已满,解决db2事务日志已满及日志磁盘空间已满问题办法详解
  4. 带色彩恢复的视网膜增强算法实现 (MATLAB版本)
  5. tags与categories
  6. HTB_Secret
  7. 四通一达归于阿里后就涨价,证明资本的目标就是以垄断攫取利润
  8. PHP全国快递寄件接口,1天接入四通一达,极兔,宅急送,德邦,京东,天天
  9. 【隔离的CAN通信接口-1Mbps】
  10. 人工智能的基础--知识分类