読者です 読者をやめる 読者になる 読者になる

twitterで特定ユーザの画像を全部ダウンロードする

twitterユーザの画像を全部ダウンロードするスクリプトを書きました。

特定のユーザのtwitterの画像を全部ダウンロードしたかったので、
プログラムを書きました。

APIキー取得

twitterAPIを利用するには、APIキーの取得が必要です。
Twitter developersからログインして、「Create an application」から
アプリケーション登録すると取得できます。
Twitter Developers

twitter REST API

twitterの情報を取得するにはREST APIを使用します。
かなりたくさんのAPIが揃っていて、ほとんどなんでも操作できます。
REST APIs | Twitter Developers

ユーザの写真を取得するには「GET statuses/user_timeline」を使用します。
GET statuses/user_timeline | Twitter Developers

このAPIでは取得したいユーザ名を指定することで、一度に最大200のツイートをjson形式で取得できます。
200以上取得したい場合は、複数回GETする必要があります。
そして対象ユーザの過去3,200ツイートまで取得できます。

また、各APIには叩ける回数が決まっています。

下記に各APIの15分で叩ける回数が記載されています。
Twitter API 1.1で「Application-only authentication」が追加されました。
これはアプリケーションのみがAPIを利用する場合に利用できます。
Rate Limits: Chart | Twitter Developers

これをみると「GET statuses/user_timeline」は15分で300回叩けます。
f:id:pppurple:20160312231042j:plain

1度のリクエストで200ツイート取得できるので、3,200 ÷ 200 = 16回取得すれば全部取得できます。
15分で300回たたけるので16回は全然余裕です。

max_id

ツイートには64bitの一意なidが設定されているので、
max_idパラメータにidを指定することで、そのidより前のツイートを取得できます。
注意点はmax_idを含むツイートが取得されるので、含ませたくない場合、
下図のように(max_id - 1)を指定する必要があります。

f:id:pppurple:20160305220203p:plain

画像URL取得

取得したjsonの中から画像URLを取得します。
1枚の画像の場合、entities.mediaに画像情報が入っているのですが、
複数枚画像がある場合はextended_entitiesに画像情報が配列で入っています。

jsonレスポンス

  extended_entities:
   { media:
      [ { id: 704325644088311800,
          id_str: '704325644088311808',
          indices: [ 139, 140, [length]: 2 ],
          media_url: 'http://pbs.twimg.com/media/CcZEcQ4VAAAj5pa.jpg',
          media_url_https: 'https://pbs.twimg.com/media/CcZEcQ4VAAAj5pa.jpg',
          url: 'https://t.co/FJ0ptKC1S1',
          display_url: 'pic.twitter.com/FJ0ptKC1S1',
          expanded_url: 'http://twitter.com/RLE000/status/704325645526904832/photo/1',
          type: 'photo',
          sizes:
           { large: { w: 700, h: 700, resize: 'fit' },
             medium: { w: 600, h: 600, resize: 'fit' },
             thumb: { w: 150, h: 150, resize: 'crop' },
             small: { w: 340, h: 340, resize: 'fit' } },
          source_status_id: 704325645526904800,
          source_status_id_str: '704325645526904832',
          source_user_id: 265123955,
          source_user_id_str: '265123955' },
        { id: 704325637356408800,
          id_str: '704325637356408834',
          indices: [ 139, 140, [length]: 2 ],
          media_url: 'http://pbs.twimg.com/media/CcZEb3zUUAIQc5R.jpg',
          media_url_https: 'https://pbs.twimg.com/media/CcZEb3zUUAIQc5R.jpg',
          url: 'https://t.co/FJ0ptKC1S1',
          display_url: 'pic.twitter.com/FJ0ptKC1S1',
          expanded_url: 'http://twitter.com/RLE000/status/704325645526904832/photo/1',
          type: 'photo',
          sizes:
           { small: { w: 340, h: 299, resize: 'fit' },
             medium: { w: 600, h: 528, resize: 'fit' },
             thumb: { w: 150, h: 150, resize: 'crop' },
             large: { w: 700, h: 616, resize: 'fit' } },
          source_status_id: 704325645526904800,
          source_status_id_str: '704325645526904832',
          source_user_id: 265123955,
          source_user_id_str: '265123955' },
        [length]: 2 ] },

URLが下記の様に色々ありますが、media_urlを使用します。

  • media_url:画像URL
  • media_url_https:画像URL(SSL)
  • url:画像を含むtweet⇒expanded_urlにリダイレクトされる
  • display_url:http://が含まれない短縮URLの形式⇒expanded_urlにリダイレクトされる
  • expanded_url:media_urlを含むツイートの完全なURL

画像はthumb, small, medium, largeという4つのサイズがあり、デフォルトはmediumになってます。
画像URLにコロンをつけてサイズを指定することができます。
せっかくなので一番大きいlargeを指定します。

http://pbs.twimg.com/media/CcZEcQ4VAAAj5pa.jpg:large

twitモジュール

twitter REST APIを普通に呼んでもいいのですが、簡単に操作ができる
twitというライブラリを使用しました。
github.com

