

全文検索用にユーザー様がキーワードをGoogle検索のように、1つや、複数入力した場合、各キーワードを分割する方法を探しています。

半角及び全角スペースで区切られた下記の様な文字列を分割したいのですが、  
正規表現がわかりません。

### 分割条件

  1. 半角、全角、スペース区切り。
  2. 但し、ダブルクォーテーションで括られている文字列は中にスペースやタブなどの特殊文字が入っていても一つの文字列として扱う。  
従って、  
『ABC あいう"え "" おか きく"けこ -"おはよう"』を分割すると、  
『ABC』  
『あいう"え "" おか きく"けこ』 ← 途中に ""があるが、" "と間にスペースがないので連続した文字列と判断。  
『 -"おはよう"』  
の３つになります。

### 元の文字列の例

` きた A+!^"*-= " P" 雨 "" 雪 " "み ず た に" abc 時間123-"門`限" "やぶるな`

Java文字列リテラル表現では:

[code]

    String moto = "　きた　A+!^\"*-= \"　P\" 雨 \"\" 雪 \"　　　\"み　　ず た    に\"　　　　abc　時間123-\"門`限\"　 \"やぶるな";
    
[/code]

### 分割後の各文字列

  * `きた`
  * `A+!^"*-= "`
  * `P" 雨 "" 雪 "`
  * `"み ず た に"`
  * `abc`
  * `時間123-"門限"`
  * `"やぶるな` ←わざと最後のダブルクォーテーションを外してあります。

下記正規表現でできる？かもしれませんが、このコードをJAVAへ記述すると、  
実行以前の問題で、eclipsで記述文字列が正しくないエラーで赤く指摘状態になります。  
（恐らく`\`を追加したりしてエスケープ処理をしないといけないのですが、`\`を`/`や`"`の前にいくつ追加しても、文字列が正しくないというエラーで赤く指摘状態になります。）

[code]

    Pattern p = Pattern.compile("\s+("(?:"|[^"])*"?|[^"]\S*)");
    String[] result = p.split(moto);
    for (int i=0; i<result.length; i++){
        System.out.println("[" + result[i] + "]");
    }
    
[/code]

### 質問 １

正規表現は、正しいでしょうか？

### 質問 ２

Javaに記述するには、\を追加などして、どのように記述すれば、Eclipseが文字列が不正というエラーをださなくなるでしょうか？

やった見ている多数のパターンの一例ですが、

[code]

    Pattern p = Pattern.compile("\\s+(\"(?:\"|[^\"])*\"?|[^\"]\S*)");
    
[/code]

* * *

下記の様に \ を入れたところ、eclipsで赤文字で、文字列エラーがなくなりました。

[code]

    String[] result = test.split("\\s+(\"(?:\"|[^\"])*\"?|[^\"]\\S*)");
    for (int i=0; i<result.length; i++){
        out.print("[" + result[i] + "]");
    }
    
[/code]

但し、正規表現が悪いようで、下記の様に分割できませんでした。  
`test = "雨 "み ず"";`  
↓ 分割後  
`[雨 "み ず"]`


CC BY-SA 3.0
2016-09-01T00:43:42.143
28613
2016-09-07T14:38:07.837
2016-09-06T12:43:19.780
8000
17129
question
java 正規表現
ダブルクォーテーション含む文字列をスペースで分割するには


正規表現だと難しそうですね。  
意図とあっているかわからないですが、こんな感じですかね？

