正则表达式 - 检查输入是否仍有可能匹配

31

我们有这样的正则表达式:

var regexp = /^one (two)+ three/;

因此,只有像"one two three""one two three four""one twotwo three"等字符串才会与其匹配。

但是,如果我们有像这样的字符串:

"one " - 仍然“有希望”,也许很快就可以匹配成功

但是这个字符串:"one three"无论我们做什么都不会匹配成功。

有没有办法检查给定的字符串是否有可能成为匹配项?

我需要它来在写作时为一些提示,当我想推荐以给定输入开头的所有选项时(我使用的正则表达式非常长,我真的不想去修改它们)。


换句话说 - 我想检查字符串是否在检查期间已经结束,并且没有遇到任何“不匹配”的情况。

更直白的说 - 答案将在不匹配的原因内。如果原因是字符串的结尾- 那么它有希望被匹配成功。但是,我不知道如何检查某个字符串为什么不匹配。


1
你是否正在寻找一个函数,它可以用数字表示部分匹配的完整性或者在输入不匹配时指示无匹配?因此对于 "one" 它将返回 .3076923 (4/13),但对于 "one three" 它将返回 -1? - Jason Aller
似乎最好的方法是编写一个完整的库来实现这一点,并满足JS正则表达式可以提供的所有不同情况... - Jerry
5
非常有趣的问题。你可以将你的表达式改写成以下形式:^o(?:n(?:e(?: (?:(?:two)*(?:t(?:w(?:o(?: (?:t(?:h(?:r(?:e(?:e.*?)?)?)?)?)?)?)?)?)?)?)?)?)?$,以检查文本是否有前途。如果你的表达式不太复杂,我相信可以自动从主表达式派生有前途的正则表达式。 - Ulugbek Umirov
1
你是否与JavaScript有婚约?这个问题经常被问到,但除非在正则表达式实现中内置了该能力,否则是不可能的。通常称为部分匹配,据我所知,Java和Boost是唯一支持它的版本。 - Alan Moore
@AlanMoore FYI PCRE 也支持此功能 - Lucas Trzesniewski
10个回答

44

这是一种名为部分匹配的正则表达式特性,在PCRE、Boost、Java等多个正则表达式引擎中可用,但在JavaScript中不可用。

Andacious的回答展示了一种非常好的方法来克服这个限制,我们只需要自动化一下。

好的...接受挑战 :)

幸运的是,JavaScript具有非常简单的语法和非常有限的正则表达式功能集合,因此我编写了一个简单的解析器和即时转换器来完成此任务,基于MDN上列出的特性。此代码已更新以处理ES2018特性。

一些注意点:

  • 这会产生一个几乎总是匹配空字符串的正则表达式。因此,当exec的结果为null或第一个元素为空字符串的数组时,失败的部分匹配就会发生。
  • 否定先行断言保持不变。我认为这是正确的做法。唯一导致匹配失败的方式是通过它们(例如在正则表达式中放置(?!))和锚点(^$)。正先行和负先行断言也保持不变。
  • 解析器假定输入模式有效:首先无法从无效模式创建RegExp对象。如果引入新的正则表达式特性,这可能会在将来发生故障。
  • 此代码不能正确处理反向引用:^(\w+)\s+\1$不会对hello hel进行部分匹配。

