Qiita含め、**一般個人が書くネット上の記事にはコメントが少ないことがよくあります。**読んで面白いなぁと思ったのに、コメント欄は意外と閑散としてること、あるじゃないですか。なので投稿者は、記事の感想を知りたくても、なかなか伝わってこないことも多いと思うんですよね。
そもそもコメントするために**そのサービスのアカウントが必要な時点でハードルが高い。**さらに、コメントが付いたとしても、意識高めの人(良い意味で)か、単に自己主張が強めの人の割合が高いように見えます。なので、中間にいる大多数の感想が把握できません。
例えば数日前に書いたこの記事は、Qiita上では現時点で**「7いいね、0コメント」**という閑散っぷりですが、裏ではTwitterで記事が結構シェアされていて、5日で17000viewsを超えていました(ちなみに前回の157いいねの記事は約8500views)。
これほどシェアされても、Qiitaだけ見ているとコメントどころか見てくれた人がいたことに気づくことすらできません。
Twitterには感想がいっぱい
でもTwitterをしっかり見ると、結構コメントがあるんですよね。Twitterなら多くの人が使っていて、そもそもが「呟き」なので、大多数の純粋な感想を集めるには適しているはず。誰かへの気遣いや遠慮が入ってこない本音の感想は、1番のフィードバックになり得ます。
ただし、単純にTwitterで記事URLで検索して感想を探そうとすると、2つの問題が出てきます。
①感想がないツイートも多い
「記事のURLとタイトルだけ」というデフォルトの状態でツイートする人も多いので、読んだ人の感想が書かれていないツイートがたくさんヒットしてしまいます。できれば感想のあるツイートだけ抽出したいところ。
②感想を別ツイートに書く人が多い
リツイート後に、通常のツイートとして感想をボソッと呟いている人が意外と多いです。これは、検索にヒットすらしません。例えばこんなツイートです。
後で読もう。
— さやか (@sayaka_show) 2018年11月21日
RT
ということで今回はこれらを探し出すプログラムを書いてみました。
作ったもの
記事へのTwitterコメント収集プログラム
※コードは最後にまとめて載せます。Pythonです。
記事のURLを指定すると、その記事へのコメントを含むツイートを収集し、まとめてHTML化します。やっていることはこんな感じ。
① 記事のURLを含むツイートを取得
② 出力対象を抽出
・①のツイートが通常ツイートの場合:ユーザ独自コメントを含むものを抽出(※1)
・①のツイートがリツイートの場合:リツイート直後5分以内の通常ツイートを抽出(※2)
③抽出したツイートの埋め込みHTMLを取得
④HTMLファイル作成
※1:記事URLやタイトル以外の文字列を独自コメントとする。
※2:中でもコメントの可能性が高い(リンクやハッシュタグなどがない)ものに絞る。「5分」は調整の余地あり。
ちなみに無料の場合は1週間以内のツイートに対して有効です。(TwitterAPI制約)
結果
先程のジャニーズのCD売上の記事に対してプログラムを動かした結果のHTMLを抜粋して載せてみます。ジャニーズ知識の浅い素人が勢いで書いた記事なので、ジャニーズ愛に溢れたファンから見たら、**辛辣で心が折れるようなコメントを浴びせられる内容なのでは…**と恐る恐る見てみると
色々なことが読み取れてとても面白いデータ! ありがたい!https://t.co/xfmFbgf7pI
— まる太 (@marumaruta2018) 2018年11月21日
RT こう見るとシンプルに面白い
— まどか (@madokamusubi) 2018年11月21日
グラフはわかりやすいし、ちょっと意外で見てしまうね、これ。
— もの (@green7057xx) 2018年11月21日
ジャニーズのシングルCD売上データをまとめて可視化してみた https://t.co/QwJB7Jsi15 #Qiita
生データって面白いな〜https://t.co/t2q0pqhjuc
— ゆあ🌏🚀-ENCORE- (@black0v0dark) 2018年11月21日
これ卒論で作ろうと思ってたやつ!笑
— ゆーき (@v1000encore6) 2018年11月21日
RT
— かこるか🎩👞 (@kakorukatkv) 2018年11月21日
これ、こういうのありがたい!
普段何かあるとちまちまデータ漁っている私なので、まとめていただけると大変ありがたい。
こんな所でPythonのコード見ると思わなかった(笑)
— にょろ。 (@nyorons) 2018年11月20日
これすげー!ありがてえー!こういうの見たかった!
— みーーーーーら (@3854no_arashi) 2018年11月22日
え、まってぱいそんこんなことできんの??ちょっと覚えようかな
— なお (@70nao1030) 2018年11月21日
あべちゃんが勉強してたプログラミング言語、すごい。なんて読むかもわからんけど、データの可視化すごい。これやりたい。勉強したい>RT
— イク (@s_iku) 2018年11月20日
SMAPからキスマイまでのCD売り上げデータを、ネットで機械的に集めた男性的分析レポ。ジャニ所やヲタの汚い世論操作やステマの影響低いデータを見るとうれしい、リケジョな私。ありがとう。
— アイドル氣録JDD☆ジャニーズJrデビュー応援中 (@JDD_idleki6) 2018年11月21日
CD売れてるジャニはスマキンキ嵐
1997あたりがアイドル全体のCD販売のピークhttps://t.co/xMPIOGGqhb #Qiita
こんな感じのが数十件。予想より全然評判良い。何より、プログラミングとは縁のなさそうなジャニーズファンに「Python学びたい」って思わせるのは割と凄いことな気がする。キータにジャニーズネタ!(そこ)
— りま (@mr_1s25) 2018年11月20日
結果2
一応他の方の多くシェアされていそうなQiita記事でも試してみます。
新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜
良さみがある記事だ。サクサク感が心地良い。
— あんど@クイズのプラットフォームを作ってるよ (@ampersand_xyz) 2018年11月24日
新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜 https://t.co/qU1zUvtrxQ
GASはAPIサーバにも使えるのか! | 新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜 https://t.co/nNyug9o79g
— Momo Watanabe (@mom0tomo) 2018年11月24日
着くまでに絶対仕上げるってアドレナリンでるよね,きっと.:
— takala4 (@4planing) 2018年11月24日
新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜 https://t.co/6U982QXsIQ #Qiita
短期間で何かつくれるような人になりたかった…
— nori (@nori0__) 2018年11月23日
新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜 https://t.co/nRQkDu8vhf
空いてる時間で効率よくスピード感持って作るのいいな〜
— 高橋 光 (@light940) 2018年11月23日
新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜 https://t.co/UmpqXKO0ZW
もう1記事。
「AIで〇〇したいんだけど」の相談前に確認してほしい3つのこと
これ重要
— rosev👨🏻💻🎓🔥☄️ (@rosev838) 2018年11月23日
・人間を超えるAIという幻想を抱いていないか
・AIなしでもビジネスが成り立っているか
・必要なデータの形を知っているか
- 「AIで〇〇したいんだけど」の相談前に確認してほしい3つのことhttps://t.co/ZHW8ZxjtaH
優良記事で、5分あれば読めるので広まってほしい!AIで何かしたいってめちゃくちゃ言われるけど、だいたいこれ。
— Chisato Kunimoto @ Cinnamon 🇻🇳 (@chi_kunimoto) 2018年11月23日
「AIで〇〇したいんだけど」の相談前に確認してほしい3つのこと - Qiita https://t.co/C8cZpUK5rk
人間の判断が何でもできるようなイメージを持つ人が多いもんね。//「AIで〇〇したいんだけど」の相談前に確認してほしい3つのこと #人工知能 #techfeed via @techfeedapp https://t.co/zBE5Xxmteq
— あいいろ@Swift勉強中 (@deepbluesan) 2018年11月22日
ちょっと待って!これ読んで!https://t.co/5Jt9Lz67Ap
— kazuteru (@kazuteru_dev) 2018年11月22日
めちゃくちゃ親切。こんな書かなきゃやっとられんくらい忙しいのでしょう。「まずこれ御社内で熟読玩味していただけますか」ができるので、状況マシになるのでは。 / “「AIで〇〇したいんだけど」の相談前に確認してほしい3つのこと - …” https://t.co/YahUwGfvUt
— Shinnosuke Suzuki (@sasasin_net) 2018年11月22日
どちらの記事も、先程とはターゲットが違うせいか、最初から記事URLをコメント付きでツイートする人が多かったですね。
ソースコード
1ファイルにしたコードをまるごと載っけます。
勢いで書いてるので綺麗ではないと思いますが動きはするはず。。
※TwitterのKey関連は指定が必要です。
import pandas as pd
import datetime
import json
from requests_oauthlib import OAuth1Session
import time
import urllib.request
from bs4 import BeautifulSoup
MIN_TWEET_COUNT = 100000 #取得対象ツイートが多すぎる場合はこの件数まで取得
SEARCH_COUNT = 100 #1度に取得する件数(最大100)
SEARCH_URL = 'https://api.twitter.com/1.1/search/tweets.json'
TIMELINE_URL = 'https://api.twitter.com/1.1/statuses/user_timeline.json'
OEMBED_URL = 'https://publish.twitter.com/oembed'
VIEW_HTML = './comment_tweet.html'
TITLE_DEFAULT = [' - Qiita']
TRIM_WORDS_DEFAULT = ['-', 'Qiita', '【', '】']
NORMAL = 1
RETWEET = 2
# 下記は変更の上実行
CK = 'Consumer Key'
CS = 'Consumer Secret '
AT = 'Access Token'
AS = 'Access Token Secret'
twitter = OAuth1Session(CK, CS, AT, AS)
# ツイート取得
def get_tweets_by_api(query, since, until, max_id):
# APIパラメータ設定
params = {
'q': query,
'lang': 'ja',
'locale': 'ja',
'count': SEARCH_COUNT,
'until': until,
'tweet_mode': 'extended',
'max_id': max_id,
}
# API呼び出し
res = twitter.get(SEARCH_URL, params=params)
tweets = json.loads(res.text)
status = res.status_code
return tweets, status
# 指定ツイート直後5分以内の通常ツイートのIDを取得
def get_next_tweet(screen_name, target_id, created_at):
datetime_rt = datetime.datetime.strptime(
created_at, '%a %b %d %H:%M:%S +0000 %Y')
# APIパラメータ設定
params = {
'screen_name': screen_name,
'since_id': target_id,
'count': SEARCH_COUNT,
'exclude_replies': True,
'include_rts': True,
'tweet_mode': 'extended',
}
last_id = None
while True:
# API呼び出し
try:
res = twitter.get(TIMELINE_URL, params=params)
except:
pass
return None
tweets = json.loads(res.text)
status = res.status_code
if status != 200:
return None
if len(tweets) != 0:
last_tweet = tweets[-1]
last_id = last_tweet['id']
last_created_at = last_tweet['created_at']
params['max_id'] = last_id - 1
time.sleep(1)
else:
if last_id is not None \
and get_retweet_pattern(last_tweet) == NORMAL \
and len(last_tweet['entities']['hashtags']) == 0 \
and len(last_tweet['entities']['urls']) == 0 \
and 'media' not in last_tweet['entities']:
# 5分以内判定
datetime_next = datetime.datetime.strptime(last_created_at, '%a %b %d %H:%M:%S +0000 %Y')
delta = datetime_next - datetime_rt
if delta.total_seconds() <= 300:
return last_id
else:
return None
else:
return None
# リツイートパターン(通常/リツイート)を取得
def get_retweet_pattern(tweet):
if 'retweeted_status' in tweet:
return RETWEET
else:
return NORMAL
# 文字列から特定の単語を削除
def get_trim_words(text, words):
for word in words:
text = text.replace(word, '')
return text.strip()
# ツイートからタイトル、URL、ハッシュタグ、一部記号の文字列を削除
def get_comment(tweet, title):
text = tweet['full_text']
trim_words = [title]
trim_words.extend(TRIM_WORDS_DEFAULT)
for hashtag in tweet['entities']['hashtags']:
text = text.replace(f'#{hashtag["text"]}', '')
for url in tweet['entities']['urls']:
text = text.replace(f'{url["url"]}', '')
trimed_tweet = get_trim_words(text, trim_words)
return trimed_tweet
# URLから記事タイトルを取得
def get_title(url):
req = urllib.request.Request(url)
res = urllib.request.urlopen(req)
html = res.read()
soup = BeautifulSoup(html, "lxml")
title = soup.title.string
trim_words = TITLE_DEFAULT
title_org = get_trim_words(title, trim_words)
return title_org
# コメント情報のあるツイートのIDとScreenNameを取得
def get_comment_tweet(tweet, title):
# パターン取得:オリジナル/RT
pattern = get_retweet_pattern(tweet)
# 通常ツイートの場合
if pattern == NORMAL:
# コメントがある場合に表示対象に格納
comment = get_comment(tweet, title)
if comment != '':
id = tweet['id']
screen_name = tweet['user']['screen_name']
else:
return None
# リツイートの場合
elif pattern == RETWEET:
# 本ツイートの次のNORMALツイートIDを取得
next_id = get_next_tweet(
tweet['user']['screen_name'], tweet['id'], tweet['created_at'])
if next_id is not None:
id = next_id
screen_name = tweet['user']['screen_name']
else:
return None
comment_tweet = pd.Series([id, screen_name], ['id', 'screen_name'])
return comment_tweet
# ツイート情報のDataframeを取得
def get_comment_tweets(df, tweets, title):
for tweet in tweets['statuses']:
tweet_info = get_comment_tweet(tweet, title)
if tweet_info is not None:
df = df.append(tweet_info, ignore_index=True)
return df
def get_comment_df(url):
# 変数初期化
max_id = 9999999999999999999
tweet_count = 0
# ツイート格納Dataframe
df = pd.DataFrame(columns=['id', 'screen_name'])
# 当日から10日前までを対象とする(制約上7日程度に絞られる)
until_datetime = datetime.datetime.now()
since_datetime = until_datetime - datetime.timedelta(days=10)
since_date = since_datetime.strftime("%Y-%m-%d")
until_date = until_datetime.strftime("%Y-%m-%d")
# URLの記事タイトルを取得
title = get_title(url)
# 100件ずつ取得し、結果が0件 or 回数リミットまで繰り返し
while True:
# 取得済みツイートが一定数を超えても終了
if tweet_count > MIN_TWEET_COUNT:
break
# ツイートを取得
tweets, status = get_tweets_by_api(url, since_date, until_date, max_id)
new_tweet_count = len(tweets['statuses'])
print(f'取得件数:{new_tweet_count} StatusCode:{status}')
# API取得結果が0件 または APIが回数リミット超え の場合は終了
if new_tweet_count == 0 or status == 429:
break
else:
# 結果が異常の場合は次のループへ
if status != 200:
continue
else:
df = get_comment_tweets(df, tweets, title)
max_id = tweets['statuses'][-1]['id'] - 1
# 取得件数の合計カウント
tweet_count += new_tweet_count
print(f'合計{tweet_count}件取得')
return df
# 対象ツイートの埋め込みHTML取得
def get_tweet_html(screen_name, id):
tw_url = f'https://twitter.com/{screen_name}/status/{id}'
oembed_params = {
'url': tw_url,
'lang': 'ja',
'hide_media': True, # リンク先画像展開なし
}
res = twitter.get(OEMBED_URL, params=oembed_params)
data = json.loads(res.text)
print(f'{id} HTML取得完了')
return data['html']
# HTMLファイル出力
def make_html(df):
if len(df) > 0:
print(f'{len(df)}件 HTML作成開始')
with open(VIEW_HTML, mode='w', encoding='utf-8') as f:
f.write('<html lang="ja"><head><meta charset="utf-8"/></head><body>')
for index, row in df.iterrows():
tw_html = get_tweet_html(row['screen_name'], str(row['id']))
f.write(tw_html)
f.write('</body></html>')
print('HTML作成完了')
else:
print('HTML作成対象ツイートなし')
url = 'https://qiita.com/yossymura/items/9312b1e2a198d2728ebe'
df = get_comment_df(url)
make_html(df)
以上です。
あとがき
前々回の記事で、Twitterの「リツイート」とQiitaの「いいね」は比例しないことがわかったので、どうせならTwitterでシェアされることだけに極端にフォーカスしてみようと思って書いたのが、前回のジャニーズ記事でした。
技術面の説明を最低限に抑えたり、ジャニーズを超える売上を出すAKBや乃木坂には触れなかったり、個人的な感想を語らないようにしたりと、とにかくジャニーズファン(特に嵐ファン)だけをターゲットに、見た人がすんなり読めてシェアしやすい内容を意識しました。
結果として想像以上にうまくいったのですが、一方で「コメントがないつまらなさ」も感じました。良いも悪いもわからない。きっとそんな投稿者は他にもいるはず。そんな人たちのモチベーションを上げれるものを作ってみようと考えたのが今回の記事を書いたきっかけです。
閲覧数は多いんだけど感想が伝わってこないなぁ…なんて思っている人は、試してみてもいいかも。