5日と20日は歌詞と遊ぼう。

歌詞を読み、統計したりしています。

AKBの“懐古厨”はいったいいつの時代が一番好きなのかをスクレイピング!


【MV】恋するフォーチュンクッキー / AKB48[公式]
わたしはYouTubeの動画を連続再生して、それを作業用BGMにしたりしています。
きょうはAKB48だ!!って思って聴いていて、ふとしたときに動画ページに戻ってみると、こういうコメントが目につきました。
「このときが一番よかった!」
「この頃がまさに黄金期!」
これは…! いろんなジャンルでよく見る“懐古厨”のみなさんだ!!
ちょっと見てみると、AKBの少し古めの曲のMVなら、どの動画にもこういうコメントをつけている人がいらっしゃいます。
わたしは思いました。
“懐古厨”のみなさんは、ほんとはいったいいつの時代が一番好きなんだろう…。
というわけで、今回はプログラムを書いて、それを調べてみることにしました。
途中までずっとプログラム書いているので、結論を知りたいひとはジャンプしてください!
(今回の記事は歌詞関係ありません。ざんねん…。)

あつめようYouTubeコメ

まずはYouTubeから、AKBの動画を探しましょう。
AKBの動画は、公式のものから、TV番組を勝手にアップロードしたもの(きっとすぐ消されちゃう)までたくさんあります。
今回は、公式のMVを対象にします。AKBはわりと早い時期から公式の動画をYouTubeに上げてた実績があるので、蓄積がたくさんあって最高です。
公式の動画はここにあります。再生リストっていうんだって。
www.youtube.com
この44曲のコメントを収集することにしましょう。

ネットとかから情報を体系的に収集することをスクレイピングって言うそうです。HTMLを解析してごりごり…って方法もありますが、YouTubeにはAPIという仕組みがあります。我々の味方です!
developers.google.com
にほんごだ!!
前にTwitterAPIを使ったことがあるんですが、このときにはドキュメント英語でした…。
hacosato.hatenablog.com
それに比べたら、(英語の部分もあるけど)日本語で説明してくれるなんてYouTube最高です♪
ここの指示通りにアカウントをつくったりしましょう。「プロジェクト ID」は自動で付くし変更もできるけど、べつに自分がわかればなんでもいいみたい。

再生リストからどうやって情報を得るのかは、ここに書いてあります。
developers.google.com
いろいろ説明があって難しいように思えますが、とりあえずこれを入力して、ブラウザのアドレスバーに入力しましょう。

「HTTP リクエスト」って書いてある「https://www.googleapis.com/youtube/v3/playlistItems」のあとに「?」を付け足して、あとパラメーターを「&」で区切りながら書いていけばいいんですね!
「自分のキー」っていうのは
console.developers.google.com
から確認できます。
アドレスバーに入力したらEnter!
f:id:hacosato:20160220224022p:plain
すごい! プログラミングやってる感ある!!
あとはここから必要な情報を拾っていけばいいです♪ 今回は動画のIDだけ拾っておいて、あとでIDからコメントを探していこうと思います。
IDを拾っておけば、そこからさらに詳しい情報を得ることができます。今回はそういう作戦を取ります。

developers.google.com
IDからコメントを拾うには、CommentThreadの情報を使います。
developers.google.com
コメントについているコメントを拾うには、Commentsを使います。
CommentThreadは100までしか手に入りませんが、pageTokenっていうパラメーターを設定すれば、次の100件、その次の100件…って探すことができます。これを順に繰り返していけばいいので、たぶんこれはプログラミングでいうと再帰ってやつだと思います! はじめて使った!
これをすべての動画に対して繰り返して処理するようにしたらよいです。
たくさんの情報を得ることができますが、そんなにいらないので、今回はコメントの中身と、それについているlikeの数を集めました。やる気になればコメントを書いた人とか、その日時とかもつかむことができますが、今回はなしです。
たいていの言語で可能だとは思いますが、わたしはRubyを使います。

# AKB48のプレイリストから、動画のIDを取得し、コメントをテキストファイルで出力

# -*- coding: utf-8 -*-
start_time = Time.now

require 'open-uri'
require 'json'
require 'retryable'

apikey = "APIキーだよ〜〜"
order = "relevance"
part = "replies,snippet"
maxresults = 100

