2013-12-12 3 views
4

Я пытаюсь выяснить «правильный» способ разобрать конкретный текстовый файл в Haskell.Разбор текстового файла для печати в 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 789/11/2013 2006 NCORPORAT X7 

Я первый встроенный анализатор, чтобы проверить каждую строку, чтобы увидеть, если бы это был рекорд. Если бы это была запись, я просто разрезал линию, основанную на позиции символа, с помощью моей подстрочной функции. Это прекрасно работает.

Тогда я обнаружил, что у меня действительно была библиотека регулярных выражений в моей установке Haskell, поэтому я решил попробовать использовать регулярные выражения, как в F #. Это потерпело неудачу, так как библиотека отвергает абсолютно правильные регулярные выражения.

Тогда я подумал: как насчет Parsec? Но кривая обучения для использования этого становится все круче, чем выше я поднимаюсь, и мне кажется, что это правильный инструмент для такой простой задачи, как разбор этого отчета.

Так что я думал, что попрошу некоторых экспертов Haskell: как бы вы решили разобрать этот отчет? Я не прошу код, хотя, если у вас есть, я бы хотел его увидеть. Я действительно прошу техники или технологии.

Спасибо!

P.s. Вывод - это только файл с разделяемым двоеточием с строкой имен полей в верхней части файла, за которыми следуют только записи, которые могут быть импортированы в Excel для конечного пользователя.

Edit:

Спасибо всем большое за большие комментарии и ответы!

Потому что я не дал понять, изначально: первые четырнадцать строк примера повторяются для каждой страницы вывода (печати) с количеством записей, изменяющихся на страницу от нуля до полной страницы (выглядит как 45 записей). Я прошу прощения за то, что раньше этого не делал, поскольку это, вероятно, повлияет на некоторые из уже предложенных ответов.

Система My Haskell в настоящее время ограничена Parsec (у нее нет attoparsec) и Text.Regex.Base и Text.Regex.Posix. Мне нужно будет увидеть установку attoparsec и/или дополнительных библиотек Regex. Но пока вы убедили меня продолжать изучать Parsec. Спасибо за очень полезные примеры кода!

+2

Я бы определенно пошел с Parsec или лучше, attoparsec. Есть ли у вас какие-то особые проблемы? –

+2

Что касается ваших отклонений регулярных выражений, попробовали ли вы как «Text.Regex», так и «Text.Regex.PCRE»? 'Text.Regex' - это теневой пакет' Text.Regex.Posix', который, вероятно, не поддерживает функции, которые вы использовали для использования. PCRE является регулярным выражением perl-esque и имеет расширенное функциональное предложение. –

+1

Для сравнения библиотек Regexp см. Http://www.haskell.org/haskellwiki/Regular_expressions –

ответ

2

Есть очень мало языков, которые я рекомендовал бы использовать парсер для чего-то так просто (я разобран многих файлов, как этого, используя регулярные выражения в прошлом), но парсеки делают это так

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

parseFile = endBy parseLine (char '\n') 

main = interact $ show . parse parseFile "-" 

Функция «parseLine» создает парсер для отдельной строки, объединяя два поля, состоящих из фиксированной длины (4 символа, любой символ будет делать).

Функция «parseFile» затем объединяет их в список строк.

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

Это, возможно, гораздо легче читать, чем регулярные выражения ....

+0

Я отмечаю это как ответ, потому что это решение, с которого я начал, что я действительно мог работать. Я получил монадическую форму для работы, а затем применил аппликативный стиль к ней, как видно из других ответов, чтобы сделать ее намного короче и красивее. Спасибо! –

4

Это, безусловно, работа стоит из библиотеки синтаксического анализа. Моя главная цель заключается в обычном режиме (то есть, за что я намерен использовать более чем один или два раза), чтобы получить данные в нетекстового форму как можно скорее, что-то вроде

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 

