将这两个正则表达式合并成一个

11

我在 C# 中有以下代码:

public static bool IsAlphaAndNumeric(string s)
{
    return Regex.IsMatch(s, @"[a-zA-Z]+") 
        && Regex.IsMatch(s, @"\d+");
}
我想检查参数s是否至少包含一个字母一个数字,我编写了上面的方法来执行此操作。
但是,是否有一种方法可以将这两个正则表达式 ("[a-zA-Z]+""\d+") 组合成一个?

2
如果您只想验证其中至少一个存在,不要使用“+”运算符来匹配不必要的较长字符串。 - kennytm
2
我认为原始版本比大多数答案更优雅和易读。 - Kobi
3
在我看来,这个方法应该被称为 HasAlphaAndNumeric。你只是检查它是否 包含 每种字符类型中的一个;其余的字符可以是任何东西,也可以什么都没有。例如,A1!@#1%^&A()_都可以通过检查--这是你想要的吗? - Alan Moore
@Alan Moore:是的,您说得对;您建议的方法名比我的好。 - Andreas Grech
6个回答

15

对于使用 LINQ 的 C#:

return s.Any(Char.IsDigit) && s.Any(Char.IsLetter);

最坏情况下需要两次完整的字符串迭代。 - particle
@affan - 最糟糕的情况下,你必须检查每个字符两次;这对于任何可能的解决方案都是正确的。无论它在一个循环中发生还是在两个循环中发生,除了创建另一个字符迭代器之外没有区别-对于内存中的字符串来说,这最多只是一点点额外的开销。 - Kobi
@affan - 在你给予负评之前,请先阅读说明,并检查原始函数的功能。它说“至少一个字母和一个数字”。正如 @gnarf 向你解释的那样,你的代码是错误的。 - Kobi
1
如果原帖作者不打算使用正则表达式,那么这可能是最好的建议。 - Alan Moore
尽管这是一种非常简洁和清晰的方法,但我不能接受它,因为我要求的是一个正则表达式。我仍然给了你一个+1,因为你展示了LINQ的替代方案。 - Andreas Grech

12
@"^(?=.*[a-zA-Z])(?=.*\d)"

 ^  # From the begining of the string
 (?=.*[a-zA-Z]) # look forward for any number of chars followed by a letter, don't advance pointer
 (?=.*\d) # look forward for any number of chars followed by a digit)

使用两个正向先行断言确保在成功之前找到一个字母和一个数字。添加^只尝试从字符串开头向前查找一次。否则,正则表达式引擎会尝试在字符串的每个点进行匹配。


3

虽然不是完全符合您的要求,但假设我有更多时间。以下方法应该比正则表达式更快。

    static bool IsAlphaAndNumeric(string str) {
        bool hasDigits = false;
        bool  hasLetters=false;

        foreach (char c in str) {
            bool isDigit = char.IsDigit(c);
            bool isLetter = char.IsLetter(c);
            if (!(isDigit | isLetter))
                return false;
            hasDigits |= isDigit;
            hasLetters |= isLetter;
        }
        return hasDigits && hasLetters;
    }

为什么它这么快,让我们来看一下。以下是测试字符串生成器。它生成1/3的完全正确的字符串集和2/3个不正确的字符串集。在这2/3中,1/2都是字母,另一半都是数字。
    static IEnumerable<string> GenerateTest(int minChars, int maxChars, int setSize) {
        string letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        string numbers = "0123456789";            
        Random rnd = new Random();
        int maxStrLength = maxChars-minChars;
        float probablityOfLetter = 0.0f;
        float probablityInc = 1.0f / setSize;
        for (int i = 0; i < setSize; i++) {
            probablityOfLetter = probablityOfLetter + probablityInc;
            int length = minChars + rnd.Next() % maxStrLength;
            char[] str = new char[length];
            for (int w = 0; w < length; w++) {
                if (probablityOfLetter < rnd.NextDouble())
                    str[w] = letters[rnd.Next() % letters.Length];
                else 
                    str[w] = numbers[rnd.Next() % numbers.Length];                    
            }
            yield return new string(str);
        }
    }

