考虑以下 CSV 文档的简单语法(ABNF 表示):
csv = *crow
crow = *(ccell ',') ccell CR
ccell = "'" *(ALPHA / DIGIT) "'"
我们希望编写一个将此语法转换为TSV(制表符分隔值)文档的转换器:
tsv = *trow
trow = *(tcell HTAB) tcell CR
tcell = DQUOTE *(ALPHA / DIGIT) DQUOTE
首先,让我们创建一个代数数据类型来描述我们的抽象语法树。类型同义词包括在内以方便理解:
data XSV = [Row]
type Row = [Cell]
type Cell = String
编写这个语法的解析器非常简单。我们编写一个解析器,就像我们描述ABNF一样:
csv :: Parser XSV
csv = XSV <$> many crow
crow :: Parser Row
crow = do cells <- ccell `sepBy` (char ',')
newline
return cells
ccell :: Parser Cell
ccell = do char '\''
content <- many (digit <|> letter)
char '\''
return content
这个解析器使用
do
-notation。在
do
后面,跟着一系列语句。对于解析器来说,这些语句只是其他解析器。可以使用
<-
来绑定解析器的结果。这样,通过链接多个较小的解析器,就可以构建一个大的解析器。为了获得有趣的效果,还可以使用特殊的组合器来组合解析器(例如
a <|> b
,它可以解析
a
或
b
,或者
many a
,它尽可能多地解析
a
)。请注意,Parsec 默认不会回溯。如果一个解析器在消耗字符后可能失败,请在其前缀中加上
try
以启用回溯。但是,
try
会减慢解析速度。
结果是一个解析器
csv
,它将我们的 CSV 文档解析成一个抽象语法树。现在,将其转换为另一种语言(如 TSV)就很容易了。
xsvToTSV :: XSV -> String
xsvToTSV xst = unlines (map toLines xst) where
toLines = intersperse '\t'
连接这两个事物,就得到了一个转换函数:
csvToTSV :: String -> Maybe String
csvToTSV document = case parse csv "" document of
Left _ -> Nothing
Right xsv -> xsvToTSV xsv
就是这样了!Parsec还有许多其他功能可以构建极其复杂的解析器。书籍Real World Haskell有一个不错的章节介绍了解析器,但它有点过时。大部分内容仍然正确。如果你有进一步的问题,请随时提出。