MANKA の Blog
怎么爬 Twitter

2020-02-27

#Twitter#Twitter Monitor#Twitter Api

由于各种各样的原因,大家会有想要爬取twitter的用户的信息的想法,但申请官方api的那几篇小作文不是谁都能写得出的(本人就写不出,泻药,已被拒),所以需要直接开始爬取内容,而爬twitter有时就是一个大坑。这里就讲讲本人处理Twitter Monitor期间遇过的坑。

Javascript文件

Twitter会在首页引入5个JavaScript文件,由于webpack打包的原因,它们的名字不一定与本文的相同,但可以提供参考,也为下文的说明提供参考

namelink
polyfillshttps://abs.twimg.com/responsive-web/web/polyfills.321d1c14.js
vendorshttps://abs.twimg.com/responsive-web/web/vendors~main.483e4ab4.js
i18n-rweb/zhhttps://abs.twimg.com/responsive-web/web/i18n-rweb/zh.322c7be4.js
i18n-horizon/zhhttps://abs.twimg.com/responsive-web/web/i18n-horizon/zh.15b97c64.js
mainhttps://abs.twimg.com/responsive-web/web/main.f18fcbb4.js

第3、4位的是语言文件

还有一些其他可能有用的文件

namelink
bundle.UserProfilehttps://abs.twimg.com/responsive-web/web/bundle.UserProfile.e36cd9b4.js

鉴权

爬内容的api来来去去就是那几个,已经好几年没有更新过了,但是大多数人会遇到的第一个问题,那就是鉴权。举个例子

curl 'https://api.twitter.com/2/timeline/profile/783214.json?tweet_mode=extended&count=20' \ -H 'x-guest-token: 1232704521454999999' \ -H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \ --compressed

上面是爬取一个用户推文时间线(timeline)的最低限度的请求,其中链接不需要多说都能理解,这里出现了x-guest-tokenauthorization。虽然这两个值看起来让人毫无头绪,其实都是可以自行取得的。

x-guest-token

x-guest-token 决定你的rate-limit,当此值为空或者不正确时twitter会返回如

{  "errors": [    {      "message": "Rate limit exceeded",      "code": 88    }  ]}

的错误,此值会在用户第一次访问twitter的时候在网页上赋予,所以直接构造一个请求

curl 'https://twitter.com' --compressed

此时得到的网页会有以下几行赋予x-guest-token

<script nonce="MDRjZmJlNWItYWNmOC00MTdiLWIxYjUtYTFhZTUyYTc2ODg4">  document.cookie = decodeURIComponent("gt=1232704521454999999; Max-Age=10800; Domain=.twitter.com; Path=/; Secure");</script>

2020-06-24 更新: 上述方法已失效,twitter 已改为在 响应头(Response header) 赋予gt

set-cookie: gt=1232704521454999999; Max-Age=10200; Expires=Wed, 24 Jun 2020 08:31:03 GMT; Path=/; Domain=.twitter.com; Secure

2020-07-14 更新: 他们又改回来了

因此可以构建正则表达式 /gt=([0-9]+)/ 取得此值。

* 偶然在一条issue翻到一个1.1 版的 api,因为是旧版api,所以不知道什么时候失效

curl 'https://api.twitter.com/1.1/guest/activate.json' \-X 'POST' \-H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \--compressed

会返回

{  "guest_token": "1290584024826540032"}

method 要使用 POST ,用 GET 会返回

{  "errors": [{    "code": 86,    "message": "This method requires a POST."  }]}

authorization

AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA

这没什么好说的,此值固定,出现在 https://abs.twimg.com/responsive-web/web/main.f18fcbb4.js ,即使未来twitter更新了前端也会出现在类似文件名称的js文件中。

<link rel="preload" as="script" crossorigin="anonymous" href="https://abs.twimg.com/responsive-web/web/main.f18fcbb4.js" nonce="OGE1NzNmZTQtNzQxMS00Y2FiLTllYTItMDFlNGZlNTM1ZDFh" />

差不多是这样的。


rate-limit

rate-limit 限制了用户在一定时间内请求的次数,并且会在相对时间后重置,在twitter,这个时间是15分钟。

根据上文我们可以知道twitter是通过x-guest-token判断rate-limit的,在用户的每次请求所返回的header上都会有以下内容

x-rate-limit-limit: 180x-rate-limit-remaining: 179x-rate-limit-reset: 1567401449

很好理解对吧,https://api.twitter.com/1.1/application/rate_limit_status.json 这个文件详细说明了各个api的rate-limit。