# YouTube動画のURLを受け取って、付いているコメントと評価のハッシュを配列にしたものを返すdef。
# resultには空の配列を与えておくと埋めて返してくれる。
def scrapecomments(url, result)
	p url
	begin
		hash = Retryable.retryable(:tries => 3) do |retries, exception|
			next if retries == 2
			JSON.parse(open(url).read)
		end
	rescue => err
		p err
		return result
	end
	# エラーじゃなければ続ける
	unless hash.has_key?("error")
		hash["items"].each do |comment|
			result.push({"text" => comment["snippet"]["topLevelComment"]["snippet"]["textDisplay"], "likeCount" => comment["snippet"]["topLevelComment"]["snippet"]["likeCount"]})
			# リプがあるならそれも数える
			if comment.has_key?("replies")
				comment["replies"]["comments"].each do |replycomment|
					result.push({"text" => replycomment["snippet"]["textDisplay"], "likeCount" => replycomment["snippet"]["likeCount"]})
				end
			end
		end
		# 次のページがあったら再帰で読み込む
		if hash.has_key?("nextPageToken")
			url = url.split("&pageToken=")[0]+"&pageToken=#{hash["nextPageToken"]}"
			scrapecomments(url, result)
		end
	end
	return result
end

# https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=PL72C7672926C69324&key=キーだよ〜〜
# AKB48のシングルMVコレクションプレイリストのデータを取得できるよ。
# 今回はサーバに負担をかけないように、一度ローカルに落としました。
mvlist = File.read("ファイルパスだよ〜〜"
hash = JSON.parse(mvlist)
# 1曲ずつ処理を回す
hash["items"].each_with_index do |movie, idx|
	# タイトルにスラッシュが入るとうまく動かない
	title = movie["snippet"]["title"].gsub!(/【MV】\s*|\s*\/\s*AKB48\s*\[公式\]/, "")
	videoId =  movie["snippet"]["resourceId"]["videoId"]
	result = []
	url = "https://www.googleapis.com/youtube/v3/commentThreads?part=#{part}&order=#{order}&key=#{apikey}&textFormat=plainText&videoId=#{videoId}&maxResults=#{maxresults}"
	# この行で、実際にスクレイピングの処理を呼んでる
	result = scrapecomments(url, result)
	sleep 1
	# 結果は1曲ずつファイルに書き出す
	File.write("ファイルパスだよ〜〜/result/#{idx}_#{title}.txt", result)
	p "#{idx}.『#{title}』のコメント数は#{result.length}でした。"
end

p "所要時間は #{Time.now - start_time}s でした。"

こういう感じ。動かすと、指定したフォルダにファイルがひとつずつできてきます。
ところで、今回ここに積み残しがあります。
コメント数が2000を超えると調子が悪くなるのです…。
曲ごとの取得率をグラフにしたところ、こういう感じでした。
f:id:hacosato:20160220224248p:plain
2000ぐらいまでしかコメントを取得できていないことがわかります。が、原因ははっきりしませんでした。PageTokenが数千文字にもなる(ページが増すに連れてどんどん伸びるのです)のが遠因かなと思うんですが、よくわかりませんでした…)。

見つけよう懐古コメ

さて、コメントを集めたら、今度はそこから懐古コメントを探します。
懐古コメントの判断方法はカンタンです!
「あの頃は〜〜」とか「この頃は〜」とかっていってるコメを探せばいいのです。
今回は強引に、正規表現/[あこ]の(ころ|頃|とき|時)/にマッチするコメをすべて懐古コメント

  • あの頃
  • あのころ
  • あの時
  • あのとき
  • この頃
  • このころ
  • この時
  • このとき

のどれかを含むコメを探せばいいだけのカンタン仕様です♪

さっきと違うプログラムを書いて、コメントを探します。
こっちはネットに繋がるプログラムじゃないし、テキストファイルから文字を探して数を数えるだけなので、さっきよりもカンタンです!

# AKB48のプレイリストから、動画のIDを取得し、コメントをテキストファイルで出力したのを受け取って、
# そこから懐古厨のコメントを集計する

require 'pp'

