

文字列の処理で括弧が入れ子になっていた場合に一番外側の括弧だけ検出するにはどうすればいいでしょうか。

あいう「えお「か「き」く「け」こ」さし」すせそたち「つて」と

例えば上のような文字列から

「えお「か「き」く「け」こ」さし」  
「つて」  
の２箇所だけ抽出して前後に改行コードを追加したいのです。

[code]

    text = "あいう「えお「か「き」く「け」こ」さし」すせそたち「つて」と"
    regex = re.compile(r'(「([^「」]*「[^「」]*」[^「」]*)*[^「」]*」)')
    text = re.sub(regex, r'\n\1\n', text)
    
[/code]

上記のコードだと「か「き」く「け」こ」の前後、 「つて」の前後に改行コードが入ります。  
2重の入れ子までなら対応できるのですが、3重、4重と入れ子が何重まであるか不明な場合でも対応できるようにしたいです。

python 3.5.1を使っています。

* * *

皆様の回答を受けまして、以下のような関数にまとめました。  
「」だったり引用符だったりで囲われた部分を抜き出せるようになりました。

[code]

    # 括弧の前後に改行を挿入(括弧が入れ子の場合は一番外側の括弧に対して適用)
    def add_separator_before_and_after_brackets(text, width=10, separator_pre='\n', separator_post='\n'):
        import regex # 拡張正規表現のregexモジュール、標準モジュールのreとは別物なので注意
    
        # 括弧部分の長さがwidth以上だったら前後に改行を挿入
        def add_separator(match_obj):
            match_text = match_obj.group()
            if len(match_text) >= width:
                return '{}{}{}'.format(separator_pre, match_text, separator_post)
            else:
                return match_text
    
        rexp1 = regex.compile(r'(「(?>[^「」]+|(?R))*」)')
        rexp2 = regex.compile(r'(『(?>[^『』]+|(?R))*』)')
        rexp3 = regex.compile(r'(【(?>[^【】]+|(?R))*】)')
        rexp4 = regex.compile(r'(\((?>[^\(\)]+|(?R))*\))')
        rexp5 = regex.compile(r'(\[(?>[^\[\]]+|(?R))*\])')
        rexp6 = regex.compile(r'(“(?>[^“”]+|(?R))*”)')
        for rexp in [rexp1, rexp2, rexp3, rexp4, rexp5, rexp6]:
            text = rexp.sub(add_separator, text)
        return text
    
[/code]


CC BY-SA 3.0
2017-02-25T07:11:44.157
32938
2017-02-26T15:56:47.927
2017-02-26T08:10:53.293
question
python 正規表現
pythonで括弧の入れ子を検出


英語の質問に答がありました。  
<https://stackoverflow.com/questions/26385984/recursive-pattern-in-regex>

再帰マッチを使うため、regexをインストール

[code]

    (venv) > pip install regex
    Collecting regex
      Downloading regex-2017.02.08-cp35-none-win_amd64.whl (242kB)
        100% |################################| 245kB 1.5MB/s
    Installing collected packages: regex
    Successfully installed regex-2017.2.8
    
[/code]

実行

[code]

    (venv) > python3
    >>> import regex
    >>> text = "あいう「えお「か「き」く「け」こ」さし」すせそたち「つて」と"
    >>> f = regex.compile(r'「((?>[^「」]+|(?R))*)」')
    >>> f.findall(text)
    ['えお「か「き」く「け」こ」さし', 'つて']
    >>> print(f.sub(r'\n\1\n', text))
    あいう
    「えお「か「き」く「け」こ」さし」
    すせそたち
    「つて」
    と
    
[/code]


CC BY-SA 3.0
2017-02-25T07:59:47.117
32940
2017-02-25T07:59:47.117
2017-05-23T12:38:56.083
-1
806
32938
answer


一番簡単なのは、括弧の数を数えるやり方だと思います。つまり、以下の方法です。

  * 括弧のネストのレベルを数える変数を1つ用意し、文字列の最初から1文字ずつ読んでいって、開き括弧があればレベルを+1、閉じ括弧があればレベルを-1にしていきます。
  * その途中で「レベルが0から1になったところ」から「レベルが1から0になったところ」までが一番外側の括弧に囲まれているところです。
  * どこかでレベルがマイナスの値になったら、括弧の対応が上手く取れていません。エラーです。

あるいは適当なパーザーを書いてもいいですが、本質的には似たようなことをするような気がします。

* * *

