【備忘録】Twitterデータの収集とクリーニング処理

この記事はTwitterデータのクリーニング処理をどうやったか，具体的に何をやったか，などを記録として残しておく目的で作成した．
新しい処理を追加したら随時更新する予定．

# 背景
深層学習を用いて複数の感情の分析・分類（文章に特定の感情がどれほど含まれるかなど）をするために感情のラベルとその強度が付いたデータセットが必要だった．ちなみに強度の推定ではなく単純な分類にすると感情が含まれていない文章に対処ができない．

# 目的
- TwitterAPIから取得した特定の感情を含む文章の選別をしたい
    - 画像がついているツイートを削除
    - URLを含むツイート，引用リツイートなどを削除（元の内容がわからないので）
- 選別後の文章を更にクリーニングをしたい**（ここは今回の記事ではやらない）**
    - 特定の言葉を含んだ文章を削除
    - 顔文字や絵文字などの学習に使いずらいものを削除（単純な文章の意味を取ってほしい）

# TwitterAPIからツイートの取得
## API認証（アプリケーション認証）
Twitter APIを使うためには認証を行う必要があるが，その認証には2種類の方法が用意されている．１つ目がツイートの作成・リツイートなど個人のアカウントが必要な処理に使う**「ユーザ認証」**で，２つ目がツイートの検索などの個人のアカウントが必要のない**「アプリケーション認証」**となっている．

ユーザ認証でもツイートの検索は可能だが，APIのレート制限の関係でアプリケーション認証のほうが検索だけであればかなり有利になっている．（ユーザ認証が15分に180リクエストなのに対し，アプリケーション認証は15分で450リクエストすることが可能）

今回はアプリケーション認証を使っていく．
認証方法はTwitterデベロッパーポータルからコンシューマーキー（API KeyとSecretの２種）を取得してきて，それらを使ってBearerトークンを作成（取得）し，各APIにそのトークンを付与するだけ．

一応手順：

``` shell:Bearerトークンの取得
# API KeyとSecretを合わせたものをBase64エンコードしてあげる
export CERT=`echo -n "<API Key>:<Secret>" | base64`

# Twitter APIのトークン取得用URLへPOST
curl -X POST -H "Authorization: Basic ${CERT}" -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" -d "grant_type=client_credentials" https://api.twitter.com/oauth2/token
```
これで取得できたBearerトークンをどこかへ保存しておく．注意点として，このPOSTリクエストにもレート制限があるためなるべく保存したものを使用したほうがいい．

詳しい認証方法

http://westplain.sakuraweb.com/translate/twitter/Documentation/OAuth/Application-only-authentication.cgi

## 検索APIを用いたツイートの取得
今回は「特定の感情を含んだ文章のコーパス」を作成するために，**感情を表す絵文字（😊，😭，😡，etc.）が含まれるツイート**を検索APIで収集していく．
検索APIの仕様については以下のサイトにドキュメントを参照．

http://westplain.sakuraweb.com/translate/twitter/Documentation/REST-APIs/Public-API/The-Search-API.cgi

使用方法は以下の通り．

```shell:検索用APIの使用方法
# search/tweets.jsonへGETリクエスト
# q：検索用クエリ
# count：取得するツイート数（例として1に設定している）
# lang：取得するツイートの言語
# そのままだとUTF-8でエンコードされているのでjqコマンドでデコード＆整形をして見やすくする
curl -X GET \
  -H "Authorization: Bearer <TOKEN>" \
  https://api.twitter.com/1.1/search/tweets.json?q=%F0%9F%98%8A&count=1&lang=ja | jq
```

```shell:検索結果の例
# 公開されているツイートだが，念のためIDは伏せておく
# 以下のようにstatusesの下にリストとなって複数のツイートが取得できる（今回はcount=1のため，1つのみ）
# 取得できる情報としては，テキスト・画像・言語・RTかどうかなどなど．
# search_metadataから検索条件が見れるのと同時にnext_resultsという連続で検索する際にかなり便利な値も返ってくる．
{
  "statuses": [
    {
      "created_at": "Thu Oct 21 17:07:53 +0000 2021",
      "id": 1451233794095128600,
      "id_str": "1451233794095128577",
      "text": "RT @<ID>: 知育とは関係ないのですが…\n使い捨てカイロを使われる方、ぜひ使い終わったカイロは捨てずに置いておいて、春にまとめてGoGreen物流センターへ送ってください！😊\nカイロの原料を水質改善に再利用する会社です。\nわたしも 去年Twitterで知りましたが、…",
      "truncated": false,
      "entities": {
        "hashtags": [],

# ・・・・・・・・・・
# 非常に長いので省略
# ・・・・・・・・・・

      "lang": "ja"
    }
  ],
  "search_metadata": {
    "completed_in": 0.02,
    "max_id": 1451233794095128600,
    "max_id_str": "1451233794095128577",
    "next_results": "?max_id=1451233794095128576&q=%F0%9F%98%8A&lang=ja&count=1&include_entities=1",
    "query": "%F0%9F%98%8A",
    "refresh_url": "?since_id=1451233794095128577&q=%F0%9F%98%8A&lang=ja&include_entities=1",
    "count": 1,
    "since_id": 0,
    "since_id_str": "0"
  }
}
```

