【初心者】LINEのトーク履歴をデータセットに加工する

# LINEのトーク履歴を加工する
Pythonを使って、LINEのトーク履歴をいじります。

## そもそもLINEって？

>LINE(ライン)は、24時間、いつでも、どこでも、無料で好きなだけ通話やメールが楽しめる新しいコミュニケーションアプリです。

この記事を読んでいる方は全員LINEを知っている、という前提で話を進めます。

## 今回の目標
送信した日時、送信した人の名前、送信した内容を上手いことPandasのDateFrameにまとめます。
理想はこんな感じ↓

|Datetime|Name|Content|
|----|----|----|
|2020/7/30 12:00|わたし|こんにちは|
|2020/7/30 12:01|わたし|[スタンプ]|
|...|...|...|

## フォーマット

### テキストファイルのフォーマット
LINEのトーク履歴はtxt形式でダウンロードできます。開くと大体これに近い形になっているはず。
最近頻繁にアップデートされているので、また形式が変わってるかも。

~~~
2019.12.23 月曜日
15:16 --がグループに参加しました。
15:16 --がグループに参加しました。
15:16 -- こんにちは
15:16 -- [スタンプ]
15:16 -- よろしくお願いします
15:16 -- [スタンプ]
~~~

\nは改行です。

- 日付

~~~
yyyy.mm.dd -曜日\n
~~~

- 普通のトーク

~~~
hh:mm 名前 内容\n
~~~

- 改行が含まれるトーク

~~~
hh:mm 名前 内容1\n
内容2....\n
~~~

これ以外は割愛します。自分で試してみてね。

## 実装する
txtファイルを読み込む
→改行を含むトーク内容を1つにまとめ直す
→時刻 名前 内容を抽出する
→日付をトークにアペンドする
→DataFrameに加工する

### 全体のスクリプト

スクリプトはこんな感じ。

~~~python
import pandas as pd

# txtファイルの読み込み
f = open("line_--.txt", encoding="UTF-8")
line_data = f.readlines()
f.close()

# 連結を定義
def appending(list, row):
    row = list[-1] + row
    del list[-1]
    list.append(row)

data = []
for row in line_data:
    # 10文字未満の行
    if len(row) < 10:
        row = row[:-1]
        appending(data, row)
    # 15文字未満の行
    elif len(row) < 15:
        # 時刻+名前+内容
        if row[2] == ":" and row[5] == " ":
            row = row[:-1]
            data.append(row)
        # 改行後のトーク内容
        else:
            row = row[:-1]
            appending(data, row)
    # 15文字以上の行   
    else:
        # 日付
        if row[4] == "." and row[7] == "." and row[-3:-1] == "曜日":
            row = row[:10]
            data.append(row)
        # 時刻+名前+内容
        elif row[2] == ":" and row[5] == " ":
            row = row[:-1]
            data.append(row)
        # 改行後のトーク内容
        else:
            row = row[:-1]
            appending(data, row)

data2 = []
for row in data:
    # 日付を変数dateに代入
    if row[4] == ".":
        date = row
    # 時刻+名前+内容に日付を連結
    else:
        row = date + "." + row
        row = row.split(" ")
        if len(row) == 3:
            data2.append(row)

# リストのデータフレーム化
df = pd.DataFrame(data2, columns=["Datetime", "Name", "Content"])
# 時刻をDatetime型に直す
df["Datetime"] = pd.to_datetime(df["Datetime"], format="%Y.%m.%d.%H:%M")
~~~

### txtファイルの読み込み
先頭から1行ずつ読み込みます。

~~~python
f = open("line_--.txt", encoding="UTF-8")
line_data = f.readlines()
f.close()
~~~

どうやらline_dataはリストのようです。

~~~python
print(type(line_data))
# <class 'list'>
~~~

### 連結を定義
listの一番最後の要素に、現在の行を連結させます。

~~~python
def appending(list, row):
    row = list[-1] + row
    del list[-1]
    list.append(row)
~~~

遊んでみましょう。

~~~python
thanks = ["いつも", "ありがとう"]
appending(thanks, "ございます")
# thanks = ['いつも', 'ありがとうございます']
~~~

### 10行未満の行
~~~
hh:mm 名前 内容\n
~~~

文字数が最小になるのは、名前が1文字、内容が1文字のときです。
つまり、10文字を下回る行はすべて「改行を含むトークの、改行後の部分」になります。

~~~python
row = row[:-1] # 最後の一文字は改行\nなので削除
appending(data, row)
~~~

この処理で、これを↓

~~~
01:10 わたし あけましておめでとう！\n
ことしもよろしく！\n
~~~

↓こうします。

~~~
01:10 わたし あけましておめでとう！ことしもよろしく！\n
~~~

