質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.37%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

正規表現

正規表現とは特定の文字列によるパターンマッチングを行う際に用いられる宣言型プログラミングです。

Q&A

解決済

2回答

889閲覧

[PYTHON3.10] 正規表現のエスケープシーケンス処理を自前で行いたい

Aromatibus

総合スコア11

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

正規表現

正規表現とは特定の文字列によるパターンマッチングを行う際に用いられる宣言型プログラミングです。

1グッド

2クリップ

投稿2023/10/18 12:53

編集2023/10/24 01:51

実現したいこと

PYTHON 3.10で正規表現を使った文字列の置き換えをしています。
自前でエスケープシーケンスを処理しようとしていますが行き詰まっています。
試行錯誤中のコードを掲載いたします。
アドバイスよろしくお願い致します。

前提

  • \はエスケープシーケンス文字として扱う
  • \1はグループ1の文字列として扱う
  • グループは0から9までに対応する
  • \\は\の文字列に置き換えられる
  • \\\1は\とグループ1の文字列に置き換えられる

コード

以下のようなテストコードを使って試しています。
テストではグループ1固定のコードになっています。

PYTHON

1# Versions confirmed to work : PYTHON 3.10.11 on Windows 10 2# -*- coding: utf-8 -*- 3 4import re 5 6# \はエスケープシーケンス文字として扱う 7# \1はグループ1の文字列として扱う 8# グループは0から9までに対応する 9# \\1は\1の文字列に置き換えられる 10# \\\1は\とグループ1の文字列に置き換えられる 11 12# エスケープシーケンスの奇数回・偶数回のマッチ用文字列 13reg_odd = r"((?<!\\)\\(?:\\\\)*(?!\\))" # 奇数回マッチ 14reg_even = r"((?<!\\)(?:\\\\)+(?!\\))" # 偶数回マッチ 15 16target_str = r"o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1)" 17regex_str = reg_even + r"?\\1" 18replace_str = r"A" 19 20result_str = re.sub(regex_str, replace_str, target_str) 21 22print(r"Target String : {}".format(target_str)) 23print(r"Test Result : {}".format(result_str)) 24print(r"Correct Result : o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A)") 25

試したこと

0または偶数回のエスケープシーケンス文字のあとの\1を置き換えれば良いと考え試行錯誤をしています。

補足情報(FW/ツールのバージョンなど)

OS : Windows 10

完成しました♪

おかげ様でいちおうの完成をしましたのでコードを掲載いたします。
ありがとうございました。

PYTHON

1# Versions confirmed to work : PYTHON 3.10.11 on Windows 10 2# -*- coding: utf-8 -*- 3 4 5def unescape_str(target_str: str, replace_str: list) -> str: 6 """ 7 エスケープシーケンスの実装 8 9 1. "\"はエスケープシーケンス文字として扱う 10 2. "\1"はグループ1の文字列として扱う 11 3. グループは0から9までに対応する 12 4. "\\"は"\"の文字列に置き換えられる 13 5. "\\\1"は"\"とグループ1の文字列に置き換えられる 14 6. エスケープシーケンスに続く文字が数字以外の場合はそのまま出力する 15 16 Args: 17 target_str (str): 置き換え対象の文字列 18 replace_str (list): "\"に続く数字(0-9) の置換対象リスト 19 replace_strが10未満の場合は、10になるまで空文字列で補完する 20 Returns: 21 str: 置き換え後の文字列 22 """ 23 import re 24 25 # エスケープシーケンスの正規表現 26 # regex_str = r"(?<!\\)((?:\\)+)(\d+)?" 27 regex_str = r"((?:\\)+)(\d+)?" # 多分こっちで十分 28 29 # 置き換え文字列のリストの長さを10に補完する 30 for i in range(len(replace_str), 10): 31 replace_str.append("") 32 33 # エスケープシーケンスの置き換え関数 34 def regex_replace_rtn(matches): 35 matches_length = len(matches.group(1)) 36 tail = matches.group(2) 37 if matches_length % 2 == 0: 38 if tail is None: 39 tail = "" 40 else: 41 if tail is None: 42 # (奇数の)エスケープシーケンスに続く文字が数字以外 43 # raise ValueError("Escape sequence followed by a non-numeric character") 44 tail = "\\" 45 else: 46 tail = replace_str[int(tail)] 47 return "\\" * (matches_length // 2) + str(tail) 48 49 result_str = re.sub(regex_str, regex_replace_rtn, target_str) 50 return result_str 51 52 53if __name__ == "__main__": 54 # Test Code : Sequential escape characters 55 target_str = r"o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1) (\\\\\n)" 56 Correct_Result = r"o:(B) x:(\1) o:(\B) x:(\\1) o:(\\B) (\\\n)" 57 replace_str = ["A", "B", "C"] # \数字 の置換対象リスト 58 result_str = unescape_str(target_str, replace_str) 59 60 print(r"Target String :", target_str) 61 print(r"Test Result :", result_str) 62 print(r"Correct Result :", Correct_Result) 63 print(r"Match :", (result_str == Correct_Result)) 64 65 # Test Code : Replacing escape characters 66 target_str = r"(\0) (\1) (\2) (\3) (\4) (\5) (\6) (\7) (\8) (\9) (\x)" 67 replace_str = ["A", "B", "C"] # \Number(0-9) replacement character list 68 result_str = unescape_str(target_str, replace_str) 69 70 print(r"Target String :", target_str) 71 print(r"Test Result :", result_str) 72
CHERRY👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

