蓝天,小湖,湖水中一方小筑

Golang and JSON API

最近在尝试用golang做爬虫类的东西,避免不了需要处理JSON API。其间碰到了些问题,记在这里以便下次查阅。

生成URL

嗯,反正我的blog已经被墙了,所以这次就用不存在的网站来作为示例吧。这个网站的名字叫Facebook(其实是不想为国内的账号上传身份证,所以只有用国外的A PI了)。找了一圈,决定拿这个不存在的网站的CEO来测试。

用的是Facebook提供的[Graph API](https://developers.facebook.com/docs/graph- api),下面演示的是如何用golang拼出来一个URL:

package main

import (
    "fmt"
    "net/url"
)

func main() {
    fmt.Println(userinfo_api("4", ""))
}

func userinfo_api(account_id string, access_token string) string {
    base_url := "http://graph.facebook.com/"

    params := make(url.Values)
    params.Set("fields", "id,name,picture")
    if access_token != "" {
        params.Set("access_token", access_token)
    }

    api_url := base_url + account_id + "?" + params.Encode()

    return api_url
}

程序会在终端上打印出一个编码过的URL地址。这个地址直接贴到浏览器里面是会返回错误信息的,所以下一步我们需要在golang中去访问这个地址。多提一句,到目前 为止还不需要科学上网的技能。还是一样,如果不想在本机测试结果,可以到这个地址去 瞧一瞧看一看。

调用API

在网上查了查发现大家对golang自带的http库表示比较满意,所以本着少用第三方库的原则,就直接使用了内置的http库来获取JSON。程序如下:

package main

import (
    "fmt"
    "net/http"
    "net/url"
)

func main() {
    info_api := userinfo_api("4", "")
    fmt.Println(info_api)
    get_api_resp(info_api)
}

func userinfo_api(account_id string, access_token string) string {
    base_url := "http://graph.facebook.com/"

    params := make(url.Values)
    params.Set("fields", "id,name,picture")
    if access_token != "" {
        params.Set("access_token", access_token)
    }

    api_url := base_url + account_id + "?" + params.Encode()

    return api_url
}

func get_api_resp(api_url string) {
    resp, err := http.Get(api_url)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    fmt.Println(resp)
}

可以看到,直接使用http.Get即可向服务器端发送GET请求。这个程序最后打印出来的resp是一个http.Response的类型,而我们真正 需要的是这个响应所带来的内容,所以需要用下面的调用来取得真正的响应内容:

import "io/ioutil"

...

content, err := ioutil.ReadAll(resp.Body)

此时得到的content[]byte类型的数据,即byte类型的数组,如果直接用fmt.Println(content)打印出来则会在屏幕 上显示数据的Ascii码,所以如果想以字符串的形式打印到屏幕上需要将其转换为string类型:

fmt.Println(string(content))

此时屏幕上显示的即是正常的JSON响应了。

处理JSON数据

此时的JSON数据还只是一个字符串,程序并不认为它和其它的字符串有什么不同,此时就需要使用内置的JSON库去解码,解码所得的结果放于map结构中。

golang的解码有两种方式。第一种方式是明确知道返回JSON的数据格式,包括返回数据的结构,每一项的名称,以及值的类型,这种类型的返回数据可以直接解码到预 先定义的struct中,然后使用点操作符调用获取其中的值,如下所示:

type userinfo_api_resp struct {
    Id      string
    Name    string
    Picture userinfo_picture_data_wrapper
}

type userinfo_picture_data_wrapper struct {
    Data userinfo_picture_data_detail
}

type userinfo_picture_data_detail struct {
    Url           string
    Is_silhouette bool
}

在定义好这些内容后,使用下面的代码负责把JSON内容填入其中:

// content saves json repsonse in []byte formated
err = json.Unmarshal(content, &json_rslt)
if err != nil {
    panic(err)
}
fmt.Println(json_rslt)

此时即可以像普通的struct那样调用JSON中的值了,如使用json_rslt.Name可以获取到名字,用json_rslt.Picture.Dat a.Url可以获取到图片的地址等。

注意struct中字段名的首字母都需要大写,猜测是由于golang中非首字母大写的变量不会被导出。(话说,真的好想好想吐槽这个设定啊,实在不习惯看大小写混杂的变量名,诶。)

当JSON返回的格式未知,或者不想定义过多的struct时,可以使用第二种方法。interface{}。这个东西是一个空的interface,表示的 是一个没有任何方法的interface,因为任何golang的类型都至少实现了0个方法(唔,直接翻译过来的,读着有点拗口,回头揣摩明白了再回来改),所以 这个类型实际可以用于任何类型的变量。第二种方法就是使用interface{}来表示所有未定的类型,然后由程序去处理相应的数据。还是上面那段JSON数据, 想取到图片地址,则需要这么处理:

json_rslt := map[string]interface{}{}

...

for k1, v1 := range json_rslt {
    if k1 == "picture" {
        for k2, v2 := range v1.(map[string]interface{}) {
            if k2 == "data" {
                for k3, v3 := range v2.(map[string]interface{}) {
                    if k3 == "url" {
                        fmt.Println(v3)
                    }
                }
            }
        }
    }
}

唔,其实也没简单到哪去的感觉……

range后调用的诸如v1.(map[string]interface{})被称为[type assertions](http://golan g.org/ref/spec#Type_assertions),它的意思是断言v1非空而且v1的类型是(map[string]interface{ })。文档中给出的更加精确的说法是x.(T)表示x的动态类型和T一致,T必需实现x的所有接口。更加具体的代码请参考文档

到这,差不多就完成了,POST请求的事情还没用到,等用到后发现有什么需要写的再写吧。另外需要注意的一点是,http://play.golang.org/ 网站上是不允许使用net/http库的,所以我后面的程序才没有给出代码链接,有兴趣的可以在自己机器上试验,记得要科学上网哦,要不然就会返回奇怪的结果了~