从 XML 到对象
现在可以添加的最简单的 API 是 public_timeline,它收集 Twitter 从所有用户处接收到的最新的 n 更新,并返回它们以便于进行使用。与之前讨论的另外两个 API 不同,public_timeline API 返回一个响应主体(而不是仅依赖于状态码),因此我们需要分解生成的 XML/RSS/ATOM/,然后将它们返回给 Scitter 客户机。
现在,我们编写一个探索测试,它将访问公共提要并将结果转储到 stdout 以便进行分析,如清单 8 所示:
清单 8. 大家都在忙什么?
package com.tedneward.scitter.test { class ExplorationTests { // ... @Test def callTwitterPublicTimeline = { val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml" // HttpClient API 101 val client = new HttpClient() val method = new GetMethod(publicFeedURL) method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false)) client.executeMethod(method) val statusLine = method.getStatusLine() assertEquals(statusLine.getStatusCode(), 200) assertEquals(statusLine.getReasonPhrase(), "OK") val responseBody = method.getResponseBodyAsString() System.out.println("callTwitterPublicTimeline got... ") System.out.println(responseBody) } } } |
运行后,结果每次都会有所不同,因为公共 Twitter 服务器上有许多用户,但通常应与清单 9 的 JUnit 文本文件转储类似:
清单 9. 我们的 Tweets 结果
<statuses type="array"> <status> <created_at>Tue Mar 10 03:14:54 +0000 2009</created_at> <id>1303777336</id> <text>She really is. http://tinyurl.com/d65hmj</text> <source><a href="http://iconfactory.com/software/twitterrific">twitterrific</a> </source> <truncated>false</truncated> <in_reply_to_status_id></in_reply_to_status_id> <in_reply_to_user_id></in_reply_to_user_id> <favorited>false</favorited> <user> <id>18729101</id> <name>Brittanie</name> <screen_name>brittaniemarie</screen_name> <description>I'm a bright character. I suppose.</description> <location>Atlanta or Philly.</location> <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/ 81636505/goodish_normal.jpg</profile_image_url> <url>http://writeitdowntakeapicture.blogspot.com</url> <protected>false</protected> <followers_count>61</followers_count> </user> </status> <status> <created_at>Tue Mar 10 03:14:57 +0000 2009</created_at> <id>1303777334</id> <text>Number 2 of my four life principles. "Life is fun and rewarding"</text> <source>web</source> <truncated>false</truncated> <in_reply_to_status_id></in_reply_to_status_id> <in_reply_to_user_id></in_reply_to_user_id> <favorited>false</favorited> <user> <id>21465465</id> <name>Dale Greenwood</name> <screen_name>Greeendale</screen_name> <description>Vegetarian. Eat and use only organics. Love helping people become prosperous</description> <location>Melbourne Australia</location> <profile_image_url>http://s3.amazonaws.com/twitter_production/profile_images/ 90659576/Dock_normal.jpg</profile_image_url> <url>http://www.4abundance.mionegroup.com</url> <protected>false</protected> <followers_count>15</followers_count> </user> </status> (A lot more have been snipped) </statuses> |
通过查看结果和 Twitter 文档可以看出,调用的结果是一组具备一致消息结构的简单 “状态” 消息。使用 Scala 的 XML 支持分离结果相当简单,但我们会在基本测试通过后立即简化它们,如清单 10 所示:
清单 10. 大家都在忙什么?
package com.tedneward.scitter.test { class ExplorationTests { // ... @Test def simplePublicFeedPullAndParse = { val publicFeedURL = "http://twitter.com/statuses/public_timeline.xml" // HttpClient API 101 val client = new HttpClient() val method = new GetMethod(publicFeedURL) method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false)) val statusCode = client.executeMethod(method) val responseBody = new String(method.getResponseBody()) val responseXML = scala.xml.XML.loadString(responseBody) val statuses = responseXML \\ "status" for (n <- statuses.elements) { n match { case <status>{ contents @ _*}</status> => { System.out.println("Status: ") contents.foreach((c) => c match { case <text>{ t @ _*}</text> => System.out.println("\tText: " + t.text.trim) case <user>{ contents2 @ _* }</user> => { contents2.foreach((c2) => c2 match { case <screen_name>{ u }</screen_name> => System.out.println("\tUser: " + u.text.trim) case _ => () } ) } case _ => () } ) } case _ => () // or, if you prefer, System.out.println("Unrecognized element!") } } } } } |
随着示例代码模式的变化,这并不值得推荐 — 这有点类似于 DOM,依次导航到各个子元素,提取文本,然后导航到另一个节点。我可以仅执行两个 XPath 样式的查询,如清单 11 所示:
清单 11. 替代解析方法
for (n <- statuses.elements) { val text = (n \\ "text").text val screenName = (n \\ "user" \ "screen_name").text } |
这显然更加简短,但它带来了两个基本问题:
- 我们可以强制 Scala 的 XML 库针对每个元素或子元素遍历一次图,其速度会随时间减慢。
- 我们仍然需要直接处理 XML 消息的结构。这是两个问题中最为重要的。
也就是说,这种方式不具备可伸缩性 — 假设我们最终对 Twitter 状态消息中的每个元素都感兴趣,我们将需要分别从各状态中提取各个元素。
这又造成了另一个与各格式本身相关的问题。记住,Twitter 可以使用四种不同的格式,并且我们不希望 Scitter 客户机需要了解它们之间的任何差异,因此 Scitter 需要一个能返回给客户机的中间结构,以便未来使用,如清单 12 所示:
清单 12. Breaker,您的状态是什么?
abstract class Status { val createdAt : String val id : Long val text : String val source : String val truncated : Boolean val inReplyToStatusId : Option[Long] val inReplyToUserId : Option[Long] val favorited : Boolean val user : User } |
这与 User 方式相类似,考虑到简洁性,我就不再重复了。注意,User 子元素有一个有趣的问题 — 虽然存在 Twitter 用户类型,但其中内嵌了一个可选的 “最新状态”。状态消息还内嵌了一个用户。对于这种情况,为了帮助避免一些潜在的递归问题,我选择创建一个嵌入在 Status 内部的 User 类型,以反映所出现的 User 数据;反之亦然,Status 也可以嵌入在 User 中,这样可以明确避免该问题。(至少,在没发现问题之前,这种方法是有效的)。
现在,创建了表示 Twitter 消息的对象类型之后,我们可以遵循 XML 反序列化的公共 Scala 模式:创建相应的对象定义,其中包含一个 fromXml 方法,用于将 XML 节点分离到对象实例中,如清单 13 所示:
清单 13. 分解 XML
/** * Object wrapper for transforming (format) into Status instances. */ object Status { def fromXml(node : scala.xml.Node) : Status = { new Status { val createdAt = (node \ "created_at").text val id = (node \ "id").text.toLong val text = (node \ "text").text val source = (node \ "source").text val truncated = (node \ "truncated").text.toBoolean val inReplyToStatusId = if ((node \ "in_reply_to_status_id").text != "") Some((node \"in_reply_to_status_id").text.toLong) else None val inReplyToUserId = if ((node \ "in_reply_to_user_id").text != "") Some((node \"in_reply_to_user_id").text.toLong) else None val favorited = (node \ "favorited").text.toBoolean val user = User.fromXml((node \ "user")(0)) } } } |