BsBsこうしょう

これは考えたことではなく思ったことです。

2021年にピアノを弾いた合計時間を測定した話

あけましておめでとうございます。今年もまあまあ頑張ります。Badlyluckyです。

色々あって遅くなってしまいましたが、去年の総括として「去年1年間に弾いたピアノの合計時間」を測定したのでその方法について書きます。

1. はじめに。測定を始める前に守ること

僕のツイートを見ると、以下のようなツイートが散見されることに気づくと思います。

ピアノを弾いたおおよその時間と、それに関するコメントをセットで載せたツイートです。この種類のツイートは必ず以下の構文で成り立っています。

ピアノ?([数字]時間)?([数字]分)
(コメント)

これが最初の大事な点なのですが、 データを取るときに最初にフォーマットを定め、それを厳守するようにデータを取ります。 これによって後からプログラムが収集しやすい形にデータを加工しやすくなります。別に加工しやすい最適な形にしなくても、一定のフォーマットにさえ基づいていれば(頑張れば)プログラムが扱いやすい形にまで復元可能です。

もっとも、最初から専用のカウンターを導入していればプログラムを書く必要もないのですが、Twitterでつぶやくのは楽なのでこの形になりました。

2. 1年間の全ツイートを取得する

作業を行うに先立って、Twitterのサービスを用いて自分の過去1年間のツイートを全て取得します。当初Twitter APIを用いてこの作業を行うことを計画していましたが、 Twitter APIは無料では過去7日間、有料でも過去30日間のツイートしか取得することができません でした。そのため、自分の過去の全ツイートを取得するTwitterのサービスを使ってこの作業を行いました。結論から言うと、ストレージの問題を考慮しなくて良いならば、自分のツイートを処理する際にはこのサービスを用いると良いです。

help.twitter.com

このサービスを使ってツイートを取得し、ダウンロードしたzipファイルを展開すると以下のようになります。

.
├── Your archive.html
├── assets
│   ├── fonts
│   ├── images
│   └── js
└── data
    ├── README.txt
    ├── account-creation-ip.js
    ├── account-suspension.js
    ├── account-timezone.js
    ├── account.js
     :
    ├── tweet.js
    ├── tweet_media
    ├── user-link-clicks.js
    └── verified.js

13 directories, 60 files

自分の過去のツイートに関する情報だけ欲しかったのですが、自分が過去にアップロードした写真、動画、プロフィール画像、ダイレクトメッセージ、ブロックしたユーザーなど、Twitterの自分のアカウントに関する情報がおおよそ全て含まれていました。このデータを使ってTwitterの自分のアカウントを完全に構成することができそうです。実際 Your archive.html を開くと、

f:id:A8pf:20220201175505p:plain
Your archive.html

ローカル上に自分のタイムラインが構成されています。

これらから必要な情報だけを取り出します。とはいえ、ローカルにダウンロードした自分の過去のツイートから、特定のツイートだけを抽出できるAPIはこの中には含まれていないようです。というわけで、手動で抽出します。

今回必要なのは自分のツイートのうち、ツイート内容のテキストのみです。ツイート内容のテキストは data/tweet.js にあるので参照します。

window.YTD.tweet.part0 = [
  {
    "tweet" : {
      "retweeted" : false,
      "source" : "<a href=\"http://twitter.com/download/android\" rel=\"nofollow\">Twitter for Android</a>",
      "entities" : {
        "hashtags" : [ ],
        "symbols" : [ ],
        "user_mentions" : [ ],
        "urls" : [ ]
      },
      "display_text_range" : [
        "0",
        "29"
      ],
      "favorite_count" : "0",
      "id_str" : "1074003298615611392",
      "truncated" : false,
      "retweet_count" : "0",
      "id" : "1074003298615611392",
      "created_at" : "Sat Dec 15 18:08:35 +0000 2018",
      "favorited" : false,
      "full_text" : "いくつかフォロー飛ばしました\nよろしくお願いいたしますです",
      "lang" : "ja"
    }
  },
// 中略
]

ひとつのツイートはツイート時間などのフィールドを持つオブジェクトとして管理されているようです。これをPythonで読み込みたいのですが……。