RegExp.prototype.toPartialMatchRegex = function() {
    "use strict";
    
    var re = this,
        source = this.source,
        i = 0;
    
    function process () {
        var result = "",
            tmp;

        function appendRaw(nbChars) {
            result += source.substr(i, nbChars);
            i += nbChars;
        };
        
        function appendOptional(nbChars) {
            result += "(?:" + source.substr(i, nbChars) + "|$)";
            i += nbChars;
        };

        while (i < source.length) {
            switch (source[i])
            {
                case "\\":
                    switch (source[i + 1])
                    {
                        case "c":
                            appendOptional(3);
                            break;
                            
                        case "x":
                            appendOptional(4);
                            break;
                            
                        case "u":
                            if (re.unicode) {
                                if (source[i + 2] === "{") {
                                    appendOptional(source.indexOf("}", i) - i + 1);
                                } else {
                                    appendOptional(6);
                                }
                            } else {
                                appendOptional(2);
                            }
                            break;

                        case "p":
                        case "P":
                            if (re.unicode) {
                                appendOptional(source.indexOf("}", i) - i + 1);
                            } else {
                                appendOptional(2);
                            }
                            break;

                        case "k":
                            appendOptional(source.indexOf(">", i) - i + 1);
                            break;
                            
                        default:
                            appendOptional(2);
                            break;
                    }
                    break;
                    
                case "[":
                    tmp = /\[(?:\\.|.)*?\]/g;
                    tmp.lastIndex = i;
                    tmp = tmp.exec(source);
                    appendOptional(tmp[0].length);
                    break;
                    
                case "|":
                case "^":
                case "$":
                case "*":
                case "+":
                case "?":
                    appendRaw(1);
                    break;
                    
                case "{":
                    tmp = /\{\d+,?\d*\}/g;
                    tmp.lastIndex = i;
                    tmp = tmp.exec(source);
                    if (tmp) {
                        appendRaw(tmp[0].length);
                    } else {
                        appendOptional(1);
                    }
                    break;
                    
                case "(":
                    if (source[i + 1] == "?") {
                        switch (source[i + 2])
                        {
                            case ":":
                                result += "(?:";
                                i += 3;
                                result += process() + "|$)";
                                break;
                                
                            case "=":
                                result += "(?=";
                                i += 3;
                                result += process() + ")";
                                break;
                                
                            case "!":
                                tmp = i;
                                i += 3;
                                process();
                                result += source.substr(tmp, i - tmp);
                                break;

                            case "<":
                                switch (source[i + 3])
                                {
                                    case "=":
                                    case "!":
                                        tmp = i;
                                        i += 4;
                                        process();
                                        result += source.substr(tmp, i - tmp);
                                        break;

                                    default:
                                        appendRaw(source.indexOf(">", i) - i + 1);
                                        result += process() + "|$)";
                                        break;        
                                }
                                break;
                        }
                    } else {
                        appendRaw(1);
                        result += process() + "|$)";
                    }
                    break;
                    
                case ")":
                    ++i;
                    return result;
                    
                default:
                    appendOptional(1);
                    break;
            }
        }
        
        return result;
    }
    
    return new RegExp(process(), this.flags);
};






// Test code
(function() {
    document.write('<span style="display: inline-block; width: 60px;">Regex: </span><input id="re" value="^one (two)+ three"/><br><span style="display: inline-block; width: 60px;">Input: </span><input id="txt" value="one twotw"/><br><pre id="result"></pre>');
    document.close();

    var run = function() {
        var output = document.getElementById("result");
        try
        {
            var regex = new RegExp(document.getElementById("re").value);
            var input = document.getElementById("txt").value;
            var partialMatchRegex = regex.toPartialMatchRegex();
            var result = partialMatchRegex.exec(input);
            var matchType = regex.exec(input) ? "Full match" : result && result[0] ? "Partial match" : "No match";
            output.innerText = partialMatchRegex + "\n\n" + matchType + "\n" + JSON.stringify(result);
        }
        catch (e)
        {
            output.innerText = e;
        }
    };

    document.getElementById("re").addEventListener("input", run);
    document.getElementById("txt").addEventListener("input", run);
    run();
}());

我进行了一些测试,似乎工作得很好,如果你发现任何错误,请告诉我。


1
@Patrick 哦,好的抱歉。如果在末尾添加点,则仍然是完全匹配,因为您的模式未锚定。将您的模式包装在“^”和“$”之间以锚定它。当寻找部分匹配时,这可能是您始终想要的。 - Lucas Trzesniewski
1
FYI,此代码在进入命名捕获组时会进入无限循环。例如,这是我的正则表达式的一部分,使用了命名反向引用:(?<city>KAT|BYT)-[A-Z]{2,}/00\d{4}/%y/\k<city>。 - Tomasz Pala
1
@TomaszPala 谢谢你的信息!当我编写那段代码时,JS 中并不存在命名捕获组,这就解释了为什么它不起作用。我不确定是否应该更新此答案以处理新功能(并在将来继续这样做),还是将其保留为原样。另外,反向引用未被正确处理(遗憾)。 - Lucas Trzesniewski
1
@TomaszPala 我更新了代码以处理新的ES2018功能。现在应该可以处理您的模式,除了反向引用。 - Lucas Trzesniewski
1
我必须说,我对结果非常印象深刻!命名反向引用必须以完整捕获形式输入,但对于这样的短期解决方案来说,这是完全可以理解的:) 赞! - Tomasz Pala
显示剩余14条评论

16

