用 XQuery采集数据

阅读更多

从 HTML 文档提取数据的方法有许多种,但是我真的很喜欢 Sam 采用的方法:既把 XQuery 当作屏幕搜集工具(从页面中提取相当的数据),又把它当作样式表工具(重新格式化数据,以便数据适应页面,不需要进行页面滚动)。只要少量基础设施和一些 非常简单的 XQuery 表达式,就可以从大量数据源提取出相关数据 —— 例如交通、天气和财务报价等,并在电话上完好地显示数据。

我过去经常处于这种情况:对 HTML 页面进行屏幕搜集对某些特定问题来说似乎是可行的方案,但是几乎没有用于屏幕搜集的 Java 工具包。有许多 HTML 解析工具,但它们通常缺少足够的抽象能力(把屏幕搜集代码弄得乱七八糟),大量不符合 HTML 规范的应用限制了它们,它们也无法处理那些结构可能随时间发生变化的、动态生成的页面。

为了弥补质量低下的 HTML 和丰富的 XML 处理工具之间的空白,首先要把 HTML 转换成 XML。许多工具有助于完成这项工作;JTidy 工具包做得很好,可以使这项工作变得轻松一些。JTidy 的设计目标是读入典型质量(即很糟)的 HTML 并输出更整洁的结果(有选项可供选择),它还提供了一个 DOM 接口,用来遍历能够发送给 XML 解析器的 HTML 文档。清单 1 中的代码将从 InputStream 中读取 HTML 文档,并生成文档的 DOM 表示:



清单 1. 用 JTidy 把 HTML 转换成 XML 兼容的 DOM

java 代码
  1. Tidy tidy = new Tidy();
  2. tidy.setQuiet(true);
  3. tidy.setShowWarnings(false);
  4. Document tidyDOM = tidy.parseDOM(inputStream, null);

用这个简单的转换,就差不多能把每个 Web 页面都当作 XML 文档进行处理,还能用自己喜欢的任何 XML 工具(比如 SAX、XSL、XPath,等等)提取数据。虽然 XSL 可能是很明智的选择(因为其设计目标就是为了从 XML 文档中提取信息并转换这些信息,以便显示它们),但是如果不了解 XSL 的话,它的学习曲线就很难掌握,即使是最简单的 XSL 转换也复杂得让人心烦。XPath 是处理信息提取的一个好选择 —— XSL 和 XQuery 都用它进行内容选择,可以很容易地使用 XPath 把需要的数据提取出来,然后对 HTML 进行格式化,但是 XQuery 会让这项工具更加容易。







XQuery:简介

XQuery 的设计目标是从可能非常大的 XML 数据集中提取数据。输入的数据集不必是 XML 文档,虽然它可能是 XML 文档,但是也可能是已经编入索引并保存在 XML 数据库中的文档集合,甚至是一组关系数据库中的表。像 SQL 一样,XQuery 包含从多个数据集中提取数据、汇总数据、聚合数据和连接数据的函数。

就像 JSP、ASP 或 Velocity 这样的表示性模板语言一样,XQuery 把两个域(表示域和计算域)中的元素组合成一种组合语法。结果,所有 XML 文档都自动成为有效的 XQuery 表达式,并对自身进行评估。XQuery 还包含一些语言语句(language statement),例如“for”和“let”,它们可以与 XML 元素混合使用。

清 单 2 显示了一个示例 XML 文档 bib.xml,它表示一个书目。然后我们将介绍一些快速的 XQuery 表达式,让您对 XQuery 能够做什么形成一种认识,最后我们将再转到屏幕搜集的示例上。


清单 2. 示例 XML 书目

xml 代码
  1. <bib>
  2. <book year="1994">
  3. <title>TCP/IP Illustratedtitle>
  4. <author><last>Stevenslast><first>W.first>author>
  5. <publisher>Addison-Wesleypublisher>
  6. <price> 65.95price>
  7. book>
  8. . . . more books . . .
  9. bib>

