LoginSignup
42
49

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-11-06

はじめに

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

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

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

下準備

# いずれも標準ライブラリ
import re
import string

共通引数:inv (inverse)

否定表現(~以外)を選ぶかどうか、デフォルト値はFalse

その1:文字列単体に対する存在判定

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

個人的には、内包表記において条件判定に用いるケースが多いです。

その2:Seriesに対する存在判定

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 において抽出条件として使用するケースが多いです。

その3:文字の種類の識別

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パターンをまとめて出力すると、以下のようになります。

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

'[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ|ぁ-ゖ|ー|ァ-ヺ|ー|ヲ-ッ|ア-ン|⺀-⿕|㐀-䶵|一-鿕|0123456789]+'

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

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

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

また、特定の文字のunicodeが知りたい場合には以下を適用します。

# txtのunicode値を16進数変換
hex(ord(txt))

実際の使用例

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文字以上の漢字を含む

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

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

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

英字またはカナしか存在しない(記号は許可)

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文字)せん*"

ptn3 = "(?<=焼き).{3,3}(?=せん)"

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

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

"かな/カナ/漢字の全てに加えて、記号を含む"

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
"""

応用例

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"],
)
# 実際にお試しください
pd.crosstab(index=df_reshape["type2"], columns=df_reshape["type1"])

おわりに

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

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

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

42
49
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
42
49