我之前使用过的另一个有趣选项是将期望匹配的每个字符与$符号进行OR运算。这种方法可能并不适用于所有情况,但对于需要查找特定字符并在每个字符上进行部分匹配的情况,这种方法非常有效。

例如(在Javascript中):

var reg = /^(o|$)(n|$)(e|$)(\s|$)$/;

reg.test('')      -> true;
reg.test('o')     -> true;
reg.test('on')    -> true;
reg.test('one')   -> true;
reg.test('one ')  -> true;
reg.test('one t') -> false;
reg.test('x')     -> false;
reg.test('n')     -> false;
reg.test('e')     -> false;
reg.test(' ')     -> false;

虽然这个正则表达式不是最漂亮的,但是它是可重复的,所以如果你因为某些原因需要动态生成它,你知道了一般的模式。

同样的模式也可以应用于整个单词,但可能并不那么有用,因为他们不能逐个输入以到达这些点。

var reg = /^(one|$)(\stwo|$)$/;

reg.test('')        -> true;
reg.test('one')     -> true;
reg.test('one ')    -> false;
reg.test('one two') -> true;

2
+1. 这并不能解决原帖作者的问题,但它是一种很棒的技巧。在适用的情况下,它可能会成为救命稻草。 - Alan Moore
3
我根据你的想法回答了我的答案,希望你不介意 :) - Lucas Trzesniewski
1
这是一个很棒的答案。多么巧妙的技术啊。 - Lars Holdaas
1
这真是太聪明了。我正准备卷起袖子,编写我的答案:https://stackoverflow.com/questions/12015612/convert-a-regular-expression-a-that-accepts-a-set-of-strings-to-one-that-accepts/67570793#answer-67570793(我非常自豪),然后我发现@LucasTrzesniewski的实现方式,我想:“为什么这么简短?”它太简单了。太棒了。 - Don Hatch
@AlanMoore,它在什么方面没有解决OP的问题? - Don Hatch
@DonHatch,我原以为OP的真正正则表达式会更复杂,但现在看到了他自己的答案,似乎我错了。没关系。 - Alan Moore

2
我发现了一个npm包,其中包含有JavaScript实现的RegEx,支持增量正则表达式匹配:https://www.npmjs.com/package/incr-regex-package。看起来值得一试。它可以针对给定的输入报告DONEMOREMAYBEFAILED结果。
此外,这里还有一个React输入组件的示例实现:https://www.npmjs.com/package/react-maskedinput。它使用{RXInputMask} from incr-regex-package,提供了更加用户友好的交互方式,以便与RegEx库进行交互。

0

我还不完全确定你在问什么,但你也可以像这样嵌套它们:

var regexp = /^(one)+((\s+two)+((\s+three)+((\s+four)+)?)?)?$/;

匹配:

  • 一个
  • 一二二
  • 一二二三
  • 一二二三三四

不匹配:

  • 一三

0

编辑:在您编辑了澄清帖子之后,这个答案可能与您的具体问题无关。将其保留作为参考。


关于在正则表达式中快速失败,一旦你知道找不到任何内容:

你的正则表达式中的锚点意味着一旦正则引擎到达“three”的“h”,它将停止寻找匹配项,并且不会尝试在第二个字符上开始匹配。在这种情况下,我认为你已经做得很好了(但它已经是线性复杂度,所以并不那么糟糕)。

在其他情况下,我相信你有一些通常的最佳实践可以学习,以便在你知道无法找到匹配项时尽快失败。

如果你还不知道占有量词,你可以看一下,但还有许多其他技巧...


0

你最初的问题只是测试一个字符串在另一个字符串中的位置,特别是开头。最快的方法是在匹配字符串上使用 substr,然后跟着使用 indexOf。我已经更新了我的原始答案以反映这一点:

function check(str){
  return 'one two three'.substr(0, str.length) === str;
};
console.log(check('one'));           // true
console.log(check('one two'));       // true
console.log(check('one two three')); // true
console.log(check('one three'));     // false