不要以为你刷新了 guest-token 就不会受到限制,那只是说明你的请求还不够多


用户信息

爬取用户信息很轻松,这里有几个接口

这里使用的 1.1 版本 api 已失效,请使用 GraphQL api , 关于 GraphQL api 的使用,请阅读 怎么爬Twitter(GraphQL)

  • 第一次加载用户信息时使用
    curl 'https://api.twitter.com/graphql/P8ph10GzBbdMqWZxulqCfA/UserByScreenName?variables=%7B%22screen_name%22%3A%22twitter%22%2C%22withHighlightedLabel%22%3Afalse%7D' \  -H 'x-guest-token: 1232704521454999999' \  -H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \  --compressed

    其中P8ph10GzBbdMqWZxulqCfA出现在前文提到的那个main开头名称的js文件内,此值是否固定未知,故不推荐
    {  queryId: "P8ph10GzBbdMqWZxulqCfA",  operationName: "UserByScreenName",  operationType: "query"}

    此处的变量是urlencode化的json
    {  "screen_name": "twitter",  "withHighlightedLabel": false}
    • Twitter 为各国官媒添加了标记,如 政府机构(小旗子)、 官方媒体(小讲台)的帐号(包括部分个人)以及 候选人(投票箱),此类信息藏身于 data.user.affiliates_highlighted_label.label 节点,其中,来自中国的帐号不包括港澳台的官方机构,且对应的跳转链接对应的内容比其他版本的开头多了一段话,一般版本见文章末尾的参考网页
      下面是新华社的信息节选
      {  "affiliates_highlighted_label": {      "label": {          "url": {              "url_type": "DeepLink",              "url": "https://help.twitter.com/rules-and-policies/state-affiliated-china"          },          "badge": {              "url": "https://pbs.twimg.com/semantic_core_img/1290398851254247424/qxZbv2Fr?format=png&name=orig"          },          "description": "China state-affiliated media"      }  }}
  • 刷新时间线时使用
    curl 'https://api.twitter.com/1.1/users/show.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&screen_name=twitter' \ -H 'x-guest-token: 1232704521454999999' \ -H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \ --compressed
    include_profile_interstitial_type: 1include_blocking: 1include_blocked_by: 1include_followed_by: 1include_want_retweets: 1include_mute_edge: 1include_can_dm: 1include_can_media_tag: 1skip_status: 1user_id: 783214screen_name: twitter

    其中,user_idscreen_name需要二选一即可。
    返回的数据大同小异,在以后都以第二种形式返回的为参考
    {  "id": 783214,  "id_str": "783214",  "name": "Twitter",  "screen_name": "Twitter",  "location": "Everywhere",  "profile_location": null,  "description": "What\u2019s happening?!",  "url": "https:\/\/t.co\/TAXQpsHa5X",  "entities": {    "url": {      "urls": [        {          "url": "https:\/\/t.co\/TAXQpsHa5X",          "expanded_url": "https:\/\/about.twitter.com\/",          "display_url": "about.twitter.com",          "indices": [0,23]        }      ]    },    "description": {      "urls":[]    }  },  "protected": false,  "followers_count": 57276889,  "fast_followers_count": 7785,  "normal_followers_count": 57269104,  "friends_count": 28,  "listed_count": 90682,  "created_at": "Tue Feb 20 14:35:54 +0000 2007",  "favourites_count": 6404,  "utc_offset": null,  "time_zone": null,  "geo_enabled": true,  "verified": true,  "statuses_count": 13096,  "media_count": 1993,  "lang": null,  "contributors_enabled": false,  "is_translator": false,  "is_translation_enabled": false,  "profile_background_color": "ACDED6",  "profile_background_image_url": "http:\/\/abs.twimg.com\/images\/themes\/theme18\/bg.gif",  "profile_background_image_url_https": "https:\/\/abs.twimg.com\/images\/themes\/theme18\/bg.gif",  "profile_background_tile": true,  "profile_image_url": "http:\/\/pbs.twimg.com\/profile_images\/1111729635610382336\/_65QFl7B_normal.png",  "profile_image_url_https": "https:\/\/pbs.twimg.com\/profile_images\/1111729635610382336\/_65QFl7B_normal.png",  "profile_banner_url": "https:\/\/pbs.twimg.com\/profile_banners\/783214\/1556918042",  "profile_link_color": "1B95E0",  "profile_sidebar_border_color": "FFFFFF",  "profile_sidebar_fill_color": "F6F6F6",  "profile_text_color": "333333",  "profile_use_background_image": true,  "has_extended_profile": true,  "default_profile": false,  "default_profile_image": false,  "pinned_tweet_ids": [],  "pinned_tweet_ids_str": [],  "has_custom_timelines": true,  "can_dm": false,  "can_media_tag": true,  "following": false,  "follow_request_sent": false,  "notifications": false,  "muting": false,  "blocking": false,  "blocked_by": false,  "want_retweets": false,  "advertiser_account_type": "promotable_user",  "advertiser_account_service_levels": [    "media_studio",    "dso",    "analytics",    "dso",    "dso"  ],  "profile_interstitial_type": "",  "business_profile_state": "none",  "translator_type": "regular",  "followed_by": false,  "require_some_consent": false}

    有几点比较有意思
    • entities 中不含hashtag的信息,所以我也不知道它使用什么骚套路实现的
    • 用户的的背景图的格式是https://pbs.twimg.com/profile_banners/:userid/:bannerid,所以保存的时候其实可以把它拆开到使用的时候再组装……
    • profile_interstitial_type是一个很有意思的字段,它留空代表正常用户,其他都会得到twitter的警告,以下有对应警告,仅供参考 注:此表格内容来自bundle.UserProfile
      valuemessage
      fake_account警告:此账号暂时受限。
      你看到这则警告,因为该账号有异常活动。是否仍要查看?
      sensitive_media警告:此个人资料可能包含敏感内容。
      你看到这则警告,因为其中涉嫌使用不良图片或语言。是否仍要查看?

      注:这种警告经常在各种NSFW号上出现,为了找例子本人心灵受到了莫大的震撼
      {  FakeAccount: "fake_account",  OffensiveProfileContent: "offensive_profile_content",//What this is?  SensitiveMedia: "sensitive_media",  Timeout: "timeout"}
    • 用户有几种状态
      • 锁推:好吧这是我的说法,官方的说法叫做“保护”,被保护的帐号的帐号信息的protect字段为true,访问被保护的用户的页面会显示
        这些推文受到保护
        只有经过批准的关注者才可查看 @baristabar 的推文。若要申请访问,点击关注。
      • 删号或账号不存在
        • 帐号不存在:请求用户信息会返回
          {  "errors": [    {      "code": 50,      "message": "User not found."    }  ]}
        • 自删:即自己删号,这只是一种相对的说法,因为直接请求此用户的信息所返回的内容同上条,但若要修改用户名(screen_name)到该自删帐号的用户最后的用户名会被提示该用户名已被占用。请另选一个。
      • 被冻结:顾名思义,就是被封了
        {  "errors": [    {      "code": 63,      "message": "User has been suspended."    }  ]}
    • 因为Twitter有一个non_username_paths,顾名思义,就是不可做为用户名的目录,即便如此,那个列表并不是一定可靠的,因为即使需要用户名在列表上也是可以买的……下面列一个这个列表的现状,全表请参阅参考网页。(2020-3-8 0:44 UTC+8)
      用户名返回代码返回信息/全名备注
      accounts63User has been suspended.
      all0ALL - Accor Live Limitless认证用户
      anywhere0Anywhere一般用户
      blog0steve跳转 https://blog.twitter.com/
      business0Bloomberg认证用户
      faq0Th\ufffderryTwitter FAQ页面
      followers63User has been suspended.
      friends0Friends一般用户
      home0[email protected]登录用户显示用户时间线,未登录用户跳转登录界面
      jobs63User has been suspended.
      list0ya认证用户
      logout0Waterfall登录用户显示登出确认,未登录用户跳转登录界面
      me0Maine.com一般用户
      retweets0All the crap I get on Whatsapp一般用户
      sent0Sent一般用户
      settings0Settings用户设置
      signup0Feanamacatangay跳转到 https://twitter.com/i/flow/signup
      signin0Signin一般用户
      terms0Terms页面不存在
      tos63User has been suspended.
      twttr0-已锁
      welcome63User has been suspended.

推文内容

Tweet id

Tweet id 我原本以为是很简单的玩意,但踩了坑以后觉得还是得说一说。

先放个例子

tweet_link: https://twitter.com/twitter/status/1458144219286093827tweet_link2: https://twitter.com/i/status/1458144219286093827origin_link: https://twitter.com/TwitterBlue/status/1458110302793338892tweet_id: 1458144219286093827conversation_id: 1458144219286093827origin_tweet_id: 1458110302793338892

tweet_id 是一条推文的唯一标识,使用雪花算法(Snowflake) 生成,具有唯一性。不论是新发推还是转推又或者是回复都会产生新的 tweet_id,而讨论串的 conversation_id 就是串最开始的那条推文的 tweet_id,查找转推的信息时(如异步获取投票结果、audiospace的状态信息等)就需要使用转推的原始 tweet_id 获取,因为转推回归虚无只需要取消就可以了,这就是为什么twitter monitor的外链经常会消失,因为我并没有存储原始 tweet_id

时间线(Timeline)

此处 api 已失效,请使用 GraphQL api , 关于 GraphQL api 的使用,请阅读 怎么爬Twitter(GraphQL)

针对单个用户的时间线还是相对很简单的,只需要如同上面请求个人资料一样请求api接口,比如下面这个请求 twitter官方 的最近时间线的例子

curl 'https://api.twitter.com/2/timeline/profile/783214.json?tweet_mode=extended' \ -H 'x-guest-token: 1232704521454999999' \ -H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \ --compressed

实际上获取推文用上的变量会很多,比如 twitter 网页的请求如下,注意三个变量uid, count, cursor

"https://api.twitter.com/2/timeline/profile/${uid}.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_composer_source=true&include_ext_alt_text=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweets=true&ext=mediaStats%2CcameraMoment&count=${count}&cursor=${cursor}"

我把这些变量列个表格出来,并逐步更新其意义(太好啦,不用写了)

变量默认值意义
include_profile_interstitial_type1
include_blocking1
include_blocked_by1
include_followed_by1
include_want_retweets1
include_mute_edge1
include_can_dm1包含可dm,即私信
include_can_media_tag1
skip_status1
cards_platformWeb-12
include_cards1包含卡片
include_composer_sourcetrue
include_ext_alt_texttrue
include_reply_count1包含回复数量
tweet_modeextended推文模式
include_entitiestrue
include_user_entitiestrue
include_ext_media_colortrue
include_ext_media_availabilitytrue
send_error_codestrue发送 error_codes
simple_quoted_tweetstrue
extmediaStats,cameraMoment
count20推文数量
cursor

然后就能得到一个巨大的json,当初我花了点时间用 jsoneditoronline 辅助阅读整个json理清结构,理清结构以后事情就比较简单了 这里只提一些比较常用的项目,剩下的需要自行翻找

conversation

这是取得一条推文以及其回复的方式,获取到的 json 内容的结构与 时间线(Timeline) 几乎一致

curl 'https://api.twitter.com/2/timeline/conversation/1334542969530183683.json?cards_platform=Web-12&include_cards=1' \ -H 'x-guest-token: 1232704521454999999' \ -H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \ --compressed

*twitter 网页的请求如下,注意变量 tweet_id

"https://api.twitter.com/2/timeline/conversation/${tweet_id}.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_composer_source=true&include_ext_alt_text=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweets=true&count=20&ext=mediaStats%2CcameraMoment"

tweets

这里以 tweet id 为键名,键值的内容一目了然,所以只提以下几点

  • 这里包含了这一段用户时间线中出现过的所有推文,包括但不限于此用户发推(tweet)转推(retweet) 以及 引用(quote)
  • 引用(quote) 有时候会因为各种奇奇怪怪的原因导致显示 这条推文不可用。,但原推并没有被删,引用我在Twitter Monitor的注释

    //推文不可用不等于原推被删, 虽然真正的原因是什么我只能说我也不知道 //群友说可能是被屏蔽了, 仅供参考

  • 我翻过网站爬取的内容,只找出五种 entities 类型,分别是
    名称描述
    symbols这个貌似是根据上市代码搜索相关公司或虚拟货币的推文,例如 Twitter$TWTR、比特币的 $BTC,官方管这玩意作cashtag, 个人感觉除了把#换成$以外并没有什么区别
    hashtagshashtag,顾名思义,主题标签,话题标签,平时会出现在侧边的 有什么新鲜事? ,其中有一些标签会带有小图标,那些是活动图标,官方称之为 Hashflags,关于Hashflags请阅读后面的内容
    urls链接,有原始链接和t.co短链接
    user_mentions@的形式提及的用户名
    media媒体信息,后面会详细提及
  • 置顶推文会反反复复地出现在最新内容中,要注意去重
  • 更新的参数 cursor 藏得很深,初次处理会找得很头疼,一般出现在倒数第二项(凡事没绝对,为了靠谱我扔了个foreach去处理),路径参考下图左下角
    其实这里有张图

    其实这里有张图

  • 根据 About public-interest exceptions on Twitter ,一些可能违反twitter规则但可能有利于公众利益的推文不会被删,但twitter会在此推文上加注一段告示,并且会限制对该推文的互动,最近的经典例子就是 Donald J. Trump 关于 在明尼苏达州发生的事件 的发言

Cards

卡片很麻烦,各种意义上的麻烦,不容易发,也不容易找,找不到是很正常的事,总之就很让人头疼

在twitter首页可以找到以下一段json

{  "responsive_web_unified_cards_all_cards_enabled":{"value":false},  "responsive_web_unified_cards_amplify_enabled":{"value":true},  "responsive_web_unified_cards_app_enabled":{"value":true},  "responsive_web_unified_cards_appplayer_enabled":{"value":true},  "responsive_web_unified_cards_audio_enabled":{"value":true},  "responsive_web_unified_cards_broadcast_enabled":{"value":true},  "responsive_web_unified_cards_direct_store_link_app_enabled":{"value":true},  "responsive_web_unified_cards_image_direct_message_enabled":{"value":true},  "responsive_web_unified_cards_live_event_enabled":{"value":false},  "responsive_web_unified_cards_message_me_enabled":{"value":true},  "responsive_web_unified_cards_moment_enabled":{"value":true},  "responsive_web_unified_cards_periscope_broadcast_enabled":{"value":true},  "responsive_web_unified_cards_player_enabled":{"value":true},  "responsive_web_unified_cards_poll2choice_image_enabled":{"value":false},  "responsive_web_unified_cards_poll2choice_text_only_enabled":{"value":true},  "responsive_web_unified_cards_poll2choice_video_enabled":{"value":false},  "responsive_web_unified_cards_poll3choice_image_enabled":{"value":false},  "responsive_web_unified_cards_poll3choice_text_only_enabled":{"value":true},  "responsive_web_unified_cards_poll3choice_video_enabled":{"value":false},  "responsive_web_unified_cards_poll4choice_image_enabled":{"value":false},  "responsive_web_unified_cards_poll4choice_text_only_enabled":{"value":true},  "responsive_web_unified_cards_poll4choice_video_enabled":{"value":false},  "responsive_web_unified_cards_promo_image_app_enabled":{"value":true},  "responsive_web_unified_cards_promo_image_convo_enabled":{"value":true},  "responsive_web_unified_cards_promo_video_convo_enabled":{"value":true},  "responsive_web_unified_cards_promo_video_website_enabled":{"value":true},  "responsive_web_unified_cards_promo_website_enabled":{"value":true},  "responsive_web_unified_cards_promoted_cards_enabled":{"value":true},  "responsive_web_unified_cards_summary_enabled":{"value":true},  "responsive_web_unified_cards_summary_large_image_enabled":{"value":true},  "responsive_web_unified_cards_unified_card_enabled":{"value":true},  "responsive_web_unified_cards_video_direct_message_enabled":{"value":true},  "responsive_web_unified_cards_vine_enabled":{"value":true}}

可以看到,大多数的卡片形式都是已经支持了,但实际上一般用户能发送出来的卡片只有少数几种,剩下的需要用 Twitter for Advertisers 或者其他未知的形式发送。

我已经找出了22种卡片的例子(投票型的只有一个),存放在 /docs/cards.json ,要利用twitter的搜索引擎寻找更多的卡片可以参考 igorbrigadir/twitter-advanced-search,讲得还挺详细的。

有些类型的卡片的图会随着网页的改版而更新,有必要的时候需要再跑一轮更新这些信息;至于投票型的卡片,第一次看到的时候我是震惊的,区分每个选项用的是 choice1_labelchoice2_label……习惯了就好了(写小作文那边的api倒是很舒服https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/entities-object

下面是一些没什么用的其他知识

  • periscope_broadcastsummary_large_image 结构相似,虽然它的UI与 broadcast 相似;结构的相似的还有 audiopromo_video_websiteplayerappplayer ,这些媒体都会提供一个vmap文件,在player_url节点;direct_store_link_appapp
  • promo_image_convopromo_video_convo 都是提供了几个 hashtag 用以发推,发推后可见另一张图片和另一段信息
  • promo_image_app 是未登录时提供的卡片类型,点击下面的 安装 按钮时会跳到 Twitter 首页是它的特性,同一条推文在用户登录以后会使用 unified_card,此时提供了正确的链接和更完整的信息,比如这个例子
  • 卡片的 url 字段提供的信息是混乱的,不过一般可以回到同一条推的 entitiesurls 找到最后一条

unified_card

美好的一天从 Twitter Monitor 报警结束

因为 Twitter Monitor 已经能够处理大多数常见的卡片了,对于它还能找到新卡片还是没想到的,前往 Twitter 瞄了一眼,发现这玩意不是跟 summary_large_image 一个样嘛,想着花个几分钟处理一下就完事了,直到后来我才发现这玩意比我想象要复杂。

是的,单个组件的 unified_card 确实跟 summary_large_image 外观相似,然而根据它的名字就知道它可以拥有多个媒体块(图片、视频,类似走马灯),粗略寻找了一番就能发现(我不知道找全没有,暂时也没法把工作重心放在这里)

名称图片视频复数个媒体(走马灯)复数个底栏应用实例(待续)
image_websiteoxxxx
video_websitexoxxx
image_appoxxxo
video_appxoxxo
image_carousel_websiteoxoxx
video_carousel_websitexooxx
image_carousel_appoxoxo
video_carousel_appxooxo
image_multi_dest_carousel_websiteoxooxtomori_kusunoki/1459102612502953989
  • o 为符合; x 为不符合

其中,后三种会有多个媒体使用 swipeable_media 组件,说白了就是走马灯,其余部分与普通卡片无异。

Twitter Monitor 监控首次报警是 2020-10-16 https://twitter.com/i/status/1316889033583149057,说明可能是为了逐步淘汰掉旧版的花样繁多的卡片,对于前端的处理来说是一件化繁为简的好事,对于爬虫来说……积攒下一百多条报警真有意思……

2021-02-12 更新:

上面的列表已经更新,我暂时只能找到这几项,其中 video_carousel_app我没能找到实例,但根据命名规律我觉得它会存在。

命名规则差不多是:(image|video|mixed_media)[_multi_dest|_single_dest][_carousel]_(app|website),可以根据名字轻松理解含有的类型

  • image 代表是图片,video 代表是视频,mixed_media 代表两者皆有
  • multi_dest 代表有多个不同的底栏内容,single_dest 代表所有媒体内容共享一个底栏
  • carousel 代表有多个媒体内容
  • app 代表卡片代表一个 app ,一般会提供 App Store(iPhone 和 iPad)和 Google Play app store 的链接

处理媒体内容可以参考以下代码

//这里的 $childCardInfo 指的是反序列化后的 `string_value`$childCardInfo = json_decode($card["binding_values"]["unified_card"]["string_value"], true);//中间省略其他部分if (isset($childCardInfo["media_entities"])) {    $tmpChildMediaList = [];    if (isset($childCardInfo["layout"]["data"]["slides"])) {        foreach ($childCardInfo["layout"]["data"]["slides"] as $slide) {            $tmpChildMediaList[] = $childCardInfo["component_objects"][$slide[0]]["data"];        }    } else {        $tmpChildMediaList = $childCardInfo["component_objects"]["swipeable_media_1"]["data"]["media_list"]??[$childCardInfo["component_objects"]["media_1"]["data"]??["id" => "media_1"]];    }    foreach ($tmpChildMediaList as $childCardMediaInfoKeyInfo) {        // tw_media() 是 Twitter Monitor 的通用处理媒体函数        $card["media"] = array_merge($card["media"], tw_media($childCardInfo["media_entities"][$childCardMediaInfoKeyInfo["id"]], $uid, $tweetid, $hidden, "cards", "{$card["data"]["type"]}_{$card["data"]["secondly_type"]}_card_{$childCardInfo["media_entities"][$childCardMediaInfoKeyInfo["id"]]["type"]}", "", $online));    }}

对于我来说,Twitter monitor 需要取得 图片/视频 和各 应用/网页 的信息,所以我用了以上表格的四种分类进行了区分,现在简单介绍一下这四种类型

  • 图片和视频
    比起以前的卡片,图片可视频的信息的json格式更接近于一般推文内的格式;全部媒体信息都丢在 JSON.media_entities, 这可能是考虑到更好的复用。媒体的 media_key 作为 JSON.media_entities 里面键名,而这些 media_key 可以在 JSON.component_objects.media_1.data(单图片/视频) 或 JSON.component_objects.swipeable_media_1.data.media_list (多图片/视频)找到
    我经过简单的修改就能使用 Twitter monitor 处理媒体的函数进行处理了
  • 走马灯
    走马灯是相对单个图片或视频而言的说法,顾名思义,这种类型的卡片有多张图片或者多个视频,我至今仍然还在苦恼在前端上如何兼容这种前期完全没考虑过的类型,反正挺有意思,对吧(当年修改element-ui留下了大坑,未来可能会对其进行修正,修正后会将相关办法在Element-ui填坑指北写上)
  • 应用
    当初看到这玩意的时候我是比较意外的,因为 Twitter 已经有一系列 app 相关的卡片类型了,不过比起旧版app类的只能在登录后的浏览器查看链接,未登录的浏览器会跳转 twitter首页 ,这边同时提供了 AndroidiPhoneiPad 的应用商店信息(话说iPhone和iPad不是一样的吗?),这些内容都可以在 JSON.app_store_data.app_1 找到
  • 网页信息
    website 结尾的卡片都属于这一类,这一类跟一般的卡片的 JSON.binding_values 差不多,但命名更正常了,可以在 JSON.component_objects.details_1.data 找到相关信息

audiospace

2020年,仅限 iOS 的 clubhouse 因为各种大佬的加入而流行,各大社交网站都在做相应的竞品,而这个 audio space 就是来自 twitter 的竞品

这是一个仍在测试中的产品,根据相关介绍页,这玩意只能从 iOS 设备发起,且还有其他限制,从卡片的角度来看,这玩意的结构非常简单

{  "card": {    "rest_id": "https://t.co/k3SPfrlv9h",    "legacy": {      "binding_values": [        {          "key": "id",          "value": {"string_value": "1MnxnllrLowGO", "type": "STRING"}        },        {          "key": "card_url",          "value": {"scribe_key": "card_url", "string_value": "https://t.co/k3SPfrlv9h", "type": "STRING"}        }      ],      "card_platform": {        "platform": {          "audience": {            "name": "production"          },          "device": {"name": "Swift", "version": "12"}        }      },      "name": "3691233323:audiospace",      "url": "https://t.co/k3SPfrlv9h",      "user_refs": []    }  }}

可以获得一串id: 1MnxnllrLowGO,根据这段id可以构建请求:(graphql相关请看**另一篇文章**)

curl 'https://twitter.com/i/api/graphql/5n-vlbXQST8SRiucrlQ6rg/AudioSpaceById?variables=%7B%22id%22%3A%221MnxnllrLowGO%22%2C%22withTweetResult%22%3Atrue%2C%22withReactions%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Afalse%2C%22withUserResults%22%3Atrue%2C%22withBirdwatchPivots%22%3Afalse%2C%22withScheduledSpaces%22%3Atrue%7D' \  -H 'authorization: Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' \  -H 'x-guest-token: 1232704521454999999' \  --compressed

返回

{"data":{"audioSpace":{"metadata":{"rest_id":"1MnxnllrLowGO","state":"Ended","title":"#えみあやないと 待機中","media_key":"28_1410185578763546624","created_at":1625049416591,"started_at":1625049419307,"updated_at":1625050069660,"is_employee_only":false,"is_locked":false,"conversation_controls":0},"sharings":{"items":[],"slice_info":{}},"participants":{"total":0,"admins":[{"periscope_user_id":"1PXEdBvgddwKe","start":1625049416591,"twitter_screen_name":"nittaemi85","display_name":"Nitta Emi","avatar_url":"https://pbs.twimg.com/profile_images/1397442664753029122/i_r0Yued_normal.jpg","is_verified":false,"is_muted_by_admin":false,"is_muted_by_guest":false}],"speakers":[],"listeners":[]}}}}

我还没看过活动中的 audiospace,由于 Twitter 不再支持搜索部分卡片,可以访问 TwitterSpaces 找到很多例子(但都是已结束的)。

list

顾名思义,就是账号列表,没什么好说的,直接看例子吧:paradoxlive_PR/1461257881387294721

Image

处理图片真麻烦,以后我都不想碰它

除掉让人头疼的部分,图片还是挺有意思的,一张图片上传twitter以后会被压缩成各种大小,有时还会进行切割,抽出占比最大的五种颜色(不足五种就处理所有颜色)并记录rgb和占比,这些颜色进行处理后会成为图片未加载时图片框和大图浏览模式时模糊前的背景色(什么算法?这还真问到我了),虽然我最终还是没找到这种颜色 的算法,但找到了一个更好的替代品:blurhash ,一个很不错的图像模糊算法。

在 Twitter ,现在基本只会出现三种媒体文件格式:jpgpngmp4,其中,上传的 gif 格式的图片会被转换成 mp4,图片的大小有五种类型:large|medium|small|thumb|tiny|orig,移动端节流模时使用的是 tiny ,大图浏览使用的是 largeorig 是原图。每张图片不一定有所有的大小类型,有的图片没有 tiny 类型的大小,而是会出现一些类似 800x600 的分辨率大小,所以每次处理的时候都需要手动判断。

{  "sizes": {    "thumb": { "w": 150, "h": 150, "resize": "crop"},    "large": { "w": 1024, "h": 1024, "resize": "fit"},    "small": { "w": 680, "h": 680, "resize": "fit"},    "medium": { "w": 1024, "h": 1024, "resize": "fit"}  }}

一些大小类型的示例,位于 globalObjects.tweets.[tweet_id].entities.media[index].sizes

图片的链接与两种格式 Legacy formatModern format

Legacy format
<base_url>.<format>:<name>
For example:
https://pbs.twimg.com/media/DOhM30VVwAEpIHq.jpg:large

Modern format
The modern format for loading photos was established at Twitter in 2015 and has been defacto since 2017.  All photo media loads should move to this format.
<base_url>?format=<format>&name=<name>
For example:
https://pbs.twimg.com/media/DOhM30VVwAEpIHq?format=jpg&name=large

看完就懂,上面的带媒体卡片是没有 Legacy format 的,只能用 Modern format,其他信息可以参考 entities-object

利用 Twitter Monitor 的资源,顺手做了一个图片视频的下载页面

2022-07-02 更新:

有的媒体内容会有额外的内容,估计是来自YouTube等来源的内容

{  "additional_media_info": {    "title": "蒼き雷霆(アームドブルー) ガンヴォルト 鎖環(ギブス) 挿入歌『理壊者(リベレイター)』",    "description": "《電子の謡精(サイバーディーヴァ)》モルフォが歌う『蒼き雷霆(アームドブルー) ガンヴォルト 鎖環(ギブス)』の挿入歌「理壊者(リベレイター)」のミュージックビデオを公開!\n公式サイト: http://gunvolt.com/GV3/\n\n理壊者(リベレイター)\n\n作詞:ハコファクトリィ\n作曲:山田一法\n編曲:s-don\n歌:モルフォ(櫻川めぐ) \n\n振付:Ayano Ishimuroya",    "call_to_actions": {      "watch_now": {        "url": "https://youtu.be/bfSXvN5v7xg"      }    },    "embeddable": true,    "monetizable": false  }}

版权限制

部分地区可能会无权访问部分媒体,网页版会显示

此视频不对你的位置开放。

如下图的示例,在日本访问即可复现,原理是多次访问视频切片被403

此视频不对你的位置开放

此视频不对你的位置开放

搜索

Twitter 的搜索包罗万象,除了搜索本身,hashtagcrashtag 都是走搜索接口的。 关于搜索的技巧可以参考 igorbrigadir/twitter-advanced-search

搜索使用的是q参数

"https://twitter.com/i/api/2/search/adaptive.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweet=true&q=${query}&count=20&query_source=typed_query&pc=1&spelling_corrections=1&ext=mediaStats%2ChighlightedLabel"
  • 目前单次请求获取上限为20

趋势

趋势可以自行设置地区,默认ip所在地,然后就会显示相关 hashtag,趋势有两个入口,一个是 https://twitter.com/explore/tabs/trending 另一个是 https://twitter.com/i/trends

接口则是

"https://twitter.com/i/api/2/guide.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&send_error_codes=true&simple_quoted_tweet=true&count=20&include_page_configuration=true&entity_tokens=false&ext=mediaStats%2ChighlightedLabel"

其他都是老样子,但请求参数中的 initial_tab_id 控制内容的tab,一共有六种:

名称id
为你推荐for-you
趋势trending
COVID-19covid-19
新闻news_unified
体育sports_unified
娱乐entertainment_unified

格式还是跟时间线(Timeline)一样,需要自行探索,未登录用户只能得到请求时所在的国家/地区的趋势

Hashflags

Hashflag 是在活跃期间自动加到对应 Hashtag 后面的小图片,活跃的 hashflags 可以通过请求 https://twitter.com/i/api/1.1/hashflags.json 取得,全部内容可以前往 https://hashflags.io/ 取得

咕咕

咕咕

// TODO

//我会在这里存放一些没来得及整理的内容

参考网页


评论区