如何在Haskell中使用Parsec解析一系列行(只有一些有趣的行)

时间:2018-09-21 06:55:40

标签: haskell parsec

我有一些下面形式的输入数据(这只是一个小样本)。

ID_SID_0_LANG=eng
ID_VIDEO_FORMAT=H264
ID_VIDEO_HEIGHT=574
ID_START_TIME=0.00
ID_SUBTITLE_ID=0
ID_VIDEO_ID=0
ID_VIDEO_FPS=25.000
ID_VIDEO_WIDTH=700

我正在尝试查看是否可以使用Parsec对此进行解析。为了我们的示例,我想提取两个值,即width和height。我正在尝试使用Parsec可以做到这一点。

  • 行可以按任何顺序排列
  • 如果缺少宽度或高度,我想要一个ParseError
  • 如果宽度或高度不止一次出现,我想要一个ParseError
  • 每个输入的其他行是混合的和变化的,除了基本格式外,我不能做任何假设。

我想使用Parsec,因为我将不得不解析值(通常可能是不同类型的值-编解码器的枚举,经过类型,字符串等)。而且我希望返回的数据结构包含Naturals,而不是Maybe Natural,以简化以后的代码。

我的问题是如何“解析”对我而言并不有趣的ID_前几行,而仅选择那些我感兴趣的ID_行。因此,我想解析“任何数量的无趣ID_行;高度(或宽度);任何数量的无趣ID_行;宽度(或高度,如果已找到宽度);任何数量的无趣ID_行)。我喜欢这样做而不重复构成“有趣”键的概念,因为重复是以后维护时细微错误的主要原因。

到目前为止,我最大的努力是对行进行分析,以生成有趣的行的数据结构修饰符列表,每个修饰符都有一个键,并分别检查所需行的存在和唯一行是否重复;但这并不令人满意,因为我要重复“有趣的”键。

这可以用Parsec优雅地完成吗?

谢谢

3 个答案:

答案 0 :(得分:1)

鉴于您需要一个“优雅的” Parsec解决方案,我认为您正在寻找一种排列分析器的变体。

有关背景知识,请参见Text.Parsec.Perm库的模块Control.Applicative.Permutation中的parser-combinators文档及其更现代的形式。此外,这篇功能性Pearl论文Parsing Permutation Phrases描述了该方法,并且阅读起来非常有趣。

您的问题有两个特殊方面:首先,我不知道现有的置换解析器允许以清晰的方式在匹配部分之前,之间和之后的“不匹配”内容,以及诸如构建跳过逻辑的黑客攻击进入组件解析器或派生一个额外的解析器,以从intercalateEffect中识别要在Control.Applicative.Permutation中使用的可跳过行。其次,您输入的特殊结构-行可以由标识符识别,而不是仅由通用组件解析器识别-这意味着我们可以编写一种比通常的排列解析器更有效的解决方案,该解析器在其中查找标识符映射,而不是按顺序尝试解析器列表。

下面是一个可能的解决方案。一方面,它使用大锤杀死苍蝇。在您的简单情况下,编写一个临时解析器以读取标识符及其RHS,检查所需的标识符和重复项,然后为RHS调用特定于标识符的解析器似乎更简单。另一方面,也许在更复杂的情况下,下面的解决方案是合理的,并且我认为这可能对其他人有用。

无论如何,这就是想法。首先,一些准备:

