F#中使用主动模式映射正则表达式匹配

22
我在使用Active Patterns和正则表达式方面发现了这篇有用的文章:http://www.markhneedham.com/blog/2009/05/10/f-regular-expressionsactive-patterns/ 文章中使用的原始代码片段如下:
open System.Text.RegularExpressions

let (|Match|_|) pattern input =
    let m = Regex.Match(input, pattern) in
    if m.Success then Some (List.tl [ for g in m.Groups -> g.Value ]) else None

let ContainsUrl value = 
    match value with
        | Match "(http:\/\/\S+)" result -> Some(result.Head)
        | _ -> None

这将让您知道是否至少找到一个URL,以及该URL是什么(如果我正确地理解了代码片段)。

然后,在评论部分,Joel建议进行以下修改:

Alternative, since a given group may or may not be a successful match:

List.tail [ for g in m.Groups -> if g.Success then Some g.Value else None ]

Or maybe you give labels to your groups and you want to access them by name:

(re.GetGroupNames()
 |> Seq.map (fun n -> (n, m.Groups.[n]))
 |> Seq.filter (fun (n, g) -> g.Success)
 |> Seq.map (fun (n, g) -> (n, g.Value))
 |> Map.ofSeq)
尝试将所有内容结合在一起后,我得到了以下代码:
let testString = "http://www.bob.com http://www.b.com http://www.bob.com http://www.bill.com"

let (|Match|_|) pattern input =
    let re = new Regex(pattern)
    let m = re.Match(input) in
    if m.Success then Some ((re.GetGroupNames()
                                |> Seq.map (fun n -> (n, m.Groups.[n]))
                                |> Seq.filter (fun (n, g) -> g.Success)
                                |> Seq.map (fun (n, g) -> (n, g.Value))
                                |> Map.ofSeq)) else None

let GroupMatches stringToSearch = 
    match stringToSearch with
        | Match "(http:\/\/\S+)" result -> printfn "%A" result
        | _ -> ()


GroupMatches testString;;

当我在交互式会话中运行我的代码时,输出如下:
map [("0", "http://www.bob.com"); ("1", "http://www.bob.com")]
我试图实现的结果应该类似于这样:
map [("http://www.bob.com", 2); ("http://www.b.com", 1); ("http://www.bill.com", 1);]

基本上是对每个唯一匹配项进行映射,然后计算在文本中找到该特定匹配字符串的次数。

如果您认为我走了错误的道路,请随时建议完全不同的方法。我对Active Patterns和正则表达式都比较新,所以我甚至不知道从哪里开始修复它。

我还想到了这个,基本上是我在C#中要做的事情翻译成F#。

let testString = "http://www.bob.com http://www.b.com http://www.bob.com http://www.bill.com"

let matches =
    let matchDictionary = new Dictionary<string,int>()
    for mtch in (Regex.Matches(testString, "(http:\/\/\S+)")) do
        for m in mtch.Captures do
            if(matchDictionary.ContainsKey(m.Value)) then
                matchDictionary.Item(m.Value) <- matchDictionary.Item(m.Value) + 1
            else
                matchDictionary.Add(m.Value, 1)
    matchDictionary

运行时将返回以下内容:

val matches : Dictionary = dict [("http://www.bob.com", 2); ("http://www.b.com", 1); ("http://www.bill.com", 1)]

这基本上是我想要的结果,但我正在尝试学习函数式方法来完成此操作,我认为这应该包括活动模式。如果将其“功能化”比我的第一次尝试更有意义,请随意尝试。

提前感谢,

鲍勃

1个回答

26

我认为你探索的所有内容都很有趣,(局部)主动模式用于正则表达式匹配确实非常有效,特别是当您想要将一个字符串与多个替代情况进行匹配时。对于更复杂的正则表达式主动模式,我唯一建议的是给它们更具描述性的名称,可能建立一组不同目的的正则表达式主动模式。

至于你的C#到F#的例子,你完全可以拥有一个没有主动模式的函数式解决方案,例如:

let testString = "http://www.bob.com http://www.b.com http://www.bob.com http://www.bill.com"

let matches input =
    Regex.Matches(input, "(http:\/\/\S+)") 
    |> Seq.cast<Match>
    |> Seq.groupBy (fun m -> m.Value)
    |> Seq.map (fun (value, groups) -> value, (groups |> Seq.length))