検索結果はstatusesとsearch_metadataの2つに分かれており，前者にツイートに関するデータが入っている．後者は主に検索の情報についてだが，next_resultsなどの連続して検索する際に便利な値なども返ってくるため非常に有用．

検索用APIの制限は以下の通りになっている．

- **15分間で最大450リクエスト**
- **1回のリクエストで100件**まで取得可能

そのため，１日（1440分）だと最大で43,200回リクエストを送ることが出来る．そして，一度のリクエストで100ツイート取得できるので**1日で約400万ツイートほど取得することが可能**．（15分間以外の制限がなければの話だが，今のところ1日フルで動かしていないのでわからない）

ただ今回の記事ではとりあえずレート制限ギリギリで取得はせずに，とりあえずテスト的に数千件のみデータを取得してクリーニングを行いたいと思う．

# 用意したデータ

Twitter APIを使って取得したデータ．

- データ数：5,000件
- 形式：JSON（statuses部分のみ抽出）
- 検索クエリ：笑顔の絵文字
- 取得日時：2021/10/22 21時頃
- 言語：日本語

形式：

```json
{
  "statuses": [ 
    { "created_at": "Fri Oct 22 09:13:16 +0000 2021",
      "id": 1451476741004746761,
      "id_str": "1451476741004746761",
      "text": "ほげほげ",
       ...........
    },

    .......

    { "created_at": "Fri Oct 22 09:13:17 +0000 2021",
      "id": 1451476741004746762,
      "id_str": "1451476741004746762",
      "text": "ふがふが",
       ...........
      "entities": { 
        "media": {
           ...........
        },
        "urls": [],
         ...........
      }
    }
  ]
}
```

statusesの中に5000件のツイートデータがリストとなって格納されている．各ツイートデータにはIDやテキストなどの基本的な情報と，entitiesの下に画像(media)やURL(urls)，ハッシュタグなどの情報が書かれている．

ただ今回はそもそも欲しいのが文章と感情のラベルなので，画像データやURLなどの情報は内容まで保持しておく必要がなく”有無”が分かればいい．そこでJSONからPandasのDataFrame形式に変換してこの後のふるい分けやクリーニング処理を行っていく．

データ作成：

```python
with open(filename, "r") as f:
    json_data = json.load(f)

tweets = []
for tweet in json_data["statuses"]:
    media = True if "media" in tweet["entities"] else False
    url = True if len(tweet["entities"]["urls"]) != 0 else False
    # 引用RTとリプライは共存できないと思うから，is_retweetがTrueの場合は強制的にis_replyがFalseになる
    # というのも引用RTの場合でもin_reply_to_status_idに値が入ってしまって判別できないため
    is_retweet = True if "retweeted_status" in tweet else False
    if is_retweet:
        is_reply = False
    else:
        is_reply = True if not tweet["in_reply_to_status_id"] else False
    tweets.append([tweet["id"], tweet["text"], media, url, is_retweet, is_reply])

tweet_df = pd.DataFrame(tweets, columns=["id", "text", "media", "url", "is_retweet", "is_reply"])
```

DataFrame形式：

