上个月Java 技术讲师 Sam Pullara 向我演示了他最新的支持 Java 的电话 Nokia 这个手机使用了全面的技术 —— 嵌入式 JVMGPRS 和蓝牙但是它也遭遇了所有智能手机都苦恼的问题 —— 有限的屏幕实际使用区有些 Web 站点支持基于手机的浏览器而且嵌入式浏览器也试图在小小的屏幕上有效地渲染页面但是在电话屏幕上查看典型的 Web 页面就像要把一头大象强行塞进车后座一样(其中的每个参与者都会感到失望包括您车和大象)Sam 构建了一个简单的优雅的解决方案从他喜欢的 Web 站点上对数据进行屏幕搜集然后把数据重新格式化在小屏幕上显示
新方法
从 HTML 文档提取数据的方法有许多种但是我真的很喜欢 Sam 采用的方法既把 XQuery 当作屏幕搜集工具(从页面中提取相当的数据)又把它当作样式表工具(重新格式化数据以便数据适应页面不需要进行页面滚动)只要少量基础设施和一些非常简单的 XQuery 表达式就可以从大量数据源提取出相关数据 —— 例如交通天气和财务报价等并在电话上完好地显示数据
我过去经常处于这种情况对 HTML 页面进行屏幕搜集对某些特定问题来说似乎是可行的方案但是几乎没有用于屏幕搜集的 Java 工具包有许多 HTML 解析工具但它们通常缺少足够的抽象能力(把屏幕搜集代码弄得乱七八糟)大量不符合 HTML 规范的应用限制了它们它们也无法处理那些结构可能随时间发生变化的动态生成的页面
为了弥补质量低下的 HTML 和丰富的 XML 处理工具之间的空白首先要把 HTML 转换成 XML许多工具有助于完成这项工作JTidy 工具包做得很好可以使这项工作变得轻松一些JTidy 的设计目标是读入典型质量(即很糟)的 HTML 并输出更整洁的结果(有选项可供选择)它还提供了一个 DOM 接口用来遍历能够发送给 XML 解析器的 HTML 文档清单 中的代码将从 InputStream 中读取 HTML 文档并生成文档的 DOM 表示
清单 用 JTidy 把 HTML 转换成 XML 兼容的 DOM
Tidy tidy = new Tidy();
tidysetQuiet(true);
tidysetShowWarnings(false);
Document tidyDOM = tidyparseDOM(inputStream null);
用这个简单的转换就差不多能把每个 Web 页面都当作 XML 文档进行处理还能用自己喜欢的任何 XML 工具(比如 SAXXSLXPath等等)提取数据虽然 XSL 可能是很明智的选择(因为其设计目标就是为了从 XML 文档中提取信息并转换这些信息以便显示它们)但是如果不了解 XSL 的话它的学习曲线就很难掌握即使是最简单的 XSL 转换也复杂得让人心烦XPath 是处理信息提取的一个好选择 —— XSL 和 XQuery 都用它进行内容选择可以很容易地使用 XPath 把需要的数据提取出来然后对 HTML 进行格式化但是 XQuery 会让这项工具更加容易
XQuery简介
XQuery 的设计目标是从可能非常大的 XML 数据集中提取数据输入的数据集不必是 XML 文档虽然它可能是 XML 文档但是也可能是已经编入索引并保存在 XML 数据库中的文档集合甚至是一组关系数据库中的表像 SQL 一样XQuery 包含从多个数据集中提取数据汇总数据聚合数据和连接数据的函数
就像 JSPASP 或 Velocity 这样的表示性模板语言一样XQuery 把两个域(表示域和计算域)中的元素组合成一种组合语法结果所有 XML 文档都自动成为有效的 XQuery 表达式并对自身进行评估XQuery 还包含一些语言语句(language statement)例如for和let它们可以与 XML 元素混合使用
清单 显示了一个示例 XML 文档 bibxml它表示一个书目然后我们将介绍一些快速的 XQuery 表达式让您对 XQuery 能够做什么形成一种认识最后我们将再转到屏幕搜集的示例上要全面介绍 XQuery 的语法和使用情况可能要用几百页的篇幅有关更详细的参考材料和示例请参阅 参考资料 小节
清单 示例 XML 书目
<bib>
<book year=>
<title>TCP/IP Illustrated</title>
<author><last>Stevens</last><first>W</first></author>
<publisher>AddisonWesley</publisher>
<price> </price>
</book>
more books
</bib>
清单 显示了一个 XQuery 表达式它选择 AddisonWesley 在 年以后出版的所有书籍提取它们的标题并把标题格式化成前面有项目符号的(<ul>)列表大括号表示从表示模式(数据直接传递到输出 例如 <ul> 和 <li> 标签)到代码模式的切换然后在 return 子句之后立即进行从代码模式到表示模式的隐式切换
清单 根据查询参数选择图书标题的 XQuery 表达式
<ul>
{
for $b in doc(bibxml)/bib/book
where $b/publisher = AddisonWesley and $b/@year >
return
<li>{ data($b/title) }</li>
}
</ul>
查询语法引入了for通常称之为Flower 表达式(来自 FLWOR是 forletwhereorderreturn 的缩写)该语法从文档中选择一系列 XML 节点在该例中用 XPath 选取了来自 bibxml 文档的 <book> 节点集然后进一步过滤出与指定查询参数(出版商是 AddisonWesley出版日期是 年之后)匹配的节点对于选出的每个节点将在 return 子句中计算表达式在这里是标记(<li> 标签)与代码(提取出每个 <book> 节点的 <title> 元素的内容)的混合
这个简单的 XQuery 示例描述了 XQuery 的几个方面 —— 某一文档中表示与代码的混合XPath 的运用子条件的运用($b 引用)非凡的查询表达式XQuery 函数(data())还有一个事实输出文档的结构不必与输入文档的结构匹配就在这个相当紧凑的读起来不是很难的查询中孕育着强大的处理能力
清单 显示了一个更简单的 XQuery 表达式它把书目中不同出版商的数量在一个 <count> 元素中输出像前一个示例一样它用 XPath 表达式选择一组节点然后用 XQuery 函数选择惟一值并计算节点的数量它通过运算获得一个数字 —— bibxml即文档中不同出版商的数量
清单 计算不同出版商数量的 XQuery 表达式
<count>
{
let $d := distinctvalues(doc(bibxml)/book/publisher)
return count($d)
}
</count>
这些示例只是 XQuery 能够执行的各种查询类型的很少一部分提供这些例子仅仅是为了让您对使用 XQuery 能够做的事情有些感觉以及提示您如何才能用 XQuery 把 XML 文档转换成自己选择的格式虽然 XQuery 的大部分功能主要用于查询大型文档或者其他数据源但是也可以使用 XQuery 非常简单的子集来对 HTML 文档进行屏幕搜集为各种应用程序提取出需要的数据例如在屏幕大小有限的设备(例如蜂窝电话)上显示有关的数据或者创建一个 DIY 的门户网站聚集并显示来自多个站点的数据
用 XQuery 进行屏幕搜集
对 Web 页面做屏幕搜集的许多挑战之一是它们通常没有可以自我标识的结构而且它们的结构可能随着站点内容的编辑而变化甚至有可能根据不同的请求在页面中插入不同的动态内容(例如广告内容)因此对于页面中哪一部分的内容与要提取的数据相对应通常不得不进行猜测
股票价格
现在让我们从提取 Yahoo! 财经页面中 IBM 股票的当前价格开始()这个页面上有许多材料 —— 新闻标题广告财经数据等等但是我想要的是股票的价格数据它放在一个表格单元格中靠近包含Last Trade的单元格清单 中的查询语句将选择所有文本内容中包含Last Trade的 <td> 节点然后为每个节点(希望只有一个)输出一个包含后续<td> 节点内容的表格行内容是用 return 子句中的 data() 函数提取的否则不仅仅会得到 <td> 节点中的文本还会得到所有的标记(在这个查询中惟一包含技巧的部分是 text()[] 这个部分在这里text() 函数匹配的是 <td> 元素中的所有元素 —— 在这个例子只有一个元素但 XQuery 并不知道这一点 —— 所以必须进一步告诉它在进行文本匹配之前必须选择第一个文本节点)只要页面包含一个表格单元格的文本是Last Trade而且后续的单元格包含的是股票价格那么即使页面的结构随意变化也不会造成查询失败
清单 从 Yahoo! 财经提取股票报价的 XQuery 表达式
<table>
{
for $d in //td
where contains($d/text()[] Last Trade)
return <tr><td> { data($d/followingsibling::td) } </td></tr>
}
</table>
天气
现在来试一下另外一个页面Yahoo! 天气页面包含许多 portlet 面板我想提取上面所列城市的名称温度和图标(如果登录 Yahoo! 天气页面 则屏幕上会显示出在我的 Yahoo!中所选城市的天气否则会显示一些主要大城