清单 3 显示了一个 XQuery 表达式,它选择 Addison-Wesley 在 1991 年以后出版的所有书籍,提取它们的标题,并把标题格式化成前面有项目符号的(

    )列表。大括号表示从“表示模式”(数据直接传递到输出 ,例如
    • 标签)到“代码模式”的切换;然后在 return 子句之后立即进行从“代码模式”到“表示模式”的隐式切换。

       


      清单 3. 根据查询参数选择图书标题的 XQuery 表达式
      				
      xml 代码
      1. <ul>
      2. {
      3. for $b in doc("bib.xml")/bib/book
      4. where $b/publisher = "Addison-Wesley" and $b/@year > 1991
      5. return
      6. <li>{ data($b/title) }li>
      7. }
      8. ul>


      查询语法引入了“for”,通常称之为“Flower 表达式”(来自 FLWOR,是 for-let-where-order-return 的缩写),该语法从文档中选择一系列 XML 节点,在该例中,用 XPath 选取了来自 bib.xml 文档的 节点集,然后进一步过滤出与指定查询参数(出版商是 Addison-Wesley,出版日期是 1991 年之后)匹配的节点。对于选出的每个节点,将在 return 子句中计算表达式,在这里是标记(

    • 标签)与代码(提取出每个 节点的 元素的内容)的混合。

       

      这个简单的 XQuery 示例描述了 XQuery 的几个方面 —— 某一文档中表示与代码的混合、XPath 的运用、子条件的运用($b 引用)、非凡的查询表达式、XQuery 函数(data()),还有一个事实:输出文档的结构不必与输入文档的结构匹配。就在这个相当紧凑的、读起来不是很难的查询中,孕育着强大的处理能力。

      清单 4 显示了一个更简单的 XQuery 表达式,它把书目中不同出版商的数量,在一个 元素中输出。像前一个示例一样,它用 XPath 表达式选择一组节点,然后用 XQuery 函数选择惟一值,并计算节点的数量。它通过运算获得一个数字 —— bib.xml,即文档中不同出版商的数量。


      清单 4. 计算不同出版商数量的 XQuery 表达式
      				
      xml 代码
      1. <count>
      2. {
      3. let $d := distinct-values(doc("bib.xml")/book/publisher)
      4. return count($d)
      5. }
      6. count>


      这些示例只是 XQuery 能够执行的各种查询类型的很少一部分,提供这些例子仅仅是为了让您对使用 XQuery 能够做的事情有些感觉,以及提示您如何才能用 XQuery 把 XML 文档转换成自己选择的格式。虽然 XQuery 的大部分功能主要用于查询大型文档或者其他数据源,但是也可以使用 XQuery 非常简单的子集来对 HTML 文档进行屏幕搜集,为各种应用程序提取出需要的数据,例如在屏幕大小有限的设备(例如蜂窝电话)上显示有关的数据,或者创建一个 DIY 的门户网站,聚集并显示来自多个站点的数据。


      用 XQuery 进行屏幕搜集

      对 Web 页面做屏幕搜集的许多挑战之一是:它们通常没有可以自我标识的结构,而且它们的结构可能随着站点内容的编辑而变化,甚至有可能根据不同的请求,在页面中插 入不同的动态内容(例如广告内容)。因此,对于页面中哪一部分的内容与要提取的数据相对应,通常不得不进行猜测。

      股票价格

      现 在,让我们从提取 Yahoo! 财经页面中 IBM 股票的当前价格开始(http://finance.yahoo.com/q?s=IBM)。这个页面上有许多材料 —— 新闻标题、广告、财经数据,等等,但是我想要的是股票的价格数据,它放在一个表格单元格中,靠近包含“Last Trade”的单元格。清单 5 中的查询语句将选择所有文本内容中包含“Last Trade”的 节点,然后为每个节点(希望只有一个)输出一个包含后续 节点内容的表格行。内容是用 return 子句中的 data() 函数提取的;否则,不仅仅会得到 节点中的文本,还会得到所有的标记。(在这个查询中,惟一包含技巧的部分是 text()[1] 这个部分;在这里,text() 函数匹配的是 元素中的所有元素 —— 在这个例子只有一个元素,但 XQuery 并不知道这一点 —— 所以必须进一步告诉它在进行文本匹配之前,必须选择第一个文本节点)。只要页面包含一个表格单元格的文本是“Last Trade”,而且后续的单元格包含的是股票价格,那么,即使页面的结构随意变化,也不会造成查询失败。


      清单 5. 从 Yahoo! 财经提取股票报价的 XQuery 表达式
      				
      xml 代码
      1. <table>
      2. {
      3. for $d in //td
      4. where contains($d/text()[1], "Last Trade")
      5. return <tr><td> { data($d/following-sibling::td) } td>tr>
      6. }
      7. table>


      天气

      现 在来试一下另外一个页面。Yahoo! 天气页面包含许多 portlet 面板,我想提取上面所列城市的名称、温度和图标。(如果登录 Yahoo! 天气页面 http://weather.yahoo.com,则屏幕上会显示出在“我的 Yahoo!”中所选城市的天气,否则会显示一些主要大城市的天气情况。)清单 6 显示了一个查询,它查找包含文本“New York, NY”的子面板,然后导航到封闭表格(enclosing table),并选中所有行:


      清单 6. 从 Yahoo! 天气提取天气信息的 XQuery 表达式
               
               
               
               
      xml 代码
      1. <table>
      2. {
      3. for $d in //td[contains(a/small/text(), "New York, NY")]
      4. for $row in $d/parent::tr/parent::table/tr
      5. where contains($d/a/small/text()[1], "New York")
      6. return <tr><td>{data($row/td[1])}td>
      7. <td>{data($row/td[2])}td>
      8. <td>{$row/td[3]//img}td> tr>
      9. }
      10. table>


      然后,对于每一行,XQuery 会提取出三个相关的数据列 —— 城市名称、温度和图标 —— 并输出一个相对简单的表,表中只包含这三项信息。结果就是比较紧凑地显示了所关心城市的信息,适合在小屏幕上显示。结果如下所示:

      Chicago, IL 49...63 F Partly Cloudy
      London, UK 32...41 F Fair
      New York, NY 36...44 F Cloudy
      San Francisco, CA 52...67 F Partly Cloudy

      这个查询不像 清单 5 中的查询那么功能强大。它假设文本“New York, NY”将在 small 元素中(这就是下次 Yahoo! 重新设计他们的页面时可以轻松更改的那类标记)。而且,也很容易将“New York, NY”多次显示在天气页面上。但是,可以多花些精力来开发查询,从而减轻这些风险元素;正如许多开发选择一样,在查询的复杂性与查询的稳定性之间会有一个 权衡。

      清单 5 和 清单 6 中所示的查询不是制作这些查询的惟一方式。使用更复杂的 XPath 语法,清单 6 中的两个 for 子句可以合并到一个 XPath 表达式中,那么整个 清单 5 就能变成一个 XPath 表达式,而不是使用 FLWOR 的语法。如果是 XPath 高手,那么可能会发现,使用更加面向 XPath 的方式会更容易一些,而有更多 SQL 经验的人则会发现 FLWOR 的语法更有吸引力。

      工具

      针对 HTML 页面执行 XQuery 表达式所需要的代码非常少。可以用 JTidy 库来清理 HTML 文档,然后把它表示成 DOM 对象(请参阅 清单 1)。Saxon XQuery 引擎被用来编译和执行针对文档 DOM 对象的查询。编译和执行一个针对文档 DOM 表示的 XQuery 查询只需要 6 行代码,如清单 7 所示:


      清单 7. 用 Saxon 编译和执行 XQuery 表达式的代码
      				
      java 代码
      1. Configuration c = new Configuration();
      2. StaticQueryContext qp = new StaticQueryContext(c);
      3. XQueryExpression xe = qp.compileQuery(query);
      4. DynamicQueryContext dqc = new DynamicQueryContext(c);
      5. dqc.setContextNode(new DocumentWrapper(tidyDOM, url, c));
      6. List result = xe.evaluate(dqc);


      查询计算的结果是 DOM ElementList,您可以用自己喜欢的 DOM 操纵技术(或者最不喜欢的 DOM 操作技术)把查询结果转变成文档。


你可能感兴趣的:(数据结构,XML,出版,XSL,Yahoo)