pandasのto_csvでの文字化けエラーの原因を探す


このようにpandasを `cp932` や `shift-jis` でファイルに吐き出そうとすると、

```python
df.to_csv('tmp.csv', encoding='cp932')
``` 

こういったエラーが生じるケースがあります。

```
UnicodeEncodeError                        Traceback (most recent call last)
<ipython-input-8-c349adf3c121> in <cell line: 1>()
----> 1 df.to_csv('tmp.csv', encoding='cp932')
5 frames
/usr/local/lib/python3.10/dist-packages/pandas/io/formats/csvs.py in _save_chunk(self, start_i, end_i)
    318 
    319         ix = self.data_index[slicer]._format_native_types(**self._number_format)
--> 320         libwriters.write_csv_rows(
    321             data,
    322             ix,
writers.pyx in pandas._libs.writers.write_csv_rows()
UnicodeEncodeError: 'cp932' codec can't encode character '\u525d' in position 30: illegal multibyte sequence
```

文字コードの問題なのですが、対処法として、

1. `errors='ignore'` を指定する
1. `encoding='utf8'` にする
1. 原因となってる文字を問題とならない文字に置き換える

などがあります。諸般の事情から3つ目をやりたいときが稀にありますが、**どの文字が置き換えるべき対象なのか**知るのが面倒です。それ用のコードを備忘としてメモしておきます。



# 1. どこの行がダメか教えてくれるやつ

```python
import pandas as pd

def find_unencodable_characters(
    df: pd.DataFrame, columns: list = None, encoding: str = 'cp932'
) -> pd.DataFrame:
    """
    指定されたエンコーディングでエンコードできない文字をDataFrame内で検索する。

    Args:
        df (pd.DataFrame): エンコード可能性をチェックする対象のDataFrame。
        columns (list, optional): チェック対象のカラムのリスト。指定しない場合は、
            すべてのカラムが対象となる。デフォルトはNone。
        encoding (str, optional): チェックするエンコーディングの種類。デフォルトは
            'cp932'。

    Returns:
        pd.DataFrame: エンコードできない文字が見つかった場合、その詳細を含む
        DataFrameを返す。このDataFrameは、'Column', 'Row', 
        'Problematic Char', および 'Full Value' のカラムを持つ。
    """
    if columns is None:
        columns = df.columns  # デフォルトで全てのカラムをチェック

    unencodable_chars_list = []

    # 指定されたカラムごとにエンコードできない文字をチェック
    for column in columns:
        for i, value in enumerate(df[column].astype(str)):
            try:
                # 指定されたエンコーディングで文字列をエンコードを試みる
                value.encode(encoding)
            except UnicodeEncodeError as e:
                # エンコードに失敗した場合、問題のある文字をキャプチャ
                problematic_char = value[e.start:e.end]
                unencodable_chars_list.append({
                    'Column': column,
                    'Row': i,
                    'Problematic Char': problematic_char,
                    'Full Value': value
                })
                
    return pd.DataFrame(unencodable_chars_list)
```


### 実行例

※例文はとても適当なことを言ってます。

```python
# 例として使用するDataFrame
df = pd.DataFrame({
    "アドバイス": [
        """
            新しい塗料を使用する際には、剝離が発生する可能性があるため、
            事前に適切な表面処理や下地材の選定が重要です。
            特に、黄芩から抽出された成分が塗料の原料として使用されており、
            これにより、塗布面積m²あたりの耐久性が従来の製品よりも
            向上していることが確認されています。
        """,
        """
            製品ラベルに✕のマークが付いている場合、それは特定の条件下では
            使用が制限されている成分が含まれていることを意味します。
            また、この薬には心臓の亢進を引き起こすリスクがあり、
            一部の患者には使用後に瘙痒感が現れることがありますが、
            通常は一時的な症状であることが多いです。
        """,
    ],
    "リアクション": [
        "Müller教授がこれについての第一人者らしいので、
        彼の2014年の論文を読んでみます。",
        "ありがとうございます。よく分かりました😊"
    ]
})

# 該当する行・列の確認
unencodable_chars_df = find_unencodable_characters(df)
unencodable_chars_df
```

### 出力結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/665792/6d96bdbb-72f0-be75-3388-2e1b411a5fd7.png)

ちなみに、各セルの1文字目しかヒットしないので、たとえば「瘙」とかはエラーの原因となることが分かりません。


# 2. どの文字がダメか教えてくれるやつ

```python
def find_unencodable_characters_in_string(
    input_string: str, encoding: str = 'cp932'
) -> list:
    """
    指定されたエンコーディングでエンコードできない文字を文字列内で検索する。

    Args:
        input_string (str): チェック対象の文字列。
        encoding (str, optional): チェックするエンコーディングの種類。デフォルトは
            'cp932'。

    Returns:
        list: エンコードできない文字を含むリストを返す。
    """
    unencodable_chars = []
    unique_chars = set(input_string)  # ユニークな文字を取得

    for char in unique_chars:
        try:
            # 文字を指定したエンコーディングでエンコード
            char.encode(encoding)
        except UnicodeEncodeError:
            # エンコードできない文字をリストに追加
            unencodable_chars.append(char)

    return unencodable_chars
```


### 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/665792/0104c9c4-5059-f902-5ba1-4a1fa0f6c041.png)