はじめに
文字列処理に関する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 に渡す形で文字列整形を行う例も紹介したかったのですが、今回は割愛します。
スクレイピング等の具体的な活用ケースを含め、他にも文字列処理ネタは色々とあるのですが、残念ながら記事を作成する元気が不足しています。