如果您需要不区分大小写,最快的方法仍然是将匹配和输入字符串简单地转换为小写。 (如果感兴趣,这里有一个 jsperf 测试用例,测试了 substrindexOfRegExp 在不区分大小写的情况下查找字符串开头的性能:http://jsperf.com/substr-vs-indexof-vs-regex

它能处理更复杂的内容吗(特殊字符、闭包等)? - Adam Pietrasiak
1
@Kluska000 不会的。这只处理一个非常特定的情况,即您的正则表达式字面上是 ^something literal here,在这种情况下,您可以做 return rgxStr.substr(0,str.length) == str。因此,我会对这个答案进行投票。 - Niet the Dark Absol
@NiettheDarkAbsol 哈哈,是的。在仔细思考之前回答得太快了。我已更新答案以反映真实的答案性质。谢谢。 - rgthree

0
基于@Andacious的回答,我自己做了一个更高级的方法,并且似乎可以工作。
我制作了一种修改正则表达式以接受有前途的答案的方法。
它将任何文字字符替换为"(?:OLD_LETTER|$)",因此k变成(?:k|$)寻找匹配的字母或输入结束。
它还查找不需要替换的部分,如{1,2},并将它们保持原样。
我相信它还不完整,但非常容易添加新的检查规则和使用any_sign或者输入结束的主要技巧似乎在任何情况下都可以工作,因为它是字符串结尾匹配,不会继续匹配,所以基本上我们需要对主要的正则表达式进行这样的修改:任何文字字符或字符组都必须有替代的|$以及每个语法(有时似乎也有文字字符)都不能被破坏。
RegExp.prototype.promising = function(){
    var source = this.source;
    var regexps = {
        replaceCandidates : /(\{[\d,]\})|(\[[\w-]+\])|((?:\\\w))|([\w\s-])/g,   //parts of regexp that may be replaced
        dontReplace : /\{[\d,]\}/g,     //dont replace those
    }

    source =  source.replace(regexps.replaceCandidates, function(n){ 
        if ( regexps.dontReplace.test(n) ) return n;
        return "(?:" + n + "|$)";
    });


    source = source.replace(/\s/g, function(s){
        return "(?:" + s + "|$)";
    });

    return new RegExp(source);

}

测试 在 jsFiddle 上


-1

这实际上是一个非常有趣的问题。

个人而言,我会使用RegExp构造函数来分解查询:

var input = "one ";
var query = "^one two three";
var q_len = query.length;
var match;
for( var i=1; i<=q_len; i++) {
    match = input.match(new RegExp(query.substr(0,i));
    if( !match) {alert("Pattern does NOT match"); break;}
    if( match[0].length == input.length) {alert("Input is promising!"); break;}
    // else continue to the next iteration
}

显然,你可以事先处理“完全匹配”的情况,以避免整个循环的问题。如果整个模式与输入相匹配,那么你就万事大吉了。

编辑:我刚意识到这对于分组等是不起作用的。它将因为格式不正确的正则表达式而失败,但我希望它能为你的查询提供基础。


我已经在我的问题中编辑了正则表达式,所以它也使用了特殊字符。它还能正常工作吗? - Adam Pietrasiak
@Kluska000 我已经编辑了我的回答;)不幸的是,如果存在任何形式的分组,它将无法工作,因为生成的正则表达式将是错误的并导致错误。您可以将循环内容包装在 try...catch 块中,以简单地跳过正则表达式无效的迭代。 - Niet the Dark Absol
假设所有查询或其中大部分都被分组,这种“尝试捕获”将在大多数情况下被捕获,因此会有“下一个工作查询”的“大步骤”。 - Adam Pietrasiak
是的。我认为在不大量使用解析器等工具的情况下,没有通用的解决此问题的方法。 - Niet the Dark Absol

-1

这里有一个简单的概念证明,可以在jsFiddle中看到。你只需要逆向循环所提出的正则表达式,并寻找最长的子匹配。

注意:这个方法已知存在一个问题,就是它不能很好地处理组。例如,它会说foo bar b根本不匹配foo( bar)+,然而它应该说仍然有希望。这个问题可以通过对下面这行代码更加创造性地进行修复来解决:

temp_re = new RegExp(regex_part+'$'); // make sure this partial match ends in a piece that could still match the entire regex

基本上,您需要解析部分正则表达式以查看它是否以组结尾,然后递归地检查该结尾组的部分匹配。这相当复杂,因此我不会在我的演示中涉及它。
JavaScript(仅用于演示目的使用jQuery):
var strings = {
    matches: "Matches!",
    stillhope: "There's still hope...",
    mismatch: "Does not match",
    empty: "No text yet"
};

// Object to handle watching for partial matches
function PartialRegexMonitor(regex, input_id) {
    var self = this;
    this.relen = regex.length;
    $('body').on('keyup', '#'+input_id, function() {
         self.update_test_results(regex, input_id);
    });
}

PartialRegexMonitor.prototype.update_test_results = function(regex, input_id) {
    var input = $('#'+input_id).val(),
        matches = find_partial_matches(regex, input),
        span = $('#'+input_id).siblings('.results');
    span.removeClass('match');
    span.removeClass('stillhope');
    span.removeClass('mismatch');
    span.removeClass('empty');
    span.addClass(matches.type)
        .html(strings[matches.type]);
}

// Test a partial regex against a string
function partial_match_tester(regex_part, str) {
    var matched = false;
    try {
        var re = new RegExp(regex_part, 'g'),
            matches = str.match(re),
            match_count = matches.length,
            temp_re;
        for(var i = 0; i < match_count; i++) {
            temp_re = new RegExp(regex_part+'$'); // make sure this partial match ends in a piece that could still match the entire regex
            matched = temp_re.test(str);
            if(matched) break;
        }
    }
    catch(e) {
    }
    return matched;
}

// Find the longest matching partial regex
function find_partial_matches(regex, str) {
    var relen = regex.length,
        matched = false,
        matches = {type: 'mismatch',
                   len: 0},
        regex_part = '';
    if(str.length == 0) {
        matches.type = 'empty';
        return matches;
    }

    for(var i=relen; i>=1; i--) {
        if(i==1 && str[0] == '^') { // don't allow matching against only '^'
            continue;
        }
        regex_part = regex.substr(0,i);
        // replace {\d}$ with {0,\d} for testing
        regex_part = regex_part.replace(/\{(\d)\}$/g, '{0,$1}');

        matched = partial_match_tester(regex_part, str);
        if(matched) {
            matches.type = (i==relen ? 'matches' : 'stillhope');
            console.log(matches.type + ": "+regex.substr(0,i)+" "+str);
            matches.len = i;
            break;
        }
    }
    return matches;
}

// Demo
$(function() {
    new PartialRegexMonitor('foo bar', 'input_0');
    new PartialRegexMonitor('^foo bar$', 'input_1');
    new PartialRegexMonitor('^fo+(\\s*b\\S[rz])+$', 'input_2');
    new PartialRegexMonitor('^\\d{3}-\\d{3}-\\d{4}$', 'input_3');
});

演示用的HTML代码:

<p>
Test against <span class="regex">foo bar</span>:<br/>
    <input type="text" id="input_0" />
    <span class="results empty">No text yet</span>
</p>
<p>
    Test against <span class="regex">^foo bar$</span>:<br/>
    <input type="text" id="input_1" />
    <span class="results empty">No text yet</span>
</p>
<p>
    Test against <span class="regex">^fo+(\s*b\S[rz])+$</span> (e.g., "foo bar", "foo baz", "foo bar baz"):<br/>
    <input type="text" id="input_2" />
    <span class="results empty">No text yet</span>
</p>
<p>
    Test against <span class="regex">^\d{3}-\d{3}-\d{4}$</span>:<br/>
    <input type="text" id="input_3" />
    <span class="results empty">No text yet</span>
</p>

演示用的CSS

.empty {
    background-color: #eeeeee;
}
.matches {
    background-color: #ccffcc;
    font-weight: bold;
}
.stillhope {
    background-color: #ccffff;
}
.mismatch {
    background-color: #ffcccc;
    font-weight: bold;
}
.regex {
    border-top:1px solid #999;
    border-bottom:1px solid #999;
    font-family: Courier New, monospace;
    background-color: #eee;
    color: #666;
}

演示的示例截图

enter image description here


但这也意味着当我认为它不应该时,foo bad将通过并返回true。 - Jerry
是的,我想我误读了问题。我会更新这个答案。 - elixenide
我正在玩弄你的解决方案。 最后一个不起作用。 我在考虑修改正则表达式,以便将foo替换为foo | fo | f,找到第一个最长的可能匹配结果。 - Adam Pietrasiak
这个应该还能用。就像我说的,当它需要匹配末尾的一组时,可能需要进行微调,但希望它能给你一个开始的地方。 :) - elixenide

-2

不确定是否有一种方法可以使用正则表达式来完成这个操作,而又不需要创建一个非常复杂的模式。但是,如果你只是想检查一个字符串,那么可以像这样做:

function matchPartialFromBeginning(pattern, value) {
    if(pattern.length == value.length)
        return pattern == value;
    if(pattern.length > value.length)
        return pattern.substr(0, value.length) == value;
    return false;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接