Экранирование кавычек в регулярном выражении cl-ppcre

#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 , не так уж много. большинство регулярных выражений, которые я использую, не очень сложны (в основном это последовательности, чередования, повторения и классы символов).