SYNCER

SYNCER

Twitter Rest APIでツイートにリンクを付ける方法

9件

公開日:

Twitter Rest APIで、取得したツイートのURLやハッシュタグに、エンティティの情報を使ってリンクを付ける方法を説明します。言語はPHPです。

準備

目的を達成するには、下記の情報が必要です。

アプリケーション
読込権限(Read Only)を持つアプリケーションの、APIキー(API Key)とAPIシークレット(API Secret)。
アクセストークン
アプリケーションで認証した、ユーザーのアクセストークン(Access Token)とアクセストークンシークレット(Access Token Secret)。

説明

ツイートにリンクを付ける時、正確な処理を行なうにはエンティティオブジェクト(entities)の情報を活用しましょう。

JSONの例

下記はこちらのツイートを取得した際のJSONです。利用するのは、text(ツイートの本文)とentities(リンクを付けるための情報)です。

JSON

{"created_at":"Sun Jan 29 08:56:00 +0000 2017","id":825628550799593473,"id_str":"825628550799593473","text":"\u3053\u308c\u306f\u3001\u30c4\u30a4\u30fc\u30c8\u3067\u3059\u3002\n\nhttps:\/\/t.co\/DZGyOVnHYM\n#\u30cf\u30c3\u30b7\u30e5\u30bf\u30b0\n$Symbol\n@Twitter https:\/\/t.co\/MN6CSArGuF","truncated":false,"entities":{"hashtags":[{"text":"\u30cf\u30c3\u30b7\u30e5\u30bf\u30b0","indices":[37,44]}],"symbols":[{"text":"Symbol","indices":[45,52]}],"user_mentions":[{"screen_name":"Twitter","name":"Twitter","id":783214,"id_str":"783214","indices":[53,61]}],"urls":[{"url":"https:\/\/t.co\/DZGyOVnHYM","expanded_url":"https:\/\/syncer.jp","display_url":"syncer.jp","indices":[13,36]}],"media":[{"id":825628201758052352,"id_str":"825628201758052352","indices":[62,85],"media_url":"http:\/\/pbs.twimg.com\/media\/C3U4eaJVUAAC50R.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/C3U4eaJVUAAC50R.jpg","url":"https:\/\/t.co\/MN6CSArGuF","display_url":"pic.twitter.com\/MN6CSArGuF","expanded_url":"https:\/\/twitter.com\/arayutw\/status\/825628550799593473\/photo\/1","type":"photo","sizes":{"small":{"w":400,"h":225,"resize":"fit"},"large":{"w":400,"h":225,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":400,"h":225,"resize":"fit"}}}]},"extended_entities":{"media":[{"id":825628201758052352,"id_str":"825628201758052352","indices":[62,85],"media_url":"http:\/\/pbs.twimg.com\/media\/C3U4eaJVUAAC50R.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/C3U4eaJVUAAC50R.jpg","url":"https:\/\/t.co\/MN6CSArGuF","display_url":"pic.twitter.com\/MN6CSArGuF","expanded_url":"https:\/\/twitter.com\/arayutw\/status\/825628550799593473\/photo\/1","type":"photo","sizes":{"small":{"w":400,"h":225,"resize":"fit"},"large":{"w":400,"h":225,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":400,"h":225,"resize":"fit"}}},{"id":825628250898460672,"id_str":"825628250898460672","indices":[62,85],"media_url":"http:\/\/pbs.twimg.com\/media\/C3U4hRNUcAApOu_.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/C3U4hRNUcAApOu_.jpg","url":"https:\/\/t.co\/MN6CSArGuF","display_url":"pic.twitter.com\/MN6CSArGuF","expanded_url":"https:\/\/twitter.com\/arayutw\/status\/825628550799593473\/photo\/1","type":"photo","sizes":{"small":{"w":400,"h":225,"resize":"fit"},"large":{"w":400,"h":225,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":400,"h":225,"resize":"fit"}}}]},"source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e","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":1528352858,"id_str":"1528352858","name":"\u3042\u3089\u3086","screen_name":"arayutw","location":"\u65e5\u672c \u6771\u4eac","description":"SYNCER\u3068\u3044\u3046\u30b5\u30a4\u30c8\u3092\u904b\u7528\u3057\u3066\u3044\u307e\u3059\u3002\u63b2\u793a\u677f\u2192 https:\/\/t.co\/OAIDbACF3N","url":"https:\/\/t.co\/lW3GYq8sQl","entities":{"url":{"urls":[{"url":"https:\/\/t.co\/lW3GYq8sQl","expanded_url":"https:\/\/syncer.jp\/","display_url":"syncer.jp","indices":[0,23]}]},"description":{"urls":[{"url":"https:\/\/t.co\/OAIDbACF3N","expanded_url":"https:\/\/forum.syncer.jp\/","display_url":"forum.syncer.jp","indices":[26,49]}]}},"protected":false,"followers_count":1425,"friends_count":565,"listed_count":113,"created_at":"Tue Jun 18 17:28:51 +0000 2013","favourites_count":4464,"utc_offset":32400,"time_zone":"Asia\/Tokyo","geo_enabled":true,"verified":false,"statuses_count":35313,"lang":"ja","contributors_enabled":false,"is_translator":false,"is_translation_enabled":true,"profile_background_color":"2660A1","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/821911941396328448\/VjorK4J1.jpg","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/821911941396328448\/VjorK4J1.jpg","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/821941553774010370\/5M2umxbl_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/821941553774010370\/5M2umxbl_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/1528352858\/1484800383","profile_link_color":"D36015","profile_sidebar_border_color":"F2E195","profile_sidebar_fill_color":"FFF7CC","profile_text_color":"0C3E53","profile_use_background_image":true,"has_extended_profile":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null,"translator_type":"regular"},"geo":null,"coordinates":null,"place":{"id":"06ef846bfc783874","url":"https:\/\/api.twitter.com\/1.1\/geo\/id\/06ef846bfc783874.json","place_type":"country","name":"Japan","full_name":"Japan","country_code":"JP","country":"Japan","contained_within":[],"bounding_box":{"type":"Polygon","coordinates":[[[122.9040343,24.0133434],[153.9976966,24.0133434],[153.9976966,45.562897],[122.9040343,45.562897]]]},"attributes":{}},"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":1,"favorited":false,"retweeted":false,"possibly_sensitive":false,"possibly_sensitive_appealable":false,"lang":"ja"}

必要な情報

ここではテキスト中のハッシュタグにリンクを付けること、を例に説明します。そのために必要な情報をまとめました。

text

ツイートのテキストです。まだリンクが付いていません。

これは、ツイートです。

https://t.co/DZGyOVnHYM
#ハッシュタグ
$Symbol
@Twitter https://t.co/MN6CSArGuF

entities

entitiesの中身を切り取ったものです。ハッシュタグにリンクを付けるには、hashtags内のtextindicesを参照します。

JSON

{"entities":{"hashtags":[{"text":"\u30cf\u30c3\u30b7\u30e5\u30bf\u30b0","indices":[37,44]}]}}
text
置換後に適した文字列。ハッシュタグの場合は#が付いてないので注意。
indices
置換処理をするべき位置の情報。第1要素(37)が置換対象文字の開始位置までのオフセット、第2要素(44)が置換対象文字の終了位置。

置換処理の位置を取得

indicesには、置換処理を行なうべき位置の情報が含まれています。

開始の位置

第1要素の37は、置換対象の文字列が開始する位置が38文字目であることを表します。下記のハイライト部分は37文字ということです。

これは、ツイートです。

https://t.co/DZGyOVnHYM
#ハッシュタグ
$Symbol
@Twitter https://t.co/MN6CSArGuF

終了の位置

第2要素の44は、置換対象の文字列の最後の文字が44文字目であることを表します。つまり、下記のハイライト部分が44文字ということです。また、44-37で、置換対象の文字列の文字数が7文字であることを導き出せます。

これは、ツイートです。

https://t.co/DZGyOVnHYM
#ハッシュタグ
$Symbol
@Twitter https://t.co/MN6CSArGuF

置換処理を実行

前後の文字列を切り分ける

さて、考え方はこうです。3744という数値を利用して、該当部分よりも前の部分と後の部分を切り分けることができます。

前の部分 (0文字目〜37文字目の部分)

これは、ツイートです。

https://t.co/DZGyOVnHYM

後の部分 (45文字目〜最後の文字まで)

$Symbol
@Twitter
https://t.co/MN6CSArGuF

置換後のテキストを用意する

hashtags[]->textの情報を利用して、置換後の文字列として挿入したいaタグを付けたテキストを用意できます。

置換後のテキスト

<a href="...">#ハッシュタグ</a>

文字列を繋ぎ合わせる

そして、「前の文字列」「置換後のテキスト」「後の文字列」、これらを繋ぎ合わせます。

これは、ツイートです。

https://t.co/DZGyOVnHYM
<a href="...">#ハッシュタグ</a>
$Symbol
@Twitter
https://t.co/MN6CSArGuF

このようにして、正規表現ではなくindicesの位置を頼りに置換を行なうことで、正確にリンクを付けることが可能です。

処理の例

PHP (index.php)

<?php
// 設定
$tweet = 'これは、ツイートです。

https://t.co/DZGyOVnHYM
#ハッシュタグ
$Symbol
@Twitter https://t.co/MN6CSArGuF' ;	// ツイート本文
$text = 'ハッシュタグ' ;	// 置換後の文字列
$indices = [ 37, 44 ] ;	// indices

// 前を切り取る (37)
// 0〜37文字目を切り取る
$left_text = mb_substr( $tweet, 0, $indices[0] ) ;

// 後を切り取る (44)
// 45文字目から最後までを切り取る
$right_text = mb_substr( $tweet, ($indices[0] + ($indices[1] - $indices[0])) ) ;

// 置換後の文字列 (リンクを付ける)
$after_text = '<a href="https://twitter.com/search?q=' . rawurlencode("#" . $text) . '">#' . $text . '</a>' ;

// 前、置換後の文字列、後、を繋げる
$rich_text = $left_text . $after_text . $right_text ;

// 結果を表示
echo $rich_text ;

デモを開く

処理の順番

1つのツイートに複数の置換処理を行なう場合、ツイートの後方から前方にかけて処理を行なわないと、途中でindicesの情報が狂ってきてしまいます。この理屈について理解しておきましょう。

サンプルコード

ユーザータイムラインを取得して、各ツイートにリンクを付けるサンプルコードです。デモでは、あなたのユーザータイムラインを取得してリンクを付けます。利用する場合は、当サイトのアプリケーションを認証する必要があります。ご利用後は、お手数ですが連携を解除して下さい。連携を解除しなかったとしてもアプリケーションがユーザーデータにアクセスすることはありません。

PHP

<?php
/*****

	ツイートにリンクを付ける方法

	使い方:
		[設定項目]に必要な情報を指定して実行して下さい。

	解説:
		SYNCER
		https://syncer.jp/Web/API/Twitter/Snippet/3/

	質問掲示板:
		SYNCER FORUM
		https://forum.syncer.jp/t/twitter-rest-api/58

*****/


/***** 設定項目 *****/
$api_key = "" ;	// APIキー
$api_secret = "" ;	// APIシークレット
$access_token = "" ;	// アクセストークン
$access_token_secret = "" ;	// アクセストークンシークレット

$screen_name = "syncerjp" ;	// 取得対象のユーザーのスクリーンネーム
$tweet_count = 100 ;	// ツイートの取得数


/***** プログラムの実行 *****/
// ユーザータイムラインを取得する
list( $response_body, $response_header ) = syncer_twitter_rest_api_request ( $api_key, $api_secret, $access_token, $access_token_secret, "https://api.twitter.com/1.1/statuses/user_timeline.json", "GET", [
	"screen_name" => $screen_name ,
	"count" => $tweet_count ,
] ) ;

// 取得した各ツイートにリンクを付けて表示する
$tweet_objects = json_decode( $response_body, true ) ;

if ( $tweet_objects ) {
	foreach ( $tweet_objects as $tweet_object ) {
		echo '<p>' . syncer_tweet_text2html( $tweet_object ) . '</p>' ;
	}
}


/*** Twitter Rest APIでツイートにリンクを付ける関数 ***/
// 使い方:
// 	引数:
// 		第1引数: 個別のツイートオブジェクト (配列型、オブジェクト型、またはJSON文字列)
// 	返り値
// 		リンクを付けたツイート

// 作成者: SYNCER
// 作成日時: 2017-01-30

// 更新情報:
// 	2017-01-30: 作成しました。

// 使用条件:
// 	・再配布禁止
// 	・転載禁止

// お問い合わせ: https://twitter.com/arayutw
/*** ***/
function syncer_tweet_text2html ( $tweet_object=[] ) {
	if ( is_object($tweet_object) ) {
		$tweet_object = json_decode( json_encode( $tweet_object ), true ) ;
	} elseif ( is_string($tweet_object) ) {
		$tweet_object = json_decode( $tweet_object, true ) ;
	}

	if ( !isset($tweet_object["text"]) ) {
		return false ;
	}

	if ( !isset($tweet_object["entities"]) ) {
		return $tweet_object["text"] ;
	}

	$replace_table = [
		"hashtags" => [ "text", 0, '#', '#', 'https://twitter.com/search?q=' ] ,
		"symbols" => [ "text", 0, '$', '$', 'https://twitter.com/search?q=' ] ,
		"user_mentions" => [ "screen_name", 0, '@', '', 'https://twitter.com/' ] ,
		"urls" => [ "display_url", 1, 'url' ] ,
		"media" => [ "display_url", 1, 'url' ] ,
	] ;

	$entities = [] ;

	foreach ( $tweet_object["entities"] as $key => $values ) {
		foreach ( $values as $value ) {
			if ( !isset($replace_table[$key]) ) {
				continue ;
			}

			$replace_data = $replace_table[$key] ;
			$text = $value[ $replace_data[0] ] ;

			switch ( $replace_data[1] ) {
				case 0 :
					$after_text = '<a href="' . $replace_data[4] . rawurlencode( $replace_data[3] . $text ) . '" target="_blank">' . $replace_data[2] . $text . '</a>' ;
				break ;

				case 1 :
					$after_text = '<a href="' . $value[ $replace_data[2] ] . '" target="_blank">' . $text . '</a>' ;
				break ;
			}

			$entities[ $value["indices"][1] ] = [ $value["indices"], $after_text ] ;
		}
	}

	$rich_text = $tweet_object["text"] ;

	if ( $entities ) {
		krsort ( $entities ) ;

		foreach ( $entities as $entity ) {
			$indices = $entity[0] ;
			$after_text = $entity[1] ;

			$left_text = mb_substr( $rich_text, 0, ($indices[0]) ) ;
			$right_text = mb_substr( $rich_text, ($indices[0] + ($indices[1] - $indices[0])) ) ;

			$rich_text = $left_text . $after_text . $right_text ;
		}
	}

	return $rich_text ;
}


/*** Twitter Rest APIの汎用関数 ***/
// 作成者: SYNCER
// 作成日時: 2017-01-29
// 更新情報:
// 	2017-01-29: 作成しました。
// 使用条件:
// 	・再配布禁止
// 	・転載禁止
// お問い合わせ: https://twitter.com/arayutw
/*** ***/
function syncer_twitter_rest_api_request ( $api_key="", $api_secret="", $access_token="", $access_token_secret="", $request_url="", $request_method="", $params_a=[] ) {
	$request_headers = [] ;
	$request_body = "" ;

	$params_b = array(
		'oauth_token' => $access_token ,
		'oauth_consumer_key' => $api_key ,
		'oauth_signature_method' => 'HMAC-SHA1' ,
		'oauth_timestamp' => time() ,
		'oauth_nonce' => microtime() ,
		'oauth_version' => '1.0' ,
	) ;

	switch ( $request_method ) {
		case "POST" :
			switch( $request_url ) {
				case( 'https://api.twitter.com/1.1/account/update_profile_background_image.json' ) :
				case( 'https://api.twitter.com/1.1/account/update_profile_image.json' ) :
					$media_param = 'image' ;
				break ;

				case( 'https://api.twitter.com/1.1/account/update_profile_banner.json' ) :
					$media_param = 'banner' ;
				break ;

				case( 'https://upload.twitter.com/1.1/media/upload.json' ) :
					$media_param = ( isset($params_a['media']) && !empty($params_a['media']) ) ? 'media' : 'media_data' ;
				break ;
			}

			// multipart POST
			if ( isset($media_param) && isset($params_a[ $media_param ]) ) {
				$media_data = ( $params_a[ $media_param ] ) ? $params_a[ $media_param ] : "" ;

				if( isset( $params_a[ $media_param ] ) ) unset( $params_a[ $media_param ] ) ;

				$boundary = 's-y-n-c-e-r---------------' . md5( mt_rand() ) ;

				$request_body .= '--' . $boundary . "\r\n" ;
				$request_body .= 'Content-Disposition: form-data; name="' . $media_param . '"; ' ;
				$request_body .= "\r\n" ;
				$request_body .= "\r\n" . $media_data . "\r\n" ;

				foreach( $params_a as $key => $value ) {
					$request_body .= '--' . $boundary . "\r\n" ;
					$request_body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n" ;
					$request_body .= $value . "\r\n" ;
				}

				$request_body .= '--' . $boundary . '--' . "\r\n\r\n" ;

				$request_headers[] = "Content-Type: multipart/form-data; boundary=" . $boundary ;

				$params_c = $params_b ;

			// POST
			} else {
				switch ( $request_url ) {
					case "https://api.twitter.com/1.1/collections/entries/curate.json" :
						$params_c = $params_b ;
						$request_body = $params_a ;
					break ;

					default :
						$params_c = array_merge( $params_a , $params_b ) ;

						if ( $params_a ) {
							$request_body = http_build_query( $params_a ) ;
						}
					break ;
				}
			}
		break ;

		// GET
		case "GET" :
			$params_c = array_merge( $params_a , $params_b ) ;
		break ;
	}

	ksort( $params_c ) ;

	$signature_key = rawurlencode( $api_secret ) . '&' . rawurlencode( $access_token_secret ) ;

	$request_params = http_build_query( $params_c, '', '&' ) ;
	$request_params = str_replace( array( '+', '%7E' ) , array( '%20', '~' ) , $request_params ) ;
	$request_params = rawurlencode( $request_params ) ;
	$encoded_request_method = rawurlencode( $request_method ) ;
	$encoded_request_url = rawurlencode( $request_url ) ;
	$signature_data = $encoded_request_method . '&' . $encoded_request_url . '&' . $request_params ;
	$hash = hash_hmac( 'sha1' , $signature_data , $signature_key , TRUE ) ;
	$signature = base64_encode( $hash ) ;
	$params_c['oauth_signature'] = $signature ;
	$header_params = http_build_query( $params_c , '' , ',' ) ;

	$context = array(
		'http' => array(
			'method' => $request_method ,
			'header' => array(
				'Authorization: OAuth ' . $header_params ,
			) ,
			'content' => $request_body ,
		) ,
	) ;

	if ( $request_headers ) {
		$context['http']['header'] = array_merge( $context['http']['header'], $request_headers ) ;
	}

	if( $request_method == "GET" ) {
		$request_url .= '?' . http_build_query( $params_a ) ;
	}

	$curl = curl_init() ;
	curl_setopt( $curl, CURLOPT_URL , $request_url ) ;
	curl_setopt( $curl, CURLOPT_HEADER, true ) ; 
	curl_setopt( $curl, CURLOPT_CUSTOMREQUEST , $context['http']['method'] ) ;
	curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER , false ) ;
	curl_setopt( $curl, CURLOPT_RETURNTRANSFER , true ) ;
	curl_setopt( $curl, CURLOPT_HTTPHEADER , $context['http']['header'] ) ;
	if ( isset($context['http']['content']) ) {
		curl_setopt( $curl, CURLOPT_POSTFIELDS , $context['http']['content'] ) ;
	}
	curl_setopt( $curl, CURLOPT_TIMEOUT, 5 ) ;
	$res1 = curl_exec( $curl ) ;
	$res2 = curl_getinfo( $curl ) ;
	curl_close( $curl ) ;

	$response_body = substr( $res1, $res2['header_size'] ) ;
	$response_header = substr( $res1, 0, $res2['header_size'] ) ;

	return [ $response_body, $response_header ] ;
}

デモを開く