СтатьиПрограммирование

Построение парсеров с рекурсивным спуском на Python.
n0xi0uzz
06 марта 2008 12:40



Теги: python, parsers, pyparsing

Перевод статьи «Building Recursive Descent Parsers with Python», автор Paul McGuire


Что такое «парсинг»? Парсинг — это обработка набора символов в целях извлечения их значения. Это означает чтение слов предложения и извлечение информации из них. Когда программным предложениям необходимо обработать данные, представленные в виде текста, они должны использовать некоторую форму логики парсинга. Эта логика считывает символы текста и группы символов (слова) и распознает шаблоны групп для извлечения расположенных в них команд или информации.

Программные парсеры обычно представляют собой специализированные программы, созданные для обработки специфичной формы текста. Этим текстом может быть набор закодированных нотаций в сфере страхования или медицины; объявления функций в заголовочном файле на языке C; описание узлов и связей, показывающих связи графа; HTML-теги на веб-странице; или интерактивные команды для конфигурации сети, изменения или поворота 3D-изображения, или навигации по игре. В каждом случае, парсеры обрабатывают специфичный набор групп символов и шаблонов. Этот набор шаблонов называется грамматикой парсера.

Например, при парсинге строки Hello, World!, вы можете пожелать парсить любое приветствие, которое следует этому главному шаблону. Hello, World! начинается с фразы приветствия: слова Hello. Может быть много других фраз приветствия — Howdy, Greetings, Aloha, G'day и так далее — так что вы можете определить очень точную грамматику, начинающуюся с одного слова-приветствия. Далее следует символ запятой, а затем объект приветствия, приветствуемый, так же в виде одного слова. В конце идет некая форма пунктуации, заканчивающее приветствие, как восклицательный знак. Эта грамматика приветствия выглядит приблизительно так (:: читается как «состоит из»):
word           :: group of alphabetic characters
salutation     :: word
comma          :: ","
greetee        :: word
endPunctuation :: "!"
greeting       :: salutation comma greetee endPunctuation

Это форма Бэкоса-Наура (БНФ). Существуют различные диалекты для символов представляющие возможные/обязательные конструкции, повторения, выбор и так далее.

Указав целевую грамматику в БНФ, вы должны конвертировать её в исполняемую форму. Часто используемым методом для этого является рекурсивно-нисходящий парсер, который сначала определяет функции, которые считывают отдельные конечные грамматические конструкции, а потом высокоуровневые функции для вызова низкоуровневых функций. Функции могут возвращать значения в успешных и неуспешных случаях (или возвращать признаки совпадения в случае успеха и формировать исключения в обратном случае).


Что такое Pyparsing?

Pyparsing это библиотека классов Python которая позволяет быстро и легко создать рекурсивно-нисходящие парсеры. Вот pyparsing-реализация примера Hello, World!:
from pyparsing import Word, Literal, alphas

salutation     = Word( alphas + "'" )
comma          = Literal(",")
greetee        = Word( alphas )
endPunctuation = Literal("!")

greeting = salutation + comma + greetee + endPunctuation

Несколько возможностей pyparsing помогают разработчикам сделать их функции для парсинга текста быстро:
— Грамматики на Python, так что не требуются отдельные файлы, определяющие грамматику.
— Не требуется специальный синтаксис, за исключением + для And, ^ для Or (длиннейшее, или «жадное» совпадение), | для MatchFirst (первое совпадение) и ~ для Not
— Не требуется отдельный шаг для генерации кода
— Он полностью пропускает пробелы и комментарии, которые могут располагаться между элементами парсинга, поэтому нет нужды добавлять в вашу грамматику маркеры для игнорируемого текста.

Показанная грамматика pyparsing будет парсить не только Hello, World!, но и такие предложения, как:
* Hey, Jude!
* Hi, Mom!
* G'day, Mate!
* Yo, Adrian!
* Howdy, Pardner!
* Whattup, Dude!
Следующий листинг содержит полный Hello, World! парсер, включая вывод результатов парсинга:
from pyparsing import Word, Literal, alphas

