Parsing 在Haskell中解析可打印文本文件

Parsing 在Haskell中解析可打印文本文件,parsing,haskell,text,Parsing,Haskell,Text,我正试图找出在Haskell中解析特定文本文件的“正确”方法 在F#中,我循环每一行,用正则表达式测试它,以确定它是否是我要解析的行,如果是,我使用正则表达式解析它。否则,我将忽略该行 该文件是一个可打印的报告,每页都有标题。每个记录是一行,每个字段由两个或多个空格分隔。下面是一个例子: MY COMPANY'S NAME

我正试图找出在Haskell中解析特定文本文件的“正确”方法

在F#中,我循环每一行,用正则表达式测试它,以确定它是否是我要解析的行,如果是,我使用正则表达式解析它。否则,我将忽略该行

该文件是一个可打印的报告,每页都有标题。每个记录是一行,每个字段由两个或多个空格分隔。下面是一个例子:

                                                    MY COMPANY'S NAME
                                                     PROGRAM LISTING
                                             STATE:  OK     PRODUCT: ProductName
                                                 (DESCRIPTION OF REPORT)
                                                    DATE:   11/03/2013

  This is the first line of a a two-line description of the contents of this report. The description, as noted,
  spans two lines. This is more text. I'm running out of things to write. Blah.

          DIVISION CODE: 3     XYZ CODE: FAA3   AGENT CODE: 0007                                       PAGE NO:  1

 AGENT    TARGET NAME                      ST   UD   TARGET#   XYZ#   X-DATE       YEAR    CO          ENCODING
 -----    ------------------------------   --   --   -------   ----   ----------   ----    ----------  ----------

 0007     SMITH, JOHN                      43   3    1234567   001    12/06/2013   2004    ABC         SIZE XL
 0007     SMITH, JANE                      43   3    2345678   001    12/07/2013   2005    ACME        YELLOW
 0007     DOE, JOHN                        43   3    3456789   004    12/09/2013   2008    MICROSOFT   GREEN
 0007     DOE, JANE                        43   3    4567890   002    12/09/2013   2007    MICROSOFT   BLUE
 0007     BORGES, JORGE LUIS               43   3    5678901   001    12/09/2013   2008    DUFEMSCHM   Y1500
 0007     DEWEY, JOHN &                    43   3    6789012   003    12/11/2013   2013    ERTZEVILI   X1500
 0007     NIETZSCHE, FRIEDRICH             43   3    7890123   004    12/11/2013   2006    NCORPORAT   X7
我首先构建了解析器来测试每一行,看看它是否是一条记录。如果它是一个记录,我只是用我自己开发的子字符串函数根据字符位置切掉了行。这个很好用

然后我发现我的Haskell安装中确实有一个正则表达式库,所以我决定尝试像在F#中一样使用正则表达式。这失败得很惨,因为库拒绝了完全有效的正则表达式

然后我想,帕塞克呢?但是,我爬得越高,使用它的学习曲线就越陡峭,我发现自己在想,对于解析这份报告这样的简单任务,它是否是正确的工具

所以我想问一些哈斯克尔的专家:你会如何分析这种报告?我不是要代码,不过如果你有,我很想看看。我真的在要求技术或技术

谢谢

另外,输出只是一个以冒号分隔的文件,文件顶部有一行字段名,后面是记录,可以为最终用户导入Excel

编辑:

非常感谢大家的评论和回答

因为我最初没有说清楚:示例的前十四行对每一页(打印)输出重复,每页的记录数从零到一整页不等(看起来像45条记录)。很抱歉,我之前没有说清楚,因为这可能会影响已经提供的一些答案


我的Haskell系统目前仅限于Parsec(它没有attoparsec)、Text.Regex.Base和Text.Regex.Posix。我必须了解如何安装attoparsec和/或其他正则表达式库。但现在,你说服了我继续学习Parsec。感谢您提供了非常有用的代码示例

对于如此简单的东西,我建议使用解析器的语言很少(我过去用正则表达式解析过许多类似的文件),但parsec使之变得如此简单-

parseLine = do
  first <- count 4 anyChar
  second <- count 4 anyChar
  return (first, second)

