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

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

新規登録して質問してみよう
ただいま回答率
85.35%
正規表現

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

3回答

772閲覧

正規表現で最長一致だけでなく,全ての一致したパターンを取りたい

moko_7

総合スコア4

正規表現

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

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

0グッド

0クリップ

投稿2021/11/17 11:14

前提・実現したいこと

説明が難しいので,具体例を用いて説明します.
'ABccABcccDEccccDE'
という文字列から前後パターンが前'AB',後'DE'となっている文字列を抽出したいです.
前後パターンの間は,改行以外の文字が2~10文字とします.
この例だと,
「ABccABcccDE」,「ABcccDE」,「ABcccDEccccDE」
の3つが取りたいものです.
「ABccABcccDEccccDE」は前後パターンの間の文字数が10を超えているので取得しません.
実際に利用したいのは前後パターンの間の改行以外の文字列なので,その部分だけ抽出できれば尚良いです.

試したこと

正規表現のre.findallを用いて実装してみました.

body = 'ABccABcccDEccccDE' front = 'AB' rear = 'DE' s = re.findall(front + '.{2,10}' + rear, body) print(s) #['ABccABcccDE']

結果は最長文字数の'ABccABcccDE'しか取得できませんでした.
色々調べた結果,最長一致を取得してしまうようなのは仕様で,先読みアサーションなるものを用いたものなどをみましたが,どのようにすればいいのかわかっていません.

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

Python3.9

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

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

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

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

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

Zuishin

2021/11/17 11:27

正規表現を使うとかえって難しくなります。AB と DE を検索してそのすべての組み合わせを自分で求めるのが一番楽だと思います。
moko_7

2021/11/17 11:32

そうなんですね.ありがとうございます.
guest

回答3

0

ベストアンサー

なんらかの正規表現を書いて、それを使ったfindallか何かを使って一発必中で
['ABccABcccDE', 'ABcccDE', 'ABcccDEccccDE']
を得るのは難しいです。なので、こういうプログラムを書くことになるのではないかと思います。

python3

1import re 2 3body = 'ABccABcccDEccccDE' 4front = 'AB' 5rear = 'DE' 6 7front_indexes = [m.span() for m in re.finditer(front, body)] # => [(0, 2), (4, 6)] 8rear_indexes = [m.span() for m in re.finditer(rear, body)] # => [(9, 11), (15, 17)] 9 10pairs = [ 11 (front_index, rear_index) 12 for front_index in front_indexes 13 for rear_index in rear_indexes 14 if 2 <= rear_index[0] - front_index[1] <= 10 15] # => [((0, 2), (9, 11)), ((4, 6), (9, 11)), ((4, 6), (15, 17))] 16 17result = [body[front_index[0]:rear_index[1]] for front_index, rear_index in pairs] 18 19print(result) # => ['ABccABcccDE', 'ABcccDE', 'ABcccDEccccDE'] 20

追記

上記に挙げたコードは、何をやっているかが分かりやすいかもしれませんが、ちょっと芸がない気がしました。それで

  • one-pass(所与のbodyの最初から最後まで走査するのを一回で済ませること)で、出来ないか?
  • 無駄を省く何らかの最適化が出来ないか?

ということで以下のコードになりました。

python3

1# body = 'ABccABcccDEccccDE' 2body = 'AB123AB67890DExxDEyyyABzzDE##ABDE@@@@@DE' # 動作確認用 3front = 'AB' 4rear = 'DE' 5 6MIN_LENGTH = 2 7MAX_LENGTH = 10 8 9result = [] # 条件に該当した文字列を格納するリスト 10front_indexes = [] # front文字列の開始位置をappendしつつ、使わなくなったものは先頭からpop(0)で削除していくFIFOリスト 11 12# body を左から読み、front および rear の出現を処理するループ 13for i in range(len(body)): 14 if body[i:i+len(front)] == front: 15 front_indexes.append(i) 16 elif body[i:i+len(rear)] == rear: 17 for i_fr in [*front_indexes]: 18 s = body[i_fr + len(front):i] # AB と DE の間にある文字列 19 if len(s) <= MAX_LENGTH: 20 if MIN_LENGTH <= len(s): 21 result.append(s) 22 else: # len(s) が MAX_LENGTH を超えたので、このsを作ったi_frをfront_indexesから削除 23 front_indexes.pop(0) 24 25 26print(result) 27print(front_indexes) 28