TakaiY

2023/10/18 14:19

> 行き詰まっています。 どのような問題があるのでしょうか。
Aromatibus

2023/10/19 05:01

コメントありがとうございます。 コード中、以下の部分が求める結果です。 print(r"Correct Result : o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A)") 同じ結果になるようなエスケープシーケンスの処理をしたい考えていました。
guest

回答2

0

正規表現は、複雑なテキスト処理をするのには向いていません (初心者向けの解説ではそのようなことに向いていると誤解させるものがときどき見られます)。正規表現を使っていて自分が何をしているのかわからなくなってきたら、正規表現では難しいこと、できないことをやろうとしていないか見直したほうがいいです。

正規表現は特に「構造」のあるテキストの処理が苦手です。今回だと、エスケープ文字がつく文字は特別扱いされるという構造があります。構造の他の例としては、HTMLみたいにタグの開始と終了があるようなものも苦手ですね。

こういう場合、正規表現によるマッチを最小限にして、テキストを最初から少しずつ処理していくという方法を使うといいです。こんな感じです。

python

1import re 2 3def unescape(escaped_str): 4 tok_regex = r'(?P<GROUP1>\\1)|(?P<REVSOL>\\\\)|(?P<ESCAPED>\\.)|(.)' 5 for mo in re.finditer(tok_regex, escaped_str): 6 kind = mo.lastgroup 7 value = mo.group() 8 if kind == 'GROUP1': 9 value = 'A' 10 elif kind == 'REVSOL': 11 value = '\\' 12 elif kind == 'ESCAPED': 13 value = value[1:] 14 yield kind, value 15 16if __name__ == '__main__': 17 in_str = input() 18 for k, v in unescape(in_str): 19 print(f'{k}: {v}')

5行目でテキストのマッチを繰り返すたびに、グループ1、エスケープされたバックスラッシュ、エスケープされた文字、普通の文字、のいずれかにマッチします。名前つきグループを使うことでどれにマッチしたか区別しています (普通の文字のグループ(.)|による選択肢の最後に書く必要があります。そうしないとエスケープ文字にマッチしてしまいます)。

Python公式マニュアルの「re - 正規表現操作」の正規表現の例にある「トークナイザを書く」も見てみて下さい。


名前つきグループではなく普通のグループを使う例も挙げておきます。マッチオブジェクトのgroup(n)メソッドは、正規表現中のn番目のグループ (左括弧の位置の順番) にマッチしたものを返します。マッチしていないグループについてはNoneを返します。

python

1import re 2 3def unescape(escaped_str): 4 tok_regex = r'(\\1)|(\\\\)|(\\.)|(.)' 5 for mo in re.finditer(tok_regex, escaped_str): 6 if mo.group(1) is not None: 7 kind = 'GROUP1' 8 value = 'A' 9 elif mo.group(2) is not None: 10 kind = 'REVSOL' 11 value = '\\' 12 elif mo.group(3) is not None: 13 kind = 'ESCAPED' 14 value = mo.group()[1:] 15 else: 16 kind = None 17 value = mo.group() 18 yield kind, value 19 20if __name__ == '__main__': 21 in_str = input() 22 for k, v in unescape(in_str): 23 print(f'{k}: {v}')

投稿2023/10/19 01:06

編集2023/10/19 02:28
ikedas

総合スコア4443

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Aromatibus

2023/10/19 04:19

コメントありがとうございます。 正規表現だけでは難しいのですね。。。 本回答は非常に参考になるものでした。 これを元に更に試行錯誤をしてみたいと思います。 ですが、あきらめずに(笑)しばらく回答を募集してみたいと思います。 なにかお気づきの点がありましたらコメントをお願い致します。
Aromatibus

2023/10/19 04:49