parseFile = endBy parseLine (char '\n')

main = interact $ show . parse parseFile "-" 
parseLine=do

首先这绝对是一个值得解析库做的工作。我的主要目标通常是尽快将数据转换成非文本形式,比如

module ReportParser where

import Prelude hiding (takeWhile)
import Data.Text hiding (takeWhile)

import Control.Applicative
import Data.Attoparsec.Text

data ReportHeaderData = Company Text
                      | Program Text
                      | State Text
--                    ...
                      | FieldNames [Text]

data ReportData = ReportData Int Text Int Int Int Int Date Int Text Text

data Date = Date Int Int Int
为了论证,我们可以说,报告是

data Report = Report [ReportHeaderData] [ReportData]
现在,我通常创建一个解析器,它是一个与数据类型同名的函数

-- Ending condition for a field
doubleSpace :: Parser Char
doubleSpace = space >> space

-- Clears leading spaces
clearSpaces :: Parser Text
clearSpaces = takeWhile (== ' ') -- Naively assumes no tabs

-- Throws away everything up to and including a newline character (naively assumes unix line endings)
clearNewline :: Parser ()
clearNewline = (anyChar `manyTill` char '\n') *> pure ()

-- Parse a date
date :: Parser Date
date = Date <$> decimal <*> (char '/' *> decimal) <*> (char '/' *> decimal)

-- Parse a report
reportData :: Parser ReportData
reportData = let f1 = decimal <* clearSpaces
                 f2 = (pack <$> manyTill anyChar doubleSpace) <* clearSpaces
                 f3 = decimal <* clearSpaces
                 f4 = decimal <* clearSpaces
                 f5 = decimal <* clearSpaces
                 f6 = decimal <* clearSpaces
                 f7 = date <* clearSpaces
                 f8 = decimal <* clearSpaces
                 f9 = (pack <$> manyTill anyChar doubleSpace) <* clearSpaces
                 f10 = (pack <$> manyTill anyChar doubleSpace) <* clearNewline
             in ReportData <$> f1 <*> f2 <*> f3 <*> f4 <*> f5 <*> f6 <*> f7 <*> f8 <*> f9 <*> f10
请注意,我更喜欢这种形式,但如果您愿意,也可以使用一元形式(我在
doubleSpace
中使用过)。由于名称所隐含的原因,也很有用


要使用此功能,我强烈推荐GHCI和
parseTest
函数。GHCI总体上非常方便,是测试单个解析器的好方法,而parseTest接受解析器和输入字符串,并输出运行状态、已解析字符串和任何未解析的剩余字符串。当您不太确定发生了什么时,这非常有用。

假设头是固定的,并且每行的字段是“双空格”分隔的,那么在Haskell中为该文件实现解析器就非常容易了。最终的结果可能会比您的regexp更长(如果符合您的要求,Haskell中也有regexp库),但它的可测试性和可读性要高得多。我将演示其中的一些内容,同时概述如何为这种文件格式构建一个

我会用阿托帕塞克。我们还需要使用
ByteString
数据类型(以及
OverloadedStrings
PRAGMA,它允许Haskell将字符串文本解释为
string
ByteString
)以及
Control.Applicative
Control.Monad>中的一些组合符