# ハッシュが入ったファイルを受け取って、懐古なコメントを集計する
def aggregatefromfile(path)
	# クエリ
	re = /[あこ](ころ||とき|)/
	text = File.open(path).read
	# 中身が安全とわかっているファイルにしか使えないやつeval
	hash = eval(text)
	anokorocomments = []
	# コメントの総数とlikeの総数
	commentcount = 0
	likecount = 0
	# 懐古コメントの数と、それについたlikeの数
	commentscore = 0
	likescore = 0
	hash.each do |comment|
		if comment["text"].match(re)
			anokorocomments.push(comment)
			commentscore += 1
			likescore += comment["likeCount"]
		end
		likecount += comment["likeCount"].to_i
	end
	commentcount = hash.length
	result = {"comments"=>anokorocomments, "score"=>commentscore+likescore*coefficient, "commentscore"=>commentscore, "likescore"=>likescore, "commentcount"=>commentcount, "likecount"=>likecount}
	return result
end

path = "パスだよ〜〜/result"
print "番号	曲名	該当コメ	総コメ	該当like	総like\n"
dir = Dir.open(path)
dir.each do |name|
	next if name == "."
	next if name == ".."
	result = aggregatefromfile(path + "/" + name)
	print name.split("_")[0]+"\t"+name.split("_")[1]+"\t"+result["commentscore"].to_s+"\t"+result["commentcount"].to_s+"\t"+result["likescore"].to_s+"\t"+result["likecount"].to_s+"\n"
	print name.split("_")[1]
	# コメントの中身を見るとき用
	# p result["comments"]end
dir.close

でーきた! 結果がコレです。
コメについてるlikeは、コメの半分の重みを付けてカウントしました。
懐古コメント1つが1点だとしたら、懐古コメントにつくlikeひとつが0.5点です。

番号 曲名 該当コメ 総コメ 該当like 総like 該当コメ率 該当like率 スコア
0 ハロウィン・ナイト.txt 0 197 0 301 0 0 0
1 僕たちは戦わない.txt 1 579 2 809 0.172711572 0.247218789 0.203355363
2 Green Flash.txt 0 415 0 630 0 0 0
3 希望的リフレイン.txt 1 956 0 1174 0.10460251 0 0.064808814
4 心のプラカード.txt 1 673 0 661 0.14858841 0 0.099651221
5 ラブラドール・レトリバー.txt 2 729 3 536 0.274348422 0.559701493 0.351053159
6 前しか向かねえ.txt 0 334 0 461 0 0 0
7 鈴懸の木の道で「君の微笑みを夢に見る」と言ってしまったら僕たちの関係はどう変わってしまうのか、僕なりに何日か考えた上でのやや気恥ずかしい結論のようなもの.txt 2 354 1 310 0.564971751 0.322580645 0.491159136
8 ハート・エレキ -Dance ver.-.txt 2 402 5 513 0.497512438 0.974658869 0.683371298
9 恋するフォーチュンクッキー.txt 5 2196 3 3473 0.227686703 0.086380651 0.165289256
10 さよならクロール.txt 2 1365 2 1438 0.146520147 0.139082058 0.143953935
11 So long !.txt 1 518 0 542 0.193050193 0 0.126742712
12 永遠プレッシャー.txt 10 1907 26 1463 0.524383849 1.777170198 0.87170741
13 UZA -Dance ver.-.txt 4 1688 0 2587 0.236966825 0 0.134160657
14 ギンガムチェック.txt 2 2064 12 2712 0.096899225 0.442477876 0.233918129
15 真夏のSounds good ! (Dance ver.).txt 9 1603 32 2602 0.561447286 1.229823213 0.860881543
16 GIVE ME FIVE !.txt 14 1574 15 1831 0.889453621 0.819224468 0.863627234
17 上からマリコ.txt 3 1367 0 1713 0.219458669 0 0.13492242
18 風は吹いている(DANCE! DANCE! DANCE! ver.).txt 8 895 18 2378 0.893854749 0.756938604 0.815738964
19 フライングゲット (ダンシングバージョン).txt 25 2014 65 3736 1.241310824 1.739828694 1.48119526
20 Everyday、カチューシャ.txt 16 2050 6 2901 0.780487805 0.206825233 0.542779603
21 桜の木になろう.txt 13 999 6 321 1.301301301 1.869158879 1.379905132
22 チャンスの順番.txt 7 1024 20 814 0.68359375 2.457002457 1.187980433
23 Beginner.txt 10 1834 35 1454 0.54525627 2.407152682 1.073799297
24 ヘビーローテーション.txt 15 2362 13 3353 0.635055038 0.387712496 0.532375882
25 ポニーテールとシュシュ.txt 47 2064 185 1547 2.277131783 11.95862961 4.916299559
26 涙サプライズ !.txt 61 2017 199 3280 3.024293505 6.067073171 4.388843314
27 RIVER.txt 36 2053 92 3466 1.753531417 2.654356607 2.165874274
28 大声ダイヤモンド.txt 73 1977 359 3662 3.692463328 9.803386128 6.630777311
29 言い訳Maybe.txt 69 2051 243 3351 3.364212579 7.251566697 5.112035422
30 10年桜.txt 44 1743 138 3832 2.524383247 3.60125261 3.088275485
31 野菜シスターズ.txt 25 1965 106 4019 1.272264631 2.637472008 1.962511008
32 マジスカロックンロール.txt 10 1065 36 1789 0.938967136 2.012297373 1.428935953
33 君のことが好きだから.txt 30 983 48 794 3.051881994 6.04534005 3.913043478
34 遠距離ポスター.txt 9 1020 15 2648 0.882352941 0.566465257 0.703924915
35 マジジョテッペンブルース.txt 16 1301 60 2096 1.229823213 2.86259542 1.958280119
36 涙のシーソーゲーム.txt 2 274 0 693 0.729927007 0 0.322320709
37 僕のYELL.txt 1 247 1 787 0.4048583 0.127064803 0.234192037
38 飛べないアゲハチョウ.txt 14 478 89 1348 2.928870293 6.602373887 5.078125
39 盗まれた唇.txt 10 349 28 838 2.865329513 3.341288783 3.125
40 桜の栞.txt 19 1348 23 2764 1.409495549 0.832127352 1.117216117
41 ラッキーセブン.txt 15 572 27 1287 2.622377622 2.097902098 2.344714109
42 ひこうき雲(シアターガールズver).txt 1 164 0 263 0.609756098 0 0.338409475
43 Choose me !.txt 15 697 45 3304 2.152080344 1.361985472 1.59642401

