はじめに
Lineには、トーク履歴をエクスポートする機能が付いています。これをPythonを使って解析し、合計メッセージ数、それぞれのメッセージ数、合計文字数、それぞれのメッセージ数、Line電話の時間の合計をそれぞれの月について算出する方法です。筆者は、電子機器の言語を英語に設定しているため、日本語を使用されている方は、履歴のファイル名や内容が日本語表記になっていることが予想されます。適宜読み替えてください。
Lineからトーク履歴をエクスポートする
これは、PCでもスマホでもできますが、PCとスマホでは、エクスポートされたトーク履歴のフォーマットが微妙に違うことやPCでは、エクスポートできるトーク履歴が会話全体の一部でしかないため、今回はスマホでエクスポートし、PCに送りました。
トーク履歴をCSVに変換する
エクスポートされたファイルは、[LINE] Chat with [friend name].txt
となっていました。フォーマットは以下のようでした。うーん、このフォーマットは使いにくい気が…
1~略~
22021/03/03 Wed
310:15 frinds account name 次の電話は明日の18時半がいいです。
411:37 my account name [Sticker]
5~略~
日時、タブ、アカウント名、タブ、メッセージとなっています。
Lineには様々な機能が付いています。テキストメッセージ、写真、動画、スタンプ、電話、アルバム、メッセージの取り消しなどのシステムメッセージ…。これらは履歴の中では、特定の形で表現されているので、それぞれの履歴がどの種類のメッセージであるかを正規表現を使って分け、csvに変換していきます。csvの形式は年,月,日,時,分,送信者,内容,flag
とします。flag
については後述します。
履歴ファイルを読み込む
履歴ファイルを読み込みます。
1file_path = "[LINE] Chat with [freind name].txt"
2with open(file_path, 'r', encoding="utf-8") as f:
3 log_text = f.read()
正規表現を設定する
それぞれのメッセージ種別について正規表現を設定していきます。
1# 日時データ
2date_pattern = r"20\d{2}/\d{2}/\d{2} (Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
3# テキストメッセージデータ
4message_pattern = r"\d{2}:\d{2}\t.*\t.*"
5# 写真のデータ
6photo_pattern = r"\d{2}:\d{2}\t.*\t\[Photo]"
7# スタンプのデータ
8sticker_pattern = r"\d{2}:\d{2}\t.*\t\[Sticker]"
9# ビデオデータ
10video_pattern = r"\d{2}:\d{2}\t.*\t\[Video]"
11# ファイルのデータ
12file_pattern = r"\d{2}:\d{2}\t.*\t\[File]"
13# アルバム作成、名前変更、削除のデータ
14album_build_pattern = r"\d{2}:\d{2}\t.*\t\[Albums].*"
15album_rename_pattern = r"\d{2}:\d{2}\t.* changed the name of the album.*"
16album_delete_pattern = r"\d{2}:\d{2}\t.* delete the album.*"
17# 電話のデータ関係
18missed_call_pattern = r"\d{2}:\d{2}\t.*\t☎ Missed call"
19canceled_call_pattern = r"\d{2}:\d{2}\t.*\t☎ Canceled call"
20no_answer_call_pattern = r"\d{2}:\d{2}\t.*\t☎ No answer"
21call_pattern = r"\d{2}:\d{2}\t.*\t☎ Call time (\d{1,2}:\d{2}|\d{1,2}:\d{2}:\d{2})"
22# システムのデータ、送信取り消し
23sys_unsent_pattern = r"\d{2}:\d{2}\t.* unsent a message."
データを正規表現に沿って解析する
re.match()
で正規表現に当てはまっているかを確認し、当てはまっていたら、タブでデータを分割したのち、Data型にデータを格納し、リストlogに追加していきます。データ型のflag
という変数はデータの種類を示しており、以下のように設定しています。
1# flag
2 # 0 : talk meassge
3 # 10 : call
4 # 11 : missed call
5 # 12 : canceled call
6 # 13 : no answer call
7 # 2 : photo
8 # 3 : video
9 # 4 : sticker
10 # 50 : system message unsent
11 # 60 : file
12 # 70 : create and add album
13 # 71 : changed the name of the album
14 # 72 : deleted the album
1class Data():
2 def __init__(self, year, month, day, hour, minute, person, payload, flag):
3 self.year = year
4 self.month = month
5 self.day = day
6 self.hour = hour
7 self.minute = minute
8 self.person = person
9 self.payload = payload
10 self.flag = flag
11
12date_ = datetime.datetime.now()
13logs = []
14
15# 履歴の最初の2行はエクスポートした時間と空白の行なのでとばし、3行目から解析する
16for i, log in enumerate(log_text.splitlines()[3:]):
17 #print(f"{log} : ", end='')
18 if log == '':
19 #print("no data")
20 continue
21 date_stamp = ""
22 if re.match(date_pattern, log):
23 #print("day data")
24 date_stamp = log.replace('/', ',').replace(' ', ',')[0:10]
25 date_ = datetime.datetime.strptime(date_stamp, '%Y,%m,%d')
26 elif re.match(photo_pattern, log):
27 #print("photo data")
28 splited_log = re.split('\t', log)
29 logs.append(Data(date_.year, date_.month, date_.day,
30 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 2))
31 elif re.match(video_pattern, log):
32 #print("Video data")
33 splited_log = re.split('\t', log)
34 logs.append(Data(date_.year, date_.month, date_.day,
35 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 3))
36 # ~略~
csvを保存する
以下のコードでcsvファイルを保存します。line.csv
というファイルに保存されます。
1with open('line.csv', 'w', encoding="utf-8", newline="") as f:
2 for content in logs:
3 writer = csv.writer(f)
4 writer.writerow([str(content.year), str(content.month), str(content.day), str(content.hour),str(content.minute), str(content.person), str(content.payload), str(content.flag)])
トーク履歴をCSVに変換するコードの全体
ここまでのコードの全体です
1# -*- coding: utf-8 -*-
2
3import re
4import csv
5import datetime
6import os
7import sys
8
9
10class Data():
11 # flag
12 # 0 : talk meassge
13 # 10 : call
14 # 11 : missed call
15 # 12 : canceled call
16 # 13 : no answer call
17 # 2 : photo
18 # 3 : video
19 # 4 : sticker
20 # 50 : system message unsent
21 # 60 : file
22 # 70 : create and add album
23 # 71 : changed the name of the album
24 # 72 : deleted the album
25 def __init__(self, year, month, day, hour, minute, person, payload, flag):
26 self.year = year
27 self.month = month
28 self.day = day
29 self.hour = hour
30 self.minute = minute
31 self.person = person
32 self.payload = payload
33 self.flag = flag
34
35
36# disable #print
37# sys.stdout = open(os.devnull, 'w', encoding="utf-8")
38
39file_path = "[LINE] Chat with friend.txt"
40
41date_ = datetime.datetime.now()
42logs = []
43
44# open file and load data
45with open(file_path, 'r', encoding="utf-8") as f:
46 log_text = f.read()
47
48date_pattern = r"20\d{2}/\d{2}/\d{2} (Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
49message_pattern = r"\d{2}:\d{2}\t.*\t.*"
50photo_pattern = r"\d{2}:\d{2}\t.*\t\[Photo]"
51sticker_pattern = r"\d{2}:\d{2}\t.*\t\[Sticker]"
52video_pattern = r"\d{2}:\d{2}\t.*\t\[Video]"
53file_pattern = r"\d{2}:\d{2}\t.*\t\[File]"
54album_build_pattern = r"\d{2}:\d{2}\t.*\t\[Albums].*"
55album_rename_pattern = r"\d{2}:\d{2}\t.* changed the name of the album.*"
56album_delete_pattern = r"\d{2}:\d{2}\t.* delete the album.*"
57missed_call_pattern = r"\d{2}:\d{2}\t.*\t☎ Missed call"
58canceled_call_pattern = r"\d{2}:\d{2}\t.*\t☎ Canceled call"
59no_answer_call_pattern = r"\d{2}:\d{2}\t.*\t☎ No answer"
60call_pattern = r"\d{2}:\d{2}\t.*\t☎ Call time (\d{1,2}:\d{2}|\d{1,2}:\d{2}:\d{2})"
61sys_unsent_pattern = r"\d{2}:\d{2}\t.* unsent a message."
62
63for i, log in enumerate(log_text.splitlines()[3:]):
64 #print(f"{log} : ", end='')
65 if log == '':
66 #print("no data")
67 continue
68 date_stamp = ""
69 if re.match(date_pattern, log):
70 #print("day data")
71 date_stamp = log.replace('/', ',').replace(' ', ',')[0:10]
72 date_ = datetime.datetime.strptime(date_stamp, '%Y,%m,%d')
73 elif re.match(photo_pattern, log):
74 #print("photo data")
75 splited_log = re.split('\t', log)
76 logs.append(Data(date_.year, date_.month, date_.day,
77 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 2))
78 elif re.match(video_pattern, log):
79 #print("Video data")
80 splited_log = re.split('\t', log)
81 logs.append(Data(date_.year, date_.month, date_.day,
82 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 3))
83 elif re.match(sticker_pattern, log):
84 #print("Sticker data")
85 splited_log = re.split('\t', log)
86 logs.append(Data(date_.year, date_.month, date_.day,
87 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 4))
88 elif re.match(call_pattern, log):
89 #print("call data")
90 splited_log = re.split('\t', log)
91 time_data = splited_log[2][12:]
92 time_data = re.split(':', time_data)
93 time_length = 0
94 for i in range(len(time_data)):
95 time_length += int(time_data[len(time_data) - i - 1]) * (60 ** i)
96 # print(time_length)
97 logs.append(Data(date_.year, date_.month, date_.day,
98 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], time_length, 10))
99 elif re.match(missed_call_pattern, log):
100 #print("Missed call data")
101 splited_log = re.split('\t', log)
102 logs.append(Data(date_.year, date_.month, date_.day,
103 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 11))
104 elif re.match(canceled_call_pattern, log):
105 #print("Canceled call data")
106 splited_log = re.split('\t', log)
107 logs.append(Data(date_.year, date_.month, date_.day,
108 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 12))
109 elif re.match(no_answer_call_pattern, log):
110 #print("no answer call data")
111 splited_log = re.split('\t', log)
112 logs.append(Data(date_.year, date_.month, date_.day,
113 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], "", 13))
114 elif re.match(sys_unsent_pattern, log):
115 #print("sys unsent data")
116 splited_log = re.split('\t', log)
117 logs.append(Data(date_.year, date_.month, date_.day,
118 splited_log[0][0:2], splited_log[0][3:5], "", "", 50))
119 elif re.match(file_pattern, log):
120 #print("file data")
121 splited_log = re.split('\t', log)
122 logs.append(Data(date_.year, date_.month, date_.day,
123 splited_log[0][0:2], splited_log[0][3:5], "", "", 60))
124 elif re.match(album_build_pattern, log):
125 #print("create album data")
126 splited_log = re.split('\t', log)
127 logs.append(Data(date_.year, date_.month, date_.day,
128 splited_log[0][0:2], splited_log[0][3:5], "", "", 70))
129 elif re.match(album_rename_pattern, log):
130 #print("rename album data")
131 splited_log = re.split('\t', log)
132 logs.append(Data(date_.year, date_.month, date_.day,
133 splited_log[0][0:2], splited_log[0][3:5], "", "",71))
134 elif re.match(album_delete_pattern, log):
135 #print("delete album data")
136 splited_log = re.split('\t', log)
137 logs.append(Data(date_.year, date_.month, date_.day,
138 splited_log[0][0:2], splited_log[0][3:5], "", "", 72))
139 elif re.match(message_pattern, log):
140 #print("message data")
141 splited_log = re.split('\t', log)
142 logs.append(Data(date_.year, date_.month, date_.day,
143 splited_log[0][0:2], splited_log[0][3:5], splited_log[1], splited_log[2], 0))
144 elif (len(re.split('\t', log)) == 1):
145 splited_log = re.split('\t', log)
146 #print("returned data")
147 logs[-1].payload += log
148 else:
149 pass
150 #print("\nNo classified data\n")
151
152with open('line.csv', 'w', encoding="utf-8", newline="") as f:
153 for content in logs:
154 writer = csv.writer(f)
155 writer.writerow([str(content.year), str(content.month), str(content.day), str(content.hour),str(content.minute), str(content.person), str(content.payload), str(content.flag)])
156
157print("Success🎉")
CSVを解析するし、グラフを生成する
CSVの解析にはpandas
、グラフの生成にはmatplotlib
をpandas
のラッパーを通して利用してます。ラッパーなので生成されるグラフはmatplotlib
そのものです。
CSVを読み込む
CSVをpandas
を利用して読み込みます
1file_path = "line.csv"
2df = pd.read_csv(file_path, names=('year', 'month', 'day',
3 'hour', 'minute', 'person', 'payloads', 'flag'), encoding="UTF-8")
全体の月別メッセージ数
pandas
のgroupby
機能によって月ごとのメッセージ数を数えます。これをグラフにします。ほかのデータを解析する場合も基本は同様です。これを少し変えることで、曜日別や時間別なども簡単に作ることができそうです。
1month_message = df[["year", "month", "flag"]
2 ].groupby(['year', 'month']).count()
3
4month_message.plot(y='flag', kind='bar', label="count", figsize=figsize)
5plt.ylabel("message count")
6plt.legend()
7plt.ylim(0,)
8plt.title('message')
9plt.savefig('message_count.png')
人ごとの月別メッセージ数
pandas
では、クロスタブを利用することで、簡単にクロス集計分析を行うことができます
1person_month_message = pd.crosstab([df['year'], df["month"]], df['person'])
2person_month_message.plot(kind='line', figsize=figsize)
3plt.title("message count by person")
4plt.ylabel("count")
5plt.ylim(0,)
6plt.savefig('message_count_by_person.png')
月別電話時間
電話のflag
は10なのでまずは、csvからそのデータを取り出します。さらに電話時間はCSVに秒で記録されておりそのままでは、値が大きく理解しずらいため、3600でわり、時間にしました。
1call_time = df[df['flag'] == 10]
2call_time = call_time[["year", "month", "payloads"]]
3call_time = call_time.astype('int64').groupby(['year', 'month']).sum() / 3600
4call_time.plot(y='payloads', kind='bar', label='time', figsize=figsize)
5plt.ylabel("time(hours)")
6plt.legend()
7plt.ylim(0,)
8plt.title('Call Time')
9plt.savefig('call_time.png')
月別メッセージの文字数の合計
charというcolumnを作成し、そこにメッセージの文字数を入れたあとメッセージ数を数えた時と同じように、集計を行いました。
1# メッセージを取り出す
2char_count_data = df[df['flag'] == 0]
3# charというcolumnを作成し、そこにメッセージの文字数を入れる
4char_count_data["char"] = char_count_data["payloads"].apply(lambda x: len(x))
5char_count = char_count_data[["year", "month", "char"]]
6char_count = char_count.astype('int64').groupby(['year', 'month']).sum()
7char_count.plot(y='char', kind='bar', label='char', figsize=figsize)
8plt.ylabel("char")
9plt.legend()
10plt.ylim(0,)
11plt.title('char data')
12plt.savefig('char_count.png')
人べつ月ごとの文字数の合計
月別メッセージの文字数の合計で作成したデータフレームを再利用し人べつのデータを作成します。
1char_count_by_person = char_count_data[["year", "month", "person", "char"]]
2char_count_by_person["char"].astype('int64')
3char_count_by_person = pd.pivot_table(
4 char_count_by_person,
5 values="char",
6 index=["year", "month"],
7 columns="person",
8 aggfunc="sum"
9)
10char_count_by_person.plot(kind='line', figsize=figsize)
11plt.ylabel("char")
12plt.title('Char by person')
13plt.ylim(0,)
14plt.legend()
15plt.savefig('char_count_by_person.png')
解析するコードの全体
1# -*- coding: utf-8 -*-
2
3import pandas as pd
4import matplotlib.pyplot as plt
5
6file_path = "line.csv"
7
8figsize = (12, 8)
9df = pd.read_csv(file_path, names=('year', 'month', 'day',
10 'hour', 'minute', 'person', 'payloads', 'flag'), encoding="UTF-8")
11month_message = df[["year", "month", "flag"]
12 ].groupby(['year', 'month']).count()
13
14month_message.plot(y='flag', kind='bar', label="count", figsize=figsize)
15plt.ylabel("message count")
16plt.legend()
17plt.ylim(0,)
18plt.title('message')
19plt.savefig('message_count.png')
20
21person_month_message = pd.crosstab([df['year'], df["month"]], df['person'])
22person_month_message.plot(kind='line', figsize=figsize)
23plt.title("message count by person")
24plt.ylabel("count")
25plt.ylim(0,)
26plt.savefig('message_count_by_person.png')
27
28call_time = df[df['flag'] == 10]
29call_time = call_time[["year", "month", "payloads"]]
30newdf = pd.DataFrame([[2018, 3, 0], [2018, 10, 0], [2018, 12, 0], [2019, 1, 0], [2019, 3, 0], [2019, 5, 0], [2020, 10, 0], [2020, 11, 0], [2021, 2, 0], ],
31 columns=["year", "month", "payloads"])
32call_time.append(newdf, ignore_index=True)
33call_time = call_time.append(newdf)
34call_time = call_time.astype('int64').groupby(['year', 'month']).sum() / 3600
35call_time.plot(y='payloads', kind='bar', label='time', figsize=figsize)
36plt.ylabel("time(hours)")
37plt.legend()
38plt.ylim(0,)
39plt.title('Call Time')
40plt.savefig('call_time.png')
41
42char_count_data = df[df['flag'] == 0]
43char_count_data["char"] = char_count_data["payloads"].apply(lambda x: len(x))
44char_count = char_count_data[["year", "month", "char"]]
45char_count = char_count.astype('int64').groupby(['year', 'month']).sum()
46char_count.plot(y='char', kind='bar', label='char', figsize=figsize)
47plt.ylabel("char")
48plt.legend()
49plt.ylim(0,)
50plt.title('char data')
51plt.savefig('char_count.png')
52
53char_count_by_person = char_count_data[["year", "month", "person", "char"]]
54char_count_by_person["char"].astype('int64')
55char_count_by_person = pd.pivot_table(
56 char_count_by_person,
57 values="char",
58 index=["year", "month"],
59 columns="person",
60 aggfunc="sum"
61)
62char_count_by_person.plot(kind='line', figsize=figsize)
63plt.ylabel("char")
64plt.title('Char by person')
65plt.ylim(0,)
66plt.legend()
67plt.savefig('char_count_by_person.png')
以上でグラフを作成することができました。一度作ってしまえば実行するだけなので、定期的に実行して変化を試したいと思います。
Reference
以下のページを参考にしました