LoginSignup
37
26

More than 3 years have passed since last update.

記事へのTwitterコメントを収集するプログラムを作ってみた

Last updated at Posted at 2018-11-25

Qiita含め、一般個人が書くネット上の記事にはコメントが少ないことがよくあります。読んで面白いなぁと思ったのに、コメント欄は意外と閑散としてること、あるじゃないですか。なので投稿者は、記事の感想を知りたくても、なかなか伝わってこないことも多いと思うんですよね。

そもそもコメントするためにそのサービスのアカウントが必要な時点でハードルが高い。さらに、コメントが付いたとしても、意識高めの人(良い意味で)か、単に自己主張が強めの人の割合が高いように見えます。なので、中間にいる大多数の感想が把握できません。

例えば数日前に書いたこの記事は、Qiita上では現時点で「7いいね、0コメント」という閑散っぷりですが、裏ではTwitterで記事が結構シェアされていて、5日で17000viewsを超えていました(ちなみに前回の157いいねの記事は約8500views)。

aaaaa.jpg

これほどシェアされても、Qiitaだけ見ているとコメントどころか見てくれた人がいたことに気づくことすらできません。

Twitterには感想がいっぱい

でもTwitterをしっかり見ると、結構コメントがあるんですよね。Twitterなら多くの人が使っていて、そもそもが「呟き」なので、大多数の純粋な感想を集めるには適しているはず。誰かへの気遣いや遠慮が入ってこない本音の感想は、1番のフィードバックになり得ます。

ただし、単純にTwitterで記事URLで検索して感想を探そうとすると、2つの問題が出てきます。

①感想がないツイートも多い
「記事のURLとタイトルだけ」というデフォルトの状態でツイートする人も多いので、読んだ人の感想が書かれていないツイートがたくさんヒットしてしまいます。できれば感想のあるツイートだけ抽出したいところ。

②感想を別ツイートに書く人が多い
リツイート後に、通常のツイートとして感想をボソッと呟いている人が意外と多いです。これは、検索にヒットすらしません。例えばこんなツイートです。

ということで今回はこれらを探し出すプログラムを書いてみました。

作ったもの

記事へのTwitterコメント収集プログラム
※コードは最後にまとめて載せます。Pythonです。

記事のURLを指定すると、その記事へのコメントを含むツイートを収集し、まとめてHTML化します。やっていることはこんな感じ。

① 記事のURLを含むツイートを取得
② 出力対象を抽出
 ・①のツイートが通常ツイートの場合:ユーザ独自コメントを含むものを抽出(※1)
 ・①のツイートがリツイートの場合:リツイート直後5分以内の通常ツイートを抽出(※2)
③抽出したツイートの埋め込みHTMLを取得
④HTMLファイル作成

※1:記事URLやタイトル以外の文字列を独自コメントとする。
※2:中でもコメントの可能性が高い(リンクやハッシュタグなどがない)ものに絞る。「5分」は調整の余地あり。

ちなみに無料の場合は1週間以内のツイートに対して有効です。(TwitterAPI制約)

結果

先程のジャニーズのCD売上の記事に対してプログラムを動かした結果のHTMLを抜粋して載せてみます。ジャニーズ知識の浅い素人が勢いで書いた記事なので、ジャニーズ愛に溢れたファンから見たら、辛辣で心が折れるようなコメントを浴びせられる内容なのでは…と恐る恐る見てみると

こんな感じのが数十件。予想より全然評判良い。何より、プログラミングとは縁のなさそうなジャニーズファンに「Python学びたい」って思わせるのは割と凄いことな気がする。

結果2

一応他の方の多くシェアされていそうなQiita記事でも試してみます。

新幹線の移動時間でサービスを作ってリリースするまでの軌跡〜サクッと作るための技術スタックとは〜

もう1記事。
「AIで〇〇したいんだけど」の相談前に確認してほしい3つのこと

どちらの記事も、先程とはターゲットが違うせいか、最初から記事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や乃木坂には触れなかったり、個人的な感想を語らないようにしたりと、とにかくジャニーズファン(特に嵐ファン)だけをターゲットに、見た人がすんなり読めてシェアしやすい内容を意識しました。

結果として想像以上にうまくいったのですが、一方で「コメントがないつまらなさ」も感じました。良いも悪いもわからない。きっとそんな投稿者は他にもいるはず。そんな人たちのモチベーションを上げれるものを作ってみようと考えたのが今回の記事を書いたきっかけです。

閲覧数は多いんだけど感想が伝わってこないなぁ…なんて思っている人は、試してみてもいいかも。

37
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
26