Conduit и Attoparsec: неожиданное завершение при ошибке разбора

Я пытаюсь преобразовать анализатор файла журнала, который я написал некоторое время назад, в канал, и я столкнулся с проблемой. Я упросту детали самого парсера, так как это не имеет отношения к вопросу. У меня есть файл журнала, который выглядит так:

200 GET
404 POST
500 GET
FOO
301 PUT
302 GET
201 POST

Таким образом, код разбора довольно прост:

data SimpleLogEntry = SimpleLogEntry {
      status :: Int
    , method :: String
} deriving (Show, Eq)


parseHTTPStatus :: Parser Int
parseHTTPStatus = validate <$> decimal
    where validate d = if (d >= 200 && d < 999) then d else 100


parseHTTPMethod :: Parser String
parseHTTPMethod =
        (stringCI "GET" *> return "Get")
    <|> (stringCI "POST" *> return "Post")
    <|> (stringCI "PUT" *> return "Put")
    <|> return "Unknown"


parseLogLine :: Parser SimpleLogEntry
parseLogLine = fmap SimpleLogEntry
        parseHTTPStatus
    <*> (space *> parseHTTPMethod)

Все идет нормально. Вот как я реализовал это в канале:

import Prelude hiding (lines)

import Control.Applicative
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Resource (runResourceT, ResourceT)
import Data.Attoparsec.ByteString.Char8
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import Data.Conduit
import qualified Data.Conduit.Attoparsec as CA
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL


logLines:: Source (ResourceT IO) B.ByteString
logLines = CB.sourceFile "~/test.log" $= CB.lines


parseEntry :: ConduitM B8.ByteString SimpleLogEntry (ResourceT IO) ()
parseEntry = CA.conduitParserEither parseLogLine =$= awaitForever go
    where
        go (Left err) = liftIO $ putStrLn ("Got an error: " ++ CA.errorMessage err)
        go (Right (_, logEntry)) = yield logEntry


sink :: Sink SimpleLogEntry (ResourceT IO) ()
sink = CL.mapM_ (\t -> liftIO $ putStrLn $ "Got a status: " ++ (show . status) t)


main :: IO ()
main = runResourceT $ logLines $= parseEntry $$ sink

При беге main Я получаю этот вывод:

Got a status: 200
Got a status: 404
Got a status: 500
Got an error: Failed reading: takeWhile1

У меня возникают проблемы с пониманием того, почему конвейер завершается на этом этапе, а не с продолжением анализа следующей строки файла, как я хотел бы сделать. Чтение документов для Data.Conduit.Attoparsecкажется, это именно тот случай использования conduitParserEither был разработан для.

ОБНОВИТЬ

Согласно @Fabian, получается, что conduitParserEither не совсем то, что я хотел здесь. Вот определение parseEntry это делает то, что я хотел сделать:

parseEntry' :: ConduitM B8.ByteString SimpleLogEntry (ResourceT IO) ()
parseEntry' = (CL.map (parseOnly parseLogLine)) =$= awaitForever go
    where
        go (Left err) = liftIO $ putStrLn ("Got an error: " ++ err)
        go (Right logEntry) = yield logEntry

1 ответ

Решение

conduitParser (или же conduitParserEither) также может использовать несколько токенов в одной строке: например, следующий ввод дает тот же результат:

200 GET404 POST
500 GET
FOO
301 PUT
302 GET
201 POST

Поэтому имеет смысл, что анализатор не продолжает работу, потому что он не знает, где начнется следующий токен.

Другие вопросы по тегам