本回答も非常に参考になり勉強させていただけるものでした。 誠にありがとうございました。
ikedas

2023/10/19 10:45

偶数個や奇数個のバックスラッシュを目安に置き換える方法だと、\1 を置き換えた結果がバックスラッシュを (1個またはそれ以上) 含んでいたときに困ったことになりますよね。 正規表現は、思っていたのと違う場所にマッチしてしまう事故が起きやすいので、使い勝手は決してよくないです。 実際の正規表現ライブラリのエスケープ処理でも、私の回答のように、文字列の先頭から順番に処理していく方法を取っているはずです。
guest

0

ベストアンサー

駄目な理由としては、一旦?を無視すると、
「直前が\でなく、\が偶数個で、直後が\でない」という正規表現に「\1」をつないでいるので、
「直前が\でなく、\が偶数個で、直後が\でないが、直後が\1」という、矛盾を含むために何にもマッチしない正規表現になっています。
なので、r"?\\1"?が効いて前の部分が無い場合になり、regex_str = r"\\1"と等価です。
ちょっとはしょった説明ですが、多分おわかりになると思います。

何をしたいのかいまいち不明です。
Target String : o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1)

Correct Result : o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A)
に置換したいのだと思いますが、置換の規則が不明です。日本語で書けますか?

追記

質問のコードを全く無視して書くとこんな感じでしょうか。
「奇数個の\の後に数字が続かないケース」の対応が不明なのでそこは適当に書いてます。

Python

1s = r"o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1) (\\\\\n)" #変換対象 2t = r"o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A) (\\\n)" # こうなって欲しい 3r = ["","A","B"] # \数字 の置換対象リスト 4 5import re 6 7def foo(m): 8 l = len(m.group(1)) # 1つめの括弧内の \ の連続の個数 9 tail = m.group(2) # 2つめの括弧内の数字列。? 付きなので数字は無いかも知れずその場合は None 10 if l % 2 == 0: 11 if tail is None: 12 tail = "" 13 else: 14 if tail is None: 15 tail = "\\" # 奇数個の \ の後に数字が来ないケース 16 else: 17 tail = r[int(tail)] # 数字を添え字にしてリストから文字列取得。実際は範囲チェック要 18 return "\\"*(l//2)+tail 19 20 21w = re.sub(r"(?<!\\)((?:\\)+)(\d+)?",foo,s) 22# w = re.sub(r"((?:\\)+)(\d+)?",foo,s) # 多分こっちで十分 23print("変換前 ",s) 24print("変換後 ",w) 25print("目標値 ",t) 26 27 28結果: 29変換前 o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1) (\\\\\n) 30変換後 o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A) (\\\n) 31目標値 o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A) (\\\n)

投稿2023/10/18 14:31

編集2023/10/19 05:31
otn

総合スコア85766

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Aromatibus

2023/10/18 19:32 編集

コメントありがとうございます。 説明不足な点、ご指摘いただきありがとうございます。 現在のコードがダメな理由はわかっていたんですが対処方法が私の拙い知識ではわかりませんので質問をさせいいただきました。 実現したいことですが、前提条件にある 1. \はエスケープシーケンス文字として扱う 2. \1はグループ1の文字列として扱う 3. グループは0から9までに対応する 4. \\は\の文字列に置き換えられる 5. \\\1は\とグループ1の文字列に置き換えられる ※3に関して  これは同じような置換処理を繰り返すことでも処理可能と考え本コードでは省略しています。  一度にできるのであればそれがベストです。 というものです。 結果として求めているのは今回のテストコードではお察しの通り次のとおりになることが求められています。 置換前 o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1) 置換後 o:(A) x:(\1) o:(\A) x:(\\1) o:(\\A) 以上となります。 至らない点がありましたらご指摘いただけると助かります。 よろしくお願い致します。
otn

2023/10/19 04:14 編集

寝ぼけてたのか > 置換の規則が不明です。日本語で書けますか? と書きましたが、規則性がありますね。 「偶数個の \ を置換。残った \数字 があれば置換」 プログラム例を追記しておきます。
Aromatibus

2023/10/19 04:13

コメントありがとうございます。 質問が読みづらい点、申し訳ありませんでした。 引き続き何かお気づきの点がありましたらコメントをいただけると嬉しく思います。 よろしくお願い致します。
Aromatibus

2023/10/19 04:23

>プログラム例を追記しておきます。 ここを見逃していました。 誠に申し訳ありません。早速、検証してみます。 ありがとうございました。
otn

2023/10/19 04:29