グラフにすると、こうなります。
f:id:hacosato:20160220224359p:plain
というわけで、結論!
1番は『大声ダイヤモンド』でした。2番は『言い訳Maybe』です!
2008年から2009年ごろの曲に人気が集まっているみたいだ!


どちらもわたしもスキな曲♪
こうやって見ると、全体としてやっぱり古い曲にみんな想いを寄せていることがわかります。最近の曲だと懐古コメントと判断されたものはほとんどなかったりするので、あの正規表現もわりと筋がいいのかもしれません。
個人的には『ヘビーローテーション』といったもっとめっちゃ有名な曲にも票が集まるかな?って思ったんですが、そうでもありませんでした。有名だといまでもTVとかで露出が多いので、そんなに懐かしい気持ちにならないのかもしれないです。単にコメントの総数が多いので、割合だけでカウントすると不利なのかもしれません(だれか教えてください…Guiraud値とか関係ある…?)。



おそまつさまでした。
やってみてとってもたのしかったです。わたしが勝手に思う“懐古厨”よりは、暗くないコメントが多くて、AKB界隈のヲタのみなさんの愛を感じました。昔はよかったけど、いまはダメだよ!!ってタイプのコメントはすごく少なくて、らぶに満ち溢れた世界です〜。

こういうのは、基礎的なプログラミングができれば、動画のコメントを集めるのはとってもカンタンです! みんなもやろう! わたしにもたくさん教えてください!
参考:

何度もこのブログで書いていますが、この本はとっても素晴らしくて大好きです。
今回みたいに、ネットの情報を効率的に収集するにはどうすれば?っていうのが書いてある本です。
この本でひと通り学習すれば、もうスクレイピングに困ることはほとんどなくなるんじゃないかなって思っています。
わたしはまだ最後まで読破できてないけど、これからもたゆまずに学んでいこうって思っています☆
ところでこの本、YouTubeスクレイピングに関してはなぜか記述がちょっと手薄なのですが、この本が出た直前にYouTubeAPIが変わったので、それが原因かなぁってわたしは勝手に考えています。ネットの情報も、旧APIのものが散見されるので、検索するとき大変でした。「v3」ってキーワードで絞り込みがおすすめです。
またあそびたい〜!٩(๑❛ᴗ❛๑)۶
※もちろん今回コメント拾えてないところもあるし、重みの付け方もなんとなくなので、ぜったいこの記事おあそびとしてお楽しみください!