[code]

    import java.util.Iterator;
    
    public class Tokenizer implements Iterator<String> {
    
        private static final char DELIMITER_SPACE = ' ';
        private static final char DELIMITER_SPACE_JP = '　';
        private static final char DELIMITER_DOUBLE_QUOTE = '"';
    
        private String nextToken;
        private String target;
        private Character delimiter = null;
    
        private int pos = 0;
        private int start = 0;
    
        public Tokenizer(String target) {
            this.target = target;
            this.nextToken = getNextToken();
        }
    
        @Override
        public boolean hasNext() {
            return nextToken != null;
        }
    
        @Override
        public String next() {
            String next = this.nextToken;
            this.nextToken = getNextToken();
            return next;
        }
    
        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    
        private String getNextToken() {
            int size = target.length();
            while (pos < size) {
                char c = target.charAt(pos);
                pos++;
                int length = pos - start;
                if (isDelimitor(c, delimiter)) {
                    if (length > 1) {
                        boolean isDoubleQuote = isDoubleQuote(delimiter);
                        String token = getToken(isDoubleQuote, start, length, false);
                        if (isDoubleQuote) {
                            delimiter = null;
                        }
                        start = pos;
                        return token;
    
                    } else {
                        delimiter = c;
                        start = pos;
                    }
    
                } else if (c == DELIMITER_DOUBLE_QUOTE && length <= 1) {
                    delimiter = c;
                    start = pos;
    
                } else if ((c == DELIMITER_SPACE || c == DELIMITER_SPACE_JP) && length <= 1) {
                    if (delimiter == null || delimiter != DELIMITER_DOUBLE_QUOTE) {
                        delimiter = c;
                        start = pos;
                    }
                }
            }
    
            int length = pos - start;
            if (length > 0) {
                String token = getToken(isDoubleQuote(delimiter), start, length, true);
                start = pos;
                return token;
            }
            return null;
        }
    
        private boolean isDoubleQuote(Character delimiter) {
            return delimiter != null && delimiter == DELIMITER_DOUBLE_QUOTE;
        }
    
        private boolean isDelimitor(char c, Character delimiter) {
            if (delimiter == null) {
                return c == DELIMITER_SPACE || c == DELIMITER_SPACE_JP || c == DELIMITER_DOUBLE_QUOTE;
            } else {
                return c == delimiter;
            }
        }
    
        private String getToken(boolean isDoubleQuote, int start, int length, boolean isLast) {
            if (isDoubleQuote) {
                return target.substring(start - 1, start + length);
            } else {
                return target.substring(start, start + length - (isLast ? 0 : 1));
            }
        }
    }
    
[/code]

こうやって使って下さい。

[code]

    String moto = "　きた　A+!^\"*-=　　P\" 雨 \"\" 雪 \"　　　\"み　　ず た    に\"　　　　abc　時間123-\"門`限\"　 \"やぶるな";
    Tokenizer tokenizer = new Tokenizer(moto);
    while(tokenizer.hasNext()) {
        System.out.println("[" + tokenizer.next() + "]");
    }
    
[/code]

結果