以下是两种解决方案,一种是已编译版本,另一种是非编译版本。
class DarinDimitrovSolution
{
    const string regExpression = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$";
    private static readonly Regex _regex = new Regex(
        regExpression, RegexOptions.Compiled);

    public static bool IsAlphaAndNumeric_1(string s) {
        return _regex.IsMatch(s);
    }
    public static bool IsAlphaAndNumeric_0(string s) {
        return Regex.IsMatch(s, regExpression);
    }

以下是测试循环的主要内容。
    static void Main(string[] args) {

        int minChars = 3;
        int maxChars = 13;
        int testSetSize = 5000;
        DateTime start = DateTime.Now;
        foreach (string testStr in
            GenerateTest(minChars, maxChars, testSetSize)) {
            IsAlphaNumeric(testStr);
        }
        Console.WriteLine("My solution : {0}", (DateTime.Now - start).ToString());

        start = DateTime.Now;
        foreach (string testStr in
            GenerateTest(minChars, maxChars, testSetSize)) {
            DarinDimitrovSolution.IsAlphaAndNumeric_0(testStr);
        }
        Console.WriteLine("DarinDimitrov  1 : {0}", (DateTime.Now - start).ToString());

        start = DateTime.Now;
        foreach (string testStr in
            GenerateTest(minChars, maxChars, testSetSize)) {
            DarinDimitrovSolution.IsAlphaAndNumeric_1(testStr);
        }
        Console.WriteLine("DarinDimitrov(compiled) 2 : {0}", (DateTime.Now - start).ToString());

        Console.ReadKey();
    }

以下是结果。
My solution : 00:00:00.0170017    (Gold)
DarinDimitrov  1 : 00:00:00.0320032  (Silver medal) 
DarinDimitrov(compiled) 2 : 00:00:00.0440044   (Gold)

因此,第一种解决方案是最好的。在发布模式下,还有一些遵循规范的结果。
   int minChars = 20;
   int maxChars = 50;
   int testSetSize = 100000;

My solution : 00:00:00.4060406
DarinDimitrov  1 : 00:00:00.7400740
DarinDimitrov(compiled) 2 : 00:00:00.3410341 (now that very fast)

我再次检查了使用RegexOptions.IgnoreCase标志的情况,其余参数与上述相同。

My solution : 00:00:00.4290429 (almost same as before)
DarinDimitrov  1 : 00:00:00.9700970 (it have slowed down )
DarinDimitrov(compiled) 2 : 00:00:00.8440844 ( this as well still fast but look at .3 in last result)

在gnarf提到我的算法存在问题后,我进行了更改。它之前只检查字符串是否只包含字母和数字,现在它检查字符串至少应该有一个字符和一个数字。

