正規表現のポテンシャルを引き出す自作関数３選 [Python]

# はじめに
文字列処理に関する3種類の自作関数を紹介します。

具体的には、主に以下の2点を目的としています。
・正規表現の円滑な適用
・英字 / かな / カナ / 漢字 / 数字 の識別

☆ 02/26 時点で全体構成を刷新しました。


# 下準備
```py
# いずれも標準ライブラリ
import re
import string
```

## 共通引数：inv (inverse)
否定表現（～以外）を選ぶかどうか、デフォルト値はFalse



# その１：文字列単体に対する存在判定
```python
def search_mod(ptn, txt, inv=False):
    judge_base = re.search(ptn, txt) is not None
    judge = judge_base if inv == False else not judge_base
    return judge
```
個人的には、内包表記において条件判定に用いるケースが多いです。


# その２：Seriesに対する存在判定
```python
def extract_mod(series, ptn, inv=False):
    judge_base = series.astype(str).str.extract(r"(" + ptn + ")", expand=False)
    judge = judge_base.notna() if inv == False else judge_base.isna()
    return judge
```
ptnに単なる文字列を渡した場合、str.contains() と同様になります。
デフォルトでは"該当する"場合を、inv (inverse) = True であれば"該当しない"場合を検出します。

個人的には、pandas の loc において抽出条件として使用するケースが多いです。


# その３：文字の種類の識別
```python
def set_ctype_ptn(kind, range_="+", inv=False):
    ptn_list = [
        string.ascii_letters,
        "\u3041-\u3096|\u30FC",
        "\u30A1-\u30FA|\u30FC|\uFF66-\uFF6F|\uFF71-\uFF9D",
        "\u2E80-\u2FD5|\u3400-\u4DB5|\u4E00-\u9FD5",
        string.digits,
    ]

    # 5種類全ての否定（記号等に相当）
    if kind == -1:
        return f"[^{'|'.join(ptn_list)}]"

    if kind in [0, 1, 2, 3, 4]:
        ptn = ptn_list[kind]

    if type(kind) is list:
        if len(set(kind) - {0, 1, 2, 3, 4}) == 0:
            ptn = "|".join([ptn_list[i] for i in kind])

    if "ptn" in locals():
        ptn = (f"[{ptn}]" if inv is False else f"[^{ptn}]") + range_
    else:
        raise ValueError("'kind' must be an integer between -1-4 or a list containing them")

    return ptn
```
☆ 旧仕様を改善し、文字の種類を複数指定できるようにしました。
→引数に int 単体を渡しても、リストに複数入れて渡しても通ります。

デフォルトでは "1文字以上" が対象（range_="+"）ですが、正規表現によって適宜調整できます。

細かくカスタマイズするために unicode レベルで指定していますが、結果を出力すると文字列で表示されます。
5パターンをまとめて出力すると、以下のようになります。

```python
set_ctype_ptn([0, 1, 2, 3, 4])

'[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ|ぁ-ゖ|ー|ァ-ヺ|ー|ｦ-ｯ|ｱ-ﾝ|⺀-⿕|㐀-䶵|一-鿕|0123456789]+'
```


なお、以下の点について御注意ください。

- "ー"（\u30FC、長音符）はかな/カナで共通しており、（かなは除いて）カナだけを指定するような場合には追加で調整が必要です
- \uFF70（半角ハイフン）は記号として用いられる場合もあり、個人的には邪魔なので除外しています

 
微調整等の際には以下のリンクよりunicode表を御参照ください。
http://www.tamasoft.co.jp/ja/general-info/unicode.html

また、特定の文字のunicodeが知りたい場合には以下を適用します。
```python
# txtのunicode値を16進数変換
hex(ord(txt))
```



# 実際の使用例

```python
import numpy as np
import pandas as pd

np.random.seed(20220223)

feature_list1 = ["薄焼き", "厚焼き", "堅焼", "<BIG>", "プレミアム"]
feature_list2 = ["醤油", "塩", "ザラメ", "わさび", "激辛唐辛子", "chocolate"]
feature_list3 = ["せんべい", "せん", "煎餅", "[期間限定]", ""]

df = pd.DataFrame(
    [
        (
            np.random.choice(feature_list1, 1)[0]
            + np.random.choice(feature_list2, 1)[0]
            + np.random.choice(feature_list3, 1)[0]
        )
        for i in range(10000)
    ],
    columns=["type"],
)
```


### 8文字以上の漢字を含む

```python
ptn1 = set_ctype_ptn(kind=3, range_="{8,}")

df.loc[extract_mod(df["type"], ptn1)].value_counts()

"堅焼激辛唐辛子煎餅    71"
```


### 英字またはカナしか存在しない（記号は許可）

```python
ptn2 = set_ctype_ptn(kind=[1, 3])

# かな/漢字の否定
df.loc[extract_mod(df["type"], ptn2, inv=True)].value_counts()

"""
プレミアムchocolate    72
プレミアムザラメ          69
<BIG>ザラメ          58
<BIG>chocolate    48
"""
```


### "\*焼き（3文字）せん\*"
```python
ptn3 = "(?<=焼き).{3,3}(?=せん)"

df.loc[extract_mod(df["type"], ptn3)].value_counts()

"""
厚焼きザラメせんべい    73
薄焼きザラメせん      71
薄焼きザラメせんべい    71
厚焼きわさびせんべい    68
厚焼きわさびせん      67
薄焼きわさびせんべい    65
厚焼きザラメせん      62
薄焼きわさびせん      59
"""
```


### "かな/カナ/漢字の全てに加えて、記号を含む"
```python
ptn4_1 = set_ctype_ptn(kind=1)
ptn4_2 = set_ctype_ptn(kind=2)
ptn4_3 = set_ctype_ptn(kind=3)
ptn4_4 = set_ctype_ptn(kind=-1)

# ANDの表現は冗長になりがちです
df.loc[
    (extract_mod(df["type"], ptn4_1))
    & (extract_mod(df["type"], ptn4_2))
    & (extract_mod(df["type"], ptn4_3))
    & (extract_mod(df["type"], ptn4_4))
].value_counts()

"""
厚焼きザラメ[期間限定]      73
プレミアムわさび[期間限定]    69
薄焼きザラメ[期間限定]      61
"""
```


### 応用例
```python
df_reshape = pd.DataFrame(
    [
        [
            re.sub("(?<=焼).+", "", senbei),
            re.split("(?<=塩).", re.sub(".+焼", "", senbei))[0],
        ]
        for senbei in df["type"].str.replace("焼き", "焼").tolist()
        if search_mod("焼.{2,3}$", senbei)
    ],
    columns=["type1", "type2"],
)
```

```python
# 実際にお試しください
pd.crosstab(index=df_reshape["type2"], columns=df_reshape["type1"])
```



# おわりに
以上になります、お付き合いいただきありがとうございました。

lambda 式に落とし込んだ処理を apply に渡す形で文字列整形を行う例も紹介したかったのですが、今回は割愛します。

スクレイピング等の具体的な活用ケースを含め、他にも文字列処理ネタは色々とあるのですが、残念ながら記事を作成する元気が不足しています。