分享

第 16 期,用 Scitter 更新 Twitter

旧收音机 发表于 2015-5-17 20:47:53 [显示全部楼层] 回帖奖励 阅读模式 关闭右栏 0 11727
本帖最后由 旧收音机 于 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



















没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条