salutation     = Word( alphas + "'" )
comma          = Literal(",")
greetee        = Word( alphas )
endPunctuation = Literal("!")

greeting = salutation + comma + greetee + endPunctuation

tests = ("Hello, World!", 
"Hey, Jude!",
"Hi, Mom!",
"G'day, Mate!",
"Yo, Adrian!",
"Howdy, Pardner!",
"Whattup, Dude!" )

for t in tests:
        print t, "->", greeting.parseString(t)

===========================
Hello, World! -> ['Hello', ',', 'World', '!']
Hey, Jude! -> ['Hey', ',', 'Jude', '!']
Hi, Mom! -> ['Hi', ',', 'Mom', '!']
G'day, Mate! -> ["G'day", ',', 'Mate', '!']
Yo, Adrian! -> ['Yo', ',', 'Adrian', '!']
Howdy, Pardner! -> ['Howdy', ',', 'Pardner', '!']
Whattup, Dude! -> ['Whattup', ',', 'Dude', '!']



Pyparsing — «комбинатор»

С помощью модуля pyparsing, вы сначала определяете базовые части вашей грамматики. Затем вы комбинируете их в более сложные выражения для различных ветвей полного грамматического синтаксиса. Их комбинирование возможно с помощью определения связией, таких как:
— Какие выражения должны следовать друг за другом в грамматике, например: «за ключевым словом if следует булево выражение, заключенное в скобки»
— Какие выражения являются заменами друг друга в определенном случае в грамматике, например: «команда SQL может начинаться со слов SELECT, INSERT, UPDATE или DELETE»
— Какие выражения являются необязательными, например: «телефонный номер необязательно начинается с кода города, заключенного в скобки»
— Какие выражения являются повторяющимися, например: «открытый тег XML может содержать ноль или более атрибутов»

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


Определение простой грамматики.

Наименьшие составные блоки большинства грамматик являются точными строками из символов. Например, вот простая БНФ для парсинга телефонного номера:
number      :: '0'.. '9'*
phoneNumber :: [ '(' number ')' ] number '-' number

Поскольку она ищет дефисы и скобки в строке телефонного номера, вы можете также определить простые символьные знаки для этих пунктуационных знаков:
dash   = Literal( "-" )
lparen = Literal( "(" )
rparen = Literal( ")" )

Чтобы определить группы цифр в телефонном номере, вам надо держать группы символов разной длины. Для этого, используйте метку Word:
digits = "0123456789"
number = Word( digits )

Набор цифр будет соответствовать следующим друг за другом последовательностям, состоящих из символов, перечисленных в строке digits; на самом деле, это «слово», состоящее из цифр (по аналогии с обычным словом, которое состоит из букв алфавита). Теперь у вас есть достаточный набор отдельных частей телефонного номера, так что вы можете соединить их вместе с помощью класса And:
phoneNumber = 
    And( [ lparen, number, rparen, number, dash, number ] )

Это выглядить ужасно уродливо и непривычно для чтения. К счастью, модуль pyparsing определяет методы операторов для комбинирования отдельных элементов более просто. Более понятные определения используют + для And:
phoneNumber = lparen + number + rparen + number + dash + number

Для ещё более чистой версии, оператор + будет соединять строки для парсинга элементов, полностью преобразуя строки в константы. Это сделает пример очень простым для чтения:
phoneNumber = "(" + number + ")" + number + "-" + number

Наконец, чтобы указать, что код города в начале телефонного номера является необязательным, используем класс Optional:
phoneNumber = Optional( "(" + number + ")" ) + number + "-" + number



Использование грамматики.

Следующим шагом после определения грамматики, будет применение её к исходному тексту. Выражения pyparsing поддерживают три метода для обработки входного текста с заданной грамматикой:
— Метод parseString использует грамматику, которая полностью определяет содержимое входящей строки, парсит строку и возвращает набор строк и подстрок каждой грамматической конструкции.
— Метод scanString использует грамматику, которая может соответствовать только частям входной строки, проходит строку в поиске соответствий и возвращает кортеж, содержащий совпадения и их начальные и конечные положения во входящей строке.
— Метод transformString — вариант метода scanString. Он накладывает некоторые изменения к совпадениям и возвращает одну строку, представляющую оригинальный входящий текст, модифицированный отдельными совпадениями.

Исходный парсер Hello, World! вызывает parseString и возвращает непосредственные результаты:
Hello, World! -> ['Hello', ',', 'World', '!']

Не смотря на то, что это выглядит как простой список строк, pyparsing возвращает данные, используя объект ParseResults. В приведенном примере, переменная results ведет себя, как простой список в Python. В самом деле, вы можете работать с results как со списком:
print results[0]
print results[-2]

Выведет:
Hello
World

ParseResults также позволяет вам определять имена отдельных синтаксических элементов, делая проще получения частей текста, подвергшегося парсингу. Это особенно полезно, когда грамматика включает необязательные элементы, которые могут менять длину и элементы возвращаемого списка. Изменив определения salute и greetee:
salute  = Word( alphas+"'" ).setResultsName("salute")
greetee = Word( alphas ).setResultsName("greetee")

вы можете ссылаться на соответствующие элементы, так, словно они являются атрибутами возвращаемого объекта results:
print hello, "->", results    
print results.salute
print results.greetee

Теперь программа выведет:
G'day, Mate! -> ["G'day", ',', 'Mate', '!']
G'day
Mate

Имена результатов могут сильно повысить читаемость и возможность расширения ваших программ для парсинга.

В случае грамматики телефонных номеров, вы можете парсить входящую строку, содержащую список телефонных номеров, один за другим:
phoneNumberList = OneOrMore( phoneNumber )
data            = phoneNumberList.parseString( inputString )

Это возвратит данные в виде объекта pyparsing ParseResults, содержащего список всех входящих телефонных номеров.

Pyparsing включает несколько вспомогательных выражений, таких как delimetedList, так что если у вас на входе разделенный запятыми список телефонных номеров, вы можете просто поменять phoneNumberList на:
phoneNumberList = delimitedList( phoneNumber )

Это вернет тот же список телефонных номеров, что был ранее. (delimetedList поддерживает любую строку или выражение в качестве разделителя, но запятые в качестве разделителей используются наиболее часто, поэтому они являются вариантом по умолчанию)

Если вместо строки, содержащей только телефонные номера, у вас есть более полный список имен, адресов, индексов и телефонных номеров, вы можете извлечь телефонные номера, используя scanString. scanString является Python-функцией генератором, поэтому вы должны использовать её в цикле for, проходу по списку, или в выражении-генераторе.
for data,dataStart,dataEnd in 
    phoneNumber.scanString( mailingListText ):
    .
    .
    # do something with the phone number tokens, 
    # returned in the 'data' variable
    .
    .

Наконец, если у вас есть такой же список адресов, но вы хотите скрыть номера от, скажем специалиста по телефонному маркетингу, вы можете преобразовать строку с помощью добавления действия парсинга, который просто меняет все телефонные номера на строку (000)000-0000. Замена входящих знаков на фиксированную строку — часто используемое действие для парсинга, так что pyparsing предоставляет встроенную функцию replaceWith, чтобы сделать это просто:
phoneNumber.setParseAction( replaceWith("(000)000-0000") )
sanitizedList = 
    phoneNumber.transformString( originalMailingListText )



Когда хороший ввод становится плохим.

Pyparsing будет обрабатывать входящий текст до тех пор, пока не выйдет за совпадающий текст для данных элементов парсера. Если он встретит неожидаемый знак или символ и нет совпадающего элемента, тогда pyparsing вызовет ParseExeption. ParseExeption выводит диагностическое сообщение по умолчанию; он также имеет атрибуты, чтобы помочь вам определить номер строки, столбца, текстовую строку и строку, снабженную комментариями.

Если вы дадите вашему парсеру в качестве строки "Hello, World?", вы получите следующее исключение:
pyparsing.ParseException: Expected "!" (at char 12), (line:1, col:13)

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


Полное приложение.

Представим приложение, которое обрабатывает химические формулы, такие как NaCl, H2O или C6H5OH. Для этого приложения, грамматика химических формул будет представлять собой один или более символов элемента, за каждым из которых необязательно будет следовать целое число. В стиле нотации БНФ:
integer       :: '0'..'9'+
cap           :: 'A'..'Z'
lower         :: 'a'..'z'
elementSymbol :: cap lower*
elementRef    :: elementSymbol [ integer ]
formula       :: elementRef+

Модуль pyparsing поддерживает эти концепции с помощью классов Optional и OneOrMore. Определение elementSymbol будет использовать конструктор Word с двумя аргументами: первый аргумент перечисляет набор первых символов, а второй дает набор разрешенных символов, содержащихся в теле строки. С использованием модуля pyparsing, простая версия грамматики выглядит так:
caps       = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowers     = caps.lower()
digits     = "0123456789"

element    = Word( caps, lowers )
elementRef = element + Optional( Word( digits ) )
formula    = OneOrMore( elementRef )

elements   = formula.parseString( testString )

Таким образом, данная программа разбирает строку, преобразуя поступающие формулы в отдельные знаки. Обычным результатом для pyparsing является возвращение всех знаков в одном списке совпавших подстрок:
H2O -> ['H', '2', 'O']
C6H5OH -> ['C', '6', 'H', '5', 'O', 'H']
NaCl -> ['Na', 'Cl']

Конечно, вы хотите как-то обработать эти полученные результаты, помимо простого вывода их в качестве списка. Представим, что вы хотите посчитать молярную массу каждой данной химической формулы. Программа где-то будет иметь словарь химических символов, определяющий соответствующую атомную массу каждого:
atomicWeight = {
    "O"  : 15.9994,
    "H"  : 1.00794,
    "Na" : 22.9897,
    "Cl" : 35.4527,
    "C"  : 12.0107,
    ...
    }

Затем было бы неплохо установить более логичное группировку символов химических элементов и их количества, прошедших через парсер, чтобы вернуть структурированный набор результатов. К счастью, модуль pyparsing предоставляет класс Group как раз для этой цели. Изменив объявление elementRef с:
elementRef = element + Optional( Word( digits ) )

на:
elementRef = Group( element + Optional( Word( digits ) ) )

вы получите результаты, сгруппированные по химическому символу:
H2O -> [['H', '2'], ['O']]
C6H5OH -> [['C', '6'], ['H', '5'], ['O'], ['H']]
NaCl -> [['Na'], ['Cl']]

Следующее упрощение — включить значение по умолчанию для качественной части elementRef, используя аргумент по умолчанию класса Optional:
elementRef = Group( element + Optional( Word( digits ), 
                                default="1" ) )

Теперь каждый elementRef будет возвращать пару значений: символ химического элемента и количество атомов этого элемента, со значением 1 в случае неуказанного значения. Теперь тестируемые формулы возвращают очень понятный список упорядоченных пар символов элементов и их числа:
H2O -> [['H', '2'], ['O', '1']]
C6H5OH -> [['C', '6'], ['H', '5'], ['O', '1'], ['H', '1']]
NaCl -> [['Na', '1'], ['Cl', '1']]

Последним шагом будет подсчет атомной массы для каждого. Добавление одной строки кода на Python после вызова parseString:
wt = sum( [ atomicWeight[elem] * int(qty) 
                    for elem,qty in elements ] )

даст следующий результат:
H2O -> [['H', '2'], ['O', '1']] (18.01528)
C6H5OH -> [['C', '6'], ['H', '5'], ['O', '1'], ['H', '1']]
        (94.11124)
NaCl -> [['Na', '1'], ['Cl', '1']] (58.4424)

Следующий листинг содержит полную программу.
from pyparsing import Word, Optional, OneOrMore, Group, ParseException

atomicWeight = {
    "O"  : 15.9994,
    "H"  : 1.00794,
    "Na" : 22.9897,
    "Cl" : 35.4527,
    "C"  : 12.0107
    }
    
caps = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowers = caps.lower()
digits = "0123456789"

element = Word( caps, lowers )
elementRef = Group( element + Optional( Word( digits ), default="1" ) )
formula = OneOrMore( elementRef )

tests = [ "H2O", "C6H5OH", "NaCl" ]
for t in tests:
    try:
        results = formula.parseString( t )
        print t,"->", results,
    except ParseException, pe:
        print pe
    else:
        wt = sum( [atomicWeight[elem]*int(qty) for elem,qty in results] )
        print "(%.3f)" % wt

========================
H2O -> [['H', '2'], ['O', '1']] (18.015)
C6H5OH -> [['C', '6'], ['H', '5'], ['O', '1'], ['H', '1']] (94.111)
NaCl -> [['Na', '1'], ['Cl', '1']] (58.442)

Одним из преимуществ использования парсера является неотъемлемая проверка, которую он проводит над входящим текстом. Знайте, что вычисление переменной wt не требует проверки, что строка qty состоит только из цифр или ловить исключения ValueError на получение неправильного значения. Если qty не будет состоять только из цифр, и, поэтому не будет являться правильным значением для int(), она не будет обработана парсером.


Выборка из HTML
Последним примером будет построение простой выборки из HTML. Мы не будем делать полный HTML парсер, так как такой парсер потребовал бы множество выражений парсинга. К счастью, обычно необязательно иметь полное определение грамматики HTML чтобы извлечь значительную часть данных из большинства веб-страниц, особенно из тех, которые сгенерированы автоматически с помощью CGI или других программ.

Этот пример будет извлекать данные с помощью простого парсера, нацеленного работать с специальной веб-страницей — в данном случае, страница содержит список NTP-серверов. Эта задача может быть частью более большого NTP-приложения, которое при запуске проверяет доступность NTP-серверов.

Прежде чем начать разрабатывать выборку из HTML, вы должны посмотреть, какой вид HTML текста вам необходимо обработать. Посетив веб-сайт и просмотрев исходный код HTML, вы увидите, что страница представляет собой список имен и IP-адресов NTP-серверов в виде HTML-таблицы:
Name 	IP Address 	Location
time-a.nist.gov 	129.6.15.28 	NIST, Gaithersburg, Maryland
time-b.nist.gov 	129.6.15.29 	NIST, Gaithersburg, Maryland

Исходный код этой таблицы содержит теги <table>, <tr> и <td>, чтобы упорядочить данные NTP-серверов:
<table border="0" cellpadding="3" cellspacing="3" frame="" width="90%">
                <tr align="left" valign="top">
                        <td><b>Name</b></td>
                        <td><b>IP Address</b></td>
                        <td><b>Location</b></td>
                </tr>
                <tr align="left" valign="top" bgcolor="#c7efce">
                        <td>time-a.nist.gov</td>
                        <td>129.6.15.28</td>
                        <td>NIST, Gaithersburg, Maryland</td>
                </tr>
                <tr align="left" valign="top">
                        <td>time-b.nist.gov</td>
                        <td>129.6.15.29</td>
                        <td>NIST, Gaithersburg, Maryland</td>
                </tr>
       ...

Эта таблица — часть гораздо большего кода HTML, но pyparsing дает вам возможность определить выражение для парсинга, которое соответствует подмножеству всего входящего текста для поиска текста, совпадающего данному выражению. Так что вам надо определить минимальное количество грамматики, которая бы соответствовала необходимому HTML-коду.

Программа должны извлекать IP-адреса и расположения серверов, поэтому вы можете направить вашу грамматику только на эти столбцы таблицы. Проще говоря, вам нужно извлечь значения, соответствующие шаблону:
<td> IP address </td> <td> location name </td>

Вам необходимо сделать выражение более конкретным, чем просто <td> какой-то текст </td> <td> ещё какой-то текст </td>, так как такое выражение будет соответствовать первым двум столбцам таблицы вместо двух вторых (так же, как и первым двум столбцам любой таблицы на странице!). Используйте конкретный формат IP-адреса, чтобы направить ваш шаблон поиска, игнорируя неправильные соответствия в другой таблице страницы.

Чтобы собрать элементы IP-адреса, начните с определения целого числа, собрав потом четыре целых числа и разделив их точками:
integer   = Word("0123456789")
ipAddress = integer + "." + integer + "." + integer + "." + integer

Вам также потребуется обозначить HTML-теги <td> и </td>:
tdStart = Literal("<td>")
tdEnd   = Literal("</td>")

В общем случае, тег <td> может также содержать атрибуты, указывающие расположение, цвет и так далее. Но этот парсер не для общих случаев, а написан специально для этой веб-страницы, которая, к счастью, не использует сложные <td> теги (последняя версия pyparsing включает в себя вспомогательный метод для создания HTML-тегов, с поддержкой указания атрибутов в открывающих тегах).

Наконец, вам необходимо некоторое выражение для соответствия описанию расположения сервера. Эти данные могут быть форматированы по-разному, — неизвестно, как будут прописаны текстовые данные, запятые, точки или цифры, — поэтому простейшим решением будет просто принимать все до закрывающего тега </td>. Pyparsing включает в себя класс под названием SkipTo для этого рода грамматических элементов.

Теперь у вас есть все необходимые части, чтобы определить текстовый шаблон сервера:
timeServer = tdStart + ipAddress + tdEnd + \
                 tdStart + SkipTo(tdEnd) + tdEnd

Чтобы извлечь данные, вызовите timeServer.scanString, который является функцией-генератором и возвращает совпавшие значения, а также начальные и конечные позиции строки для каждого совпавшего текста. Это приложение использует только совпадавшие значения.
from pyparsing import *
import urllib

# define basic text pattern for NTP server 
integer = Word("0123456789")
ipAddress = integer + "." + integer + "." + integer + "." + integer
tdStart = Literal("<td>")
tdEnd = Literal("</td>")
timeServer =  tdStart + ipAddress + tdEnd + tdStart + SkipTo(tdEnd) + tdEnd

# get list of time servers
nistTimeServerURL = "http://tf.nist.gov/service/time-servers.html"
serverListPage = urllib.urlopen( nistTimeServerURL )
serverListHTML = serverListPage.read()
serverListPage.close()

for srvrtokens,startloc,endloc in timeServer.scanString( serverListHTML ):
    print srvrtokens

Запуск этот программы вернет следующие значения:
[' <td>', '129', '.', '6', '.', '15', '.', '28', '</td> ',
 ' <td>', 'NIST, Gaithersburg, Maryland', '</td> ']
[' <td>', '129', '.', '6', '.', '15', '.', '29', '</td> ',
 ' <td>', 'NIST, Gaithersburg, Maryland', '</td> ']
[' <td>', '132', '.', '163', '.', '4', '.', '101', '</td> ',
 ' <td>', 'NIST, Boulder, Colorado', '</td> ']
[' <td>', '132', '.', '163', '.', '4', '.', '102', '</td> ',
 ' <td>', 'NIST, Boulder, Colorado', '</td> ']
:

Взглянув на эти результаты, сразу же видна пара недостатков. Первый заключается в том, что парсер записывает IP адрес как несколько отдельных значений, отделенных друг от друга. Было бы неплохо, если pyparsing соединял эти поля в одну строку в процессе парсинга. Класс Combine из pyparsing сделает это. Измените определение ipAdress следующим образом:
ipAddress = Combine( integer + "." + integer + "." + integer + "." + integer )

чтобы получить одну строку с IP-адресом.

Второй недостаток в том, что результаты включают в себя открывающие и закрывающие HTML-теги, обозначающие столбцы таблицы. В то время, как эти теги важны для процесса парсинга, сами по себе они не нужны в извлеченных данных. Чтобы убрать их из возвращаемых данных, добавьте к константам тегов метод suppress:
tdStart = Literal("<td>").suppress()
tdEnd   = Literal("</td>").suppress()

Листинг:
from pyparsing import *
import urllib

# define basic text pattern for NTP server 
integer = Word("0123456789")
ipAddress = Combine( integer + "." 
		+ integer + "." + integer + "." 
		+ integer )
tdStart = Literal("<td>").suppress()
tdEnd = Literal("</td>").suppress()
timeServer =  tdStart + ipAddress + tdEnd + tdStart 
		+ SkipTo(tdEnd) + tdEnd

# get list of time servers
nistTimeServerURL = "http://tf.nist.gov/service/time-servers.html"
serverListPage = urllib.urlopen( nistTimeServerURL )
serverListHTML = serverListPage.read()
serverListPage.close()

for srvrtokens,startloc,endloc in timeServer.scanString( serverListHTML ):
    print srvrtokens

Теперь запустите программу из этого листинга. Возвращаемые данные стали гораздо лучше:
['129.6.15.28', 'NIST, Gaithersburg, Maryland']
['129.6.15.29', 'NIST, Gaithersburg, Maryland']
['132.163.4.101', 'NIST, Boulder, Colorado']
['132.163.4.102', 'NIST, Boulder, Colorado']

Наконец, добавьте имена результатам, чтобы иметь доступ к ним по имени атрибута. Простейшим путем сделать это будет следующее определение timeServer:
timeServer = tdStart + ipAddress.setResultsName("ipAddress") + tdEnd 
        + tdStart + SkipTo(tdEnd).setResultsName("locn") + tdEnd

Вы можете делать обработку в теле цикла for и получать доступ к значениям, как к членам в словаре:
servers = {}

for srvrtokens,startloc,endloc in timeServer.scanString( serverListHTML ):
    print "%(ipAddress)-15s : %(locn)s" % srvrtokens
    servers[srvrtokens.ipAddress] = srvrtokens.locn

Следующий листинг содержит итоговую рабочую программу:
from pyparsing import *
import urllib

# define basic text pattern for NTP server 
integer = Word("0123456789")
ipAddress = Combine( integer + "." + integer + "." + integer + "." + integer )
tdStart = Literal("<td>").suppress()
tdEnd = Literal("</td>").suppress()
timeServer = tdStart + ipAddress.setResultsName("ipAddress") + tdEnd + \
             tdStart + SkipTo(tdEnd).setResultsName("locn") + tdEnd

# get list of time servers
nistTimeServerURL = "http://tf.nist.gov/service/time-servers.html"
serverListPage = urllib.urlopen( nistTimeServerURL )
serverListHTML = serverListPage.read()
serverListPage.close()

servers = {}
for srvrtokens,startloc,endloc in timeServer.scanString( serverListHTML ):
    print "%(ipAddress)-15s : %(locn)s" % srvrtokens
    servers[srvrtokens.ipAddress] = srvrtokens.locn

print servers

Теперь программа успешно извлекает NTP-сервера и их IP-адреса, а также присваивает значение переменной, так что ваше NTP-приложение может использовать результаты этого парсинга.


В заключение.

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

Скачать pypasring.


Теги: python, parsers, pyparsing

Статьи с такими же тегами:

Одеваем скрипты Python с помощью EasyGui