{-# LANGUAGE OverloadedStrings #-}

import           Data.Attoparsec.Char8
import           Control.Applicative
import           Control.Monad
import qualified Data.ByteString.Char8         as S
首先,我们将构建一个表示每个记录的数据类型

data YearMonthDay =
  YearMonthDay { ymdYear  :: Int
               , ymdMonth :: Int
               , ymdDay   :: Int
               }
    deriving ( Show )

data Line =
  Line { agent     :: Int
       , name      :: S.ByteString
       , st        :: Int
       , ud        :: Int
       , targetNum :: Int
       , xyz       :: Int
       , xDate     :: YearMonthDay
       , year      :: Int
       , co        :: S.ByteString
       , encoding  :: S.ByteString
       }
    deriving ( Show )
如果需要,您可以为每个字段填充更多描述性类型,但这不是一个坏的开始。因为每一行都可以独立解析,所以我将这样做。第一步是构建一个
解析器行
类型——将其作为解析器类型读取,如果成功,它将返回一个

为此,我们将使用解析器的
Applicative
接口在解析器的“内部”构建
Line
类型。这听起来很复杂,但很简单,看起来很漂亮。我们将以
YearMonthDay
类型作为热身开始

parseYMDWrong :: Parser YearMonthDay
parseYMDWrong =
  YearMonthDay <$> decimal
               <*> decimal
               <*> decimal
我们可以使用Attoparsec的
parseOnly
函数测试这是一个有效的解析器

>>> parseOnly parseYMD "2013/12/12"
Right (YearMonthDay {ymdYear = 2013, ymdMonth = 12, ymdDay = 12})

现在,我们想将此技术推广到整个
解析器。然而,有一个障碍。我们想通过testring来解析
字段,比如
“SMITH,JOHN”
,它可能包含空格。。。同时也用双空格分隔我们的
的每个字段。这意味着我们需要一个特殊的
ByteString
解析器,它使用任何字符,包括单个空格。。。但当它看到两个空格连成一行时就退出了

我们可以使用
scan
combinator构建它
scan
允许我们在解析中使用字符时累积状态,并确定何时动态停止解析。我们将保持一个布尔状态——“最后一个字符是空格吗?”——当我们看到一个新的空格而知道前一个字符也是空格时,就停止

parseStringField :: Parser S.ByteString
parseStringField = scan False step where
  step :: Bool -> Char -> Maybe Bool
  step b ' ' | b         = Nothing
             | otherwise = Just True
  step _ _               = Just False
我们可以使用
parseOnly
再次测试这个小部件。让我们尝试解析三个字符串字段

>>> let p = (,,) <$> parseStringField <*> parseStringField <*> parseStringField
>>> parseOnly p "foo  bar  baz"
Right ("foo "," bar "," baz")
>>> parseOnly p "foo bar  baz quux  end"
Right ("foo bar "," baz quux "," end")
>>> parseOnly p "a sentence with no double space delimiters"
Right ("a sentence with no double space delimiters","","")
现在我们可以构建行解析
parseStringField :: Parser S.ByteString
parseStringField = scan False step where
  step :: Bool -> Char -> Maybe Bool
  step b ' ' | b         = Nothing
             | otherwise = Just True
  step _ _               = Just False
>>> let p = (,,) <$> parseStringField <*> parseStringField <*> parseStringField
>>> parseOnly p "foo  bar  baz"
Right ("foo "," bar "," baz")
>>> parseOnly p "foo bar  baz quux  end"
Right ("foo bar "," baz quux "," end")
>>> parseOnly p "a sentence with no double space delimiters"
Right ("a sentence with no double space delimiters","","")
someSpaces :: Parser Int
someSpaces = do
  sps <- some space
  let count = length sps
  if count >= 2 then return count else mzero

>>> parseOnly someSpaces "  "
Right 2
>>> parseOnly someSpaces "    "
Right 4
>>> parseOnly someSpaces " "
Left "Failed reading: mzero"
lineParser :: Parser Line
lineParser =
  Line <$> (decimal <* someSpaces)
       <*> (parseStringField <* someSpaces)
       <*> (decimal <* someSpaces)
       <*> (decimal <* someSpaces)
       <*> (decimal <* someSpaces)
       <*> (decimal <* someSpaces)
       <*> (parseYMD <* someSpaces)
       <*> (decimal <* someSpaces)
       <*> (parseStringField <* someSpaces)
       <*> (parseStringField <* some space)

>>> parseOnly lineParser "0007     SMITH, JOHN                      43   3    1234567   001    12/06/2013   2004    ABC         SIZE XL      "
Right (Line { agent = 7
            , name = "SMITH, JOHN "
            , st = 43
            , ud = 3
            , targetNum = 1234567
            , xyz = 1
            , xDate = YearMonthDay {ymdYear = 12, ymdMonth = 6, ymdDay = 2013}
            , year = 2004
            , co = "ABC "
            , encoding = "SIZE XL "
            })
parseFile :: S.ByteString -> [Either String Line]
parseFile = map (parseOnly parseLine) . drop 14 . lines