もっといい例はなかったのか。

### 15文字未満の行

~~~
yyyy.mm.dd -曜日\n
~~~

日時を表す行は文字数が15です。
15文字を下回る行は「普通のトーク」か「改行を含むトークの、改行後の部分」になります。

#### 普通のトーク

~~~
01:10 わたし あけましておめでとう！\n
~~~

3文字目が「:」かつ6文字目が「 」になるので、その部分を抽出します。

~~~python
if row[2] == ":" and row[5] == " ":
    row = row[:-1]
    data.append(row)
~~~

改行を含むトークに対しては、先ほどと同じ処理を施します。

### 15文字以上の行
#### 日付

~~~
yyyy.mm.dd -曜日
~~~

5文字目と8文字目が「.」で、最後の2文字が「曜日」です。
上手いこと日付の行をピックアップします。

~~~python
if row[4] == "." and row[7] == "." and row[-3:-1] == "曜日":
    row = row[:10]
    data.append(row)
~~~

「曜日」の部分はなんとなく削除しました。
もちろん残してもいいです。


#### 日付以外

さっきと同じです。

ここまでの作業で、リストdata内に「日付」と「時刻 名前 内容」の要素が入りました。
リストdataが空の人、エラーを吐かれている人はLINEのアップデートを恨んでください。

### 日付をアペンドする
data内の要素のうち、5文字目が「.」なら日付、そうでなければトーク内容です。
日付を変数dateに代入します。

~~~python
if row[4] == ".":
    date = row
~~~

その後「.」で区切り、「時刻 名前 内容」とアペンドすると、

~~~python
else:
    row = date + "." + row
~~~

行(要素)rowは↓になります。

~~~
yyyy.mm.dd.hh:mm 名前 内容
~~~

最後に「 」で区切り、リストを更新します。

### データフレーム化
リストdata2は二重リスト構造です。
Pandasでデータフレームに直し、カラム名をつけてあげます。
せっかくなので、日付と時刻をDatetime型に直しました。

これでおしまいです。お疲れさまでした。
to_csvを使って書き出すこともできます。

## 最後に
LINEのトーク履歴は自然言語のデータセットとして活用できるのでは？と思ったのがきっかけです。
人間の会話を学習したり、会話から感情を読み取ったり…活用方法はたくさんあると思います。

今回は行の抽出に正規表現を用いませんでした。
このコードでは抽出が上手く行かない場合（抽出したくない行まで取り出してしまう等）、Pythonのreパッケージをお試しください。

自分がとりかかったのは3ヶ月以上前だったのですが、それからこの記事を上げるまでの間にフォーマットが変わっていました。
少しずつ書いていた記事が全部パーに。泣きながらコードを新しく書き直しています。
供養のために、以前のコードも書いておきます。

~~~python
import pandas as pd

# txtファイルを1行ずつ読み込み
f = open("line_--.txt", encoding="UTF-8")
line_data = f.readlines()
f.close()

# 改行データを前の行に連結
def appending(list, row):
    row = list[-1] + row
    del list[-1]
    list.append(row)

data = []
# 4行目から読み込み
for row in line_data[3:]:
    # 9文字未満の行は連結
    if len(row) < 9:
        row = row[:-1]
        appending(data, row)
    # 13文字未満のとき
    elif len(row) < 13:
        # 時刻+名前+内容
        if row[2] == ":" and row[5] == "\t":
            row = row[:-1]
            data.append(row)
        # 時刻が一桁の場合
        elif row[1] == ":" and row[4] == "\t":
            row = row[:-1]
            data.append(row)
        # 連結
        else:
            row = row[:-1]
            appending(data, row)
    # 13文字以上のとき
    else:
        # 日付
        if row[4] == "/" and row[-4] == "(" and row[-2] == ")":
            row = row[:-4]
            data.append(row)
        # 時刻+名前+内容
        elif row[2] == ":" and row[5] == "\t":
            row = row[:-1]
            data.append(row)
        # 時刻が一桁の場合
        elif row[1] == ":" and row[4] == "\t":
            row = row[:-1]
            data.append(row)
        # 連結
        else:
            row = row[:-1]
            appending(data, row)

data2 = []
for row in data:
    # 日付をdateに代入
    if row[4] == "/":
        date = row
    # 時刻+名前+内容に日付を連結
    else:
        row = date + " " + row
        row = row.split("\t")
        if len(row) == 3:
            # 内容に""が付く場合は削除
            if row[2][0] == '"' and row[2][-1] == '"':
                row[2] = row[2][1:-1]
            data2.append(row)

df = pd.DataFrame(data2, columns=["Datetime", "Name", "Content"])
df["Datetime"] = pd.to_datetime(df["Datetime"], format="%Y/%m/%d %H:%M")
~~~