出力結果:

['123AB67890', '67890', '67890DExx', 'zz', 'zzDE##AB', 'DE@@@@@']

[29]

基本的な流れは以下です。

  • body を左端から右端まで読み、front文字列が見つかったら、front_indexesにその位置を追加します。
  • rear文字列が見つかったら、front_indexesに保持している、frontのインデクスを小さいほうから読み、frontrearに挟まれている部分文字列をs として作ります。
  • s の長さが要件を満たしていれば、該当文字列のリストに追加します。
  • s の長さが要件の最大長を超えていれば、そのsを作ったfrontインデクスは、以後、使われないので、front_indexesから削除します。

上記の実行例では、front文字列の出現箇所は、0, 5, 21, 29 の4箇所ですが、プログラムの実行中、front_indexesの長さは最大2までにしかならず、プログラム終了時には最後の出現位置の 29 のみを含むリストになっています。

※いちおう良さげに動いてるっぽいですが、どこかバグっていたら、コメントからお知らせください。

投稿2021/11/17 11:41

編集2021/11/17 15:07
退会済みユーザー

退会済みユーザー

総合スコア0

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

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

moko_7

2021/11/17 11:47

やはり難しそうですね. これを実装してうまくいくことも確認できました. みなさん同じ回答をしてくださっていますが,ソースコード付きですぐに解決に至ったのでこの回答をベストアンサーとさせていただきます. 回答してくださった皆様ありがとうございました
退会済みユーザー

退会済みユーザー

2021/11/17 15:00

コメントありがとうございます。別案を追記しました。参考にしていただければと思います〜????
guest

0

同じ文字部分を繰り返し使うようなのは正規表現では無理そうな気がします。

ABDEの位置を全部求めて、その組み合わせ(直積)を作って(例えばforの二重ループ)、
位置の差が希望通りのものだけを抽出すればいいかと思います。
あとはその位置でスライスで抜き出す。

投稿2021/11/17 11:27

otn

総合スコア85901

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

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

moko_7

2021/11/17 11:45

みたいですね.ありがとうございます.
guest

0

既に解決済みですが参考までに。。。

python

1import re 2 3body = 'ABccABcccDEccccDE' 4 5greedy = re.findall(r'.?(?=(AB.{2,10}DE))', body) 6non_greedy = re.findall(r'.?(?=(AB.{2,10}?DE))', body) 7 8matched_all = list(set(greedy + non_greedy)) 9 10print(matched_all) 11 12# 13# ['ABccABcccDE', 'ABcccDEccccDE', 'ABcccDE']

投稿2021/11/17 11:57

melian

総合スコア20655

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

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

moko_7

2021/11/17 12:48

ありがとうございます. 正規表現でも一応できるんですね.参考になります. しかし,処理時間に難がありそうですね. 膨大なデータを扱うとなると,ベストアンサーとさせていただいた手法と比べるとかなり差が出ました.
melian

2021/11/17 13:09 編集

もしかしたら、 r'.*?(?=(AB.{2,10}DE))' と r'.*?(?=(AB.{2,10}?DE))' に変える(先頭の .? を .*? に変更する)と少しはマシになるかもしれません。ちなみに何倍程度の差になりましたでしょうか? 後々の参考に知っておきたいです。
moko_7

2021/11/17 14:26

いくつかの前後パターンで約30000記事(1記事あたり500文字程度)に対して検索を行なったところ,正確ではないですが,大体20倍ほどの差が出ました. .*?に変えてやってみたところさらに時間がかかり,計測を諦めました.何か自分が間違ってしまっているかもしれません.すみません
melian

2021/11/17 14:30

ありがとうございます、お手間を取らせてしまいました。
moko_7

2021/11/17 14:49

少し取れてるものが少ないと思ったので検証した結果, body = 'ABccDEcDEcDE'のようなとき ['ABccDE', 'ABccDEcDEcDE'] となってしまいます
melian

2021/11/17 14:55

ああ、その問題がありました。最短一致と最長一致の2通りしか拾っていませんので。。。
moko_7

2021/11/17 15:03

なるほど.やはり正規表現では難しそうですね
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.35%

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

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

質問する

関連した質問