    static bool IsAlphaNumeric(string str) {
        bool hasDigits = false;
        bool hasLetters = false;

        foreach (char c in str) {
            hasDigits |= char.IsDigit(c);
            hasLetters |= char.IsLetter(c);
            if (hasDigits && hasLetters)
                return true;
        }
        return false;
    }

结果

My solution : 00:00:00.3900390 (Goody Gold Medal)
DarinDimitrov  1 : 00:00:00.9740974 (Bronze Medal)
DarinDimitrov(compiled) 2 : 00:00:00.8230823 (Silver)

我的速度快得多。

我已经发布了性能结果。在我的回答中告诉你有时间。 - particle
嗯,我没有注意到这一点。但是结论是编译后的正则表达式更快,尽管它需要.NET框架进行汇编的初始编译,并且应该在某些静态构造函数中完成。有人能否改进我的方法? - particle
2
此外,您的版本检查所有字符是否为数字或数字... OP仅测试它是否具有字母和数字字符...也许尝试调整循环,一旦找到每个字符就返回true,它可能会赶上编译版本的正则表达式(它们不检查整个字符串,而是扫描一个字母,然后扫描一个数字。最糟糕的测试用例是一个字符串,其中仅包含一个字母在最后,没有数字,在正则表达式引擎中需要最长时间。 - gnarf
2
维护那个庞然大物的时间性能如何? - Andrew Dyster
3
为什么要保留不准确的版本?在你回答的前半段提到的所有函数都不能满足OP的需求,我会重新编写并使用能够返回正确答案的方法进行测试。 - gnarf
显示剩余5条评论

3

如果你所使用的系统只接受一个正则表达式,你可以使用[a-zA-Z].*[0-9]|[0-9].*[a-zA-Z],但我只建议在这种情况下使用。我无法想象这比不带替换的两个简单模式更有效。


2
private static readonly Regex _regex = new Regex(
    @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$", RegexOptions.Compiled);

public static bool IsAlphaAndNumeric(string s)
{
    return _regex.IsMatch(s);
}

如果您想忽略大小写,您可以使用RegexOptions.Compiled | RegexOptions.IgnoreCase

对于 OP,请在此页面上查找正向先行断言:http://msdn.microsoft.com/en-us/library/1400241x(VS.85).aspx - Aryabhatta
1
这个正则表达式只匹配包含小写字母和大写字母的字符串... - danielschemmel
此外,它将需要使用RegexOptions.Singleline选项或不匹配包含三个必需字符(大写字母、小写字母和数字)之一前有换行符的字符串。 - danielschemmel
3
如果您传递了 RegexOptions.IgnoreCase,则无需使用 (?=.*[A-Z]) 的 lookahead。 - kennytm

0

以下不仅比其他向前查看结构更快,而且(在我看来)更接近需求:

[a-zA-Z\d]((?<=\d)[^a-zA-Z]*[a-zA-Z]|[^\d]*\d)

在我的(虽然粗略的)测试中,它运行所需时间比其他正则表达式解决方案少了一半,并且具有这样的优点:它不会关心输入字符串中的换行符。 (如果由于某种原因它应该关心,那么如何包含它是显而易见的)。

以下是它的工作原理:

步骤1:它匹配一个单个字符(我们称之为c),该字符是数字或字母。
步骤2:它进行回溯以检查c是否为数字。 如果是:
步骤2.1:它允许无限数量的非字母字符,后跟一个字母。 如果匹配成功,则有一个数字(c)后跟一个字母。
步骤2.2:如果c不是数字,则必须是字母(否则它不会被匹配)。 在这种情况下,我们允许无限数量的非数字,后跟一个数字。 这意味着我们有一个字母(c)后跟一个数字。


从逻辑上讲,这与匿名回答类似,但更为复杂。你确定这很快吗?如果失败了,它不会测试每一个匹配的字母吗?(例如,600个“X”) - Kobi
与@affan的答案一样,这种做法根本不值得花费精力。人们过于担心正则表达式的性能问题。 - Alan Moore
@匿名答案将匹配第一个字母之前的任何字符两次,如果第一个分支失败,因为第二个分支会回溯到最开始。如果您可以合理地确定输入字符串在开头附近有一个字母,它将导致相同的性能(并且在替换点后甚至具有相同的含义)。--还要感谢您放置了缺失的插入符号 - 我不知道我在发布过程中如何杀死它;) - danielschemmel
哦,至于担心正则表达式的性能:我在这里只是为了好玩,而不是为了赚钱 ;) - danielschemmel
你可以通过在正则表达式前面添加 ^(?>[^A-Za-z0-9]*) 来完全避免回溯问题。这样做后,我认为回顾将不再起到作用。为了获得最佳性能,我会选择 ^(?>[^A-Za-z0-9]*)(?:[a-zA-Z](?>[^0-9]*)[0-9]|[0-9](?>[^A-Za-z]*)[a-zA-Z])。如果我担心性能的话... ;) - Alan Moore

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