プログラムを解説しておくと、「\ が連続したもの」「\が連続したものの後に数字があるもの」にマッチさせ、 ¥の個数を数えて、偶数奇数で分岐します。あとは数字のあるなしで数字ならリストの添え字にして置換。 \ は2つを1つに減らす。
Aromatibus

2023/10/19 05:03 編集

ありがとうございます。 目標通り動いていることを確認しました。 このようなfooの扱い方を知らなかったため、使いこなすにはココを理解しないとだめですね。 これが理解できれば応用もできると希望を胸に挑戦したいと思います♪ 非常に助かりました。 ベストアンサーとさせていただきたいと思います。 誠にありがとうございました。
otn

2023/10/19 11:41 編集

正規表現で複雑な処理をする場合は、正規表現とプログラムロジックでそれぞれどこまでやるかをうまく考える必要があります。複雑な正規表現は、作成者も後日読解困難で、メンテ不可能というケースもあるかと思います。 置換だと、複雑な場合は置換処理を関数指定で行います。 他のよくある例では、「~~を含んで~~を含まない」は、プログラムロジック側で論理演算子を使うのが簡単です。 if re.search(~~) and not re.search(~~) とか。
Aromatibus

2023/10/19 05:19 編集

追記と解説、ありがとうございます。 仰るとおりですね。 まだコードを理解していないのですが、 今回の回答は複雑になりすぎないようにうまくロジック部分と使い分けることができる最良の方法なのだろうと思っています。 日々、精進ですね♪
Aromatibus

2023/10/20 07:16 編集

実装するコードができたので掲載します。 ご回答いただきましたotnさん、ikedasさんありがとうございます。 また、このコードが同じような悩みがある方の一助となれば幸いです。 改めましてありがとうございました。 ```PYTHON # Versions confirmed to work : PYTHON 3.10.11 on Windows 10 # -*- coding: utf-8 -*- def unescape_str(target_str: str, replace_str: list) -> str: """ エスケープシーケンスの実装 1. "\"はエスケープシーケンス文字として扱う 2. "\1"はグループ1の文字列として扱う 3. グループは0から9までに対応する 4. "\\"は"\"の文字列に置き換えられる 5. "\\\1"は"\"とグループ1の文字列に置き換えられる 6. エスケープシーケンスに続く文字が数字以外の場合はそのまま出力する Args: target_str (str): 置き換え対象の文字列 replace_str (list): "\"に続く数字(0-9) の置換対象リスト replace_strが10未満の場合は、10になるまで空文字列で補完する Returns: str: 置き換え後の文字列 """ import re # エスケープシーケンスの正規表現 # regex_str = r"(?<!\\)((?:\\)+)(\d+)?" regex_str = r"((?:\\)+)(\d+)?" # 多分こっちで十分 # 置き換え文字列のリストの長さを10に補完する for i in range(len(replace_str), 10): replace_str.append("") # エスケープシーケンスの置き換え関数 def regex_replace_rtn(matches): matches_length = len(matches.group(1)) tail = matches.group(2) if matches_length % 2 == 0: if tail is None: tail = "" else: if tail is None: # (奇数の)エスケープシーケンスに続く文字が数字以外 # raise ValueError("Escape sequence followed by a non-numeric character") tail = "\\" else: tail = replace_str[int(tail)] return"\\" * (matches_length // 2) + tail result_str = re.sub(regex_str, regex_replace_rtn, target_str) return result_str if __name__ == "__main__": # Test Code : Sequential escape characters target_str = r"o:(\1) x:(\\1) o:(\\\1) x:(\\\\1) o:(\\\\\1) (\\\\\n)" Correct_Result = r"o:(B) x:(\1) o:(\B) x:(\\1) o:(\\B) (\\\n)" replace_str = ["A", "B", "C"] # \数字 の置換対象リスト result_str = unescape_str(target_str, replace_str) print(r"Target String :", target_str) print(r"Test Result :", result_str) print(r"Correct Result :", Correct_Result) print(r"Match :", (result_str == Correct_Result)) # Test Code : Replacing escape characters target_str = r"(\0) (\1) (\2) (\3) (\4) (\5) (\6) (\7) (\8) (\9) (\x)" replace_str = ["A", "B", "C"] # \Number(0-9) replacement character list result_str = unescape_str(target_str, replace_str) print(r"Target String :", target_str) print(r"Test Result :", result_str) ```
Aromatibus

2023/10/20 06:50

ソースコードのマークダウンが表示できてない。。。 私らしいですが、締まらない終わり方ですね(笑)
otn

2023/10/20 08:51

コメント欄でマークダウンは効きません。質問に追記しましょう。
Aromatibus

2023/10/24 01:51

ありがとうございます。 コードを掲載させていただました♪
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.37%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問