Экранирование кавычек в регулярном выражении cl-ppcre
Фон
Мне нужно проанализировать файлы CSV, и
На данный момент синтаксический анализатор строк простой таблицы довольно хрупок, и он ломается, если символ-разделитель появляется внутри строки в кавычках. Пытаюсь заменить синтаксический анализатор строк на cl-ppcre.
Попытки
Используя Regex Coach, я нашел регулярное выражение, которое работает почти во всех случаях:
("[^"]+"|[^,]+)(?:,\s*)?
Задача состоит в том, чтобы превратить эту строку регулярного выражения Perl во что-то, что я могу использовать в cl-ppcre для строки. Я попытался передать строку регулярного выражения с различными escape-символами для
"
:
(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))
Я прочитал
Кажется, ничего не работает.
Я надеялся, что Regex Coach предоставит способ увидеть дерево синтаксического анализа S-выражения в строке синтаксиса Perl. Это была бы очень полезная функция, позволяющая поэкспериментировать со строкой регулярного выражения, а затем скопировать и вставить дерево синтаксического анализа в код Лиспа.
Кто-нибудь знает, как избежать кавычек в этом примере?
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-символы в регулярном выражении
Полученное вами дерево синтаксического анализа содержит следующее:
(... (:GREEDY-REPETITION 0 NIL #\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 видит и эту обратную косую черту, и следующий символ и интерпретирует эту последовательность как
:WHITESPACE-CHAR-CLASS
. Читатель Лиспа интерпретирует
\s
в качестве
s
, потому что это не часть символов, которые можно экранировать в Лиспе.
Я обычно работаю с деревом синтаксического анализа напрямую, потому что уходит много головной боли, связанной с побегом (и, на мой взгляд, это усугубляется с помощью \ Q и \E). Фиксированное дерево синтаксического анализа, например, следующее, где я заменил
#\s
по желаемому ключевому слову и удалили ненужные узлы:
(: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
строка с этим регулярным выражением, но оно фактически описывает поле и следующую запятую. Причина, по которой у вас есть результат NIL, заключается в том, что ваша строка представляет собой просто последовательность разделителей, как в этом примере:
(split #\, ",,,,,,")
NIL
На более простом примере вы можете увидеть, что разделение слов в качестве разделителей дает:
(split "[a-z]+" "abc0def1z3")
=> ("" "0" "1" "3")
Но если разделители также включают цифры, то результат будет NIL:
(split "[a-z0-9]+" "abc0def1z3")
=> NIL
Цикл по полям
С определенным вами регулярным выражением проще использовать
do-register-groups
. Это конструкция цикла, которая выполняет итерацию по строке, пытаясь последовательно сопоставить регулярное выражение в строке, привязывая каждое в регулярном выражении к переменной.
Если вы положите
(: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
узлы, по одному на каждую ветвь чередования; это означает привязку двух переменных, одна из которых равна NIL для каждого успешного совпадения:
(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
каждое поле в список или вектор для обработки позже.