如何在R中调用Twitter API v1.1更新推特个人主页横幅
需求说明
需要在R环境中通过Twitter API v1.1调用update_profile_banner接口更新个人推特主页横幅(头图),不限制实现方式:可以是现有R包提供的函数(目前已知主流R包暂不支持该功能),也可以通过system2("curl", args = args_strng)调用curl,或是其他任何可在R内运行的方案均可。
已尝试方案
方案1:基于rtweet(v0.7.0)实现
rtweet是对接Twitter API v1.1最主流的R包,内置大量接口函数,但暂不支持更新主页横幅的功能。
尝试基于rtweet自定义函数,传入符合尺寸要求(1500x500px)的本地图片路径,将图片编码后上传作为新的主页横幅,代码如下:
update_profile_banner <- function (banner_file, token = NULL) { token <- rtweet:::check_token(token) banner_uri <- base64enc::base64encode(banner_file) query <- "account/update_profile_banner" # 构建参数 params <- list(banner = banner_uri) # 生成请求URL url <- rtweet:::make_url(query = query, param = params) # 发送请求 r <- rtweet:::TWIT(get = FALSE, url, token) if (!r$status_code %in% c(200,201,202)) { return(httr::content(r)) } message("主页横幅图片已更新!") } # 加载依赖,准备合法Twitter token library(rtweet) # v.0.7.0 library(httr) # v.1.4.2 mytoken <- rtweet::create_token( app = "你的Twitter应用名称", consumer_key = Sys.getenv("MY_CONSUMER_API_KEY"), consumer_secret = Sys.getenv("MY_CONSUMER_API_SECRET"), access_token = Sys.getenv("MY_ACCESS_TOKEN"), access_secret = Sys.getenv("MY_ACCESS_TOKEN_SECRET"), set_renv = FALSE ) # 本地测试图片路径 test_png <- "本地符合尺寸要求的横幅图片路径" update_profile_banner(test_png, mytoken) #> Error in curl::curl_fetch_memory(url, handle = handle): HTTP/2 stream 0 was not closed cleanly: INTERNAL_ERROR (err 2)
初始报错参考价值有限,查询相关问题后将HTTP版本从2切换为1.1,得到更明确的报错:请求头过长。问题出在底层httr调用curl时,直接把超长的base64图片编码字符串拼接在URL参数里,没有区分请求头和请求体,导致请求头长度超限,请求形式如下:
POST https://api.twitter.com/1.1/account/update_profile_banner.json?banner=超长base64图片字符串+oauth鉴权信息
为验证该推测,用相同逻辑编写获取主页横幅的函数,传入短参数的场景下可以正常运行,确认问题就是POST请求携带的超长base64编码被错误放在URL参数中导致的。
get_profile_banner <- function (screen_name, token) { token <- rtweet:::check_token(token) query <- "users/profile_banner" # 构建参数 params <- list(screen_name = screen_name) # 生成请求URL url <- rtweet:::make_url(query = query, param = params) r <- rtweet:::TWIT(get = TRUE, url, token) if (!r$status_code %in% c(200,201,202)) { return(httr::content(r)) } rtweet:::from_js(r) } get_profile_banner("timteafan", mytoken) # 返回结果正常,截断展示 #> $sizes #> $sizes$ipad #> $sizes$ipad$h #> [1] 313 #> #> $sizes$ipad$w #> [1] 626 #> #> $sizes$ipad$url #> [1] "对应尺寸横幅的CDN地址"
方案2:通过system2("curl")直接调用curl
尝试的替代方案是通过R的system2()直接调用curl,构造规范请求,分离携带授权信息的请求头和携带图片字符串的请求体。
该方案的难点是需要自行生成OAuth签名,自行实现失败后,改为通过httr生成签名和全部OAuth凭证,再作为请求头传入curl命令。
首先测试获取主页横幅的逻辑,代码如下,可以正常运行:
get_banner <- function(screen_name, token) { query <- "users/profile_banner" url <- rtweet:::make_url(query = query, param = list(screen_name = screen_name)) # 生成oauth凭证 oauth <- oauth_GET(url, token) oauth <- oauth[[which(names(oauth) == "Authorization")]] # 构造curl命令 req <- "--get 'https://api.twitter.com/1.1/users/profile_banner.json?'" data <- paste0("--data 'screen_name=", screen_name, "'") header <- paste0("--header 'Authorization:", oauth, "'") args <- paste(req, data, header, collapse = " ") system2("curl", args = args) } # 辅助函数 oauth_GET <- function (url = NULL, config = list(), ..., handle = NULL) { hu <- httr:::handle_url(handle, url, ...) req <- httr:::request_build("GET", hu$url, as.token.request(config), ...) oauth_req(req, hu$handle$handle) } oauth_req <- function (req, handle) { stopifnot(httr:::is.request(req), inherits(handle, "curl_handle")) req <- httr:::request_prepare(req) req$headers } as.token.request <- function(x) auth_req(auth_token = x) auth_req <- function (auth_token) { structure(list(auth_token = auth_token), class = "request") } library(httr) get_banner("timteafan", mytoken) # 正常返回横幅信息,结果略
用相同思路编写上传横幅的函数后运行失败,返回code32“无法完成认证”的错误,原因是生成授权信息时,超长图片字符串提前拼入URL参与签名,和最终curl发送的请求结构不匹配,导致签名校验失败。
upload_banner <- function(banner_file, token) { query <- "account/update_profile_banner" banner_uri <- base64enc::base64encode(banner_file) url <- rtweet:::make_url(query = query, param = list(banner = banner_uri)) oauth <- oauth_POST(url, token) oauth <- oauth[names(oauth) == "Authorization"] req <- "--request POST 'https://api.twitter.com/1.1/account/update_profile_banner.json?'" header <- paste0("--header 'Authorization: ", oauth, "'") header2 <- paste0("--header 'Content-Type: application/json'") header3 <- paste0("--header 'Accept: application/json, text/xml, application/xml, */*'") data <- paste0('--data "banner=', banner_uri,'"') myargs <- paste(req, header2, header3, header, data, collapse = " ") system2("curl", args = myargs) } oauth_POST <- function (url = NULL, config = list(), ..., body = NULL, encode = c("multipart", "form", "json", "raw"), handle = NULL) { encode <- match.arg(encode) hu <- httr:::handle_url(handle, url, ...) req <- httr:::request_build("POST", hu$url, httr:::body_config(body, match.arg(encode)), auth_req(config), ...) oauth_req(req, hu$handle$handle) } upload_banner(test_png, mytoken) #> {"errors":[{"message":"Could not authenticate you","code":32}]}
尚未尝试的方向
- rtweet开发版:从公开的源码来看,底层对httr的调用逻辑没有大幅改动,预计问题仍然存在。
- httr2包:此前看到资料提及该包不支持OAUTH 1.0认证,暂未尝试。
- 其他R包方案:如直接使用curl、RCurl或其他可对接Twitter API的R包。
接口可用性验证
目前API v1.1的该接口仍可正常使用,通过Python的tweepy包可成功调用更新横幅,参考代码如下:
import os import tweepy # 读取环境变量中的密钥 CONKEY = os.getenv('MYTWITTER_CONSUMER_API_KEY') CONSEC = os.environ.get('MYTWITTER_CONSUMER_API_SECRET') ACCKEY = os.getenv('MYTWITTER_ACCESS_TOKEN') ACCSEC = os.environ.get('MYTWITTER_ACCESS_TOKEN_SECRET') auth = tweepy.OAuth1UserHandler( CONKEY, CONSEC, ACCKEY, ACCSEC ) api = tweepy.API(auth) image_url = '本地横幅图片路径/可访问的图片直链' api.update_profile_banner(image_url)
可行解决方案
问题核心有两个:一是OAuth 1.0签名时不能把base64编码的图片参数拼到URL里参与签名,二是图片数据要放在POST请求体中,不能放在URL参数里。直接用httr的POST函数,把banner参数放在body里用form编码即可,不需要绕弯调用系统curl,代码如下:
update_twitter_banner <- function(banner_path, token = NULL) { # 检查token合法性 token <- rtweet:::check_token(token) # 读取本地图片转base64,如果是网络图片先下载到本地临时文件 if (grepl("^https?://", banner_path)) { tmp <- tempfile(fileext = ".png") on.exit(unlink(tmp)) download.file(banner_path, tmp, mode = "wb", quiet = TRUE) banner_path <- tmp } banner_b64 <- base64enc::base64encode(banner_path) # 基础接口地址,不带任何参数 base_url <- "https://api.twitter.com/1.1/account/update_profile_banner.json" # 发送POST请求,banner参数放在form格式的请求体中 resp <- httr::POST( url = base_url, config = token, body = list(banner = banner_b64), encode = "form" ) # 处理返回结果 if (httr::status_code(resp) %in% c(200, 201, 202)) { message("主页横幅更新成功!") invisible(TRUE) } else { err_info <- httr::content(resp, as = "parsed")$errors[[1]] stop(paste0("更新失败,错误码", err_info$code, ":", err_info$message)) } } # 调用示例 update_twitter_banner(test_png, mytoken)
注意:传入的图片必须严格符合1500x500px的尺寸要求,否则接口会返回参数错误。
内容的提问来源于stack exchange,提问作者TimTeaFan

