Как использовать Data.Text.Lazy.IO для анализа файлов JSON с помощью Aeson

Я хочу разобрать все файлы json в заданном каталоге в тип данных Result.

Итак, у меня есть функция декодирования

decodeResult :: Data.ByteString.Lazy.ByteString -> Maybe Result

Я начал с Data.Text.Lazy.IO для загрузки файла в Lazy ByteString,

import qualified Data.Text.Lazy.IO as T
import qualified Data.Text.Lazy.Encoding as T

getFileContent :: FilePath -> IO B.ByteString
getFileContent path = T.encodeUtf8 `fmap` T.readFile path

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

import System.IO
import qualified Data.ByteString.Lazy as B
import qualified Data.Text.Lazy.IO as T
import qualified Data.Text.Lazy.Encoding as T

getFileContent :: FilePath -> IO (Maybe Result)
getFileContent path = withFile path ReadMode $ \hnd -> do
   content <- T.hGetContents hnd
   return $ (decodeAnalytic . T.encodeUtf8) content

loadAllResults :: FilePath -> IO [Result]
loadAllResults path = do
   paths <- listDirectory path
   results <- sequence $ fmap getFileContent (fmap (path ++ ) $ filter (endswith ".json") paths)
   return $ catMaybes results

В этой версии ленивый ввод-вывод никогда не оценивался, он всегда возвращает пустой список. Но если я печатаю содержимое внутри функции getFileContent, то все работает правильно.

getFileContent :: FilePath -> IO (Maybe Result)
getFileContent path = withFile path ReadMode $ \hnd -> do
   content <- T.hGetContents hnd
   print content
   return $ (decodeAnalytic . T.encodeUtf8) content

Поэтому я не уверен, что мне не хватает, должен ли я использовать трубопровод для таких вещей?


person Yuan Wang    schedule 10.01.2017    source источник
comment
Простой ответ: да, используйте трубопровод или что-то подобное. Более сложный ответ заключается в том, что ваш loadAllResults невероятно ленив - простое выполнение loadAllResults x на самом деле не читает файлы (или даже не открывает их). При попытке оценить получившийся список вы одновременно открываете все файлы и пытаетесь их прочитать. withFile вам не поможет, потому что лень исходит из hGetContents - попробуйте переключиться на неленивый текстовый ввод-вывод.   -  person user2407038    schedule 10.01.2017


Ответы (1)


Вообще говоря, я рекомендовал бы использовать библиотеку потоковой передачи для анализа данных произвольного размера, таких как файл JSON. Однако в конкретном случае разбора JSON с помощью aeson проблемы переполнения памяти не так значительны, IMO, поскольку сама библиотека aeson в конечном итоге будет представлять весь файл в памяти как тип Value. Таким образом, вы можете просто использовать строгий ввод-вывод строки байтов. Я привел пример использования как конвейера, так и строгого ввода-вывода для анализа значения JSON. (Я думаю, что версия канала уже существует в некоторых библиотеках, я не уверен.)

#!/usr/bin/env stack
{- stack --resolver lts-7.14 --install-ghc runghc
   --package aeson --package conduit-extra
-}
import           Control.Monad.Catch     (MonadThrow, throwM)
import           Control.Monad.IO.Class  (MonadIO, liftIO)
import           Data.Aeson              (FromJSON, Result (..), eitherDecodeStrict',
                                          fromJSON, json, Value)
import           Data.ByteString         (ByteString)
import qualified Data.ByteString         as B
import           Data.Conduit            (ConduitM, runConduitRes, (.|))
import           Data.Conduit.Attoparsec (sinkParser)
import           Data.Conduit.Binary     (sourceFile)

sinkFromJSON :: (MonadThrow m, FromJSON a) => ConduitM ByteString o m a
sinkFromJSON = do
    value <- sinkParser json
    case fromJSON value of
        Error e -> throwM $ userError e
        Success x -> return x

readJSONFile :: (MonadIO m, FromJSON a) => FilePath -> m a
readJSONFile fp = liftIO $ runConduitRes $ sourceFile fp .| sinkFromJSON

-- Or using strict I/O
readJSONFileStrict :: (MonadIO m, FromJSON a) => FilePath -> m a
readJSONFileStrict fp = liftIO $ do
    bs <- B.readFile fp
    case eitherDecodeStrict' bs of
        Left e -> throwM $ userError e
        Right x -> return x

main :: IO ()
main = do
    x <- readJSONFile "test.json"
    y <- readJSONFileStrict "test.json"
    print (x :: Value)
    print (y :: Value)

EDIT Забыл упомянуть: я настоятельно рекомендую против использовать текстовый ввод-вывод для чтения файлов JSON. Файлы JSON должны быть закодированы с помощью UTF-8, в то время как текстовые функции ввода-вывода будут использовать любые настройки вашей системы, указанные для кодировки символов. Надежнее полагаться на Data.ByteString.readFile и подобные. Подробнее я рассказал в недавнем сообщении в блоге.

person Michael Snoyman    schedule 10.01.2017
comment
Большое спасибо за подробный ответ! - person Yuan Wang; 10.01.2017