> [きた]  
>  [A+!^"*-=]  
>  [P" 雨 "" 雪 "]  
>  ["み ず た に"]  
>  [abc]  
>  [時間123-"門`限"]  
>  ["やぶるな]

  
  
**UPDATE1**

プログラムの意図としては以下のようなものです。

  * 半角スペース、全角スペース、ダブルクォーテーションで区切る。
  * 左から右へ文字列を走査して区切る。
  * ある文字で区切ったら、次に同じ文字が出てくるまで区切らない。  
例）半角スペースで区切ったら、次に半角スペースが現れるところまで一つの文字列にする。

  * ただし、文字列の終わりまで走査して、同じ区切り文字が出てこなくても、一つの文字列とする。
  * ダブルコーテーションは結果の文字列の入れる。

**UPDATE2**

[code]

    String moto = " きた A+!^\"*-= P\" 雨 \"\" 雪 \" \"み ず た に\" abc 時間123-\"門`限\" \"やぶるな";
    
[/code]

でプログラムを実行すると次のような結果になります。

> [きた]  
>  [A+!^"*-=]  
>  [P"]  
>  [雨]  
>  [" 雪 "]  
>  ["み ず た に"]  
>  [abc]  
>  [時間123-"門`限"]  
>  ["やぶるな]


CC BY-SA 3.0
2016-09-01T14:39:45.080
28627
2016-09-06T03:26:37.260
2016-09-06T03:26:37.260
15228
15228
28613
answer


ほぼ解決策が見えてきているときに別解もなんなんですが、その仕様( _正解は、`[A+!^"*-=
P"]`_)ならば、正規表現でも実現できますよということで。

[code]

        String moto = " きた A+!^\"*-= P\" 雨 \"\" 雪 \" \"み ず た に\" abc 時間123-\"門`限\" \"やぶるな";
        Pattern p = Pattern.compile("((?:[^\\s　\"\\\\]|\\\\.|\"(?:\\\\.|[^\\\\\"])*(?:\"|$))+)");
        Matcher m = p.matcher(moto);
        while( m.find() ) {
            System.out.println("["+m.group(1)+"]");
        }
    
[/code]

出力:

[code]

    [きた]
    [A+!^"*-= P"]
    [雨]
    [""]
    [雪]
    [" "み]
    [ず]
    [た]
    [に" abc 時間123-"門`限" "やぶるな]
    
[/code]

質問文にある期待結果と随分違う結果に見えますが、最初の`A+!^"*-=`が`A+!^"*-=
P"`になって、`"`で挟まれる範囲が交互になることを考えると、 _★分割条件_ に従えばこの結果になるはずです。

アイデアとしては

  * 「空白と`"`, `\`」以外の文字
  * `\`+一文字
  * `"`から「`"`または文字列末」まで

をひとかたまりと考えて、最長一致(正規表現のデフォルト)でそのかたまりの連続を抜き出す、というものです。

お時間が取れるようでしたら、是非お試しください。

(少し前に書いたコードですが、「動作が違う」と言われたらどこを直すのか、かなり悩んでしまいそうです。やはりこの辺の処理はわかりやすさ優先にした方が良いかもしれません。)


CC BY-SA 3.0
2016-09-06T10:33:53.840
28724
2016-09-06T10:33:53.840
13972
28613
answer


### 正規表現

[Le Pere
d'OOさんの回答](https://ja.stackoverflow.com/a/28724/3054)から`\`の処理を抜いたものです。

[code]

    import java.util.regex.*;
    
    public class Main{
        public static void main(String[] args){
            String moto = "　きた　A+!^\"*-= \"　P\" 雨 \"\" 雪 \"　　　\"み　　ず た    に\"　　　　abc　時間123-\"門`限\"　 \"やぶるな";
    
            Pattern p = Pattern.compile("((?:[^\\s　\"]|\"[^\"]*(?:\"|$))+)");
            Matcher m = p.matcher(moto);
    
            while(m.find()){
                System.out.println("[" + m.group(1) + "]");
            }
        }
    }
    
[/code]

`((?:[^\\s \"]|\"[^\"]*(?:\"|$))+)` という正規表現は若干読み辛いかと思いますが、インデントを付けたり、

[code]

        Pattern p = Pattern.compile(
                "(" +
                    "(?:" +
                        "[^\\s　\"]|" +
                        "\"[^\"]*(?:\"|$)" +
                    ")+" +
                ")"
                );
    
[/code]

部分ごとに名前を付けて構築したり、

[code]

        // 通常文字:   空白、全角スペース、ダブルクォート以外の文字
        String nomal_char = "[^\\s　\"]";
    
        // クオートされた文字列:   ダブルクォートか行末に挟まれた、0文字以上の文字列
        String quoted_str = "\"[^\"]*(?:\"|$)";
    
        // トークン:   通常文字かクォートされた文字列、一つ以上
        String token = "(?:" + nomal_char + "|" + quoted_str + ")+";
    
        // トークンにマッチした部分はグループとして記憶させる
        Pattern p = Pattern.compile("(" + token + ")");
    
[/code]

対処法はあります。

### 似たケース

クォートを考慮して文字列を分割したい、という需要は多いですから、検索すると色々と出て来ます。

<https://stackoverflow.com/a/7804472/4368502>  
<https://stackoverflow.com/a/3366634/4368502>

クォート位置でも分割される、という点で今回とは少し違いますが参考にして下さい。  
Google検索の動作にはこれらの方が近そうです。  
質問の仕様はGoogle検索の入力欄よりも、シェル(sh, bashなど)のクォートに近いですね。

### 正規表現不使用

もしかしたら正規表現より簡潔になるかも、と思い書いてみましたが、そんな分けは無くやはり長くなってしまいました。  
解り易く拡張性を持たせて書けばもっと長くなりそうです。  
長いのが悪いとも思いませんが、これ位であれば、やはり正規表現を使ってよいのではないでしょうか。

[code]

    public class Main{
        public static void main(String[] args){
            String moto = "　きた　A+!^\"*-= \"　P\" 雨 \"\" 雪 \"　　　\"み　　ず た    に\"　　　　abc　時間123-\"門`限\"　 \"やぶるな";
    
            State state = State.DELIMITER;
            for(int i=0, start=0, length=moto.length(); i < length; i++){
                // 区切り文字:
                //  スペース、タブ、全角スペース、改行、用紙送り、
                //  キャリッジ・リターン、垂直タブ
                final String delimiters = " \t　\n\f\r\u000b";
                final String c = moto.substring(i, i+1);
    
                if(delimiters.contains(c)){
                    if(state == State.UNQUOTED){
                        out(moto.substring(start, i));
                        state = State.DELIMITER;
                    }
                }
                else{
                    if(state == State.DELIMITER){
                        start = i;
                        state = State.UNQUOTED;
                    }
    
                    if(c.equals("\"")){
                        state = (state == State.QUOTED) ?
                            State.UNQUOTED : State.QUOTED;
                    }
                }
    
                if(i == length-1 && state != State.DELIMITER){
                    out(moto.substring(start, i+1));
                }
            }
        }
    
    
        private enum State { DELIMITER, QUOTED, UNQUOTED }
    
        private static void out(String s){
            if(s.length() == 0){ return; }
            System.out.println("[" + s + "]");
        }
    }
    
[/code]


CC BY-SA 3.0
2016-09-07T14:20:29.597
28747
2016-09-07T14:38:07.837
2017-05-23T12:38:56.083
-1
3054
28613
answer