Pythonにはjsonライブラリがあり、jsonオブジェクトをPythonのオブジェクトに変換することができます。しかし、それをこのファイル全体に行うには先頭の行が邪魔です。しかし、JavaScriptではよく見られる書き方なのですが、このファイルは巨大なjsonオブジェクトをひとつの変数に格納するためのものなので、 最初の行を [ に置換することでjsonオブジェクトとして読むことができる ようになります。

この部分のコードは以下のようになります。

allmytweets = []
with open('./data/tweet.js', mode = 'r') as f:
    lines = f.readlines()
    lines[0] = '['
    allmytweets = json.loads('\n'.join(lines))

3. プログラムを書く

次に読み込んだツイート群を、適切に処理します。最初に述べたフォーマットに従うならば、このようなツイートが抽出できれば良いはずです。

  • 投稿日時が2021年(JST)の間である
  • ツイートのテキストが'ピアノ'で始まる

実際には練習期間の関係で、2022年1月8日までを集計対象としました。ツイートのテキストについては、実際にピアノを弾いた時間を取り出す処理と重複する部分があるので後述するとして、まずは投稿日時について考えます。

3.1. 投稿日時で絞り込みを行う

Twitterでは tweet['tweet']['created_at'] という位置に、以下のように投稿日時を保管しています。

Wed Dec 12 14:47:51 +0000 2018

曜日 月 日 時:分:秒 タイムゾーン 年 という順番のようです。このままではPythonが処理できる形になりません。Twitter独自の形式っぽいので手動でパースしてdatetimeオブジェクトに変換します。

# convert 'created_at' in twitter to date object
def gettweettime(tweet):
    rawdata = tweet['tweet']['created_at'].split(' ')
    year = int(rawdata[5])
    months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
    month = months.index(rawdata[1]) + 1
    day = int(rawdata[2])
    instant = rawdata[3].split(':')
    hour = int(instant[0])
    minute = int(instant[1])
    second = int(instant[2])
    date = datetime.datetime(year, month, day, hour, minute, second, tzinfo=datetime.timezone.utc)
    return date.astimezone(datetime.timezone(datetime.timedelta(hours=9)))

月のように、番号ごとに固有名詞がついているものの番号を取り出したい時には、全てリストに格納して list.index() を使うのが便利です。また、過去のツイートの生データではタイムゾーンUTCであることにも注意します。

このようにしてツイートをした日時のデータをうまく読み込むことに成功しました。あとはこの中から2021年のものを抽出するだけですが、 Pythonのdatetimeオブジェクトは比較演算子による比較をサポートしています。 便利〜

日時で抽出したあとはツイートのテキストのみが問題となるので、抽出するのはテキストだけで良いでしょう。

3.2. テキストで絞り込みを行う

もう一度自分のピアノについてのツイートのフォーマットを観察します。

ピアノ?([数字]時間)?([数字]分)
(コメント)

今必要なのはピアノを演奏していた時間の情報だけなので、1行目だけに注目すれば良さそうです。また1行目には必ず 'ピアノ' が含まれています。これも利用できそうです。今回は 'ピアノ' で始まり '時間' または '分' で終わる行全てについて処理を行うことにしました。

prefix = '^ピアノ.*'
suffix = '.*((分$)|(時間$))'
for text in targettweets:
    lines = text.split('\n')
    for line in lines:
        line.strip()
        if re.match(prefix, line) and re.match(suffix, line):
            screentime += calcscreentime(line)

正規表現は便利ですが、毎回ググります。

そして実際にピアノを弾いていた時間を計測するのですが、この部分だけフォーマットに穴がありました。「ピアノX時間Y分」「ピアノX時間」「ピアノX分」の3通りに解釈できる曖昧さがあり、かつXとYが記載されていない状態でも区別する必要があるため、綺麗な形でXとYを分離することは難しそうです。そのため手動でパーサを書きます。

def calcscreentime(line):
    ret = [0, 0]
    substr = line[3:]
    i = 0
    tmp = ''
    while i < len(substr):
        if substr[i] == '時':
            ret[0] = int(tmp)
            tmp = ''
            i += 2
        elif substr[i] == '分':
            ret[1] = int(tmp)
            tmp = ''
            i += 1
        else:
            tmp += substr[i]
            i += 1
    return ret[0]*60 + ret[1]

これでピアノを演奏したツイートを取得して、実際に合計時間を求めることができそうです。

4. 結果と反省

では早速実行してみましょう。

> python3 pianocalc.py 
Traceback (most recent call last):
  # 中略
ValueError: invalid literal for int() with base 10: '弾かなと思ってクネクネしていたらこんな'

あれま。エラーで落ちてしまいました。

どうやらこのツイートを誤検出していたようです。確かに1行目かつ'ピアノ'から始まり'時間'で終わる行……。

誤検出した場合は0分加算すれば良いだけなので、単純なエラーハンドリングで解決できます。

            try:
                ret[0] = int(tmp)
            except ValueError:
                ret = (0, 0)
                break

では気を取り直して、もう一度実行しましょう。

> python3 pianocalc.py   
Your piano screentime ... 391時間10分

.

..

...

....

非常にコメントしづらいリアルなタイムが出てしまい、困惑している。。。

500時間を超えることを密かな目標にしていたのですが、達成は叶わずということになりました。原因はいくつかありますが最大のものは、2度のワクチン接種によって合計1ヶ月ほどピアノを弾けない期間があったためでしょう。ワクチン接種がなければ50時間くらいは上乗せできたか。また、2度目のワクチン接種以降、非常に疲れる上時間がかかる基礎練習を大幅に短縮して練習をしていたためこの影響も大きいです。基礎練習は再開したいのですが忙しく現在でも手をつけられていません。

ゲームをする人たち、競技プログラミングをする人たち、夢中になって何かをする人たち全てに共通するのですが、合計500時間というのはサラリと飛び越えてしまうものだという認識があります。今回僕はかなり意識してピアノに取り組んだのですが、それでも400時間に満たなかったということは、僕は何をするにしても年300時間〜400時間で頭打ちになってしまうということでしょう。今後何をする際にもこの枷は大きな課題となるのではないかなと思います。ノロマな亀として頑張るしかない。

5. 終わりに

僕は習慣的に続けることが何よりも苦手なのですが、(機械が勝手にやってくれるのではなく)自分でフォーマットを考えて、データを収集して、プログラミングでその集計を行うという一連の作業はとても楽しんで取り組むことができました。機械や誰かにやらされている感のないことが、最大の要因だと思います。

ブログの公開時期の問題で2022年はすでに12分の1以上終わってしまいましたが、Twitterやその他SNSライフログ等を使って、何かを集計するということに皆さんもチャレンジしてみてはいかがでしょうか?

ちなみに今年はピアノの練習時間を集計しませんし、すでに記録漏れが複数あるのでできません。しかし、大阪大学のまちかね祭に、同じ曲目でぜひ出席したいと考えています。自分のピアノを外に出すのは初めてで、大舞台です。緊張で全部崩壊しないようにいっそう努力します。

今回作成したプログラムをGitHubで公開しています。

github.com