#common-lisp #cl-ppcre
Вопрос:
Фон
Мне нужно анализировать CSV-файлы, а cl-csv и др. работают слишком медленно для больших файлов и зависят от cl-unicode, который моя предпочтительная реализация lisp не поддерживает. Итак, я совершенствую cl-simple-таблицу, которую Sabra-на-холме оценила как самый быстрый считыватель csv в обзоре.
На данный момент анализатор строк simple-table довольно хрупок, и он ломается, если символ разделителя появляется в строке, заключенной в кавычки. Я пытаюсь заменить анализатор строк на cl-ppcre.
Попытки
Используя тренер регулярных выражений, я нашел регулярное выражение, которое работает почти во всех случаях:
("[^"] "|[^,] )(?:,s*)?
Задача состоит в том, чтобы превратить эту строку регулярного выражения Perl во что-то, что я могу использовать в cl-ppcre для split
строки. Я попытался передать строку регулярного выражения с различными побегами для "
:
(defparameter bads ""AER","BenderlyZwick","Benderly and Zwick Data: Inflation, Growth and Stock returns",31,5,0,0,0,0,5,"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv","https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html"
"Bad string, note a separator character in the quoted field, near Inflation")
(ppcre:split "("[^"] "|[^,] )(?:,s*)?" bads)
NIL
Ни одноместная, ни двухместная, ни трехместная, ни четырехместная
работа.
Я проанализировал строку, чтобы посмотреть, как выглядит дерево синтаксического анализа:
(ppcre:parse-string "("[^"] "|[^,] )(?:,s*)?")
(:SEQUENCE (:REGISTER (:ALTERNATION (:SEQUENCE #" (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #")) #") (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #,)))) (:GREEDY-REPETITION 0 1 (:GROUP (:SEQUENCE #, (:GREEDY-REPETITION 0 NIL #s)))))
и передал полученное дерево в split
:
(ppcre:split '(:SEQUENCE (:REGISTER (:ALTERNATION (:SEQUENCE #" (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #")) #") (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #,)))) (:GREEDY-REPETITION 0 1 (:GROUP (:SEQUENCE #, (:GREEDY-REPETITION 0 NIL #s))))) bads)
NIL
Я также пробовал различные формы *allow-quoting*
:
(let ((ppcre:*allow-quoting* t))
(ppcre:split "(\Q"\E[^\Q"\E] \Q"\E|[^,] )(?:,s*)?" bads))
Я прочитал документы cl-ppcre, но очень мало примеров использования деревьев синтаксического анализа и нет примеров ускользающих цитат.
Кажется, ничего не работает.
Я надеялся, что тренер регулярных выражений предоставит способ увидеть форму дерева синтаксического анализа S-выражения синтаксической строки Perl. Это было бы очень полезной функцией, позволяющей вам поэкспериментировать со строкой регулярного выражения, а затем скопировать и вставить дерево синтаксического анализа в код Lisp.
Кто-нибудь знает, как избежать кавычек в этом примере?
Комментарии:
1. Я не думаю, что вы можете превзойти скорость конкретного синтаксического анализатора с помощью регулярного выражения.
2. Глядя на cl-простую таблицу, первое, что они делают,-это разделяют строку. Это неправильно, и попытка использовать разделение регулярных выражений (вместо этого, но в одном и том же месте) либо также будет неправильной, либо повлияет на производительность неприемлемым образом. Я вполне уверен, что для вас это не лучший путь вперед.
3. Какую реализацию вы используете?
4. @Svante, возможно, ты прав, и я подумал о том же самом. Написание CSV-анализатора для общего случая-довольно большая задача. К сожалению, похоже, что для common lisp нет ничего, что было бы надежным и хорошо работающим. Мне не нужно использовать регулярное выражение, и я открыт для альтернативных решений.
5. Я всегда был вполне доволен тарифами, но я не знаю, как это соотносится с производительностью.
Ответ №1:
В этом ответе я сосредоточусь на ошибках в вашем коде и попытаюсь объяснить, как вы могли бы заставить его работать. Как объяснил @Svante, это может быть не лучшим вариантом действий для вашего варианта использования. В частности, ваше регулярное выражение может быть слишком адаптировано для ваших известных тестовых входов и может пропустить случаи, которые могут возникнуть позже.
Например, в вашем регулярном выражении поля рассматриваются либо как строки, разделенные двойными кавычками, без внутренних двойных кавычек (даже экранированных), либо как последовательность символов, отличная от запятой. Однако если ваше поле начинается с обычной буквы, а затем содержит двойную кавычку, оно будет частью имени поля.
Исправление тестовой строки
Возможно, возникла проблема при форматировании вашего вопроса, но форма ввода bads
искажена. Вот фиксированное определение для *bads*
(обратите внимание на звездочки вокруг специальной переменной, это полезное соглашение, которое помогает отличать их от лексических переменных (звездочки вокруг имен также известны как «наушники»)):
(defparameter *bads*
""AER","BenderlyZwick","Benderly and Zwick Data: Inflation, Growth and Stock returns",31,5,0,0,0,0,5,"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv","https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html"")
Escape-символы в регулярном выражении
The parse tree you obtain contains this:
(... (:GREEDY-REPETITION 0 NIL #s) ...)
В вашем дереве синтаксического анализа есть буквальный символ #s
. Чтобы понять, почему, давайте определим две вспомогательные функции:
(defun chars (string)
"Convert a string to a list of char names"
(map 'list #'char-name string))
(defun test (s)
(list :parse (chars s)
:as (ppcre:parse-string s)))
Например, вот как анализируются различные строки ниже:
(test "s")
=> (:PARSE ("LATIN_SMALL_LETTER_S") :AS #s)
(test "s")
=> (:PARSE ("LATIN_SMALL_LETTER_S") :AS #s)
(test "\s")
=> (:PARSE ("REVERSE_SOLIDUS" "LATIN_SMALL_LETTER_S")
:AS :WHITESPACE-CHAR-CLASS)
Только в последнем случае, когда обратная косая черта (обратный солидус) экранирована, анализатор PPCRE видит как эту обратную косую черту, так и следующий символ #s
и интерпретирует эту последовательность как :WHITESPACE-CHAR-CLASS
. Читатель Lisp интерпретирует s
как s
, потому что это не часть символов, которые могут быть экранированы в Lisp.
Я склонен работать с деревом синтаксического анализа напрямую, потому что многие головные боли, связанные с побегом, исчезают (и, на мой взгляд, это усугубляется Q и E). Фиксированное дерево синтаксического анализа-это, например, следующее, где я заменил #s
ключевое слово на нужное и удалил :register
узлы, которые не были полезны:
(:sequence
(:alternation
(:sequence #"
(:greedy-repetition 1 nil
(:inverted-char-class #"))
#")
(:greedy-repetition 1 nil (:inverted-char-class #,)))
(:greedy-repetition 0 1
(:group
(:sequence #,
(:greedy-repetition 0 nil :whitespace-char-class)))))
Почему результат равен НУЛЮ
Помните, что вы пытаетесь split
ввести строку с помощью этого регулярного выражения, но регулярное выражение на самом деле описывает поле и следующую запятую. Причина, по которой у вас нулевой результат, заключается в том, что ваша строка представляет собой просто последовательность разделителей, как в этом примере:
(split #, ",,,,,,")
NIL
На более простом примере вы можете видеть, что разделение слов в качестве разделителей дает:
(split "[a-z] " "abc0def1z3")
=> ("" "0" "1" "3")
Но если разделители также включают цифры, то результат равен НУЛЮ:
(split "[a-z0-9] " "abc0def1z3")
=> NIL
Зацикливание на полях
С определенным вами регулярным выражением его проще использовать do-register-groups
. Это конструкция цикла, которая повторяет строку, пытаясь последовательно сопоставить регулярное выражение в строке, привязывая каждое (:register ...)
регулярное выражение к переменной.
Если вы поставите (:register ...)
вокруг первого (:alternation ...)
, вы иногда будете захватывать двойные кавычки (первая ветвь чередования).:
(do-register-groups (field)
('(:SEQUENCE
(:register
(:ALTERNATION
(:SEQUENCE #"
(:GREEDY-REPETITION 1 NIL
(:INVERTED-CHAR-CLASS #"))
#")
(:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #,))))
(:GREEDY-REPETITION 0 1
(:GROUP
(:SEQUENCE #,
(:GREEDY-REPETITION 0 NIL :whitespace-char-class)))))
*bads*)
(print field))
""AER""
""BenderlyZwick""
""Benderly and Zwick Data: Inflation, Growth and Stock returns""
"31"
"5"
"0"
"0"
"0"
"0"
"5"
""https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv""
""https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html""
Другой вариант-добавить два :register
узла, по одному для каждой ветви чередования; это означает привязку двух переменных, одна из которых равна НУЛЮ для каждого успешного совпадения:
(do-register-groups (quoted simple)
('(:SEQUENCE
(:ALTERNATION
(:SEQUENCE #"
(:register ;; <- quoted (first register)
(:GREEDY-REPETITION 1 NIL
(:INVERTED-CHAR-CLASS #")))
#")
(:register ;; <- simple (second register)
(:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #,))))
(:GREEDY-REPETITION 0 1
(:GROUP
(:SEQUENCE #,
(:GREEDY-REPETITION 0 NIL :whitespace-char-class)))))
*bads*)
(print (or quoted simple)))
"AER"
"BenderlyZwick"
"Benderly and Zwick Data: Inflation, Growth and Stock returns"
"31"
"5"
"0"
"0"
"0"
"0"
"5"
"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv"
"https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html"
Внутри цикла вы можете push
поместить каждое поле в список или вектор, которые будут обработаны позже.
Комментарии:
1. это отличный ответ. Используете ли вы ссылку для разгадки версии дерева синтаксического анализа? В своих поисках я вообще мало что увидел. Возможно, во 2-м издании рецептов CL будет больше, но на данный момент это кажется черным искусством.
2. помимо edicl.github.io/cl-ppcre/#create-scanner2 , не так уж много. большинство регулярных выражений, которые я использую, не очень сложны (в основном это последовательности, чередования, повторения и классы символов).