twitではREST APIとStreaming APIを使用できます。

npmでインストールします。

# npm install twit

使い方は簡単で、twitter developersで取得した各種キーを設定するだけです。

var Twit = require('twit')

var T = new Twit({
  consumer_key:         '...',
  consumer_secret:      '...',
  access_token:         '...',
  access_token_secret:  '...'
})

getもとても簡単でREST URLを指定して、送信したいパラメータをつけてやるだけです。

  • screen_name : @で始まるtwitterのユーザ名
  • count : 一度で取得するツイート数
  • max_id : 指定したtweet IDより昔のツイートを取得
  • exclude_replies : 返信を含めるか
  • trim_user : ユーザ情報を省略するか
  • include_rts : リツイートを含めるか
var params = {
	screen_name : '@flomorrissey',
	count : 200,
	max_id : maxId,
	exclude_replies : false,
	trim_user : true,
	include_rts : true
};

T.get('statuses/user_timeline', params, function(err, data, response) {
   :
})

javascriptで64bit整数演算

twitterではすべてのツイートに64bitの一意なidが設定されているのですが、
javascriptでは2^{53}-1以上の数値は精度が保障されず、演算に誤差が発生します。

twitter REST APIではこの問題に対応して、idをstringで表現したid_strという項目があります。
なのでidではなくid_strを取得して使用します。

  • id:tweetのユニークなID
  • id_str:idのstringフォーマット。idを''でくくったもの

ただ困ったのが、max_idを指定するときに(id - 1)の演算をする必要があります。
stringでidを受け取っても演算ができません。
なので演算できるライブラリを使用します。

npmで探すと下記のように色々あるみたいです。
一番ユーザ数が多そうなlong.jsを使ってみることにします。

https://www.npmjs.com/package/long
https://www.npmjs.com/package/bigint
https://www.npmjs.com/package/bignum
https://www.npmjs.com/package/int64-native

long.jsライブラリ

long.jsは64bitの数値を扱えるライブラリです。
www.npmjs.com

npmでインストールします。

# npm install long
long@3.0.3 node_modules/long

longオブジェクトを生成する方法は色々ありますが、今回はstringから生成するので
formString()を使います。

var Long    = require("long");

var longId = Long.fromString(str_id);

演算用のメソッドも色々用意されてますが、今回は1減算するので
subtract()を使用します。

var longIdSub = longId.subtract(1);

結果をstringで取得します。

var longStr = longIdSub.toString();

画像ダウンロード

取得した画像URLを使用して画像をダウンロードします。
ダウンロードにはrequestモジュールを使用します。

requestモジュール

requestモジュールは多機能なHTTPクライアントです。
www.npmjs.com

npmでインストールします。

# npm install request
request@2.69.0 node_modules/request

requestモジュールでは簡単にストリーミングができます。
画像URLを使用して画像をダウンロードするのも、pipe()で簡単に書けます。

request('http://xxxxxxxx/image.png').pipe(fs.createWriteStream('image.png'))

twitter画像ダウンロード

最終的には下記の様になりました。

dlTwitterUserImages.js

var twit    = require('twit');
var request = require('request');
var fs      = require('fs');
var Long    = require("long");
var URL     = require('url');

var T = new twit({
	consumer_key:         'YOUR CONSUMER KEY',
	consumer_secret:      'YOUR CONSUMER SECRET',
	access_token:         'YOUR ACCESS TOKEN',
	access_token_secret:  'YOUR ACCESS SECRET',
	app_only_auth:        true
});

var MAX_LOOP = 16;
var maxId;
var params = {
	screen_name : '@SCREEN_NAME',
	count : 200,
	max_id : maxId,
	exclude_replies : false,
	trim_user : true,
	include_rts : true
};

var urls = [];
var cnt  = 1;
getImage(0, download);

function getImage(loop, callback) {
	if (loop >= MAX_LOOP) {
		callback(urls);
		return;
	}
	T.get('statuses/user_timeline', params, function(err, data, response) {
		if (data.length === 0) {
			callback(urls);
			return;
		}
		var minId;
		for(var i = 0; i < data.length; i++) {
			var d = data[i];
			if (d.extended_entities) {
				for(var j = 0; j < d.extended_entities.media.length; j++) {
					console.log(cnt + " : " + d.extended_entities.media[j].media_url);
					urls.push(d.extended_entities.media[j].media_url);
				}
			} else {
				console.log(cnt + " : no image");
			}
			minId = d.id_str;
			cnt++;
		}
		var longId = Long.fromString(minId);
		var longIdSub = longId.subtract(1);
		params.max_id = longIdSub.toString();
		getImage(loop + 1, callback);
	});
}

function download(urls) {
	for (var i = 0; i < urls.length; i++) {
		var url = urls[i];
		var pathname = URL.parse(url).pathname;
		var filename = pathname.replace(/[^a-zA-Z0-9\.]+/g, '_');
		request
			.get(url + ":large")
			.on('end', done(url))
			.on('error', function(err) {
				console.log(err);
			})
			.pipe(fs.createWriteStream(filename));
	}
}

