LoginSignup
1
0

PythonでLINEの履歴を検索する

Last updated at Posted at 2023-11-30

概要

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_startcount_endを決定します.すなわち,検索したい範囲の最初と最後の行のインデックスを求めます.
enumerate関数は,インデックスと行の文字列を同時に取得できます.それぞれをilineとします.

日付の場所だけが分かればいいので,if文で,日付の行かどうかを判定します.それでない場合は,continueで次のループに移ります.

日付の行の場合には,datetime型に変換して,これをcurrent_dateとします.
current_datetarget_dateが一致する場合には,この行が検索対象の日付の最初の行であるということになります.そのため,count_startiを代入し,collect_flagTrueにします.

collect_flagTrueの場合,すなわち検索対象の日付の最初の行に到達しているかつtarget_date < current_date2である場合には,この行が検索対象の日付の一つ後の日付の行であるということになります.そのため,count_endi-1を代入し,breakでループを抜けます.

forと同じ字下げの位置にあるelseについて,
python else: count_end = len(self.history_data)
これは,forに対してのもので,if-elseとは異なるので,注意してください.これは,forbreakで抜けずに終了した場合に実行され,breakが使用された場合には実行されません.すなわち,検索対象の日付の次の日付が見つからないまま,ファイルの最後まで到達した場合に,count_endlen(self.history_data)を代入するという処理です.

検索にヒットしなかった場合には,count_start-1のままです.その場合には,outputに「検索対象の日付の履歴はありません」というメッセージを代入します.3
ヒットした場合には,count_startcount_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より大きい場合には,datemax_dateを更新します.
日付の行でない場合で,keywordlineに含まれていない場合には,次のループに移ります.
keywordlineに含まれている場合には,countを1増やします.lineの先頭の時刻を削除し,lineの文字数が60文字以上の場合には,61文字目以降を削除します.その後,outputlineを追加します.

最終的に,outputが空の場合には,「見つかりませんでした」というメッセージを返します.

そうでない場合には,countoutputの先頭に追加して,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に公開しています.

1
0
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
1
0