その1(環境構築編)はこちら その2(失敗プログラミング編)はこちら
※筆者がTwitter APIに触れたのは2018/12/06〜2018/12/08くらいです。
TwitterAPIは仕様変更がたびたび起こっているみたく…。
今のTwitterAPIの仕様とは色々と異なる場面もあるかもしれません。ご了承ください。
前回のおさらい
前回では、うまくいかなかったので…。
改めて、「TwitterAPI」の最新の仕様に関して、見ていこうと思います。
早速取り組みましょう!
TwitterAPIを勉強-公式の入門記事を読む
さて、まずは公式ページのDocumentを覗くところから。
公式さんにGetting Started…入門ページがあったので、色々見てみました。
…ただ、記事が全て英語の文章で…。
プログラマーとしてアカンとは思いますが、自分の貧弱な英語力では、入門ページの全容を理解することはできませんでした…。
ただしかし、Twitterの公式Developerチームが直々に、TwitterAPIを使ったサンプルコードをGithubに置いてくれていることは分かりました。
@TwitterDev · GitHub
覗いてみた感じ、Javaやjavascript、Ruby、Pythonを用いたサンプルコードが提供されているみたいです。
前回の記事ではNode.jsを扱ってたので…。
ここはJavascriptのサンプルコードを参考にしたいなと思い、Javascript系のリポジトリ内にあるコードを見てみました。
…見てみましたが…見てもわからない…。
こう、何というか…自分としては、非常に簡単なソースコードが見れればいいのですが…。
Githubに上がっているものはどれもかなりリッチな機能を兼ね備えたものみたく…。
(少なくとも、自分が見てみた範囲はそうでした)
TwitterAPI、ひいてはNode.jsの初学者にはチンプンカンプンでした…ふぉへぇぇ…。
ただ、1つ判明したこともありました。
どうやらNode.jsでTwitterAPIにアクセスするには、TwitterAPI専用のnpm
パッケージが必要っぽく…。
という流れになってるみたいです。
これは…Rubyとかでも同様なんですかね?
ともあれ、開発のためにはこれが必要とのこと…。
ならまずこの「TwitterAPI用のnpm
パッケージ」を探す所からですね。一歩前進!
早速探してみましょう!
TwitterAPI用のnpm
パッケージ探し
まぁ探すって言っても…そんなに難しいことではありません。
npm
公式サイトで「Twitter」と検索すれば、一発で出てきました。
どうやら公式が提供しているものではなさそうですが…。
提供機能的には、「TwitterAPI用のnpm
パッケージ」で間違い無いようです。
おまけになんと!このnpm
パッケージの説明文には、下記のような簡単なチュートリアルコードが記載されておりました。
こういうのが欲しかったんだよ…!
var Twitter = require('twitter'); var client = new Twitter({ consumer_key: '', consumer_secret: '', access_token_key: '', access_token_secret: '' }); var params = {screen_name: 'nodejs'}; client.get('statuses/user_timeline', params, function(error, tweets, response) { if (!error) { console.log(tweets); } });
サンプルコード実行
ものは試し、早速このサンプルコードをターミナル上で走らせてみました。
すると、下記のようなデータが出力されていきました。
(長いので、途中を...
で省略しています)
(また、見せてまずそう?な情報(URLにも記載されてない情報かつID情報など情報源が一意に特定されやすそうな情報)は...
で伏せてます)
[ { created_at: 'Fri Dec 07 22:17:01 +0000 2018', id: 1071166716728799200, id_str: '1071166716728799232', text: `Cool use for Node.js! 🏊In your experience, what's the "coolest" thing you've seen Node.js do? https://t.co/wB2BFRtMXL`, truncated: false, entities: { hashtags: [], symbols: [], user_mentions: [], urls: [ { url: 'https://t.co/wB2BFRtMXL', expanded_url: 'https://bit.ly/2BPPWUF', display_url: 'bit.ly/2BPPWUF', indices: [ 94, 117 ] } ] }, source: '<a href="https://sproutsocial.com" rel="nofollow">Sprout Social</a>', in_reply_to_status_id: null, in_reply_to_status_id_str: null, in_reply_to_user_id: null, in_reply_to_user_id_str: null, in_reply_to_screen_name: null, user: { id: ...(number型), id_str: '...'(String型), name: 'Node.js', screen_name: 'nodejs', location: 'Earth and Beyond', description: 'The Node.js JavaScript Runtime. Need some help with Node.js? Please ask here:https://t.co/Y9svHPPIni', url: 'https://t.co/X32n3a0B1h', entities: { url: { urls: [ { url: 'https://t.co/X32n3a0B1h', expanded_url: 'http://nodejs.org', display_url: 'nodejs.org', indices: [ 0, 23 ] } ] }, description: { urls: [ { url: 'https://t.co/Y9svHPPIni', expanded_url: 'https://github.com/nodejs/helpR', display_url: 'github.com/nodejs/helpR', indices: [ 81, 104 ] } ] } }, protected: false, followers_count: 554425, friends_count: 736, listed_count: 7054, created_at: 'Mon Nov 23 10:57:50 +0000 2009', favourites_count: 1902, utc_offset: null, time_zone: null, geo_enabled: true, verified: true, statuses_count: 5997, lang: 'en', contributors_enabled: false, is_translator: false, is_translation_enabled: false, profile_background_color: 'C0DEED', profile_background_image_url: 'http://abs.twimg.com/images/themes/theme1/bg.png', profile_background_image_url_https: 'https://abs.twimg.com/images/themes/theme1/bg.png', profile_background_tile: false, profile_image_url: 'http://pbs.twimg.com/profile_images/702185727262482432/n1JRsFeB_normal.png', profile_image_url_https: 'https://pbs.twimg.com/profile_images/702185727262482432/n1JRsFeB_normal.png', profile_banner_url: 'https://pbs.twimg.com/profile_banners/91985735/1542044720', profile_link_color: '1DA1F2', profile_sidebar_border_color: 'C0DEED', profile_sidebar_fill_color: 'DDEEF6', profile_text_color: '333333', profile_use_background_image: true, has_extended_profile: false, default_profile: true, default_profile_image: false, following: false, follow_request_sent: false, notifications: false, translator_type: 'none' }, geo: null, coordinates: null, place: null, contributors: null, is_quote_status: false, retweet_count: 20, favorite_count: 73, favorited: false, retweeted: false, possibly_sensitive: false, lang: 'en' }, { created_at: 'Fri Dec 07 21:02:02 +0000 2018', ... ... lang: 'en' } ]
出力された情報やソースコードをTwitter公式ドキュメントと照らし合わせながらみた感じ…。
どうやらこのデータは、ソースコードにあるscreen_name
で設定されたTwitterアカウントのツイート情報のようです。
確認のため、今回指定されているアカウント…@nodejsさんのTwitterアカを覗いてみると…。
Cool use for Node.js! 🏊In your experience, what's the "coolest" thing you've seen Node.js do? https://t.co/wB2BFRtMXL
— Node.js (@nodejs) 2018年12月7日
出力情報のうち、text:
に書かれている文と上記ツイート文が一致しますね。
このデータは@nodejsさんのもので間違いなさそうです!
ツイートの画像URLを取得するコード作り
さて、仕組みがわかってしまえばあとはこっちのもの。
このソースコードを元に、取得ツイートから画像URLっぽいのを探していけば、また一歩前進できるはず!
また、今回は下記に示す自分のツイート情報を取得してみました。
※下側のツイート「最近の絵ばかりですが〜」って書いてる方のツイートです
最近の絵ばかりですが…あとこの辺とか?? pic.twitter.com/2qvJ8pWXpP
— Aik(アイク) (@aik0aaac) 2018年12月8日
このツイートから取得されたデータはこんな感じです。
(ここも、見せてまずそう?な情報(URLにも記載されてない情報かつID情報など情報源が一意に特定されやすそうな情報)は...
で伏せてます)
{ created_at: 'Sat Dec 08 06:37:29 +0000 2018', id: 1071292664580960300, id_str: '1071292664580960256', text: '最近の絵ばかりですが…あとこの辺とか?? https://t.co/2qvJ8pWXpP', truncated: false, entities: { hashtags: [], symbols: [], user_mentions: [], urls: [], media: [ { id: 1071292654187507700, id_str: '1071292654187507712', indices: [ 21, 44 ], media_url: 'http://pbs.twimg.com/media/Dt3-_jdU8AA5Gyw.jpg', media_url_https: 'https://pbs.twimg.com/media/Dt3-_jdU8AA5Gyw.jpg', url: 'https://t.co/2qvJ8pWXpP', display_url: 'pic.twitter.com/2qvJ8pWXpP', expanded_url: 'https://twitter.com/aik0aaac/status/1071292664580960256/photo/1', type: 'photo', sizes: { large: { w: 1000, h: 1404, resize: 'fit' }, thumb: { w: 150, h: 150, resize: 'crop' }, medium: { w: 855, h: 1200, resize: 'fit' }, small: { w: 484, h: 680, resize: 'fit' } } } ] }, extended_entities: { media: [ { id: 1071292654187507700, id_str: '1071292654187507712', indices: [ 21, 44 ], media_url: 'http://pbs.twimg.com/media/Dt3-_jdU8AA5Gyw.jpg', media_url_https: 'https://pbs.twimg.com/media/Dt3-_jdU8AA5Gyw.jpg', url: 'https://t.co/2qvJ8pWXpP', display_url: 'pic.twitter.com/2qvJ8pWXpP', expanded_url: 'https://twitter.com/aik0aaac/status/1071292664580960256/photo/1', type: 'photo', sizes: { large: { w: 1000, h: 1404, resize: 'fit' }, thumb: { w: 150, h: 150, resize: 'crop' }, medium: { w: 855, h: 1200, resize: 'fit' }, small: { w: 484, h: 680, resize: 'fit' } } }, { id: 1071292654174928900, id_str: '1071292654174928896', indices: [ 21, 44 ], media_url: 'http://pbs.twimg.com/media/Dt3-_jaVAAANfK8.jpg', media_url_https: 'https://pbs.twimg.com/media/Dt3-_jaVAAANfK8.jpg', url: 'https://t.co/2qvJ8pWXpP', display_url: 'pic.twitter.com/2qvJ8pWXpP', expanded_url: 'https://twitter.com/aik0aaac/status/1071292664580960256/photo/1', type: 'photo', sizes: { medium: { w: 683, h: 1024, resize: 'fit' }, thumb: { w: 150, h: 150, resize: 'crop' }, small: { w: 454, h: 680, resize: 'fit' }, large: { w: 683, h: 1024, resize: 'fit' } } }, { id: 1071292654254608400, id_str: '1071292654254608384', indices: [ 21, 44 ], media_url: 'http://pbs.twimg.com/media/Dt3-_jtU0AA156C.jpg', media_url_https: 'https://pbs.twimg.com/media/Dt3-_jtU0AA156C.jpg', url: 'https://t.co/2qvJ8pWXpP', display_url: 'pic.twitter.com/2qvJ8pWXpP', expanded_url: 'https://twitter.com/aik0aaac/status/1071292664580960256/photo/1', type: 'photo', sizes: { medium: { w: 848, h: 1200, resize: 'fit' }, thumb: { w: 150, h: 150, resize: 'crop' }, small: { w: 481, h: 680, resize: 'fit' }, large: { w: 1448, h: 2048, resize: 'fit' } } }, { id: 1071292654170734600, id_str: '1071292654170734592', indices: [ 21, 44 ], media_url: 'http://pbs.twimg.com/media/Dt3-_jZVAAAWrx5.jpg', media_url_https: 'https://pbs.twimg.com/media/Dt3-_jZVAAAWrx5.jpg', url: 'https://t.co/2qvJ8pWXpP', display_url: 'pic.twitter.com/2qvJ8pWXpP', expanded_url: 'https://twitter.com/aik0aaac/status/1071292664580960256/photo/1', type: 'photo', sizes: { small: { w: 481, h: 680, resize: 'fit' }, thumb: { w: 150, h: 150, resize: 'crop' }, large: { w: 1157, h: 1637, resize: 'fit' }, medium: { w: 848, h: 1200, resize: 'fit' } } } ] }, source: '<a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>', in_reply_to_status_id: ...(number型), in_reply_to_status_id_str: '...'(String型), in_reply_to_user_id: ...(number型), in_reply_to_user_id_str: '...'(String型), in_reply_to_screen_name: 'aik0aaac', user: { id: ...(number型), id_str: '...'(String型), name: 'Aik(アイク)', screen_name: 'aik0aaac', location: '( ˘ω˘ )', description: 'お絵かきしてます。最近忙しさが増して来てます。 イッパイ ヤルコトアッテ タノシイナ 無言フォロー失礼します。 ■普通のブログ→https://t.co/cMrvKgYkSx■技術ブログ→https://t.co/jALDcfQmQI', url: 'https://t.co/27lnr958R6', entities: { url: { urls: [ { url: 'https://t.co/27lnr958R6', expanded_url: 'http://pixiv.me/aik0aaac', display_url: 'pixiv.me/aik0aaac', indices: [ 0, 23 ] } ] }, description: { urls: [ { url: 'https://t.co/cMrvKgYkSx', expanded_url: 'https://aik0aaac.hatenablog.com/', display_url: 'aik0aaac.hatenablog.com', indices: [ 65, 88 ] }, { url: 'https://t.co/jALDcfQmQI', expanded_url: 'https://aik0aaat.hatenadiary.jp/', display_url: 'aik0aaat.hatenadiary.jp', indices: [ 95, 118 ] } ] } }, protected: false, followers_count: 1301, friends_count: 906, listed_count: 28, created_at: 'Thu Jun 06 15:57:27 +0000 2013', favourites_count: 8138, utc_offset: null, time_zone: null, geo_enabled: false, verified: false, statuses_count: 5360, lang: 'ja', contributors_enabled: false, is_translator: false, is_translation_enabled: false, profile_background_color: '000000', profile_background_image_url: 'http://abs.twimg.com/images/themes/theme4/bg.gif', profile_background_image_url_https: 'https://abs.twimg.com/images/themes/theme4/bg.gif', profile_background_tile: false, profile_image_url: 'http://pbs.twimg.com/profile_images/1019509047857303552/_acpYVgG_normal.jpg', profile_image_url_https: 'https://pbs.twimg.com/profile_images/1019509047857303552/_acpYVgG_normal.jpg', profile_banner_url: 'https://pbs.twimg.com/profile_banners/1488097759/1540914934', profile_link_color: 'F58EA8', profile_sidebar_border_color: '000000', profile_sidebar_fill_color: '000000', profile_text_color: '000000', profile_use_background_image: false, has_extended_profile: false, default_profile: false, default_profile_image: false, following: false, follow_request_sent: false, notifications: false, translator_type: 'none' }, geo: null, coordinates: null, place: null, contributors: null, is_quote_status: false, retweet_count: 2, favorite_count: 16, favorited: false, retweeted: false, possibly_sensitive: false, lang: 'ja' }
どうやら、entities
かextended_entities
の中にURLが入っている様子ですね…。
図示するとこんな感じ…ですかね。
tweet_data ├ entities ├ media[0] ├ media_url ├ media_url_https ├ extended_entities ├ media[0] -> 配列構造 ├ media_url ├ media_url_https ├ media[1] ├ media_url ├ media_url_https ├ media[2] ├ media_url ├ media_url_https ├ media[3] ├ media_url ├ media_url_https
この中の、各media_url
とmedia_url_https
に、画像URLが格納されているようです。
また、複数枚の写真投稿ツイートの場合entities
には最初の画像のURLのみが格納され…。
extended_entities
には、配列続きで全ての画像のURLが格納されているっぽいですね。
今回の私の目的「全ツイートの画像をDLする」には、extended_entities
の情報を参考にした方が良さそうです!
そして、これらのデータを出力させるには、下記のようなソースコードを書けばいいはず…。
※先のサンプルコードのclient.get
辺りの処理を書いています。
client.get('statuses/user_timeline', params, function(error, tweets, response) { if (!error) { tweets.forEach(function(t) { t.extended_entities.media.forEach(function(m) { console.log(m.media_url); }); }); } });
これで実行してみます!
が、下記のようなエラーが出現してしまいました…。
$ node tutorial1.js /Users/.../tutorial1.js:18 t.extended_entities.media.forEach(function(m) { ^ TypeError: Cannot read property 'media' of undefined at /Users/.../tutorial1.js:18:27 at Array.forEach (<anonymous>) at /Users/.../tutorial1.js:17:12 at Request._callback (/Users/.../node_modules/twitter/lib/twitter.js:227:5) at Request.self.callback (/Users/.../node_modules/request/request.js:185:22) at Request.emit (events.js:182:13) at Request.<anonymous> (/Users/.../node_modules/request/request.js:1161:10) at Request.emit (events.js:182:13) at IncomingMessage.<anonymous> (/Users/.../node_modules/request/request.js:1083:12) at Object.onceWrapper (events.js:273:13)
エラー文を見る感じ、どうやらmedia
要素がundefined
になってるって事で怒られているみたいですね。
そういえば、先の…@nodejsさんの画像無しツイートのデータには、そもそもentities
要素すらなかったような…。
それを考慮して、該当部分を下記のようなコードに書き換えてみました。
client.get('statuses/user_timeline', params, function(error, tweets, response) { if (!error) { tweets.forEach(function(t) { if (t.extended_entities != undefined) { t.extended_entities.media.forEach(function(m) { console.log(m.media_url); }); } }); } });
さて、結果は…?
$ node tutorial1.js http://pbs.twimg.com/media/Dt3-_jdU8AA5Gyw.jpg http://pbs.twimg.com/media/Dt3-_jaVAAANfK8.jpg http://pbs.twimg.com/media/Dt3-_jtU0AA156C.jpg http://pbs.twimg.com/media/Dt3-_jZVAAAWrx5.jpg http://pbs.twimg.com/media/Dt3-uSvU8AAbnbA.jpg http://pbs.twimg.com/media/Dt3-uSsUwAIDyRk.jpg http://pbs.twimg.com/media/Dt3-uSvVsAAmql7.jpg http://pbs.twimg.com/media/Dt3-uSqVYAAatBw.jpg
おお!どうやら問題なく出力できてるようです。
※出力URL数が4つでないのは、先にあったツイートだけでなく下記のツイートの情報まで読み込んでしまっているからです…。
#皆の絵幅を見せてほしい
— Aik(アイク) (@aik0aaac) 2018年12月8日
せっかくなんで割と前あたりの絵も取ってきました pic.twitter.com/zTZV2J8KaI
次回記事へ…
今回で、何と!画像ツイートのURL取得まで進むことができました。
後は下記の処理をこなすスクリプトを書けば、やりたい事は実現できるはずです。
- 現状だと最新の20件のツイート情報しか取得できてない -> 全ツイート情報を取得できるように
- 取得URLにアクセス、アクセス先の画像を保存する処理
次回はここから始めていきましょう。
それではー!!