どうしても正規表現で解決したいということであれば、再帰表現の入った正規表現であれば使えます。Pythonでは`re`の代わりに[`regex`](https://pypi.python.org/pypi/regex)を使うと再帰を使えます。たとえば以下の正規表現が考えられます。

[code]

    (?<rec>「(?:[^「」]+|(?&rec))*」)
    
[/code]

この(拡張)正規表現を説明すると以下の通りです。

  * `(?<rec> ...)`: この中身を`rec`という名前で覚えておく。
  * `(?:[^「」]+|(?&rec))`: 鍵括弧の無い文字列が続くか、rec自体にマッチする。 
    * `(?: ...)`はマッチを確認するがキャプチャしないグループを示しています([参考](https://stackoverflow.com/questions/3512471/what-is-a-non-capturing-group-what-does-a-question-mark-followed-by-a-colon))。
    * rec自体は "対応のとれた開き括弧から閉じ括弧までの文字列" を大体意味しているので、これで括弧の中に括弧があっても大丈夫になっています。

[Takayukiさんの回答](https://ja.stackoverflow.com/a/32940/19110)には（本質的には同じですが）よりコンパクトな正規表現が載っているのでこちらも参考になるかと思います。

また、正規表現を使った場合、もしかするとパフォーマンスに影響があるかもしれません。Pythonの正規表現エンジンの実装について私は詳しくないので、実際に試してみて影響があれば上に書いた単純な方法に変えた方が良いでしょう。

関連

  * ["Python: How to match nested parentheses with regex?"](https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex) \-- Stack Overflow 
    * 特に、[ovgolovinさんの回答](https://stackoverflow.com/a/12280660/5989200)が regex を使っています。

* * *

ちなみに「括弧の入れ子対応が上手くとれているか」を **純粋な** 正規表現でチェックすることはできません。  
上手く対応がとれた括弧文字列たちの集合をダイク言語 (Dyck言語) と言うのですが、理論的に、これが正規言語でないことが知られているためです。

この点についてより詳しくは、Wikipediaの記事や、書籍『正規表現技術入門』などが参考になるのではないかと思います。

  * [ディック言語](https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%A3%E3%83%83%E3%82%AF%E8%A8%80%E8%AA%9E) ／ [Dyck language](https://en.wikipedia.org/wiki/Dyck_language) \-- Wikipedia
  * [Regular language#The number of words in a regular language](https://en.wikipedia.org/wiki/Regular_language#The_number_of_words_in_a_regular_language) \-- Wikipedia
  * [『正規表現技術入門』](https://www.amazon.co.jp/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE%E6%8A%80%E8%A1%93%E5%85%A5%E9%96%80-%E2%80%95%E2%80%95%E6%9C%80%E6%96%B0%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3%E5%AE%9F%E8%A3%85%E3%81%A8%E7%90%86%E8%AB%96%E7%9A%84%E8%83%8C%E6%99%AF-WEB-PRESS-plus/dp/4774172707) \-- amazon.co.jp


CC BY-SA 3.0
2017-02-25T08:24:14.947
32941
2017-02-26T01:08:03.033
2017-05-23T12:38:56.083
-1
19110
32938
answer


正規表現を使う事が条件となっていますが、参考までに別解などを。

文字列中の `「` を `[` に、`」` を `]`
に変更して、各文字の位置情報(index)のリストに変換します。次に、リスト内のリストのみを取り出して、そのリストの最初と最後の要素から目的の部分文字列を取り出します。

[code]

    #!/usr/bin/python3
    
    def parse(txt):
      lst = eval(
        str(sum(
          [
            ['[' + str(i+1)] if v == '「' else
            [str(i-1) + ']'] if v == '」' else
            [i] for (i, v) in enumerate(txt)
          ], [])).replace("'", '')
      )
    
      return [txt[l[0]:l[-1]+1] for l in lst if isinstance(l, list)]
    
    ##
    text = [
      'あいう「えお「か「き」く「け」こ」さし」すせそたち「つて」と',
      '「」「]「「」」」 「あ「」」x_「「」い」+y「「う」」[%',
      '',
    ]
    
    [print(parse(t)) for t in text]
    ## =>
    ## ['えお「か「き」く「け」こ」さし', 'つて']
    ## ['', ']「「」」', 'あ「」', '「」い', '「う」']
    ## []
    
[/code]


CC BY-SA 3.0
2017-02-25T18:12:02.507
32947
2017-02-26T15:56:47.927
2017-02-26T15:56:47.927
32938
answer