使用FParsec从基本上自由格式的文本中挑选块

时间:2014-02-20 23:20:04

标签: f# fparsec

我正在尝试从大部分自由格式的文本中解析一些信息。我尝试在FParsec中实现,但我之前没有使用它,我不确定我是否做错了,或者即使它非常适合这个特定的问题。

问题描述

我想从降价文档(“examplecode”和“requiredcode”标签)中解析出一组特定Liquid tags的内容。降价主要是自由格式文本,偶尔会在Liquid标签内阻塞,例如:

Some free form text.
Possibly lots of lines. Maybe `code` stuff.

{% examplecode opt-lang-tag %}
ABC
DEF
{% endexamplecode %}

More text. Possibly multilines.

{% othertag %}
can ignore this tag
{% endothertag %}

{% requiredcode %}
GHI
{% endrequiredcode %}

在这种情况下,我需要解析[ "ABC\nDEF"; "GHI" ]

我所追求的解析逻辑可以强制表达。循环遍历每一行,如果我们找到一个我们感兴趣的开始标记,则采用直到我们匹配结束标记并将这些行添加到结果列表中,否则跳过行直到下一个开始标记。重复。

这可以通过循环或折叠,或with a regular expression

来完成

\{%\s*(examplecode|requiredcode).*\%}(.*?)\{%\s*end\1\s*%\}

我的FParsec尝试

我发现很难在FParsec中表达上面的逻辑。我想写一些类似between s t (everythingUntil t)的内容,但我不知道如何在没有everythingUntil消费结束令牌的情况下实现这一点,导致between失败。

我最终得到了以下内容,它不处理"{%"的嵌套出现,但似乎通过了我关心的主要测试用例:

let trimStr (s : string) = s.Trim()
let betweenStr s t = between (pstring s) (pstring t)
let allTill s = charsTillString s false maxInt
let skipAllTill s = skipCharsTillString s false maxInt
let word : Parser<string, unit> = many1Satisfy (not << Char.IsWhiteSpace)

type LiquidTag = private LiquidTag of name : string * contents : string
let makeTag n c = LiquidTag (n, trimStr c)

let liquidTag =
    let pStartTag = betweenStr "{%" "%}" (spaces >>. word .>> spaces .>> skipAllTill "%}")
    let pEndTag tagName = betweenStr "{%" "%}" (spaces >>. pstring ("end" + tagName) .>> spaces)
    let tagContents = allTill "{%"
    pStartTag >>= fun name -> 
                    tagContents 
                        .>> pEndTag name 
                        |>> makeTag name

let tags = many (skipAllTill "{%" >>. liquidTag)

然后我可以过滤标签,只包含我感兴趣的标签。

这比基本实现(如正则表达式)更多,例如描述性错误报告和更严格的输入格式验证(这既好又坏)。

更严格的格式的一个结果是在标记内的嵌套"{%"子字符串上解析失败。我不确定如何调整它以处理这种情况(应该给[ "ABC {% DEF " ]):

{% examplecode %}
ABC {% DEF
{% endexamplecode %}

问题

有没有办法更密切地表达FParsec中“问题描述”部分中描述的逻辑,或者输入的自由形式性质是否使FParsec不适合这个而不是更基本的循环或正则表达式?

(我也对在标签中允许嵌套"{%"字符串的方法以及对我的FParsec尝试的改进感兴趣。我很乐意根据需要将其分解为其他问题。)

1 个答案:

答案 0 :(得分:5)

我只使用start >>. everythingUntil end代替between start end body

以下实现与正则表达式中的逻辑相对接近:

let maxInt = System.Int32.MaxValue    
type LiquidTag = LiquidTag of string * string

let skipTillString str = skipCharsTillString str true maxInt

let skipTillStringOrEof str : Parser<unit, _> =
    fun stream -> 
        let mutable found = false
        stream.SkipCharsOrNewlinesUntilString(str, maxInt, &found) |> ignore
        Reply(())

let openingBrace = skipString "{%" >>. spaces

let tagName name = 
    skipString name 
    >>? nextCharSatisfies (fun c -> c = '%' || System.Char.IsWhiteSpace(c))

let endTag name =     
    openingBrace >>? (tagName ("end" + name) >>. (spaces >>. skipString "%}"))

let tagPair_afterOpeningBrace name = 
   tagName name  >>. skipTillString "%}"
   >>. (manyCharsTill anyChar (endTag name)
        |>> fun str -> LiquidTag(name, str))

let skipToOpeningBraceOrEof = skipTillStringOrEof "{%" 

let tagPairs =
    skipToOpeningBraceOrEof 
    >>. many (openingBrace
              >>. opt (    tagPair_afterOpeningBrace "examplecode"
                       <|> tagPair_afterOpeningBrace "requiredcode")
              .>> skipToOpeningBraceOrEof)
        |>> List.choose id
   .>> eof

一些注意事项:

  • 我只解析你感兴趣的两个Liquid声明。这个 如果其中一个语句嵌套在a中,则会有所不同 你不感兴趣的声明。它的优势还在于没有 解析器运行时必须构造解析器。

  • 我正在使用>>?组合器控制何时完全回溯 可能会发生。

  • 此实施的效果不会很好,但如有必要,有多种方法可以对其进行优化。最慢的组件可能是manyCharsTill anyChar (endTag name)解析器,可以很容易地用自定义原语替换。 many ... |> List.choose id中的tagPairs也可以使用效率更高的自定义组合器轻松替换。