```python
In [75]: tweet_df
Out[75]:
                       id                                               text  media    url  is_retweet  is_reply
0     1451476741457731584  RT @TsuruokaJun: 一日お疲れさまです😀\n\n一日家で休んでました🥲\n\n...   True  False        True     False
1     1451476741004746761  @Kanamaru_JP @_Oke7 こんにちは😃😆　　\n\n神CPありがとうございます...  False  False       False     False
2     1451476740790906886  RT @__nesan: 私のフレンドさんがカスタムマッチを\n開きますっ⸜( ᐛ )⸝✨\...  False  False        True     False
3     1451476740648235008  RT @hogokensakai: 新メンバーのエミーリエちゃん💓\n早速尻尾ブンブン振って...  False  False        True     False
4     1451476740572782599  RT @Job_Chocolat: 10/22 186日目\n\n金曜日だねジョブショコラ🕴...  False  False        True     False
...                   ...                                                ...    ...    ...         ...       ...
4995  1451475289880424454  RT @sanyobussan: ＼🌸沖海5桜ver. 導入記念🌸／\n#沖海桜祭 フォロー...  False  False        True     False
4996  1451475289788190731  地上波でやってくれるのは有難い😊\n録画の準備はOKです🔥 https://t.co/RJI...  False   True       False      True
4997  1451475289461067777                        @Karenn11118728 ありがとうございます😊  False  False       False     False
4998  1451475289410654214  @teponpeanuts @takeo8888 @writer_kazu てーぽんさん、こ...  False  False       False     False
4999  1451475289402343427   @ai_sakura_0613 今日は寒くて天気も悪いから残念だね💦明日は晴れるみたいだから😊🎵  False  False       False     False

[5000 rows x 6 columns]
```

# クリーニング処理
## 画像・URL付きデータの削除
今回は文章とそれに含まれている感情のラベルで構成されたコーパスを作成したいので**「文章のみで理解可能」**なツイートのみを使用したい．画像やURLなどがついているツイートでは文章のみでそのツイートの意味を理解することが難しい（URL元の文章を見るわけにもいかないし）と判断したため除いていく．

```python:画像・URL付きツイートの削除
In [86]: tweet_df = tweet_df[(tweet_df["url"] | tweet_df["media"]) == False]

In [87]: len(tweet_df)
Out[87]: 3858
```

上記ツイートを削除した結果，データ数は5,000件から3,858件になった．

## 引用RT・リプライの削除
こちらも「文章のみで理解可能」なツイートではないと判断したため削除する．（引用RTに関しては一応ツイートの内容を取ってこれるが，構造が複雑になってしまうためとりあえず削除する）

```python:引用RT・リプライの削除とその結果
In [100]: tweet_df = tweet_df[(tweet_df["is_retweet"] | tweet_df["is_reply"]) == False]

In [101]: tweet_df
Out[101]:
                       id                                               text  media    url  is_retweet  is_reply
14    1451476738395967493  @piyopiyokiti46 はじめして！かるはと申します🙇‍♀️タグ反応ありがとうござ...  False  False       False     False
17    1451476737934585856                                          ロモちゃん！ぺっ😊  False  False       False     False
44    1451476730506481675                                         15分ね、おけ😊👌🏻  False  False       False     False
45    1451476728258330638  💖信頼し合え一緒に助けあい活動する同志を作りたいです✨\nペット関係も取り組んでます😀\n経...  False  False       False     False
61    1451476723350999040  @ko_shinohara \nコウくんほんとにごめんなさい🙇‍♀️\nまたインスタフォロ...  False  False       False     False
...                   ...                                                ...    ...    ...         ...       ...
4955  1451475300739477505  今日担当の患者さんへの処方提案よく考えられていて良いんじゃないかと先生からお褒めのお言葉いた...  False  False       False     False
4957  1451475300496273409   観られないけど、ぶたけつ初日おめでとうございますー！\n最後まで怪我なく走り抜けられますように😊  False  False       False     False
4974  1451475296260030464  こんばんは🌃\n本日もご来院頂きありがとうございました😌\n\n明日は16時がご予約承りやす...  False  False       False     False
4975  1451475295131746317  結構、人見知りなんで\nなかなか、みなさんに\nリプ出来ないけど\nRT頑張ります👍😊\n私...  False  False       False     False
4977  1451475294943019009                                          俺の勝ち‼️😊  False  False       False     False

[436 rows x 6 columns]
```

引用RTとリプライを削除した結果，**なんと当初5,000件だったデータが436件まで削減**されてしまった．ただ，ここで残ったデータは画像やURL，元ツイートなどの情報が無いため，**単純な文章（つぶやき）として使うことができる**．ここから絵文字や顔文字，改行コードなどの使いずらいデータを削除すればとりあえず学習に使えるデータになるのではないか．

## 文章自体のクリーニング処理
とりあえず思いつく処理を書いていく．

- 絵文字の削除（感情が含まれるような絵文字は特殊な文字と置き換えるかもしれない）
- 顔文字の削除（顔文字のほうは判定ちょっと難しいかも）
- 改行コードの削除（単純に削除するか，何かに置き換えるかは未定）
- 数字の全角半角の統一
- 文章が短すぎる・長すぎるものは除く（モデルの入力によるので現時点では決められない）

ここら辺の処理もやってしまおうかなと思ったけど，形態素解析ツールやモデルによるものがあったりするのでとりあえず保留．