{-# OPTIONS_GHC -Wall #-}
module ParseLines where
import Control.Applicative
import Control.Monad
import Data.List (intercalate)
import Text.Parsec (unexpected, eof, parseTest)
import Text.Parsec.Char (char, letter, alphaNum, noneOf, newline, digit)
import Text.Parsec.String (Parser)
import qualified Data.Map.Lazy as Map
import qualified Data.Set as Set

假设我们有一个数据类型,代表了解析的最终结果:

data Video = Video
  { width :: Int
  , height :: Int
  } deriving (Show)

我们将构造一个Permutation a解析器。类型a是我们最终要返回的类型(在这种情况下,始终为Video)。这个Permutation实际上是Map,从ID_VIDEO_WIDTH之类的“已知”标识符到一种特殊类型的解析器,它将解析给定标识符的右侧(例如,整数,例如700),然后返回-不是解析的整数-而是延续Permutation a,该延续Video解析剩余的数据以构造一个700,并与解析的整数(例如{{1} })“融入”延续。延续将有一个可识别“剩余”值的映射,并且我们还将跟踪已经读取的已知标识符以标记重复项。

我们将使用以下类型:

type Identifier = String
data Permutation a = Permutation
  -- "seen" identifiers for flagging duplicates
  (Set.Set Identifier)
  (Either
    -- if there are more values to read, map identifier to a parser
    -- that parses RHS and returns continuation for parsing the rest
    (Map.Map Identifier (Parser (Permutation a)))
    -- or we're ready for an eof and can return the final value
    a)

“运行”这样的解析器需要将其转换为普通的Parser,这是我们在其中实现识别已识别行,标记重复项以及跳过无法识别的标识符的逻辑。首先,这是标识符的解析器。如果您想宽大处理,可以使用many1 (noneOf "\n=")之类的方法。

ident :: Parser String
ident = (:) <$> letter' <*> many alphaNum'
  where letter' = letter <|> underscore
        alphaNum' = alphaNum <|> underscore
        underscore = char '_'

这是一个解析器,用于在我们看到无法识别的标识符时跳过其余行:

skipLine :: Parser ()
skipLine = void $ many (noneOf "\n") >> newline

最后,这是我们运行Permutation解析器的方式:

runPermutation :: Permutation a -> Parser a
runPermutation p@(Permutation seen e)
  = -- if end of file, return the final answer (or error)
    eof *>
    case e of
      Left m -> fail $
        "eof before " ++ intercalate ", " (Map.keys m)
      Right a -> return a
  <|>
    -- otherwise, parse the identifier
    do k <- ident <* char '='
       -- is it one we're waiting for?
       case either (Map.lookup k) (const Nothing) e of
         -- no, it's not, so check for duplicates and skip
         Nothing -> if Set.member k seen
           then unexpected ("duplicate " ++ k)
           else skipLine *> runPermutation p
         -- yes, it is
         Just prhs -> do
           -- parse the RHS to get a continuation Permutation
           -- and run it to parse rest of parameters
           (prhs <* newline) >>= runPermutation

要了解其工作原理,以下是我们直接 构造一个Permutation来解析Video的方法。很长,但并不复杂:

perm2 :: Permutation Video
perm2 = Permutation
  -- nothing's been seen yet
  Set.empty
  -- parse width or height
  $ Left (Map.fromList
   [ ("ID_VIDEO_WIDTH", do
         -- parse the width
         w <- int
         -- return a continuation permutation
         return $ Permutation
           -- we've seen width
           (Set.fromList ["ID_VIDEO_WIDTH"])
           -- parse height
           $ Left (Map.fromList
            [ ("ID_VIDEO_HEIGHT", do
                  -- parse the height
                  h <- int
                  -- return a continuation permutation
                  return $ Permutation
                    -- we've seen them all
                    (Set.fromList ["ID_VIDEO_WIDTH", "ID_VIDEO_HEIGHT"])
                    -- have all parameters, so eof returns the video
                    $ Right (Video w h))
            ]))
   -- similarly for other permutation:
   , ("ID_VIDEO_HEIGHT", do
         h <- int
         return $ Permutation
           (Set.fromList ["ID_VIDEO_HEIGHT"])
           $ Left (Map.fromList
            [ ("ID_VIDEO_WIDTH", do
                  w <- int
                  return $ Permutation
                    (Set.fromList ["ID_VIDEO_WIDTH", "ID_VIDEO_HEIGHT"])
                    $ Right (Video w h))
            ]))
   ])

int :: Parser Int
int = read <$> some digit

您可以像这样测试它:

testdata1 :: String
testdata1 = unlines
  [ "ID_SID_0_LANG=eng"
  , "ID_VIDEO_FORMAT=H264"
  , "ID_VIDEO_HEIGHT=574"
  , "ID_START_TIME=0.00"
  , "ID_SUBTITLE_ID=0"
  , "ID_VIDEO_ID=0"
  , "ID_VIDEO_FPS=25.000"
  , "ID_VIDEO_WIDTH=700"
  ]

test1 :: IO ()
test1 = parseTest (runPermutation perm2) testdata1

您应该能够验证它是否为丢失的密钥提供了适当的错误,为已知密钥重复了条目,并以任何顺序接受密钥。

最后,显然,我们显然不想手动构建perm2之类的置换解析器,因此我们从Text.Parsec.Perm模块中获取一个页面,并引入以下语法:

video :: Parser Video
video = runPermutation (Video <$$> ("ID_VIDEO_WIDTH", int) <||> ("ID_VIDEO_HEIGHT", int))

并定义运算符以构造必要的Permutation对象。这些定义有些棘手,但是它们直接遵循Permutation的定义。

(<$$>) :: (a -> b) -> (Identifier, Parser a) -> Permutation b
f <$$> xq = Permutation Set.empty (Right f) <||> xq
infixl 2 <$$>

(<||>) :: Permutation (a -> b) -> (Identifier, Parser a) -> Permutation b
p@(Permutation seen e) <||> (x, q)
  = Permutation seen (Left (Map.insert x q' m'))
  where
    q' = (\a -> addQ x a p) <$> q
    m' = case e of Right _ -> Map.empty
                   Left m -> Map.map (fmap (<||> (x, q))) m
infixl 1 <||>

addQ :: Identifier -> a -> Permutation (a -> b) -> Permutation b
addQ x a (Permutation seen e)
  = Permutation (Set.insert x seen) $ case e of
      Right f -> Right (f a)
      Left m -> Left (Map.map (fmap (addQ x a)) m)

和最终测试:

test :: IO ()
test = parseTest video testdata1

给予:

> test
Video {width = 700, height = 574}
>

这是最终代码,稍有重新排列:

{-# OPTIONS_GHC -Wall #-}
module ParseLines where

import Control.Applicative
import Control.Monad
import Data.List (intercalate)
import Text.Parsec (unexpected, eof, parseTest)
import Text.Parsec.Char (char, letter, alphaNum, noneOf, newline, digit)
import Text.Parsec.String (Parser)
import qualified Data.Map.Lazy as Map
import qualified Data.Set as Set

-- * Permutation parser for identifier settings

-- | General permutation parser for a type @a@.
data Permutation a = Permutation
  -- | "Seen" identifiers for flagging duplicates
  (Set.Set Identifier)
  -- | Either map of continuation parsers for more identifiers or a
  -- final value once we see eof.
  (Either (Map.Map Identifier (Parser (Permutation a))) a)

-- | Create a one-identifier 'Permutation' from a 'Parser'.
(<$$>) :: (a -> b) -> (Identifier, Parser a) -> Permutation b
f <$$> xq = Permutation Set.empty (Right f) <||> xq
infixl 2 <$$>

-- | Add a 'Parser' to a 'Permutation'.
(<||>) :: Permutation (a -> b) -> (Identifier, Parser a) -> Permutation b
p@(Permutation seen e) <||> (x, q)
  = Permutation seen (Left (Map.insert x q' m'))
  where
    q' = (\a -> addQ x a p) <$> q
    m' = case e of Right _ -> Map.empty
                   Left m -> Map.map (fmap (<||> (x, q))) m
infixl 1 <||>

-- | Helper to add a parsed component to a 'Permutation'.
addQ :: Identifier -> a -> Permutation (a -> b) -> Permutation b
addQ x a (Permutation seen e)
  = Permutation (Set.insert x seen) $ case e of
      Right f -> Right (f a)
      Left m -> Left (Map.map (fmap (addQ x a)) m)

-- | Convert a 'Permutation' to a 'Parser' that detects duplicates
-- and skips unknown identifiers.
runPermutation :: Permutation a -> Parser a
runPermutation p@(Permutation seen e)
  = -- if end of file, return the final answer (or error)
    eof *>
    case e of
      Left m -> fail $
        "eof before " ++ intercalate ", " (Map.keys m)
      Right a -> return a
  <|>
    -- otherwise, parse the identifier
    do k <- ident <* char '='
       -- is it one we're waiting for?
       case either (Map.lookup k) (const Nothing) e of
         -- no, it's not, so check for duplicates and skip
         Nothing -> if Set.member k seen
           then unexpected ("duplicate " ++ k)
           else skipLine *> runPermutation p
         -- yes, it is
         Just prhs -> do
           -- parse the RHS to get a continuation Permutation
           -- and run it to parse rest of parameters
           (prhs <* newline) >>= runPermutation

-- | Left-hand side of a setting.
type Identifier = String

-- | Parse an 'Identifier'.
ident :: Parser Identifier
ident = (:) <$> letter' <*> many alphaNum'
  where letter' = letter <|> underscore
        alphaNum' = alphaNum <|> underscore
        underscore = char '_'

-- | Skip (rest of) a line.
skipLine :: Parser ()
skipLine = void $ many (noneOf "\n") >> newline

-- * Parsing video information

-- | Our video data.
data Video = Video
  { width :: Int
  , height :: Int
  } deriving (Show)

-- | Parsing integers (RHS of width and height settings)
int :: Parser Int
int = read <$> some digit

-- | Some test data
testdata1 :: String
testdata1 = unlines
  [ "ID_SID_0_LANG=eng"
  , "ID_VIDEO_FORMAT=H264"
  , "ID_VIDEO_HEIGHT=574"
  , "ID_START_TIME=0.00"
  , "ID_SUBTITLE_ID=0"
  , "ID_VIDEO_ID=0"
  , "ID_VIDEO_FPS=25.000"
  , "ID_VIDEO_WIDTH=700"
  ]

-- | `Video` parser based on `Permutation`.
video :: Parser Video
video = runPermutation (Video <$$> ("ID_VIDEO_WIDTH", int) <||> ("ID_VIDEO_HEIGHT", int))

-- | The final test.
test :: IO ()
test = parseTest video testdata1

答案 1 :(得分:0)

实际上,一个简单的解决方案是将文件解析为Map ByteString ByteString,在解析时检查重复项,然后从中构建目标结果,检查是否存在所有必填字段。

parseMap :: Parsec (Map ByteString ByteString)
-- ...

parseValues :: Map ByteString ByteString -> Parsec MyDataStructure
-- ...

函数parseValues可以再次使用Parsec来解析字段(也许在每个字段上使用runP)并报告错误或缺少字段。

该解决方案的缺点在于解析是在两个级别上完成的(一次获取ByteString,而第二次解析它们)。这样,我们将无法正确报告在parseValues中发现的错误的位置。但是,Parsec允许获取并设置文件中的当前位置,因此将它们包含在地图中,然后在解析单个字符串时使用它们是可行的:

parseMap :: Parsec (Map ByteString (SourcePos, ByteString))

直接使用Parsec解析完整结果是可能的,但是恐怕要想实现任意顺序并同时使用不同字段输出类型,将是一件棘手的事情。

答案 2 :(得分:0)

如果您不介意稍微降低性能,请为宽度线编写一个解析器,为长度线编写一个解析器,然后执行以下操作:

let ls = lines input in 
  case ([x | Right x <- parseWidth ls], [x | Right x <- parseLength ls]) of
    ([w],[l]) -> ...
    _         -> parserError ...

很容易为重复的/缺失的值添加单独的错误情况,而无需重复任何操作。