//FSI output:
> matches testString;;
val it : seq<string * int> =
  seq
    [("http://www.bob.com", 2); ("http://www.b.com", 1);
     ("http://www.bill.com", 1)]

更新

这个特定的示例之所以能够在没有主动模式的情况下正常工作,是因为1)您仅测试一个模式,2)您正在动态处理匹配项。

对于一个实际的使用主动模式的例子,让我们考虑这样一种情况:1)我们正在测试多个正则表达式,2)我们正在测试是否有一个正则表达式匹配多个组。对于这些场景,我使用以下两个主动模式,它们比你展示的第一个Match活动模式更加通用(我不会丢弃匹配中的第一个组,并且我返回一个包含组对象的列表,而不仅仅是它们的值——一个使用编译的正则表达式选项用于静态正则表达式模式,一个使用解释的正则表达式选项用于动态正则表达式模式)。因为.NET正则表达式API非常丰富,您从主动模式中返回的内容取决于您发现有用的东西。但是返回一个list是很好的,因为然后您就可以在该列表上进行模式匹配。

let (|InterpretedMatch|_|) pattern input =
    if input = null then None
    else
        let m = Regex.Match(input, pattern)
        if m.Success then Some [for x in m.Groups -> x]
        else None

///Match the pattern using a cached compiled Regex
let (|CompiledMatch|_|) pattern input =
    if input = null then None
    else
        let m = Regex.Match(input, pattern, RegexOptions.Compiled)
        if m.Success then Some [for x in m.Groups -> x]
        else None

还要注意这些活动模式将Null视为不匹配,而不是抛出异常。

好的,假设我们想要解析名字。我们有以下要求:

  1. 必须包含名和姓
  2. 可以有中间名
  3. 名、可选中间名和姓按照这个顺序用一个空格隔开
  4. 姓名的每一部分都可以由至少一个或多个字母或数字组成
  5. 输入可能格式不正确

首先,我们定义以下记录:

type Name = {First:string; Middle:option<string>; Last:string}

然后,我们可以在解析名称的函数中相当有效地使用我们的正则表达式活动模式:

let parseName name =
    match name with
    | CompiledMatch @"^(\w+) (\w+) (\w+)$" [_; first; middle; last] ->
        Some({First=first.Value; Middle=Some(middle.Value); Last=last.Value})
    | CompiledMatch @"^(\w+) (\w+)$" [_; first; last] ->
        Some({First=first.Value; Middle=None; Last=last.Value})
    | _ -> 
        None

请注意,我们在这里获得的一个关键优势是,就像通常的模式匹配一样,我们能够同时测试输入是否与正则表达式模式匹配,并且如果匹配成功,可以分解返回的组列表。


3
@Beaker - 当然没问题,我刚醒来,我会喝点咖啡然后马上开始。 - Stephen Swensen
1
@Beaker - 非常感谢!我曾考虑过开设一个通用博客,但到目前为止,SO一直是我的主要渠道。您可以在我的个人资料中看到我还有其他几个出口。 - Stephen Swensen
1
正如 http://blogs.msdn.com/b/bclteam/archive/2006/10/19/regex-class-caching-changes-between-net-framework-1-1-and-net-framework-2-0-josh-free.aspx 中第二条评论所解释的那样,我认为 CompiledMatch 活动模式每次应用时都会编译正则表达式... 因此大多数情况下性能实际上会适得其反... - Mauricio Scheffer
@MauricioScheffer - 我对文章和您引用的评论的理解与我的理解一致(请在您发现我错误的地方进行更正):1)由于CompiledMatch使用静态方法 Regex.Match,因此在幕后创建的Regex实例被缓存,2)RegexOptions.Compiled标志用于构建(仅创建一次并缓存)Regex实例,而不是每个匹配。因此,对于每个与CompiledMatch一起使用的唯一正则表达式模式,只会创建一个已编译的正则表达式,该正则表达式在第一次调用后被缓存以供后续调用使用。 - Stephen Swensen
@StephenSwensen:顺便说一下,我们在FSharpx中真的需要一些函数式“适配器”来处理Regex API...我邀请您在http://groups.google.com/group/fsharpx上讨论这个问题;-)网络上有许多不同的Regex活动模式,我们应该选择最好的并将其标准化... - Mauricio Scheffer
显示剩余6条评论

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