Парсинг многострочного лога с 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 или более последовательных строк имели одинаковый идентификатор (например, 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)

person Batou99    schedule 31.01.2019    source источник
comment
Подход, который я использовал в таких ситуациях, состоит в том, чтобы ввести промежуточный этап синтаксического анализа, который поддерживает состояние, а именно Block, который может быть объединен с новым Entry. Он не будет излучать Block, пока не будет уверен, что Block нельзя вырастить. Другой подход заключается в использовании способности attoparsec выполнять неограниченный просмотр вперед, чтобы определить, имеет ли следующий Entry тот же идентификатор.   -  person Bob Dalgleish    schedule 31.01.2019


Ответы (1)


Вам нужен парсер для Blocks. Он принимает Entry, ищет Entry с тем же идентификатором; если не то же самое, он отступает и возвращает то, что у него есть до сих пор.

Во-первых, давайте представим условный парсер 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.)

person Bob Dalgleish    schedule 31.01.2019