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

Фон

Мне нужно проанализировать файлы CSV, и cl-csv et. al. работают слишком медленно для больших файлов и зависят от cl-unicode, который моя предпочтительная реализация lisp не поддерживает. Итак, я улучшаю cl-simple-table, который Sabra-on-the-hill оценил как самый быстрый читатель 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))

Я прочитал документацию cl-ppcre , но есть очень мало примеров использования деревьев синтаксического анализа и нет примеров экранирования кавычек.

Кажется, ничего не работает.

Я надеялся, что 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 каждое поле в список или вектор для обработки позже.

Другие вопросы по тегам