本帖最后由 旧收音机 于 2015-5-23 23:44 编辑
问题导读
1、Twitter怎么通过post请求操作?
2、怎么通过post更新操作?
3、怎么通过post显示操作?
4、怎么通过post撤销操作?
在撰写本文时,夏季即将结束,新的学年就要开始,Twitter 的服务器上不断涌现出世界各地的网虫和非网虫们发布的更新。对于我们很多身在北美的人来说,从海滩聚会到足球,从室外娱乐到室内项目,各种各样的想法纷至沓来。为了跟上这种形势,是时候重访 Scitter 这个用于访问 Twitter 的 Scala 客户机库了。
如果 到目前为止 您一直紧随 Scitter 的开发,就会知道,这个库现在能够利用各种不同的 Twitter API 查看用户的好友、追随者和时间线,以及其他内容。但是,这个库还不具备发布状态更新的能力。在这最后一篇关于 Scitter 的文章中,我们将丰富这个库的功能,增加一些有趣的内容(终止和评价)功能和重要方法 update()、show() 和 destroy()。在此过程中,您将了解更多关于 Twitter API 的知识,它与 Scala 之间的交互如何,您还将了解如何克服两者之间不可避免的编程挑战。 注意,当您看到本文的时候,Scitter 库将位于一个 公共源代码控制库 中。当然,我还将在本文中包括 源代码,但是要知道,源代码库可能发生改变。换句话说,项目库中的代码与您在这里看到的代码可能略有不同,或者有较大的不同。 POST 到 Twitter到目前为止,我们的 Scitter 开发主要集中于一些基于 HTTP GET 的操作,这主要是因为这些调用非常容易,而我想轻松切入 Twitter API。将 POST 和 DELETE 操作添加到库中对于可见性来说迈出了重要一步。到目前为止,可以在个人 Twitter 帐户上运行单元测试,而其他人并不知道您要干什么。但是,一旦开始发送更新消息,那么全世界都将知道您要运行 Scitter 单元测试。 如果继续测试 Scitter,那么需要在 Twitter 上创建自己的 “测试” 帐户。(也许用 Twitter API 编程的最大缺点是没有任何合适的测试或模拟工具。) 目前的进展在开始着手这个库的新的 UPDATE 功能之前,我们来回顾一下到目前为止我们已经创建的东西。(我不会提供完整的源代码清单,因为 Scitter 已经开始变得过长,不便于全部显示。但是,可以在阅读本文时,从另一个窗口查看 代码。) 大致来说,Scitter 库分为 4 个部分: 1、来回发送的请求和响应类型(User、Status 等),包含在 API 中;它们被建模为 case 类。 2、OptionalParam 类型,同样在 API 中的某些地方;也被建模为 case 类,这些 case 类继承基本的 OptionalParam 类型。 3、Scitter 对象,用于通信基础和对 Twitter 的匿名(无身份验证)访问。 4、Scitter 类,存放一个用户名和密码,用于访问给定 Twitter 帐户时进行验证。 注意,在这最后一篇文章中,为了使文件大小保持在相对合理的范围内,我将请求/响应类型分开放到不同的文件中。 终止和评价那么,现在我们清楚了目标。我们将通过实现两个 “只读” Twitter API 来达到目标:end_session API(结束用户会话)和 rate_limit_statusAPI(描述在某一特定时段内用户帐户还剩下多少可用的 post)。 end_session API 与它的同胞 verify_credentials 相似,也是一个非常简单的 API:只需用一个经过验证的请求调用它,它将 “结束” 当前正在运行的会话。在 Scitter 类上实现它非常容易,如清单 1 所示: 清单 1. 在 Scitter 上实现 end_session[mw_shl_code=scala,true]package com.tedneward.scitter
{
import org.apache.commons.httpclient._, auth._, methods._, params._
import scala.xml._
// ...
class Scitter
{
/**
*
*/
def endSession : Boolean =
{
val (statusCode, statusBody) =
Scitter.execute("http://twitter.com/account/end_session.xml",
username, password)
statusCode == 200
}
}
}[/mw_shl_code] 好吧,我失言了。也不是那么容易。 POST和我们到目前为止用过的 Twitter API 中的其他 API 不一样,end_session 要求传入的消息是用 HTTP POST 语义发送的。现在,Scitter.execute 方法做任何事情都是通过 GET,这意味着需要将那些期望 GET 的 API 与那些期望 POST 的 API 区分开来。 现在暂不考虑这一点,另外还有一个明显的变化:POST 的 API 调用还需将名称/值对传递到 execute() 方法中。(记住,在其他 API 调用中,若使用 GET,则所有参数可以作为查询参数出现在 URL 行;若使用 POST,则参数出现在 HTTP 请求的主体中。)在 Scala 中,每当提到名称/值对,自然会想到 Scala Map 类型,所以在考虑建模作为 POST 一部分发送的数据元素时,最容易的方法是将它们放入到一个Map[String,String] 中并传递。 例如,如果将一个新的状态消息传递给 Twitter,需要将这个不超过 140 个字符的消息放在一个名称/值对 status 中,那么应该如清单 2 所示: 清单 2. 基本 map 语法[mw_shl_code=scala,true]val map = Map("status" -> message)[/mw_shl_code] 在此情况下,我们可以重构 Scitter.execute() 方法,使之用 一个 Map 作为参数。如果 Map 为空,那么可以认为应该使用 GET 而不是 POST,如清单 3 所示: 清单 3. 重构 execute()[mw_shl_code=scala,true]private[scitter] def execute(url : String) : (Int, String) =
execute(url, Map(), "", "")
private[scitter] def execute(url : String, username : String,
password : String) : (Int, String) =
execute(url, Map(), username, password)
private[scitter] def execute(url : String,
dataMap : Map[String,String]) : (Int, String) =
execute(url, dataMap, "", "")
private[scitter] def execute(url : String, dataMap : Map[String,String],
username : String, password : String) =
{
val client = new HttpClient()
val method =
if (dataMap.size == 0)
{
new GetMethod(url)
}
else
{
var m = new PostMethod(url)
val array = new Array[NameValuePair](dataMap.size)
var pos = 0
dataMap.elements.foreach { (pr) =>
pr match {
case (k, v) => array(pos) = new NameValuePair(k, v)
}
pos += 1
}
m.setRequestBody(array)
m
}
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler(3, false))
if ((username != "") && (password != ""))
{
client.getParams().setAuthenticationPreemptive(true)
client.getState().setCredentials(
new AuthScope("twitter.com", 80, AuthScope.ANY_REALM),
new UsernamePasswordCredentials(username, password))
}
client.executeMethod(method)
(method.getStatusLine().getStatusCode(), method.getResponseBodyAsString())
}[/mw_shl_code] execute() 方法最大的变化是引入了 Map[String,String] 参数,以及与它的大小有关的 “if” 测试。该测试决定是处理 GET 请求还是 POST 请求。由于 Apache Commons HttpClient 要求 POST 请求的主体放在 NameValuePairs 中,因此我们使用 foreach() 调用遍历 map 的元素。我们以二元组 pr 的形式传入 map 的键和值,并将它们分别提取到本地绑定变量 k 和 v,然后使用这些值作为 NameValuePair 构造函数的构造函数参数。 我们还可以使用 PostMethod 上的 setParameter(name, value) API 更轻松地做这些事情。出于教学的目的,我选择了清单 3 中的方法:以表明 Scala 数组和 Java 数组一样,仍然是可变的,即使数组引用被标记为 val 仍是如此。记住,在实际代码中,对于每个 (k,v) 元组,使用PostMethod 上的 setParameter(name, value) 方法要好得多。 还需注意,对于 if/else 返回的 “method” 对象的类型,Scala 编译器会进行 does the right thing 类型推断。由于 Scala 可以看到 if/else 返回的是GetMethod 还是 PostMethod 对象,它会选择最接近的基本类型 HttpMethodBase 作为 “method” 的返回类型。这也意味着,在 execute() 方法的其余部分中,HttpMethodBase 中的任何不可用方法都是不可访问的。幸运的是,我们不需要它们,所以至少现在没有问题。 清单 3 中的实现的背后还潜藏着最后一个问题,这个问题是由这样一个事实引起的:我选择了使用 Map 来区分 execute() 方法是处理 GET 操作,还是处理 POST 操作。如果还需要使用其他 HTTP 动作(例如 PUT 或 DELETE),那么将不得不再次重构 execute()。到目前为止,还没有这样的问题,但是今后要记住这一点。 测试在实施这样的重构之前,先运行 ant test,以确保原有的所有基于 GET 的请求 API 仍可使用 — 事实确实如此。(这里假设生产 Twitter API 或 Twitter 服务器的可用性没有变化)。一切正常(至少在我的计算机上是这样),所以实现新的 execute() 方法就非常容易: 清单 4. Scitter v0.3: endSession[mw_shl_code=scala,true]def endSession : Boolean =
{
val (statusCode, statusBody) =
Scitter.execute("http://twitter.com/account/end_session.xml",
Map("" -> ""), username, password)
statusCode == 200
}[/mw_shl_code]
这实在是再简单不过了。 接下来要做的是实现 rate_limit_status API,它有两个版本,一个是经过验证的版本,另一个是没有经过验证的版本。我们将该方法实现为Scitter 对象和 Scitter 类上的 rateLimitStatus,如清单 5 所示: 清单 5. Scitter v0.3: rateLimitStatus
[mw_shl_code=scala,true]package com.tedneward.scitter
{
object Scitter
{
// ...
def rateLimitStatus : Option[RateLimits] =
{
val url = "http://twitter.com/account/rate_limit_status.xml"
val (statusCode, statusBody) =
Scitter.execute(url)
if (statusCode == 200)
{
Some(RateLimits.fromXml(XML.loadString(statusBody)))
}
else
{
None
}
}
}
class Scitter
{
// ...
def rateLimitStatus : Option[RateLimits] =
{
val url = "http://twitter.com/account/rate_limit_status.xml"
val (statusCode, statusBody) =
Scitter.execute(url, username, password)
if (statusCode == 200)
{
Some(RateLimits.fromXml(XML.loadString(statusBody)))
}
else
{
None
}
}
}
}[/mw_shl_code]
我觉得还是很简单。
更新现在,有了新的 POST 版本的 HTTP 通信层,我们可以来处理 Twitter API 的中心:update 调用。毫不奇怪,需要一个 POST,并且至少有一个参数,即 status。 status 参数包含要发布到认证用户的 Twitter 提要的不超过 140 个字符的消息。另外还有一个可选参数:in_reply_to_status_id,该参数提供另一个更新的 id,执行了 POST 的更新将回复该更新。 update 调用差不多就是这样了,如清单 6 所示: 清单 6. Scitter v0.3: update
[mw_shl_code=scala,true]package com.tedneward.scitter
{
class Scitter
{
// ...
def update(message : String, options : OptionalParam*) : Option[Status] =
{
def optionsToMap(options : List[OptionalParam]) : Map[String, String]=
{
options match
{
case hd :: tl =>
hd match {
case InReplyToStatusId(id) =>
Map("in_reply_to_status_id" -> id.toString) ++ optionsToMap(tl)
case _ =>
optionsToMap(tl)
}
case List() => Map()
}
}
val paramsMap = Map("status" -> message) ++ optionsToMap(options.toList)
val (statusCode, body) =
Scitter.execute("http://twitter.com/statuses/update.xml",
paramsMap, username, password)
if (statusCode == 200)
{
Some(Status.fromXml(XML.loadString(body)))
}
else
{
None
}
}
}
}[/mw_shl_code]
也许这个方法中最 “不同” 的部分就是其中定义的嵌套函数 — 与使用 GET 的其他 Twitter API 调用不同,Twitter 期望传给 POST 的参数出现在执行 POST 的主体中,这意味着在调用 Scitter.execute() 之前需要将它们转换成 Map 条目。但是,默认的 Map(来自scala.collections.immutable)是不可变的,这意味着可以组合 Map,但是不能将条目添加到已有的 Map 中。(实际上,还是可以做到,但是我们不愿意那样做。请参阅侧边栏 “可变集合”,了解更多这方面的信息。)
解决这个小难题的最容易的方法是递归地处理传入的 OptionalParam 元素的列表(实际上是一个 Array[])。我们将每个元素拆开,将它转换成各自的 Map 条目。然后,将一个新的Map(由新创建的 Map 和从递归调用返回的 Map 组成)返回到 optionsToMap。 然后,将 OptionalParam 的 Array[] 传递到 optionsToMap 嵌套函数。然后,将返回的 Map与我们构建的包含 status 消息的 Map 连接起来。最后,将新的 Map 和用户名、密码一起传递给 Scitter.execute() 方法,以传送到 Twitter 服务器。 随便说一句,所有这些任务需要的代码并不多,但是需要更多的解释,这是比较优雅的编程方式。 潜在的重构理论上,传给 update 的可选参数与传给其他基于 GET 的 API 调用的可选参数将受到同等对待;只是结果的格式有所不同(结果是用于 POST 的名称/值对,而不是用于 URL 的名称/值对)。 如果 Twitter API 需要其他 HTTP 动作支持(PUT 和/或 DELETE 就是可能需要的动作),那么总是可以将 HTTP 参数作为特定参数 — 也许又是一组 case 类 — 并让 execute() 以一个 HTTP 动作、URL、名称/值对的 map 以及(可选)用户名/密码作为 5 个参数。然后,必要时可以将可选参数转换成一个字符串或一组 POST 参数。这些内容只需记在脑中就行了。 显示show 调用接受要检索的 Twitter 状态的 id,并显示 Twitter 状态。和 update 一样,这个方法非常简单,无需再作说明,如清单 7 所示: 清单 7. Scitter v0.3: show
[mw_shl_code=scala,true]package com.tedneward.scitter
{
class Scitter
{
// ...
def show(id : Long) : Option[Status] =
{
val (statusCode, body) =
Scitter.execute("http://twitter.com/statuses/show/" + id + ".xml",
username, password)
if (statusCode == 200)
{
Some(Status.fromXml(XML.loadString(body)))
}
else
{
None
}
}
}
}[/mw_shl_code]
还有问题吗? 另一种显示方法如果想再试一下模式匹配,那么可以看看清单 8 中是如何以另一种方式编写 show() 方法的: 清单 8. Scitter v0.3: show redux
[mw_shl_code=scala,true]package com.tedneward.scitter
{
class Scitter
{
// ...
def show(id : Long) : Option[Status] =
{
Scitter.execute("http://twitter.com/statuses/show/" + id + ".xml",
username, password) match
{
case (200, body) =>
Some(Status.fromXml(XML.loadString(body)))
case (_, _) =>
None
}
}
}
}[/mw_shl_code]
这个版本比起 if/else 版本是否更加清晰,这很大程度上属于审美的问题,但公平而论,这个版本也许更加简洁。(很可能查看代码的人看到 Scala 的 “函数” 部分越多,就认为这个版本越吸引人。)
但是,相对于 if/else 版本,模式匹配版本有一个优势:如果 Twitter 返回新的条件(例如不同的错误条件或来自 HTTP 的响应代码),那么模式匹配版本在区分这些条件时可能更清晰。例如,如果某天 Twitter 决定返回 400 响应代码和一条错误消息(在主体中),以表明某种格式错误(也许是没有正确地重新 Tweet),那么与 if/else 方法相比,模式匹配版本可以更轻松(清晰)地同时测试响应代码和主体的内容。
还应注意,我们还可以使用清单 8 中的方式创建一些局部应用的函数,这些函数只需要 URL 和参数。但是,坦白说,这是一种自找麻烦的解放方案,所以我不会采用。
撤销我们还想让 Scitter 用户可以撤销刚才执行的动作。为此,需要一个 destroy 调用,它将删除已发布的 Twitter 状态,如清单 9 所示: 清单 9. Scitter v0.3: destroy
[mw_shl_code=scala,true]package com.tedneward.scitter
{
class Scitter
{
// ...
def destroy(id : Long) : Option[Status] =
{
val paramsMap = Map("id" -> id.toString())
val (statusCode, body) =
Scitter.execute("http://twitter.com/statuses/destroy/" + id.toString() + ".xml",
paramsMap, username, password)
if (statusCode == 200)
{
Some(Status.fromXml(XML.loadString(body)))
}
else
{
None
}
}
def destroy(id : Id) : Option[Status] =
destroy(id.id.toLong)
}
}[/mw_shl_code]
有了这些东西,我们可以考虑将这个 Scitter 客户机库作为 “alpha” 版,至少实现一个简单的 Scitter 客户机。(按照惯例,这个任务就留给您来完成,作为一项 “读者练习”。)
结束语
编写 Scitter 客户机库是一项有趣的工作。虽然不能说 Scitter 已经可以完全用于生产,但是它绝对足以用于实现简单的、基于文本的 Twitter 客户机,这意味着它已经可以投入使用了。要发现什么人可以使用它,哪些特性是需要的,从而使之变得更有用,最好的方法就是将它向公众发布。
我已经将本文和之前关于 Scitter 的文章中的代码作为第一个修订版提交到 Google Code 上的 Scitter 项目主页。欢迎下载和试用这个库,并告诉我您的想法。同时也欢迎提供 bug 报告、修复和建议。
您也无需受我的代码库的束缚。见证了之前三篇文章中进行的 Scitter 开发,您应该对 Twitter API 的使用有很好的理解。如果对于使用该 API 有不同的想法,那么尽管去做:抛开 Scitter,构建自己的 Scala 客户机库。毕竟,做做这些内部项目也是挺有乐趣的。
现在,我们要向 Scitter 挥手告别,开始寻找新的用 Scala 解决的项目。愿您从中找到乐趣,如果发现了用 Scala 编程的工作,别忘了告诉我!
相关导读:
第 1 期,面向对象的函数编程:了解 Scala 如何利用两个领域的优点
第 2 期,类操作:理解 Scala 的类语法和语义
第 3 期,Scala 控制结构内部揭密
第 4 期,关于特征和行为:使用 Scala 版本的 Java 接口
第 5 期,实现继承:当 Scala 继承中的对象遇到函数
第 6 期,集合类型:在 Scala 使用元组、数组和列表
第 7 期,包和访问修饰符:Scala 中的 public、private 以及其他成员
第 8 期,构建计算器,第 1 部分:Scala 的 case 类和模式匹配
第 9 期,构建计算器,第 2 部分:Scala 的解析器组合子
第 10 期,构建计算器,第 3 部分:将 Scala 解析器组合子和 case 类结合起来
第 11 期,Scala 和 servlet
第 12 期,深入了解 Scala 并发性:了解 Scala 如何简化并发编程并绕过陷阱
第 13 期,深入了解 Scala 并发性:了解 actor 如何提供新的应用程序代码建模方法
第 14 期,Scala + Twitter = Scitter
第 15 期,增强 Scitter 库
第 16 期,用 Scitter 更新 Twitter
|