по надлежащему функционированию one of the parse functions и использования один из комбинаторов (например, many (и, возможно, feed, если вы закончите с частичным результатом), вы должны составить список ReportData s. Затем вы можете преобразовать их в CSV с некоторой функцией, которую вы создали.

Обратите внимание, что я не имел дело с заголовком. Должно быть относительно тривиально писать код для его анализа и строить Report, например.

-- Not tested 
parseReport = Report <$> (many reportHeader) <*> (many reportData) 

Обратите внимание, что я предпочитаю Applicative форму, но также можно использовать монадическую форму, если вы предпочитаете (я в doubleSpace). Data.Alternative также полезен по причинам, связанным с именем.

Для игры с этим я настоятельно рекомендую GHCI и функцию parseTest. GHCI просто удобна и подходит для тестирования отдельных парсеров, в то время как parseTest принимает синтаксический анализатор и входную строку и выводит статус прогона, проанализированную строку и любую оставшуюся строку, которая не обрабатывается. Очень полезно, когда вы не совсем уверены, что происходит.

+0

Спасибо за отличный ответ. Я, к сожалению, не смог заставить его работать. Я продолжал получать жалобы на преобразования типов из/в Char, ByteString, [Char] и String. Но я смог получить код для компиляции в конце концов, начиная с более простого ответа, который я назвал ответом. Еще раз спасибо! –

1

Предполагая, что заголовок исправлен, а поле каждой строки разделено на два пространства - это очень просто реализовать парсер в Haskell для этого файла. Конечный результат, вероятно, будет длиннее вашего регулярного выражения (и в Haskell есть библиотеки регулярных выражений, если это соответствует вашему желанию), но это far более читабельны и читабельны. Я продемонстрирую некоторые из них, пока я расскажу, как создать один для этого формата файла.

Я буду использовать Attoparsec. Нам также понадобится использовать тип данных ByteString (и 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) 

Вы можете заполнить более описательные типы для каждого поля, если это необходимо, но это неплохой старт. Поскольку каждая строка может быть проанализирована независимо, я сделаю именно это. Первым шагом является создание типа Parser Line --- прочитайте это как тип парсера, который возвращает Line, если он преуспеет.

Для этого мы построим наш Line тип «внутри» анализатора, используя его интерфейс Applicative. Это звучит очень сложно, но это просто и выглядит довольно красиво. Мы начнем с YearMonthDay типа, как разминка

parseYMDWrong :: Parser YearMonthDay 
parseYMDWrong = 
    YearMonthDay <$> decimal 
       <*> decimal 
       <*> decimal 

Здесь decimal встроенный в Attoparsec парсер, который разбирает интегрального типа как Int. Вы можете прочитать этот синтаксический анализатор как не более чем «разобрать три десятичных числа и использовать их для построения моего типа YearMonthDay», и вы были бы в основном правы. Оператор (<*>) (читается как «apply») последовательно выполняет парсы и собирает их результаты в нашу конструкторскую функцию YearMonthDay.

К сожалению, как я указал в типе, это немного неправильно. Чтобы указать, мы в настоящее время игнорируем символы '/', которые ограничивают числа внутри нашего YearMonthDay. Мы исправим это, используя оператор «последовательность и выброс» (<*). Это визуальный каламбур на (<*>), и мы используем его, когда хотим выполнить синтаксический анализ ... но мы не хотим сохранять результат.

Мы используем (<*) для усиления первых два decimal парсеров с их свитой '/' символов с помощью встроенных char8 анализатора.

parseYMD :: Parser YearMonthDay 
parseYMD = 
    YearMonthDay <$> (decimal <* char8 '/') 
       <*> (decimal <* char8 '/') 
       <*> decimal 

И мы можем проверить, что это достоверный анализатор с помощью функции parseOnly Attoparsec в

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

Мы хотели бы, чтобы в настоящее время обобщают эту технику для всей Line анализатор. Однако есть одна заминка. Мы хотели бы разобрать ByteString таких полей, как "SMITH, JOHN", которые могут содержать пробелы ... в то же время ограничивая каждое поле нашего Line двойными пробелами. Это означает, что нам нужен специальный парсер ByteString, который потребляет любой символ, включая одиночные пробелы ... но завершает момент, когда видит два пробела подряд.

Мы можем построить это, используя комбинатор scan. 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","","") 

В зависимости от вашего фактического формата файла это может быть идеально. Стоит отметить, что он оставляет конечные пробелы (при необходимости они могут быть обрезаны), и это позволяет некоторым полям, ограниченным пробелом, быть пустым. Легко продолжать играть с этой пьесой, чтобы исправить эти ошибки, но я оставлю это пока.

Теперь мы можем построить наш парсер Line. Как и в случае с parseYMD, мы будем следить за парсером каждого поля с помощью разделительного синтаксического анализатора someSpaces, который потребляет два или более пробела.Мы будем использовать интерфейс MonadPlus для Parser, чтобы построить это поверх встроенного анализатора space по (1) разбор some space s и (2) проверку, чтобы убедиться, что у нас есть как минимум два из них.

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 
+0

Спасибо за отличный ответ. Я, к сожалению, не смог заставить его работать. Я продолжал получать жалобы на преобразования типов из/в Char, ByteString, [Char] и String. Но я смог получить код для компиляции в конце концов, начиная с более простого ответа, который я назвал ответом. Еще раз спасибо! –

+0

Скорее всего, проблема, если у вас возникли проблемы с компилятором, запутывающим 'ByteString',' [Char] 'и' String', является прагмой 'OverloadedStrings'. –

Смежные вопросы