概述
最近做业务获取到的API中的数据格式如下:
{ "data": [ { "account_id": "xxx", "campaign_id": "xxxxxxx", "id": "x32xx284026xxx9" }, { "account_id": "xxx", "campaign_id": "xxxxxxxxx", "id": "2xx713x22xx" }, ... ], "paging": { "cursors": { "before": "xxxerrr", "after": "sdfsdf" }, "next": "https://graph.facebook.com/v7.0/xx/adsets?access_token=xxx&pretty=0&fields=xx%2Ccampaign_id&limit=4&after=sdfsdf" } }
我们可以看到:这种格式返回的数据我们只知道下一页应该请求的URL,但是并不知道数据的总量!
这跟我们在以往处理数据库中数据的思路是不一样的。在数据库中处理的数据我们大概知道数据的量级以便做预处理。但是由于这些数据我们是请求三方API获取的,事先我们是不知道数据的量级的,只有不断去请求指定的next对应的URL获取新的数据去处理。
解决思路
我的解决方案是:由于我们并不确定数据的量级,在不知到数据总量的情况下我们可以使用递归的方式,判断请求到的数据只要有next并且next对应的数据不为空,那么就继续请求接口,直到没有next为止,然后“从里往外”将数据塞到事先准备好的容器类型即可。
这里要特别注意的是:递归函数的结束条件是没有next数据!
Go代码
用于解析数据的结构体如下
// 通用的分页 type Paging struct { //Cursors interface{} `json:"cursors"` Next string `json:"next"` } type CampaignFB struct { Id string `gorm:"column:id;size:255;primaryKey; comment:'CampaignID'"` AccountId string `json:"account_id" gorm:"column:account_id;size:255; not null; comment:'所属广告账户Id'"` Name string `gorm:"column:name; size:255; comment:'Campaign名字'"` } // json解析为struct的时候用到 type CampaignFBResponse struct { Data []*CampaignFB `json:"data"` Paging *Paging `json:"paging"` }
递归的代码V1版
// 获取某个账户下的所有Ad 递归获取下一页数据 func GetAccountAds(f func(requestMethod, url string) string, requestMethod, url string, a *[]*model.AdFB){ strRet := f(requestMethod, url) //fmt.Println("strRet>>> ",strRet) var response model.AdFBResponse err := json.NewDecoder(strings.NewReader(strRet)).Decode(&response) if err != nil{ fmt.Println("AdFB json转结构体出错>>> ",err) }else{ //fmt.Println("response>>> ",response) paging := response.Paging data := response.Data for _, adObj := range data{ *a = append(*a, adObj) } // 如果有下一页则递归获取数据 递归结束的条件 if paging != nil && paging.Next != ""{ nextStrUrl := (*paging).Next nextStrUrlNew := strings.Replace(nextStrUrl, "https://graph.facebook.com/v8.0/", BaseURL, 1) // 递归获取下一页数据 GetAccountAds(f, requestMethod,nextStrUrlNew,a) } } }
相关完整代码
// 获取账户下的campaign数据 ———— 调用的地方 func handleAccountCampaigns(token, accountId string, app *app.App) { requestMethod := "GET" url := BaseURL + accountId + "/campaigns?fields=account_id,id,name,objective,status,created_time,updated_time,daily_budget,lifetime_budget&access_token=" + token camLst := []*model.CampaignFB{} // TODO 直接传入ret的地址!可以直接在ret中操作,将账户的Campaign数据写入camLst中!!! GetAccountCampaigns(FBBaseRequestString, requestMethod, url, &camLst) // fmt.Println("len_ret>>> ", len(camLst)) // 存入数据库 for _, camObj := range camLst { // Format一下 camObj.Format() // 直接用Save方法 实现 insert或update err := app.SrvStore.Campaign().FBSave(camObj) if err != nil { mlog.Error("创建Campaign失败:" + (*camObj).Id) } } } // 获取某个账户下的所有AdSet 递归获取下一页数据 func GetAccountCampaigns(f func(requestMethod, url string) string, requestMethod, url string, c *[]*model.CampaignFB) { strRet := f(requestMethod, url) //fmt.Println("strRet>>> ", strRet) // json转结构体 Campaign var response model.CampaignFBResponse err := json.NewDecoder(strings.NewReader(strRet)).Decode(&response) if err != nil { fmt.Println("CampaignFB json转结构体出错>>> ", err) } else { //fmt.Println("response>>> ",response) paging := response.Paging data := response.Data for _, camObj := range data { // 往成员变量中写入数据 *c = append(*c, camObj) } // 有下一页就递归获取 注意这里要加双重判断!!! if paging != nil && paging.Next != "" { nextStrUrl := (*paging).Next nextStrUrlNew := strings.Replace(nextStrUrl, "https://graph.facebook.com/v8.0/", BaseURL, 1) // 递归获取下一页数据 GetAccountCampaigns(f, requestMethod, nextStrUrlNew, c) } } } // FB base // 发HTTP请求并解析数据 func FBBaseRequestString(requestMethod, url string) string { client := &http.Client{} req, err := http.NewRequest(requestMethod, url, nil) if err != nil { fmt.Println("err1 >>>>> ", err) } resp, err := client.Do(req) if err != nil { fmt.Println("err2 >>>>>", err) } returnStr, err := parseResponseString(resp) //fmt.Println("returnStr>>> " + returnStr) // HTTP字节流转为str的结果 return returnStr } // 解析http请求返回的结果 ———— 转换为string func parseResponseString(response *http.Response) (string, error) { body, err := ioutil.ReadAll(response.Body) return string(body), err // 将io数据流转换为string }
简单说明
这里需要注意函数的最后一个参数:一定要是一个指针类型(引用)!这个函数并不返回任何数值,将外部事先定义好存储对应结构的切片后,将这个切片的地址传入即可:
// 事先定义好接收的切片 camLst := []*model.AdFB{} // 将camLst的地址传入,函数执行后将数据会塞进camLst中 GetAccountAds(FBBaseRequestString, requestMethod, url, &camLst)
递归V2版 ***
上面的写法还是有点问题,在实际中自己优化了一下写法:
// 获取某个账户下的所有Ad 递归获取下一页数据 func GetAccountAds(f func(requestMethod, url string) string, requestMethod, url string) ([]*model.AdFB, *model.Apperror){ strRet := f(requestMethod, url) //fmt.Println("strRet>>> ",strRet) var a []*model.AdFB var response model.AdFBResponse err := json.NewDecoder(strings.NewReader(strRet)).Decode(&response) if err != nil{ fmt.Println("AdFB json转结构体出错>>> ",err) return nil, model.NewApperror("xxx") } // 先将数据保存一下再分页 //fmt.Println("response>>> ",response) data := response.Data for _, adObj := range data{ a = append(a, adObj) } paging := response.Paging // 如果有下一页则递归获取数据 递归结束的条件 if paging != nil && paging.Next != ""{ nextStrUrl := (*paging).Next nextStrUrlNew := strings.Replace(nextStrUrl, "https://graph.facebook.com/v8.0/", BaseURL, 1) // 递归获取下一页数据并返回结果 if nextData, err := GetAccountAds(f, requestMethod,nextStrUrlNew); err != nil{ return a, nil }else{ // 注意这里的写法 return append(a,nextData...),nil } } // 没有分页数据直接返回即可 return a, nil }
~~~