function done(url) {
	return function() {
		console.log("downloaded : " + url);
	};
}

実行・確認

Flo Morrisseyという人の画像を取得してみます。
Flo Morrissey (@flomorrissey) | Twitter

実行。
全ツイートの画像URLを取得します。画像がないツイートの場合、no imageと表示されます。
そのあと、全画像をダウンロードします。

# node dlTwitterUserImages.js
1 : no image
2 : no image
3 : no image
4 : no image
5 : no image
6 : no image
7 : no image
8 : no image
9 : no image
10 : no image
11 : http://pbs.twimg.com/tweet_video_thumb/CdC-TGRW0AAl99w.jpg
12 : http://pbs.twimg.com/media/CdCUYngWEAAVq0N.jpg
13 : http://pbs.twimg.com/media/Cc9V8VBXEAAfrPv.jpg
14 : no image
15 : http://pbs.twimg.com/media/Cc1owfBVIAEnIvs.jpg
16 : http://pbs.twimg.com/ext_tw_video_thumb/705888574957338624/pu/img/fw7LE_Rv_q5Wy1Tm.jpg
17 : no image
18 : no image
19 : no image
          :
          :
downloaded : http://pbs.twimg.com/media/CW3PvBHWcAEMwmE.jpg
downloaded : http://pbs.twimg.com/media/CTm2g4CXAAAjeza.jpg
downloaded : http://pbs.twimg.com/media/CUMyUx9XAAACadi.jpg
downloaded : http://pbs.twimg.com/media/CTDOTahWUAEmfGN.jpg
downloaded : http://pbs.twimg.com/media/CRNG29eXAAAI0fv.jpg
downloaded : http://pbs.twimg.com/media/COS4tlcUcAA91IL.jpg
downloaded : http://pbs.twimg.com/media/CPURHA8VAAAkUUj.jpg
downloaded : http://pbs.twimg.com/media/COYaJkfU8AEOpf7.jpg
downloaded : http://pbs.twimg.com/media/CPMT4QyW8AAVNkN.jpg
downloaded : http://pbs.twimg.com/media/CK_kYE2WcAAPFIJ.jpg
          :
          :

確認

# ll flomorrissey/
合計 36772
-rw-r--r-- 1 root root   14866  310 11:27 _ext_tw_video_thumb_598296176211824640_pu_img_HnGfu7NvC_F7_VT.jpg
-rw-r--r-- 1 root root  135178  310 11:27 _media_B_1Sc2_UYAA71FQ.jpg
-rw-r--r-- 1 root root  133596  310 11:27 _media_CAUXkfGWYAAxbeG.jpg
-rw-r--r-- 1 root root  156079  310 11:27 _media_CAeVOPnUsAAAKwr.jpg
-rw-r--r-- 1 root root  122161  310 11:27 _media_CAzCkZ0UQAAUXos.jpg
-rw-r--r-- 1 root root   68867  310 11:27 _media_CB4Rl9CUsAEOM2z.jpg
-rw-r--r-- 1 root root   76887  310 11:27 _media_CB4fZmHUMAAKnsS.jpg
-rw-r--r-- 1 root root   58688  310 11:27 _media_CB8GT16WIAA3enR.jpg
-rw-r--r-- 1 root root   65630  310 11:27 _media_CBBO_buWsAAI4PB.jpg
-rw-r--r-- 1 root root   37661  310 11:27 _media_CBBOdj0WYAADFmf.jpg
-rw-r--r-- 1 root root   37661  310 11:27 _media_CBCj6t6XEAAWsPb.jpg
-rw-r--r-- 1 root root   19385  310 11:27 _media_CBJJeBFUwAERzTV.jpg
-rw-r--r-- 1 root root   61221  310 11:27 _media_CBJUCsqXIAE0xT8.jpg
-rw-r--r-- 1 root root   83285  310 11:27 _media_CBpVndlUEAA3UMp.jpg
-rw-r--r-- 1 root root   93858  310 11:27 _media_CBpVnduUgAIwZHk.jpg
-rw-r--r-- 1 root root   21046  310 11:27 _media_CC3yHq_WAAAmpD1.jpg
-rw-r--r-- 1 root root   75453  310 11:27 _media_CCXImJQWEAAK6IR.jpg
-rw-r--r-- 1 root root   60723  310 11:27 _media_CCqDoT2XIAEHxTz.jpg
-rw-r--r-- 1 root root   48671  310 11:27 _media_CCqK4fHWEAA04Au.jpg
-rw-r--r-- 1 root root   91041  310 11:27 _media_CCuDqlwXIAE0jay.jpg
-rw-r--r-- 1 root root  169754  310 11:27 _media_CCunxGfUUAARI9A.jpg
-rw-r--r-- 1 root root   88775  310 11:27 _media_CCzmVVgWIAI0X_h.jpg
                     :
                     :
                     :

うまく取れているようです。

画像をWindowsで見てみると、ちゃんと取れてます(^-^)
f:id:pppurple:20160312234440j:plain

終わり。

ソースは一応置いておきました。
github.com

【参考】

JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック