Вернуться на домашнюю страницу.

Перевод сделал Илья Челпанов. Ваши комментарии Вы можете прислать по почте или оставить в  гостевой книге на моей странице. Пример программы к статье: sample.zip

В статье рассматривается вопрос построения грамматики и создания парсера (синтаксического анализатора) для нее на Perl с использованием модуля Parse::RecDescent.

Parsing interesting things.

Randal L. Schwartz

Недавно некто возник в одной из конференций, из тех, что я читаю, с вопросом "как распарсить INI файл?". Возможно, вам знакомы эти файлы, с секциями и строками ключ=значение, например:

  [login]
  timeout=30
  remote=yes
  [password]
  minlength=6

Я думаю, они из мира Microsoft, потому что ни один нормальный Unix-хакер не придумал бы нечего подобного. Нет, мы придумываем что-нибудь типа .Xdefaults, sendmail.cf или termcap. Тем не менее, вопрос выглядел простым: разобрать файл и собрать информацию в хэш для быстрого доступа (двухуровневый, разумеется). 

Обычно я призываю "используйте CPAN", и действительно, существует много CPAN-модулей для разбора INI-файлов (даже слишком много, на мой взгляд). Но давайте на этот раз пойдем другим путем. Представим, что мы разбираем формат, который еще не заCPANен до смерти. Какие средства мы должны использовать? 

Регулярные выражения Perl легко справляются с задачей разбора, и  закодировать это "вручную" не составит большого труда. Но давайте пойдем немного дальше и вытащим из CPAN отличный инструмент: модуль Parse::RecDescent* (Damian Conway). Этот модуль позволяет создавать очень сложные парсеры на основе изящного иерархического описания структуры данных (грамматики) и набора действий, которые необходимо выполнить, когда очередная порция данных распознана. Он очень прост в использовании и позволяет печь парсеры как пирожки.

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

  file: sections

На самом деле, мы кое-что забыли. Если мы оставим все, как есть, грамматике будет соответствовать любой файл, в начале которого есть секция. Поэтому нам нужно указать:

  file: sections /\z/

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

Идем дальше, секции это последовательность из 0 или больше секций, что мы запишем так:

  sections: section(s?)

суффикс (s?) означает "ноль или больше''. Пока все очень прозрачно, идем дальше. Секция   это маркер секции (строка с квадратными скобками) и некоторое количество определений:

  section: section_marker definitions
  definitions: definition(s?)

Подобным образом мы определили и definitions. До сих пор,  описывая сущность INI файла, мы не описали никаких конкретных шаблонов  (за исключением конца последовательности). Дело в том, что мы определяли нетерминальные символы. Правила грамматики могут также содержать терминальные символы (подобно лексеме "конец последовательности" выше) для выделения конкретных вещей. Давайте начнем с маркера секции:

  section_marker: /\\[.*\\]/

Готово. Маркер секции  это нечто в квадратных скобках.  Обратите внимание: обратный слэш   в регулярном выражении продублирован, таковы здесь правила игры. А что такое - определение (definition)?

  definition: key /=/ value

Да! Это ключ (key) и значение (value), разделенные знаком =. А  что они представляют собой? Другие териминалы!

  key: /\w+/
  value: /.*/

Итак, мы уже описали почти всю грамматику, с помощью всего нескольких строк кода. Но теперь нам необходимо немного больше знаний о Parse::RecDescent. Между каждым из описываемых правилами грамматики элементов парсер должен уметь пропускать последовательности незначимых символов, которыми по умолчанию являются "пробельные символы" (whitespace - \s*). Это умолчание отлично работает для маркеров секции: нас устраивает, что все лидирующие пробелы будут отброшены. Но это не то, что мы хотим, если пробелы окажутся между ключом (key) и остальной частью строки. К счастью, мы можем переопределить множество незначимых символов для оставшейся части правила:

  definition: key <skip: ''> /=/ value

что означает, множество незначимых символов пустое, и равно должно быть прижато к  key и что value начинается непосредственно после равно. Отлично!

Мы должны склеить все описанные правила в строку $GRAMMAR и затем создать парсер $PARSER на основе этих правил так:

  use Parse::RecDescent;
  my $PARSER = Parse::RecDescent->new($GRAMMAR)
    or die;

Этот $PARSER может неоднократно использоваться для определения соответствия файлов описанию. Для этого мы вызываем правило самого верхнего уровня (file) как метод, передавая ему параметром $INPUT, с содержимым проверяемого файла:

  if (defined(my $result = $PARSER->file($INPUT))) {
    print "It's a valid INI file!\n";
  } else {
    print "No good.\n";
  }

Этого достаточно, если все, что мы хотим - это проверять на соответствие формату. Но мы хотим использовать результаты разбора. Для этого, мы должны знать, что каждое правило подобно вызову подпрограммы, и возвращает последнее вычисленное значение. По умолчанию, это строка, соответствующая терминалу (или $1, если в определении терминала есть круглые скобки), или значение, которое вернуло последнее подправило. (Для примера выше это ссылка на массив всех соответствий, если таковые были.) Однако мы можем добавить в конце правила блок кода, и в нем формировать возвращаемое значение.

Например, мы не хотим, чтобы квадратные скобки были включены с маркер секции. С помощью $1 мы можем отделить их:

  section_marker: /\[(.*)\]/

Готово. Теперь скобки не включаются в возвращаемое значение. Если бы мы не знали, что $1 возвращается автоматически, мы могли бы вернуть его явно:

  section_marker: /\[(.*)\]/ { $1 }

что говорит: выполни проверку на соответствие регулярному выражению и, если она успешна, выполни блок. Если блок возвращает не undef, это рассматривается как "соответствие", и окончательным значением правила является значение, возвращаемое блоком.

Но что с определениями? Мы хотим использовать и ключ и значение, поэтому мы добавим специальный блок в конце правила. Мы будем возвращать ссылку на массив из двух элементов, но чтобы сформировать ее, нам нужен доступ к значениям подправил key и value. Для этого существует магический хэш %item. Ключи этого хэша - имена подправил.

  definition: key <skip: ''> /=/ value
    { [$item{key}, $item{value}] }

Теперь правило definition возвращает ссылку на массив, состоящий из найденного ключа и его найденного значения. (Если определено больше одного подправила с именем "key", то необходимо использовать позиционный синтаксис, но всегда проще и нагляднее в таких случаях просто определить новый нетерминальный символ.)

Аналогично, для section нужно вернуть имя секции и все определения этой секции.

  section: section_marker definitions 
    { [$item{section_marker}, $item{definitions}] } 

Обратите внимание, что definitions уже будут ссылкой на массив определений, каждое из которых в свою очередь является ссылкой на массив из двух элементов. Все это автоматически обеспечит парсер построенный  Parse::RecDescent!

И последняя забавная часть. Правило file должно вернуть ссылку на все секции:

  file: sections /\z/ { $item{sections} }

Это будет ссылка на массив со списком секций, каждая секция - ссылка на массив со списком определений в этой секции, каждое определение - ссылка на массив с парой ключ/значение. Давайте, преобразуем это в хэш для удобства доступа.

  file:
    sections /\z/
    { my %return;
      my $sections = $item{sections};
      for my $section (@$sections) {
        my ($section_marker, $definitions) = @$section;
        for my $definition (@$definitions) {
          my ($key, $value) = @$definition;
          for ($return{$section_marker}{$key}) {
            if (not defined $_) {
              $_ = $value;
            } elsif (not ref $_) {
              $_ = [$_, $value];
            } else {
              push @$_, $value;
            }
          }
        }
      }
      \%return;
    }

Уф! Что это было?  Сначала мы описали хэш, ссылка на который  будет возвращена, и затем прошли по нескольким уровням ссылок. Интерес представляет код в середине, где в цикле for $_ является алиасом для $return{$section_marker}{$key}. Если значение $_ не определено, значит, мы видим этот ключ в секции в первый раз и просто присваиваем элементу хэша значение. Если оно уже определено, значит, ключ встречается в секции дважды. В этом случае я решил преобразовать значение в ссылку на массив, чтобы иметь возможность доступа к каждому значению. И, наконец, если это уже ссылка на массив, мы просто добавляем очередное значение в конец массива.

Значение, возвращаемое методом file теперь или ссылка на хэш или undef. Чтобы получить значение параметра "timeout" из примера в начале статьи мы можем написать:

  my $timeout = $result->{login}{timeout};

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

Подведем итоги: у нас есть парсер INI-файлов сделанный с помощью Parse::RecDescent. Хотелось бы надеяться, этот краткий пример использования этого мощного модуля заинтересует вас достаточно, чтобы прочитать остальную часть документации и изучить все его фантастические возможности. И вы никогда не будете бояться разбора файла с нерегулярной структурой. До следующего раза, enjoy! 

* RecDescent - Recursive Descent - метод рекурсивного спуска


Randal L. Schwartz - профессионал с двадцатилетним стажем. В сферу его деятельности входит разработка программного обеспечения, системное администрирование, преподавание и написание книг. Он является соавтором замечательных книг Programming Perl, Learning Perl, Learning Perl for Win32 Systems, and Effective Perl Programming. Кроме того он ведет колонки ** в регулярных изданиях WebTechniques, PerformanceComputing, SysAdmin, and Linux magazines. Он также активно участвует в newsgroups, посвященных Perl и Perl Monestary (perlmonks.org), является модератором comp.lang.perl с момента ее появления. Его замечательный юмор и техническое мастерство достигло общемировой известности (хотя, возможно, он сам является автором некоторых из этих легенд). Желание Рэндала внести свой вклад в Perl-сообщество вдохновило его на помощь в создании и обеспечении начального финансирование для The Perl Institute. Он также один из основателей Perl Mongers (perl.org), всемирной организации защиты основ Perl . Начиная с 1985, Рэндал имеет собственный бизнес Stonehenge Consulting Services, Inc С Рэндалом можно связаться по e-mail  merlyn@stonehenge.com или по телефону +1 503 777-0095. Он всегда готов ответить на вопросы по Perl и вязанные с ним темы.

** статьи доступны на его домашней странице и являются отличным учебником по Perl


Статья была опубликована в журнале UnixReview/PerformanceComputing/SysAdmin magazine. Перевод выполнен по авторскому тексту статьи опубликованному на сайте автора.

Авторскими правами на оригинальный текст обладает Miller-Freeman, Inc..

Вернуться на домашнюю страницу.