概要
Pythonを用いて,LINEアプリから出力できる.txt
形式の履歴を検索するプログラムを作成しました.日付による検索,キーワードによる検索,を実装しました.iOS17.1.1で,日本語のLINEの履歴を想定しています.実装の解説を書きます.
はじめに
LINEの履歴が消えてしまった際に,相手に.txt
を送ってもらえば,過去の会話を見ることができます.しかし,その履歴が長い場合に,手動で検索するのは大変です.そこで,Pythonを用いて,履歴を検索するプログラムを作成しました.
実装前の準備
Pythonの確認
% python3 --version
Python 3.11.6
履歴ファイルの形式の確認
まず,LINEアプリから出力できる.txt
形式の履歴を確認します.LINEアプリでトークを開き、横三本線のメニュー→「設定」→「トーク履歴を送信」を押すと、テキストファイルを生成できます。
[LINE] グループ名のトーク履歴
保存日時:2023/02/13 00:00
2022/03/15(火)
01:01 Aさん あいさつ
12:34 Bさん "おはよう
こんにちは
こんばんは"
2022/06/02(金)
12:00 Cさん あ
12:00 Dさんがメッセージの送信を取り消しました
最初の2行には,トーク名と保存日時が記載されています.次に改行があり,その後に日付ごとの会話が記載されています.日付は昇順に並んでいます.一番上の行は,一対一のトークの場合には,「[LINE]__名前__とのトーク履歴
」,グループトークの場合には,「[LINE]__グループ名__のトーク履歴
」となります.「のトーク履歴」が共通しています.
日付表示の後には,時刻,名前,会話内容がタブ区切りで記載されています.発言が複数行に渡る場合には,ダブルクォーテーションで囲まれています.送信取り消しや退会などのシステムメッセージは,名前の部分がありません.すなわち,時刻の後にタブが2つあり,メッセージが記載されています.
実装
Historyクラス
属性と初期化処理
核となるHistory
クラスを作成します.
class History:
history_data: list[str]
def __init__(self, data: str) -> None:
lines = data.splitlines()
if "のトーク履歴" in lines[0]:
self.history_data = lines[3:]
else:
self.history_data = lines
History
クラスは,history_data
という属性を持ちます.これは,list[str]
型です.これは履歴を行ごとに分割したものです.
このクラスの初期化を行う__init__
メソッドを定義します.このクラスのインスタンスを作成すると,このメソッドが呼び出されます.
str
型の引数data
を受け取り,これを.splitlines()
で行ごとに分割します.分割したデータに,「のトーク履歴」が含まれている場合には,その行より上の3行は不要なので,self.history_data
には,その3行より下のデータを代入します.なんらかの事情で,「のトーク履歴」が含まれていない場合には,そのままself.history_data
に代入します.
日付による検索
ソースコードの一番上に,import文と定数を追記します.
import re
from datetime import datetime
DATE_PATTERN: str = r'^\d{4}/\d{2}/\d{2}\(.+\)$'
YMD_PATTERN: str = "%Y/%m/%d"
re
は,正規表現を扱うためのモジュールです.datetime
モジュールは,日付を扱うためのモジュールです.
DATE_PATTERN
は,日付を表す正規表現です.YMD_PATTERN
は,日付をdatetime
型に変換するためのフォーマットです.のちに使います.
日付による検索を行うsearch_by_date
メソッドをHistory
クラスに追記します.すなわち,インデント(字下げ)が必要です.
def search_by_date(self, date: datetime) -> str:
target_date = date
count_start: int = -1
count_end: int = -1
collect_flag: bool = False
output: str = ""
for i, line in enumerate(self.history_data):
if not re.match(DATE_PATTERN, line):
continue
current_date = datetime.strptime(line[:10], YMD_PATTERN)
if current_date == target_date:
count_start = i
collect_flag = True
elif collect_flag and target_date < current_date:
count_end = i-1
break
else:
count_end = len(self.history_data)
if count_start == -1:
output = "There is no history of this date.\n"
else:
output += "\n".join(self.history_data[count_start:count_end])
output += f"\n\n{count_end - count_start}行\n"
return output
日付をdatetime
型の引数date
で受け取ります.self
は,このクラスのインスタンスを指し,呼び出す際には,引数を指定する必要はありません.
target_date
は,検索対象の日付を表します.dateの名前を変えただけです.
count_start
は,検索対象の日付の最初の行のインデックスを表します.
count_end
は,検索対象の日付の最後の行のインデックスを表します.
collect_flag
は,検索対象の日付の行を集めているかどうかを表します.
output
は,検索結果を表します.
次に,for
文でself.history_data
を走査します.1
このfor文の中で,count_start
とcount_end
を決定します.すなわち,検索したい範囲の最初と最後の行のインデックスを求めます.
enumerate
関数は,インデックスと行の文字列を同時に取得できます.それぞれをi
とline
とします.
日付の場所だけが分かればいいので,if
文で,日付の行かどうかを判定します.それでない場合は,continue
で次のループに移ります.
日付の行の場合には,datetime
型に変換して,これをcurrent_date
とします.
current_date
とtarget_date
が一致する場合には,この行が検索対象の日付の最初の行であるということになります.そのため,count_start
にi
を代入し,collect_flag
をTrue
にします.
collect_flag
がTrue
の場合,すなわち検索対象の日付の最初の行に到達しているかつtarget_date < current_date
2である場合には,この行が検索対象の日付の一つ後の日付の行であるということになります.そのため,count_end
にi-1
を代入し,break
でループを抜けます.
for
と同じ字下げの位置にあるelse
について,
python else: count_end = len(self.history_data)
これは,for
に対してのもので,if-elseとは異なるので,注意してください.これは,for
がbreak
で抜けずに終了した場合に実行され,break
が使用された場合には実行されません.すなわち,検索対象の日付の次の日付が見つからないまま,ファイルの最後まで到達した場合に,count_end
にlen(self.history_data)
を代入するという処理です.
検索にヒットしなかった場合には,count_start
が-1
のままです.その場合には,output
に「検索対象の日付の履歴はありません」というメッセージを代入します.3
ヒットした場合には,count_start
とcount_end
の間の行,すなわち検索対象の日付の履歴をoutput
に代入します.その後,行数をoutput
に追加します.
最後にoutput
を返します.
次のキーワードによる検索を実装する前に,先に実行にて,このメソッドを実行することもできます.
キーワードによる検索
キーワードによる検索を行うsearch_by_keyword
メソッドをHistory
クラスに追記します.すなわち,インデント(字下げ)が必要です.
def search_by_keyword(self, keyword: str) -> str:
LOWER_LIMIT = 1
if len(keyword) < LOWER_LIMIT:
return "Please enter more than one character."
count = 0
output = ''
max_date = datetime.min
for line in self.history_data:
if re.match(DATE_PATTERN, line):
date = datetime.strptime(line[:10], YMD_PATTERN)
if date >= max_date:
max_date = date
else:
if not keyword in line:
continue
count += 1
if re.match(r'^\d{2}:\d{2}.*', line):
line = line[6:]
if len(line) >= 61:
line = line[:60] + '…'
output += str(max_date)[:11].replace('-', '/') + " " + line + '\n'
if output == '':
output = 'Not found.'
return f"{count}件\n{output}"
検索したいキーワードをstr
型の引数keyword
で受け取ります.
LOWER_LIMIT
はキーワードの最低文字数を表します.例えば,一文字検索は大量にヒットして処理が遅いので,これを禁止したい場合には,LOWER_LIMIT = 2
とします.キーワードの文字数がLOWER_LIMIT
より小さい場合には,「1文字以上入力してください」というメッセージを返します.
count
はヒットした行数を表します.
output
は検索結果を表します.
date
は現在参照している日付を表します.
max_date
は参照した中で一番大きい日付を表します.
for
文でself.history_data
を走査します.
このfor文の中で,検索結果をoutput
に追加していき,
line
は現在参照している行の文字列です.
日付の行の場合に,その日付がmax_date
より大きい場合には,date
とmax_date
を更新します.
日付の行でない場合で,keyword
がline
に含まれていない場合には,次のループに移ります.
keyword
がline
に含まれている場合には,count
を1増やします.line
の先頭の時刻を削除し,line
の文字数が60文字以上の場合には,61文字目以降を削除します.その後,output
にline
を追加します.
最終的に,output
が空の場合には,「見つかりませんでした」というメッセージを返します.
そうでない場合には,count
をoutput
の先頭に追加して,output
を返します.
実行
実装で作成したHistory
クラスを実際に動かします.
同じファイルの最下部4に,以下を追記します."history.txt"
の部分は,検索したい履歴のファイル名に変更してください.
2つめのprint
文のコメントを外す(#
を消す)と,キーワードによる検索を行うことができます.
以下はHistory
の中ではなく,History
クラスの下に書きます.
def main() -> None:
with open("history.txt", "r", encoding="utf-8") as f:
history = History(f.read())
# 日付による検索
print(history.search_by_date(datetime(2022, 3, 15)))
# キーワードによる検索
# print(history.search_by_keyword("こんにちは"))
if __name__ == "__main__":
main()
全体のソースコード
import re
from datetime import datetime
DATE_PATTERN: str = r'^\d{4}/\d{2}/\d{2}\(.+\)$'
YMD_PATTERN: str = "%Y/%m/%d"
class History:
history_data: list[str]
def __init__(self, data: str) -> None:
lines = data.splitlines()
if "のトーク履歴" in lines[0]:
self.history_data = lines[3:]
else:
self.history_data = lines
def search_by_date(self, date: datetime) -> str:
target_date = date
count_start: int = -1
count_end: int = -1
collect_flag: bool = False
output: str = ""
for i, line in enumerate(self.history_data):
if not re.match(DATE_PATTERN, line):
continue
current_date = datetime.strptime(line[:10], YMD_PATTERN)
if current_date == target_date:
count_start = i
collect_flag = True
elif collect_flag and target_date < current_date:
count_end = i-1
break
else:
count_end = len(self.history_data)
if count_start == -1:
output = "There is no history of this date.\n"
else:
output += "\n".join(self.history_data[count_start:count_end])
output += f"\n\n{count_end - count_start}行\n"
return output
def search_by_keyword(self, keyword: str) -> str:
LOWER_LIMIT = 1
if len(keyword) < LOWER_LIMIT:
return "Please enter more than one character."
count = 0
output = ''
max_date = datetime.min
for line in self.history_data:
if re.match(DATE_PATTERN, line):
date = datetime.strptime(line[:10], YMD_PATTERN)
if date >= max_date:
max_date = date
else:
if not keyword in line:
continue
count += 1
if re.match(r'^\d{2}:\d{2}.*', line):
line = line[6:]
if len(line) >= 61:
line = line[:60] + '…'
output += str(max_date)[:11].replace('-', '/') + " " + line + '\n'
if output == '':
output = 'Not found.'
return f"{count}件\n{output}"
def main() -> None:
with open("/Users/riku/Library/Mobile Documents/com~apple~CloudDocs/🐸GAshare/history.txt", "r", encoding="utf-8") as f:
history = History(f.read())
# 日付による検索
print(history.search_by_date(datetime(2022, 3, 15)))
# キーワードによる検索
# print(history.search_by_keyword("こんにちは"))
if __name__ == "__main__":
main()
おわりに
LINEの履歴を検索するプログラムを作成しました.
今回作成したプログラムは,パッケージとしてGitHubに公開しています.