使用 attoparsec 解析多行日志

Parsing multi line log with attoparsec

我正在尝试像这样解析多行日志

[xxx] This is 1
[xxx] This is also 1
[yyy] This is 2

我定义了这些类型

{-# LANGUAGE OverloadedStrings #-}

module Parser where

import Prelude hiding(takeWhile)
import Data.Text
import Data.Word
import Data.Attoparsec.Text as T
import Data.Char
import Data.String

data ID    = ID String deriving (Eq, Show)
data Entry = Entry ID String deriving (Eq, Show)
data Block = Block ID [String]
data Log   = Log [Block]

并定义了这些解析器:

parseID :: Parser ID
parseID = do
  char '['
  id <- takeTill ( == ']' )
  char ']'
  return $ ID $ unpack id

parseEntry :: Parser Entry
parseEntry = do
  id <- parseID
  char ' '
  content <- takeTill isEndOfLine
  return $ Entry id (unpack content)

当我做类似 parseOnly parseEntry entryString 的事情时,这工作正常,我得到一个 Entry

问题是当我尝试解析诸如我在开始时添加的日志之类的内容时。 我会得到 [Entry] 但我想得到 [Block].

我还希望当 2 条或更多条连续的行具有相同的 ID(如 xxx)时,它们应该存储在同一个块中,因此为了解析上述日志,我想返回

[block1, block2]
-- block1 == Block "xxx" ["This is 1", "This is also 1"]
-- block2 == Block "yyy" ["This is 2"]

如何根据 ID 的变化让解析器创建新块或添加到最后生成的块中?

一个明显的解决方案是简单地生成一个 [Entry],然后使用折叠函数以适当的逻辑将其转换为 [Block],但我会进行 2 遍,1 遍日志和 [Entry] 上的另一个日志,这似乎不仅对大型日志来说性能不佳,而且感觉是错误的方式(根据我非常有限的 attoparsec 知识)

还有其他想法吗?

编辑

Bob Dalgleish 解决方案基本上有效(非常感谢!!!),只需要进行一些调整即可使其有效。 这是我的最终解决方案:

data ID    = ID String deriving (Eq, Show)
data Entry = Entry ID String deriving (Eq, Show)
data Block = Block ID [String] deriving (Eq, Show)
data Log   = Log [Block] deriving (Eq, Show)

parseID :: Parser ID
parseID = do
  char '['
  id <- takeTill ( == ']' )
  char ']'
  return $ ID $ unpack id

parseEntry :: Parser Entry
parseEntry = do
  id <- parseID
  char ' '
  content <- takeTill isEndOfLine
  return $ Entry id (unpack content)

parseEntryFor :: ID -> Parser Entry
parseEntryFor blockId = do
  id <- parseID
  if blockId == id
     then do
       char ' '
       content <- takeTill isEndOfLine
       endOfLine <|> endOfInput
       return $ Entry id (unpack content)
  else fail "nonmatching id"

parseBlock :: Parser Block
parseBlock = do
  (Entry entryId s) <- parseEntry
  let newBlock = Block entryId [s]
  endOfLine <|> endOfInput
  entries <- many' (parseEntryFor entryId)
  return $ Block entryId (s : Prelude.map (\(Entry _ s') -> s') entries)

您需要一个 Block 的解析器。它接受一个 Entry,对具有相同 id 的 Entry 进行前瞻;如果不一样,它会回溯 returns 目前为止的内容。

首先,让我们介绍一个条件Entry解析器:

parseEntryFor :: ID -> Parser Entry
parseEntryFor blockId = do
  id <- parseEntry
  if blockId == id
  then do
         char ' '
         content <- takeTill isEndOfLine
         endOfLine
         return $ Entry id (unpack content)
  else fail "nonmatching id"

-- |A Block consists of one or more Entry's with the same ID
parseBlock :: Parser Block
parseBlock = do
  (Entry entryId s) <- parseEntry
  let newBlock = Block entryId [s]
  endOfLine
  entries <- many' (parseEntryFor entryId)
  return $ Block entryId s: (map (\(Entry _ s') -> x') entries)

(此代码未经测试,因为我只使用过 Parsec。)