Haskell /管道:逐行读取文件

时间:2019-06-05 11:21:36

标签: haskell conduit

场景:我有一个〜900mb的文本文件,其格式如下:

...
Id:   109101
ASIN: 0806978473
  title: The Beginner's Guide to Tai Chi
  group: Book
  salesrank: 672264
  similar: 0
  categories: 3
   |Books[283155]|Subjects[1000]|Sports[26]|Individual Sports[16533]|Martial Arts[16571]|General[16575]
   |Books[283155]|Subjects[1000]|Sports[26]|Individual Sports[16533]|Martial Arts[16571]|Taichi[16583]
   |Books[283155]|Subjects[1000]|Sports[26]|General[11086921]
  reviews: total: 2  downloaded: 2  avg rating: 5
    2000-4-4  cutomer: A191SV1V1MK490  rating: 5  votes:   0  helpful:   0
    2004-7-10  cutomer:  AVXBUEPNVLZVC  rating: 5  votes:   0  helpful:   0
                    (----- empty line ------)    
Id :

,并希望从中解析信息。

问题:作为第一步(因为我需要在其他上下文中使用),我想逐行处理文件,然后将属于一个产品的“块”收集在一起,然后进行处理它们与其他逻辑分开。

因此,计划如下:

  1. 定义代表文本文件的来源
  2. 定义一个导管(?),该导管从该源中各取一行,然后...
  3. ...将其传递给其他组件。

现在,我尝试改写以下示例:

doStuff = do
  writeFile "input.txt" "This is a \n test." -- Filepath -> String -> IO ()

  runConduitRes                  -- m r
    $ sourceFileBS "input.txt"   -- ConduitT i ByteString m ()  -- by "chunk"
    .| sinkFile "output.txt"     -- FilePath -> ConduitT ByteString o m ()

  readFile "output.txt"
    >>= putStrLn 

因此sourceFileBS "input.txt"的类型为ConduitT i ByteString m (),即具有以下内容的管道

  • 输入类型i
  • 输出类型ByteStream
  • 单子类型t
  • 结果类型()

sinkFile将所有传入数据流式传输到给定文件中。 sinkFile "output.txt"是输入类型为ByteStream的管道。

我现在想要的是逐行处理输入源,也就是说,每个下游仅传递一行。用伪代码:

sourceFile "input.txt"
splitIntoLines
yieldMany (?)
other stuff

我该怎么做?

我目前拥有的是

copyFile = do
  writeFile "input.txt" "This is a \n test." -- Filepath -> String -> IO ()

  runConduitRes                  -- m r
    (lineC $ sourceFileBS "input.txt")   -- ConduitT i ByteString m ()  -- by "chunk"
    .| sinkFile "output.txt"     -- FilePath -> ConduitT ByteString o m ()

  readFile "output.txt"
    >>= putStrLn --

但是会出现以下类型错误:

    * Couldn't match type `bytestring-0.10.8.2:Data.ByteString.Internal.ByteString'
                     with `Void'
      Expected type: ConduitT
                       ()
                       Void
                       (ResourceT
                          (ConduitT
                             a0 bytestring-0.10.8.2:Data.ByteString.Internal.ByteString m0))
                       ()
        Actual type: ConduitT
                       ()
                       bytestring-0.10.8.2:Data.ByteString.Internal.ByteString
                       (ResourceT
                          (ConduitT
                             a0 bytestring-0.10.8.2:Data.ByteString.Internal.ByteString m0))
                       ()
    * In the first argument of `runConduitRes', namely
        `(lineC $ sourceFileBS "input.txt")'
      In the first argument of `(.|)', namely
        `runConduitRes (lineC $ sourceFileBS "input.txt")'
      In a stmt of a 'do' block:
        runConduitRes (lineC $ sourceFileBS "input.txt")
          .| sinkFile "output.txt"
   |
28 |     (lineC $ sourceFileBS "input.txt")   -- ConduitT i ByteString m ()  -- by "chunk"
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

这使我相信现在的问题是,管道中的第一个导管的输入类型与runConduitRes不兼容。

我只是无法理解它,真的需要一个提示。

非常感谢。

1 个答案:

答案 0 :(得分:0)

我今天为此苦苦挣扎,并在试图找出类似问题时发现了这个问题。我试图将 git 日志分成块以供进一步解析,例如

commit 12345
Author: Me
Date:   Thu Jan 25 13:45:16 2019 -0500

    made some changes

 1 file changed, 10 insertions(+), 0 deletions(-)

commit 54321
Author: Me
...and so on...

我需要的函数几乎是 Data.Conduit.Combinators 中的 splitOnUnBounded,但我无法弄清楚如何在那里编写谓词函数。

我想出了以下 Conduit,它是对 splitOnUnbounded 的轻微修改。 source 它将需要一个列表流。每个列表只有一行文本,因为我觉得这样考虑更容易一些,尽管这肯定不是最佳解决方案。

它将使用一个函数将文本行组合在一起,该函数接受 next 行并返回一个 Bool,指示下一行是否是下一组文本的开始。< /p>


groupLines :: (Monad m, MonadIO m) => (Text -> Bool) -> [T.Text] -> ConduitM Text [Text] m ()
groupLines startNextLine ls = start
  where
    -- If the next line in the stream is Nothing, return.
    -- If the next line is the stream is Just line, then
    --   accumulate that line
    start = await >>= maybe (return ()) (accumulateLines ls)
    accumulateLines ls nextLine = do
      -- if ls is [], then add nextLine. Try to get a new next line. If there isn't one, yield. If there is a next line,
      --     yield lines and call accumulatelines again.
      -- if ls is [Text], check if nextLine is the start of the next group. If it isn't, add nextLine to ls,
      --    try got the the next nextLine. if there isn't one, yield, and if there is one, call accumulate lines again.
      --    If nextLine _is_ the start of the next group, the yield this group of lines and call accumulate lines again.
      nextLine' <- await
      case nextLine' of
        Nothing -> yield ls'
        Just l ->
          if Prelude.null ls
            then accumulateLines ls' l
            else
              if startNextLine l
                then yield ls' >> accumulateLines [] l
                else accumulateLines ls' l
      where
        ls' = ls ++ [nextLine]

它可以用在如下的管道中。只需在 Text -> Bool 函数上方传递该函数,该函数告诉管道下一个文本集合何时开始。


isCommitLine :: Text -> Bool
isCommitLine t = listToMaybe (TS.indices "commit" t) == Just 0

logParser =
  sourceFile "logs.txt"
    .| decodeUtf8
    .| linesUnbounded
    .| groupLines isCommitLine []
    .| Data.Conduit.Combinators.map (intercalate "\n")
    -- do something with each log entry here --
    .| Data.Conduit.Combinators.print

main :: IO ()
main = runConduitRes logParser

我是 Haskell 的新手,强烈怀疑这不是实现这一目标的最佳方式。所以如果其他人有更好的建议,我会很乐意学习!否则,也许在此处发布此解决方案会对某些人有所帮助。