Python парсер строки: Pyparsing для новичков / Хабр
Парсинг формул в 50 строк на Python / Хабр
Вдохновение — задача с собеседования Яндекса и статья «Парсинг формул в 40 строк».
Моей целью было посмотреть, как будет выглядеть «pythonic» решение этой задачи. Хотелось, чтобы решение было простым, код читаемым и разделённым. В итоге ещё получился и пример применения цепочки генераторов (generators pipeline).
На классический алгоритм решения задачи указал Яндекс в своей статье — Алгоритм сортировочной станции.
Алгоритм преобразует выражения в инфиксной, привычной нам, нотации в обратную польскую.
Вычисление выражения в обратной польской нотации (ОПН) для нас привлекательно тем, что у него простой алгоритм.
Весь алгоритм вычисления выражения разбивается на три части:
- парсинг исходной строки на числа и операторы
- применение алгоритма сортировочной станции для получения выражения в ОПН
- вычисление выражения в ОПН
На выходе этапов 1 и 2 мы получаем массивы из чисел и операторов. Велик соблазн реализовать функцию как цепочку генераторов. Мы сократим потребление памяти, используя «ленивую» обработку данных, где выражение вычисляется по мере поступления чисел и операторов.
Итак, приступим
Для начала, определяем операторы в виде словаря, для каждого символа определим приоритет и функцию вычисления.
Этот словарь нам пригодится также для того, чтобы определять, является ли символ оператором:
OPERATORS = {'+': (1, lambda x, y: x + y), '-': (1, lambda x, y: x - y),
'*': (2, lambda x, y: x * y), '/': (2, lambda x, y: x / y)}
Определим нашу функцию eval_
, на вход которой подаётся строка с вычисляемым выражением:
def eval_(formula_string):
Внутри функции определим функцию и два генератора, каждый из которых будет выполнять свою часть работы.
1. Парсер исходной строки
Генератор, получает на вход строку, возвращает числа в формате float, операторы и скобки в формате символов.
def parse(formula_string):
number = ''
for s in formula_string:
if s in '1234567890.': # если символ - цифра, то собираем число
number += s
elif number: # если символ не цифра, то выдаём собранное число и начинаем собирать заново
yield float(number)
number = ''
if s in OPERATORS or s in "()": # если символ - оператор или скобка, то выдаём как есть
yield s
if number: # если в конце строки есть число, выдаём его
yield float(number)
2. Алгоритм сортировочной станции
Генератор, получает на вход итерируемый объект из чисел и операторов в инфиксной нотации, возвращает числа и операторов в обратной польской записи.
def shunting_yard(parsed_formula):
stack = [] # в качестве стэка используем список
for token in parsed_formula:
# если элемент - оператор, то отправляем дальше все операторы из стека,
# чей приоритет больше или равен пришедшему,
# до открывающей скобки или опустошения стека.
# здесь мы пользуемся тем, что все операторы право-ассоциативны
if token in OPERATORS:
while stack and stack[-1] != "(" and OPERATORS[token][0] <= OPERATORS[stack[-1]][0]:
yield stack.pop()
stack.append(token)
elif token == ")":
# если элемент - закрывающая скобка, выдаём все элементы из стека, до открывающей скобки,
# а открывающую скобку выкидываем из стека.
while stack:
x = stack.pop()
if x == "(":
break
yield x
elif token == "(":
# если элемент - открывающая скобка, просто положим её в стек
stack.append(token)
else:
# если элемент - число, отправим его сразу на выход
yield token
while stack:
yield stack.pop()
3. Вычислитель
Функция, получает на вход итерируемый объект чисел и операторов в обратной польской нотации, возвращает результат вычисления:
def calc(polish):
stack = []
for token in polish:
if token in OPERATORS: # если приходящий элемент - оператор,
y, x = stack.pop(), stack.pop() # забираем 2 числа из стека
stack.append(OPERATORS[token][1](x, y)) # вычисляем оператор, возвращаем в стек
else:
stack.append(token)
return stack[0] # результат вычисления - единственный элемент в стеке
В конце концов, составляем цепочку генераторов для вычисления результата функции eval_:
return calc(shunting_yard(parse(formula_string)))
Быстродействие
Самый главный вопрос: «Как быстро работает программа?» Сравним нашу функцию со встроенной функцией eval.
На простейших случаях наша функция даже быстрее!
%timeit eval("2+2")
100000 loops, best of 3: 12.8 µs per loop
%timeit eval_("2+2")
100000 loops, best of 3: 7.61 µs per loop
На выражениях посложнее — на 22% дольше:
%timeit eval("15/(7-(1+1))*3-(2+(1+1))")
10000 loops, best of 3: 29.7 µs per loop
%timeit eval_("15/(7-(1+1))*3-(2+(1+1))")
10000 loops, best of 3: 36.3 µs per loop
На выражениях ещё сложнее — разрыв увеличивается, но всё равно быстродействие нашей функции сравнимо со встроенной:
%timeit eval("15/(7-(1+1))*3-(2+(1+1))*15/(7-(1+1))*3-(2+(1+1))*(15/(7-(1+1))*3-(2+(1+1))+15/(7-(1+1))*3-(2+(1+1)))")
10000 loops, best of 3: 86.3 µs per loop
%timeit eval_("15/(7-(1+1))*3-(2+(1+1))*15/(7-(1+1))*3-(2+(1+1))*(15/(7-(1+1))*3-(2+(1+1))+15/(7-(1+1))*3-(2+(1+1)))")
10000 loops, best of 3: 147 µs per loop
Да, вспоминая название статьи, тут всего 50 строк, не забывая про читаемость и PEP8!Код функции целиком
OPERATORS = {'+': (1, lambda x, y: x + y), '-': (1, lambda x, y: x - y),
'*': (2, lambda x, y: x * y), '/': (2, lambda x, y: x / y)}
def eval_(formula):
def parse(formula_string):
number = ''
for s in formula_string:
if s in '1234567890.':
number += s
elif number:
yield float(number)
number = ''
if s in OPERATORS or s in "()":
yield s
if number:
yield float(number)
def shunting_yard(parsed_formula):
stack = []
for token in parsed_formula:
if token in OPERATORS:
while stack and stack[-1] != "(" and OPERATORS[token][0] <= OPERATORS[stack[-1]][0]:
yield stack.pop()
stack.append(token)
elif token == ")":
while stack:
x = stack.pop()
if x == "(":
break
yield x
elif token == "(":
stack.append(token)
else:
yield token
while stack:
yield stack.pop()
def calc(polish):
stack = []
for token in polish:
if token in OPERATORS:
y, x = stack.pop(), stack.pop()
stack.append(OPERATORS[token][1](x, y))
else:
stack.append(token)
return stack[0]
return calc(shunting_yard(parse(formula)))
Разбор аргументов командной строки
Введение
Примеры
Привет, мир в argparse
Следующая программа говорит привет пользователю. Он принимает один позиционный аргумент, имя пользователя, а также может быть сказано приветствие.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('name',
help='name of user'
)
parser.add_argument('-g', '--greeting',
default='Hello',
help='optional alternate greeting'
)
args = parser.parse_args()
print("{greeting}, {name}!".format(
greeting=args.greeting,
name=args.name)
)
$ python hello.py --help
usage: hello.py [-h] [-g GREETING] name
positional arguments:
name name of user
optional arguments:
-h, --help show this help message and exit
-g GREETING, --greeting GREETING
optional alternate greeting
$ python hello.py world
Hello, world!
$ python hello.py John -g Howdy
Howdy, John!
Для получения более подробной информации , пожалуйста , прочитайте argparse документацию .
Базовый пример с докоптом
docopt получается аргумент командной строки при разборе на голове. Вместо разбора аргументов, вы просто пишете строку использования для вашей программы, и docopt разбирает строку использования и использует его для извлечения аргументов командной строки.
"""
Usage:
script_name.py [-a] [-b] <path>
Options:
-a Print all the things.
-b Get more bees into the path.
"""
from docopt import docopt
if __name__ == "__main__":
args = docopt(__doc__)
import pprint; pprint.pprint(args)
Образцы прогонов:
$ python script_name.py
Usage:
script_name.py [-a] [-b] <path>
$ python script_name.py something
{'-a': False,
'-b': False,
'<path>': 'something'}
$ python script_name.py something -a
{'-a': True,
'-b': False,
'<path>': 'something'}
$ python script_name.py -b something -a
{'-a': True,
'-b': True,
'<path>': 'something'}
Установка взаимоисключающих аргументов с помощью argparse
Использование аргументов командной строки с argv
Всякий раз , когда скрипт Python вызывается из командной строки, пользователь может предоставить дополнительные аргументы командной строки , которые будут переданы на сценарий. Эти аргументы будут доступны для программиста от переменной системы sys.argv
( «ARGV» является традиционным названием используется в большинстве языков программирования, и это означает «ARG ument v Эктор»).
По соглашению, первый элемент в sys.argv
списке имя самого скрипта Python, в то время как остальные элементы являются лексемы , передаваемые пользователем при вызове сценария.
# cli.py
import sys
print(sys.argv)
$ python cli.py
=> ['cli.py']
$ python cli.py fizz
=> ['cli.py', 'fizz']
$ python cli.py fizz buzz
=> ['cli.py', 'fizz', 'buzz']
Вот еще один пример того , как использовать argv
. Сначала мы удаляем начальный элемент sys.argv, потому что он содержит имя скрипта. Затем мы объединяем остальные аргументы в одно предложение и, наконец, выводим это предложение, добавляя имя текущего пользователя, вошедшего в систему (чтобы оно имитировало программу чата).
import getpass
import sys
words = sys.argv[1:]
sentence = " ".join(words)
print("[%s] %s" % (getpass.getuser(), sentence))
Алгоритм обычно используется , когда «вручную» разбор числа не-позиционных аргументов перебрать sys.argv
списка. Один из способов — просмотреть список и вытолкнуть каждый его элемент:
# reverse and copy sys.argv
argv = reversed(sys.argv)
# extract the first element
arg = argv.pop()
# stop iterating when there's no more args to pop()
while len(argv) > 0:
if arg in ('-f', '--foo'):
print('seen foo!')
elif arg in ('-b', '--bar'):
print('seen bar!')
elif arg in ('-a', '--with-arg'):
arg = arg.pop()
print('seen value: {}'.format(arg))
# get the next value
arg = argv.pop()
Пользовательское сообщение об ошибке парсера с argparse
Концептуальная группировка аргументов с помощью argparse.add_argument_group ()
Расширенный пример с docopt и docopt_dispatch
Как docopt, с [docopt_dispatch] вы ремесло ваш --help
в __doc__
переменной вашей точки входа модуля. Там, вы вызываете dispatch
с доком строкой в качестве аргумента, так что он может запустить анализатор над ним.
Это делается вместо того, чтобы вручную обрабатывать аргументы (которые обычно заканчиваются структурой if / else с высокой цикломатикой), вы оставляете это для диспетчеризации, давая только то, как вы хотите обработать набор аргументов.
Это то , что dispatch.on
декоратор для: вы даете ему аргумент или последовательность аргументов , которые должны вызвать функцию, и эта функция будет выполняться со значениями соответствия в качестве параметров.
"""Run something in development or production mode.
Usage: run.py --development <host> <port>
run.py --production <host> <port>
run.py items add <item>
run.py items delete <item>
"""
from docopt_dispatch import dispatch
@dispatch.on('--development')
def development(host, port, **kwargs):
print('in *development* mode')
@dispatch.on('--production')
def development(host, port, **kwargs):
print('in *production* mode')
@dispatch.on('items', 'add')
def items_add(item, **kwargs):
print('adding item...')
@dispatch.on('items', 'delete')
def items_delete(item, **kwargs):
print('deleting item...')
if __name__ == '__main__':
dispatch(__doc__)
Синтаксис
Параметры
Примечания
Необыкновенно лёгкий парсинг в Python
Нашёл просто волшебную библиотечку для парсинга в Python (хм, правильно говорить синтаксического анализа), pyparsing
. Ниже на простом примере я покажу, как её можно использовать для разбора пользовательских форматов данных.
Нашёл так: читая Real World Haskell, узнал про комбинаторную библиотеку для синтаксического анализа Parsec. Примеры в книжке впечатлили. В отличие от традиционного подхода, при этом нет разделения на лексический анализ (выделение «слов»-лексем) и синтаксический анализ (преобразование потока «слов» в упорядоченную структуру данных) — в комбинаторном парсинге эти два этапа объединяются. Берутся небольшие функции, распознающие элементы текста, и затем они комбинируются в соответветствии с синтаксисом текста. Таким образом, сама комбинация функций непосредственно отражает грамматику, и она же, естественно, является и программой для разбора текста. Как у всякой удачной идеи, у Parsec есть множество подражаний. Для Python комбинаторных парсеров нашлось
целых двауже триуже четыре — Pysec, Pyparsing, LEPL (для Python 2.6/3.0) и funcparselib. Я буду говорить оpyparsing
.В следующей заметке — Ещё одна библиотека для комбинаторного парсинга — смотрите аналогичный пример для библиотечки
funcparserlib
.
Итак, перейдём к делу. Предположим нужно читать файлы состоящие из записей следующего вида:
Inspection
# 2 SHOULD Ref. Sys 1
X 28.7493
Y 78.9960
Z -1.0014
Всё необходимое импортируем из модуля pyparsing
. При работе поглядываем в документацию к модулю. Для простоты примера импортируем всё:
from pyparsing import *
Теперь начинаем описывать грамматику. Например, определим числа как слова, состоящие из цифр, знака точки и дефиса (минуса)
number = Word(nums + ".-")
а значения переменных определим как пару заглавной латинской буквы и числа:
var = Regex("[A-Z]") + number
Обратим внимание, что плюс между двумя простыми парсерами (регулярное выражение и слово) создаёт новый парсер, который распознаёт уже последовательность выражений. По-умолчанию pyparsing
игнорирует все лишние пробелы и переводы строк между элементами разбираемого текста (обычно именно это и нужно), поэтому указывать в грамматике наличие пробелов между элементами необязательно.
Уже на этом этапе мы можем попробовать наш парсер переменных. Запускаем интерпретатор и выполняем:
>>> var.parseString("X 42.0")
(['X', '42.0'], {})
— на выходе получили структуру данных в соответствии с нашей грамматикой (имя переменной и число за ним).
Допишем всё остальное. Для простоты будем считать комментарием всё после знака «#» до конца строки (комбинатор restOfLine
):
comment = "#" + restOfLine
Теперь мы можем описать грамматику всей записи в целом.
record = Suppress("Inspection" + comment) + OneOrMore(var)
Запись опознаём по слову «Inspection» в начале (здесь строковой литерал Python автоматически конвертируется в Literal
-парсер, проверяющий буквальное соответствие слову). Далее, обнаружив начало записи, состоящие из слова «Inspection» и следующий за ней комментарий, мы можем их просто пропустить (комбинатор Suppress
), а вот то, что следует дальше — нам интересно. Мы ожидаем, что дальше могут идти значения для одной или нескольких переменных (применяем комбинатор OneOrMore
).
Последний штрих. Нужно указать, что в файле таких записей может быть несколько. Для удобства работы с полученной структурой переменные каждой из записей группируем вместе (комбинатор Group
):
datafile = OneOrMore(Group(record))
Всё! Синтаксический анализатор для нашего формата данных готов. Использовать можно, например, так:
import sys
print datafile.parseString(sys.stdin.read())
Проверяем:
$ python example.py > Inspection
> # 2 SHOULD Ref. Sys 1
> X 28.7493
> Y 78.9960
> Z -1.0014
>
> Inspection
> # 3 SHOULD Ref. Sys 1
> X 54.0394
> Y 64.3977
> Z -0.9950
>
> END
[['X', '28.7493', 'Y', '78.9960', 'Z', '-1.0014'],
['X', '54.0394', 'Y', '64.3977', 'Z', '-0.9950']]
Получили вполне пригодную к использованию в программе структуру данных. Вся грамматика — на пять строк. В общем-то, поняв идею и поглядывая в справку, несложно описать и более сложную грамматику.
Например, чтобы разбирать также и строчку с «#» в моём примере, программку можно изменить так:
from pyparsing import *
number = Word(nums + ".-")
var = Regex("[A-Z]") + number
desc = Suppress("#") + Word(nums) + Word(alphas) \
+ Suppress("Ref. Sys") + Word(nums)
record = Suppress("Inspection") + desc + Group(OneOrMore(Group(var)))
datafile = OneOrMore(Group(record))
На выходе этот парсер даст:
[['2', 'SHOULD', '1', [['X', '28.7493'], ['Y', '78.9960'], ['Z', '-1.0014']]],
['3', 'SHOULD', '1', [['X', '54.0394'], ['Y', '64.3977'], ['Z', '-0.9950']]]]
P.S. Нормального тьюториала по pyparsing
в сети я не нашёл, но автор библиотеки написал и продаёт на O’Reilly учебное электропособие за 10 долларов. Справочная же документация и разные примеры в интернете — вполне толковы.
См. также заметку про funcparserlib.
Основы парсинга с помощью Python+lxml / Хабр
Добрый день, уважаемые читатели.
В сегодняшней статье я покажу основы разбора HTML разметки страниц с помощью библиотеки lxml для Python.
Если вкратце, то lxml это быстрая и гибкая библиотека для обработки разметки XML и HTML на Python. Кроме того, в ней присутствует возможность разложения элементов документа в дерево. В статье я постараюсь показать, насколько просто ее применение на практике.
Выбор цели для парсинга
Т.к. я активно занимаюсь спортом, в частности БЖЖ мне захотелось посмотреть статисту по болевым приемам во все проведенных турнирах мировых турнирах по MMA.
Поиски по гулу привели меня на сайт со всей официальной статистикой по крупным международным турнирам по смешанным единоборствам. Единственной загвоздкой было то, что информация на нам была представлена в неудобном для анализа виде. Это связано с тем, что результаты турниров находится отдельных страницах. Кроме того, дата турнира также с его названием вынесены на отдельную страницу отдельной странице.
Чтобы объединить всю информацию по турнирам в одну таблицу, пригодную для анализа, было принято решение написать парсер описанный ниже.
Алгоритм работы парсера
Для начала разберемся с алгоритмом работы парсера. Он будет следующим:
- За основу возьмем таблицу со всеми турнирами и их датами, которая находится
по данному
адресу - Занесем данные с этой страницы в набор данных, cо следующими столбцами:
- турнир
- ссылка на описание
- дата
- По каждой записи набора (по каждому турниру) осуществляем переход по полю
[ссылка на описание], для получения информации о боях - Записываем информацию по всем боям турнира
- К набору данных с информацией о боях добавляем дату проведения турнира из
набора (2)
Алгоритм готов и можно перейти к его реализации
Начало работы с lxml
Для работы нам понадобятся модули lxml и pandas. Подгрузим их в нашу программу:
import lxml.html as html
from pandas import DataFrame
Для удобства дальнейшего парсинга вынесем основной домен в отдельную переменную:
main_domain_stat = 'http://hosteddb.fightmetric.com'
Теперь давайте получим объект для парсинга. Сделать это можно с помощью функции parse():
page = html.parse('%s/events/index/date/desc/1/all' % (main_domain_stat))
Теперь откроем указанную таблицу в HTML редакторе и изучаем ее структуру. Больше всего нас интересует блок с классами events_table data_table row_is_link
, т.к. именно он содержит таблицу с нужными нам данными. Получить данный блок можно так:
e = page.getroot().\
find_class('events_table data_table row_is_link').\
pop()
Разберемся, что делает данный код.
Сначала с помощью функции getroot() мы получаем корневой элемент нашего документа (это нужно для последующей работы с документом).
Далее, с помощью функции find_class() мы находим все элементы с указанными классами. В результате работы функции мы получим список таких элементов. Т.к. после визуального анализа HTML кода страницы видно, что по данному критерию подходит только один элемент, то мы извлекаем его из списка с помощью функции pop().
Теперь надо получить таблицу из нашего div‘a, полученного ранее. Для этого воспользуемся методом getchildren(), который возвращает список подчерненных объектов текущего элемента. И
потому, что у нас только один такой объект, ты мы извлекаем этот его из списка.
t = e.getchildren().pop()
Теперь переменная t содержит таблицу с необходимой для нас информацией. Теперь, я получу 2 вспомогательных dataframe’a, объединив которые, мы получим данные о турнирах с датами их проведения и ссылками на результаты.
В первый набор я включу все названия турниров и ссылки на их страницы на сайте. Это легко сделать с помощью итератора iterlinks(), который возвращает список котрежей (элемент, атрибут,
внутри заданного элемента. Собственно, из этого кортежа, нам нужен адрес ссылки и ее текст.
адрес ссылки, позиция )
Тест ссылки можно получить обративший к свойству .text соответсвующего элемента. Код будет следующим:
events_tabl = DataFrame([{'EVENT':i[0].text, 'LINK':i[2]} for i in t.iterlinks()][5:])
Внимательный читатель заметит, что в цикле мы исключаем первые 5 записей. В них содержится не нужная нам информация, типа заголовков полей, поэтому я от них и избавился.
Итак, ссылки мы получили. Теперь получим 2 поднабор данных с датами проведения турниров. Это можно сделать так:
event_date = DataFrame([{'EVENT': evt.getchildren()[0].text_content(), 'DATE':evt.getchildren()[1].text_content()} for evt in t][2:])
В коде, показанном выше, мы проходим по всем строкам (теги tr) в таблице t. Затем для каждой строки получаем список дочерних колонок (элементы td). И получаем информацию записанную в первой и второй колонках с помощью метода text_content, который возвращает строку из текста всех дочерних элементов данного столбца.
Чтобы понять, как работает метод text_content приведем небольшой пример. Допустим у нас задана такая структура документа <tr><td><span>текст</span><span>текст</span>. Так вот, метод text_content вернет строку текст текст, а метод text не вернет ничего, или же просто текст.
Теперь, когда у нас есть 2 поднабора данных, объединим их в итоговый набор:
sum_event_link = events_tabl.set_index('EVENT').join(event_date.set_index('EVENT')).reset_index()
Тут, мы сначала указываем индексы нашим наборам, затем объединяем их и сбрасываем индексы итогового набора. Подробнее о этих операция можно прочитать в одной из моих прошлых статей. Осталось выгрузить полученный dataframe в текстовый файл, для сохранности:
sum_event_link.to_csv('..\DataSets\ufc\list_ufc_events.csv',';',index=False)
Обработчик события одного события UFC
Страницу с перечнем турниров мы выгрузили в удобном формате. Пришло время разобраться со страницами с результатами по соревнований. Для примера возьмем последний турнир и посмотрим HTML код страницы.
Можно заметить, что нужная нам информация содержится в элементе с классом data_table row_is_link. В целом процесс парсинга похож на показанный выше, за одним исключением: таблица результатов оформлена не совсем корректно.
Некорретность ее в том, что для каждого бойца в ней заведена отдельная строка, что никак не удобно при анализе. Чтобы избавиться от этого неудобства при разборе результатов я принял решение использовать итератор, только по нечетным строкам. Номер же четной вычислять из текущей нечетной строки.
Таким образом я буду обрабатывать сразу пару строк и переносить их в строку. Код будет следующий:
all_fights = []
for i in sum_event_link.itertuples():
page_event = html.parse('%s/%s' % (main_domain_stat,active_event_link))
main_code = page_event.getroot()
figth_event_tbl = main_code.find_class('data_table row_is_link').pop()[1:]
for figther_num in xrange(len(figth_event_tbl)):
if not figther_num % 2:
all_fights.append(
{'FIGHTER_WIN': figth_event_tbl[figther_num][2].text_content().lstrip().rstrip(),
'FIGHTER_LOSE': figth_event_tbl[figther_num+1][1].text_content().lstrip().rstrip(),
'METHOD': figth_event_tbl[figther_num][8].text_content().lstrip().rstrip(),
'METHOD_DESC': figth_event_tbl[figther_num+1][7].text_content().lstrip().rstrip(),
'ROUND': figth_event_tbl[figther_num][9].text_content().lstrip().rstrip(),
'TIME': figth_event_tbl[figther_num][10].text_content().lstrip().rstrip(),
'EVENT_NAME': i[1]}
)
history_stat = DataFrame(all_fights)
Можно заметить, что для каждого поединка дополнительно записывается название турнира. Это нужно для того, чтобы определить дату поединка.
Сохраним теперь полученные результаты в файл:
history_stat.to_csv('..\DataSets\ufc\list_all_fights.csv',';',index=False)
Посмотрим на полученный результат:
history_stat.head()
EVENT_NAME | FIGHTER_LOSE | FIGHTER_WIN | METHOD | METHOD_DESC | ROUND | TIME | |
---|---|---|---|---|---|---|---|
0 | UFC Fight Night 38: Shogun vs. Henderson | Robbie Lawler | Johny Hendricks | U. DEC | NaN | 5 | 5:00 |
1 | UFC Fight Night 38: Shogun vs. Henderson | Carlos Condit | Tyron Woodley | KO/TKO | Knee Injury | 2 | 2:00 |
2 | UFC Fight Night 38: Shogun vs. Henderson | Diego Sanchez | Myles Jury | U. DEC | NaN | 3 | 5:00 |
3 | UFC Fight Night 38: Shogun vs. Henderson | Jake Shields | Hector Lombard | U. DEC | NaN | 3 | 5:00 |
4 | UFC Fight Night 38: Shogun vs. Henderson | Nikita Krylov | Ovince Saint Preux | SUB | Other — Choke | 1 | 1:29 |
Осталось полько подтянуть к поединкам дату и выгрузить итоговый файл:
all_statistics = history_stat.set_index('EVENT_NAME').join(sum_event_link.set_index('EVENT').DATE)
all_statistics.to_csv('..\DataSets\ufc\statistics_ufc.csv',';', index_label='EVENT')
Заключение
В статье я постарался показать основы работы с библиотекой lxml, пердназначенной для парсинга разметки XML и HTML. Код указанный в статье не претендует на оптимальность, но корректно выполняет поставленную перед ним задачу.
Как видно из приведенной программы процесс работы с библиотекой довольно прост, что помогает быстро писать нужный код. Кроме того помимо указанных выше функций и методов есть и другие не менее нужные.
модуль argparse / Блог компании RUVDS.com / Хабр
Если вы занимаетесь обработкой и анализом данных с использованием Python, то вам, рано или поздно, придётся выйти за пределы Jupyter Notebook, преобразовав свой код в скрипты, которые можно запускать средствами командной строки. Здесь вам и пригодится модуль argparse. Для новичков, привыкших к Jupyter Notebook, такой шаг означает необходимость покинуть зону комфорта и перейти в новую среду. Материал, перевод которого мы публикуем сегодня, написан для того, чтобы облегчить подобный переход.
Модуль argparse
Модуль argparse
Модуль argparse можно сравнить с силами природы, которые воздвигли горные пики, возвышающиеся над облаками. Благодаря этому модулю в скриптах становится возможным работа с тем, что, без его использования, было бы скрыто от кода этих скриптов.
Надо отметить, что argparse является рекомендуемым к использованию модулем стандартной библиотеки Python, предназначенным для работы с аргументами командной строки. Мне не удалось найти хорошее руководство по argparse для начинающих, поэтому я и решил написать такое руководство сам.
Жизнь за пределами Jupyter Notebook
Когда я впервые столкнулся с argparse в Python-скрипте, который нужен был мне для проекта, которым я занимался в свободное время, я подумал: «А это что ещё за таинственная конструкция?». После этого я быстро перенёс код в Jupyter Notebook, но такой ход оказался нерациональным.
Мне нужно было, чтобы у меня была возможность просто запустить скрипт, а не работать с ним средствами Jupyter Notebook. Автономным скриптом, в котором использовался модуль argparse, было бы гораздо легче пользоваться, работать над ним было бы проще, чем полагаясь на возможности Jupyter Notebook. Однако тогда я спешил, и, когда взглянул на документацию по argparse, не смог сходу ухватить её суть, поэтому и не стал пользоваться исходной версией скрипта.
С тех пор я разобрался с argparse и этот модуль мне очень понравился. Теперь я считаю его прямо-таки жизненно необходимым. При этом освоить его не так уж и сложно.
Зачем нужен модуль argparse?
Модуль argparse позволяет разбирать аргументы, передаваемые скрипту при его запуске из командной строки, и даёт возможность пользоваться этими аргументами в скрипте. То есть речь идёт о том, что этот модуль позволяет предоставлять скрипту некие данные в момент его запуска, а этими данными скрипт сможет воспользоваться во время выполнения его кода. Модуль argparse — это средство, с помощью которого можно наладить общение между автором программы и тем, кто ей пользуется, например — между вами, когда вы сегодня пишете скрипт, и вами же, когда вы завтра его запускаете, что-то ему передавая.
Использование argparse означает, что, при необходимости изменить поведение скрипта или при необходимости передачи ему неких данных, если это предусмотрено автором скрипта, пользователю не нужно редактировать программный код. В результате скрипты обретают определённый уровень гибкости.
Пример
Предположим, вы хотите написать скрипт для преобразования видеофайлов в обычные изображения с использованием библиотеки OpenCV. Для того чтобы скрипт мог бы решить эту задачу, ему нужно знать место, где хранятся видеофайлы, и место, в которое нужно поместить готовые изображения. То есть, ему требуются сведения о двух папках, пути к которым, что не очень удобно, можно жёстко задать в коде скрипта, или, что уже куда лучше, можно позволить задавать пользователю скрипта, вводя их в качестве аргументов командной строки при запуске скрипта. Для того чтобы оснастить скрипт такой возможностью, нам и пригодится модуль argparse. Вот как может выглядеть раздел скрипта (назовём этот скрипт videos.py
), в котором осуществляется разбор аргументов командной строки:
# videos.py
import argparse
parser = argparse.ArgumentParser(description='Videos to images')
parser.add_argument('indir', type=str, help='Input dir for videos')
parser.add_argument('outdir', type=str, help='Output dir for image')
args = parser.parse_args()
print(args.indir)
Здесь, в начале файла, импортируется модуль argparse. Затем, с использованием конструкции argparse.ArgumentParser()
, создаётся объект parser
с указанием его описания. Далее, с помощью метода parser.add_argument()
, описывается переменная indir
, в которую планируется записать путь к папке с видеофайлами. При этом указывается то, что она имеет строковой тип, а также задаётся справочная информация о ней. После этого, точно так же, создаётся переменная outdir
, в которую попадёт путь к папке, в которую скрипт должен будет поместить изображения, созданные на основе видеофайлов. На следующем шаге работы в переменную args
попадает результат разбора аргументов командной строки. То, что передано скрипту при запуске, теперь будет доступно в виде свойств indir
и outdir
объекта args
. Теперь с этими значениями можно работать. В данном случае мы просто выводим в консоль то, что передано скрипту в аргументе indir
.
Вот как запустить этот скрипт из командной строки:
python videos.py /videos /images
Обратите внимание на то, что строки /videos
и /images
не нужно заключать в кавычки. Скрипт, запущенный таким образом, выведет в терминал строку /videos
, чем подтвердит возможность использования переданных ему аргументов в своём коде. Это — магия argparse в действии.
Магия разбора аргументов командной строки
Подробности об argparse
Только что мы рассмотрели простой пример работы с argparse. Теперь давайте обсудим некоторые подробности, касающиеся argparse.
▍Позиционные аргументы
Конструкция вида parser.add_argument('indir', type=str, help='Input dir for videos')
из скрипта videos.py
предназначена для создания позиционного аргумента (positional argument). При вызове скрипта важен порядок указания таких аргументов. Так, первый аргумент, переданный скрипту, становится первым позиционным аргументом, второй аргумент — вторым позиционным аргументом.
Что произойдёт в том случае, если скрипт запустить вообще без аргументов, выполнив в терминале команду python videos.py
?
В таком случае будет выведено сообщение об ошибке следующего вида:
videos.py: error: the following arguments are required: indir, outdir
В результате оказывается, что для того, чтобы запустить скрипт, в котором предусмотрено использование позиционных аргументов, такие аргументы всегда нужно указывать при его запуске.
▍Необязательные аргументы
Что произойдёт при запуске нашего скрипта командой python videos.py --help
?
В ответ будет выведена справочная информация о нём. Это — именно те сведения о позиционных аргументах, которые мы указывали при описании соответствующих переменных:
usage: videos.py [-h] indir outdir
Videos to images
positional arguments:
indir Input dir for videos
outdir Output dir for image
optional arguments:
-h, --help show this help message and exit
Скрипт сообщил нам много интересного о том, чего он ждёт от пользователя, а help
— это пример необязательного аргумента (optional argument). Обратите внимание на то, что --help
(или -h
) — это единственный стандартный необязательный аргумент, которым мы можем пользоваться при работе с argparse, но, если вам нужны и другие необязательные аргументы, их можно создавать и самостоятельно.
Необязательные аргументы создают так же, как и позиционные. Основная разница между командами их создания заключается в том, что при указании имён таких аргументов эти имена начинаются с последовательности символов --
, или, для кратких форм аргументов, с символа -
. Например, необязательный аргумент можно создать так:
parser.add_argument('-m', '--my_optional')
Вот пример того, как создавать и использовать необязательные аргументы. Обратите внимание на то, что мы, описывая здесь необязательный аргумент, указали его тип как int
. То есть он представляет собой целое число. В подобной ситуации можно использовать и другие типы Python.
# my_example.py
import argparse
parser = argparse.ArgumentParser(description='My example explanation')
parser.add_argument(
'--my_optional',
type=int,
default=2,
help='provide an integer (default: 2)'
)
my_namespace = parser.parse_args()
print(my_namespace.my_optional)
Аргумент, описанный как --my_optional
, доступен в программе в виде свойства объекта my_namespace
с именем my_optional
.
Необязательным аргументам можно назначать значения, которые они будут иметь по умолчанию. В нашем случае, если при вызове скрипта аргументу my_example
не будет задано никакого значения, в него будет записано число 2, которое и будет выведено в консоль. Для того чтобы задать значение этого аргумента во время запуска скрипта можно воспользоваться такой конструкцией:
python my_example.py --my_optional=3
Для чего ещё можно использовать argparse?
Модуль argparse можно использовать при разработке Python-приложений, которые планируется упаковывать в контейнеры Docker. Так, например, если при запуске приложения, упакованного в контейнер, ему нужно передать аргументы командной строки, то описать это, на этапе сборки контейнера, можно в Dockerfile с помощью инструкции RUN
. Для запуска скриптов во время выполнения контейнера можно пользоваться инструкциями CMD
или ENTRYPOINT
. Подробности о файлах Dockerfile вы можете найти здесь.
Итоги
Мы рассмотрели базовые способы работы с модулем argparse, используя которые, вы можете оснастить свои скрипты возможностью принимать и обрабатывать аргументы командной строки. При этом надо отметить, что возможности argparse на этом не заканчиваются. Например, использование при описании аргументов параметра nargs позволяет работать со списками аргументов, а параметр choices позволяет задавать наборы значений, которые могут принимать аргументы. На самом деле, теперь, освоив основные возможности argparse, вы, без особых сложностей, сможете изучить этот модуль более глубоко, используя документацию к нему.
Если вы привыкли работать с Jupyter Notebook и хотите отойти от этой практики, то вот и вот — материалы по работе с переменными окружениями. Вот материал, посвящённый средству, repo2docker, позволяющему преобразовывать репозитории Jupyter Notebook в образы Docker.
Уважаемые читатели! Как вы работаете с аргументами командной строки в Python-скриптах?
Урок по Scrapy в python — парсим отзывы на Amazon и сохраняем в excel
Scrapy — это продвинутый Python-фреймворк для веб-скрапинга, который помогает разработчикам извлекать данные с разных сайтов. Scrapy использует веб-краулеров, называемых Spider. Они могут доставать, обрабатывать и сохранять данные. Поскольку Scrapy построен на базе Twitsted, асинхронного сетевого фреймворка, он отправляет запросы без блокировки потока. Это влияет на скорость.
Преимущества Scrapy
- Имеется встроенный механизм под названием Selector, который отвечает за поиск и извлечение данные со страниц с помощью Xpath и CSS.
- Для работы Scrapy не нужно много дополнительного кода как в случае с остальными фреймворками. Достаточно лишь указать сайт и данные, которые требуется получить. Scrapy сделает все остальное.
- Scrapy — это бесплатный, кроссплатформенный проект с открытым исходным кодом.
- Он является быстрым, мощным и легко расширяемым за счет асинхронной обработки запросов.
- Используется для создания скраперов для крупных проектов.
- С помощью Scrapy можно извлекать данные с любых страниц вне зависимости от их доступности.
- Куда меньшее потребление памяти и мощности процессора в сравнении с другими библиотеками.
- Поддерживает экспорт данных в разные форматы, включая CSV, XML, JSON и JSON Line.
- Отличное сообщество для разработчиков.
Установка Scrapy
Scrapy работает в обеих версиях Python (2 и 3), но в этом руководстве будет использоваться Python 3. Есть два способа установки. Если имеется Anaconda, то нужно использовать канал conda-forge. Сама платформа доступна по ссылке.
Второй вариант — использовать пакетный менеджер pip. С его помощью можно установить Scrapy прямо на компьютер.
conda install -c conda-forge scrapy
pip install scrapy
В обоих случаях будет загружена и установлена последняя версия.
Создание проекта Scrapy
Архив с кодом проекта. Весь код протестирован на: python 3.8 / scrapy 2.3.0 / ubuntu 16.04
Для создания проекта нужно переместиться в папку с ним. Дальше создается проект Scrapy. В этом руководстве он будет называться
scrapy_tutorial
.
scrapy startproject scrapy_tutorial
Есть вы получили ошибку No module named ‘_cffi_backend’, установите
cffi
—pip install cffi
Подписывайтесь на телеграм каналы
Когда проект создан, появляются папка и конфигурационный файл. Новая директория содержит различные компоненты поискового роботы, который будут созданы позже. Внутри проекта следующая структура.
Рассмотрим в подробностях.
Файл | Описание |
spiders | Эта папка содержит всех Spider в формате класса Python. Если запустить Scrapy, то он выполнит поиск именно в этой папке |
items.py | Содержит контейнер, который будет загружаться вместе с извлеченными данными |
middleware.py | Содержит механизм обработки для работы с запросами и ответами |
pipeline.py | Набор классов Python для последующей обработки классов |
settings.py | Здесь находятся все настройки |
Создание Spider
Spider — это класс, содержащий методологию извлечения данных с указанного сайта. Другими словами, он определяет, как именно должен проходить процесс.
Для создания Spider используется такая команда.
scrapy genspider spidername your-link-here
В качестве
spidername
можно задать любое название, а на месте ссылки — URL сайта или домена, с которого требуется извлечь данные. В этом руководстве попробуем достать пользовательские обзоры на Apple iPhone XS Max с сайта Amazon.com.Поэтому Spider будет называться
reviewspider
.
scrapy genspider reviewspider amazon.com/Apple-iPhone-Max-Fully-Unlocked/product-reviews/B07KFNRQ5S
Scrapy Shell
Scrapy Shell — это интерактивная оболочка, напоминающая Python Shell, где можно проверять свой код. С помощью нее можно протестировать выражения Xpath и CSS, убедившись, что данные извлекаются. Spider при этом даже не будет запускаться. Это быстрый и важный инструмент для разработки и отладки.
scrapy shell https://www.amazon.com/Apple-iPhone-Max-Fully-Unlocked/product-reviews/B07KFNRQ5S
Определение структуры HTML
Прежде чем переходить к написанию кода самого Spider нужно проанализировать структуру страницы.
На изображении видно, что у каждого обзора есть текст и оценка в звездах. Будет извлекать оба элемента.
Для просмотра структуры нужно кликнуть правой кнопкой по странице и нажать «Посмотреть код» или посмотреть код с помощью инструментов разработчика в браузере.
Согласно структуре все отзывы заключены в блок с id
cm_cr-review_list
, у которого есть внутренние блоки для каждого отзыва.Если раскрывать их, то можно увидеть отдельные блоки для каждого компонента обзора. Однако сейчас важны только рейтинг в звездочках и текст.
<i data-hook="review-star-rating"> <span>2.0 out of 5 stars</span> </i>
Здесь рейтинг определяется классом
a-icon-alt
.<span data-hook="review-body"> <span> I know is use phones but for the price of $700 it looks in a bad condition unfortunately you take a risk when you buy phones with out been able to see them and are used and I am not happy with this purchase. </span> </span>
Текст же заключен в класс
a-size-base
. Эта информация пригодится для создания Spider.Создание парсера Scrapy
Если открыть Spider, который был создан раньше, то внутри будет такой класс.
import scrapy
class ReviewspiderSpider(scrapy.Spider):
name = 'reviewspider'
allowed_domains = ['amazon.com']
start_urls = ['https://www.amazon.com/Apple-iPhone-Max-Fully-Unlocked/product-reviews/B07KFNRQ5S/']def parse(self, response):
passЭто базовый шаблон, а
allowed_domains
иstart_urls
определены на основе переданных ссылок.Логика извлечения данных будет прописана в функции
parse
, которая начнет работу после перехода на страницу, указанную вstart_urls
.Scrapy предоставляет возможность поиска по нескольким URL одновременно. Для этого нужно определить базовый URL и те части, которые нужны присоединить к нему. Делается это с помощью
urljoin()
. Однако в этом примере достаточно будет лишь одной ссылки.Далее код, который будет извлекать отзывы пользователей.
import scrapy
class ReviewspiderSpider(scrapy.Spider):
name = 'reviewspider'
allowed_domains = ['amazon.com']
start_urls = ['https://www.amazon.com/Apple-iPhone-Max-Fully-Unlocked/product-reviews/B07KFNRQ5S/']def parse(self, response):
passВ Scrapy есть собственный механизм, селекторы, для извлечения данных. Они используют выражения Xpath и CSS для выбора разных элементов в HTML-документах. В этом коде в качестве селектора используется Xpath.
star_rating=response.xpath('//span[@class="a-icon-alt"]/text()').extract()
В последней строке Scrapy использует Xpath для получения узла в ответе и извлечения его данных в текстовом формате.
for item in zip(star_rating, comments): # создаем словарь для хранения собранной информации scraped_data = { 'Рейтинг': item[0], 'Отзыв': item[1], }
Здесь каждый элемент добавляется в словарь Python.
yield scraped_data
Yield возвращает извлеченные данные для обработки и сохранения.
Обработка нескольких страниц
В примере выше функция
parse
работала для одной страницы. Но это неэффективно, когда обзоры расположены на нескольких. Поэтому нужно расширить код так, чтобы была возможность перемещаться по всем доступным страницам и извлекать данные тем же способом.Перемещение по страницам происходит с помощью кнопки Next Page, и вот HTML-код для нее.
<li> <a href="/Apple-iPhone-Max-Fully-Unlocked/product-reviews/B07KFNRQ5S/ref=cm_cr_arp_d_paging_btm_2?ie=UTF8&pageNumber=2"> Next page <span></span> <span></span> → </a> </li>
Теперь осталось изменить Spider так, чтобы он мог определять кнопку и проверять, существует ли она. Если да, то он будет переходить по ней и снова вызывать парсер. Для этого достаточно добавить следующее в конец функции
parse
.
def parse(self, response):
star_rating = response.xpath('//span[@class="a-icon-alt"]/text()').extract()
comments = response.xpath('//span[@class="a-size-base review-text review-text-content"]/span/text()').extract()
count = 0for item in zip(star_rating, comments):
# создаем словарь для хранения собранной информации
scraped_data = {
'Рейтинг': item[0],
'Отзыв': item[1],
}# возвращаем собранную информацию
yield scraped_dataЗдесь в качестве селектора для Next Page использовался CSS. После определения
extract_first()
ищет первое совпадение и проверяет, существует ли оно. Если да, то для нового URL вызывается методself.parse
.Работа Spider
После создания Spider его нужно запустить с помощью следующей команды.
next_page = response.css('.a-last a ::attr(href)').extract_first()
if next_page:
yield scrapy.Request(
response.urljoin(next_page),
callback=self.parse
)Команда
runspider
принимает reviewspider.py в качестве входящего файла и возвращает CSV-файл scraped_data.csv с собранными результатами.Экспорт данных Scrapy
Scrapy предоставляет возможность экспорта для сохранения извлеченных данных в разных форматах или методах сериализации. Поддерживаются форматы CSV, XML и JSON.
Например, для получения вывода в формате CSV нужно перейти в settings.py и ввести следующие строки.
scrapy runspider scrapy_tutorial/spiders/reviewspider.py -o scraped_data.csv
После этого сохраните файл и снова запустите Spider. Сгенерированный файл окажется в папке проекта.
Если нужны временная метка или имя, то их можно передать в
FEED_URI
так:%(time)s
или%(name)s
.Например:
FEED_URI = "scraped_data_%(time)s.json"
Выводы
Scrapy — это мощный фреймворк для веб-скрапинга. На примере из материала можно было увидеть, насколько просто с ним работать. Он предназначен в первую очередь для парсинга HTML-документов, но быстро и легко изучается для самых разных сценариев примнения. Все подробности есть в официальной документации.
парсим единицы измерения на Python / Хабр
В прошлой статье мы познакомились с удобной библиотекой синтаксического анализа Pyparsing и написали парсер для выражения
'import matplotlib.pyplot as plt'
.В этой статье мы начнём погружение в Pyparsing на примере задачи парсинга единиц измерения. Шаг за шагом мы создадим рекурсивный парсер, который умеет искать символы на русском языке, проверять допустимость названия единицы измерения, а также группировать те из них, которые пользователь заключил в скобки.
Примечание: Код этой статьи протестирован и выложен на Sagemathclod. Если у Вас вдруг что-то не работает (скорее всего из-за кодировки текста), обязательно сообщите мне об этом в личку, в комментариях или напишите мне на почту или в ВК.
Начало работы. Исходные данные и задача.
В качестве примера будем парсить выражение:
s = "Н*м^2/(кг*с^2)"
Эта единица измерения была взята из головы с целью получить строку, анализ которой задействовал бы все возможности нашего парсера. Нам нужно получить:
res = [('Н',1.0), ('м',2.0), ('кг',-1.0), ('с',-2.0)]
Заменив в строке
s
деление умножением, раскрыв скобки и явно проставив степени у единиц измерения, получим: Н*м^2/(кг*с^2) = Н^1 * м^2 * кг^-1 * с^-2.Таким образом, каждый кортеж в переменной
res
содержит название единицы измерения и степень, в которую её необходимо возвести. Между кортежами можно мысленно поставить знаки умножения.Перед тем, как использовать pyparsing, его необходимо импортировать:
from pyparsing import *
Когда мы напишем парсер, мы заменим * на использованные нами классы.
Методика написания парсера на Pyparsing
При использовании pyparsing следует придерживаться следующей методики написания парсера:
- Сначала из текстовой строки выделяются ключевые слова или отдельные важные символы, которые являются «кирпичиками» для построения конечной строки.
- Пишем отдельные парсеры для «кирпичиков».
- «Собираем» парсер для конечной строки.
В нашем случае основными «кирпичиками» являются названия отдельных единиц измерения и их степени.
Написание парсера для единицы измерения. Парсинг русских букв.
Единица измерения — это слово, которое начинается с буквы и состоит из букв и точек (например мм.рт.ст.). В pyparsing мы можем записать:
ph_unit = Word(alphas, alphas+'.')
Обратите внимание, что у класса
Word
теперь 2 аргумента. Первый аргумент отвечает за то, что должно быть первым символом у слова, второй аргумент — за то, какими могут быть остальные символы слова. Единица измерения обязательно начинается с буквы, поэтому мы поставили первым аргументомalphas
. Помимо букв единица измерения может содержать точку (например, мм.рт.ст), поэтому второй аргумент уWord
–alphas + '.'
.К сожалению, если мы попробуем распарсить любую единицу измерения, мы обнаружим, что парсер работает только для единиц измерения на английском языке. Это потому, что
alphas
подразумевает не просто буквы, а буквы английского алфавита.Данная проблема обходится очень легко. Сначала создадим строку, перечисляющую все буквы на русском:
rus_alphas = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ'
И код парсера для отдельной единицы измерения следует изменить на:
ph_unit = Word(alphas+rus_alphas, alphas+rus_alphas+'.')
Теперь наш парсер понимает единицы измерения на русском и английском языках. Для других языков код парсера пишется аналогично.
Коррекция кодировки результата работы парсера.
При тестировании парсера для единицы измерения Вы можете получить результат, в котором русские символы заменены их кодовым обозначением. Например, на Sage:
ph_unit.parseString("мм").asList() # Получим: ['\xd0\xbc\xd0\xbc']
Если Вы получили такой же результат, значит, всё работает правильно, но нужно поправить кодировку. В моём случае (sage) работает использование «самодельной» функции
bprint
(better print):def bprint(obj): print(obj.__repr__().decode('string_escape'))
Используя эту функцию, мы получим вывод в Sage в правильной кодировке:
bprint(ph_unit.parseString("мм").asList()) # Получим: ['мм']
Написание парсера для степени. Парсинг произвольного числа.
Научимся парсить степень. Обычно степень — это целое число. Однако в редких случаях степень может содержать дробную часть или быть записанной в экспоненциальной нотации. Поэтому мы напишем парсер для обычного числа, например, такого:
test_num = "-123.456e-3"
«Кирпичиком» произвольного числа является натуральное число, которое состоит из цифр:
int_num = Word(nums)
Перед числом может стоять знак плюс или минус. При этом знак плюс выводить в результат не надо (используем
Suppress()
).pm_sign = Optional(Suppress("+") | Literal("-"))
Вертикальная черта означает «или» (плюс или минус).
Literal()
означает точное соответствие текстовой строке. Таким образом, выражение дляpm_sign
означает, что надо найти в тексте необязательный символ +, который не надо выводить в результат парсинга, или необязательный символ минус.Теперь мы можем написать парсер для всего числа. Число начинается с необязательного знака плюс или минус, потом идут цифры, потом необязательная точка — разделитель дробной части, потом цифры, потом может идти символ e, после которого — снова число: необязательный плюс-минус и цифры. У числа после e дробной части уже нет. На pyparsing:
float_num = pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)
У нас теперь есть парсер для числа. Посмотрим, как работает парсер:
float_num.parseString('-123.456e-3').asList() # Получим ['-', '123', '.', '456', 'e', '-', '3']
Как мы видим, число разбито на отдельные составляющие. Нам это ни к чему, и мы бы хотели «собрать» число обратно. Это делается при помощи
Combine()
:float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num))
Проверим:
float_num.parseString('-123.456e-3').asList() # Получим ['-123.456e-3']
Отлично! Но… На выходе по-прежнему строка, а нам нужно число. Добавим преобразование строки в число, используя
ParseAction()
:float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0]))
Мы используем анонимную функцию
lambda
, аргументом которой являетсяt
. Сначала мы получаем результат в виде списка(t.asList())
. Т.к. полученный список имеет только один элемент, его сразу можно извлечь:t.asList()[0]
. Функцияfloat()
преобразует текст в число с плавающей точкой. Если вы работаете в Sage, можете заменитьfloat
наRR
— конструктор класса вещественных чисел Sage.Парсинг единицы измерения со степенью.
Отдельная единица измерения — это название единицы измерения, после которой может идти знак степени ^ и число — степень, в которую необходимо возвести. На pyparsing:
single_unit = ph_unit + Optional('^' + float_num)
Протестируем:
bprint(single_unit.parseString("м^2").asList()) # Получим: ['м', '^', 2.0]
Сразу усовершенствуем вывод. Нам не нужно видеть ^ в результате парсинга, и мы хотим видеть результат в виде кортежа (см. переменную res в начале этой статьи). Для подавления вывода используем
Suppress()
, для преобразования списка в кортеж —ParseAction()
:single_unit = (ph_unit + Optional(Suppress('^') + float_num)).setParseAction(lambda t: tuple(t.asList()))
Проверим:
bprint(single_unit.parseString("м^2").asList()) # Получим: [('м', 2.0)]
Парсинг единиц измерения, обрамлённых скобками. Реализация рекурсии.
Мы подошли к интересному месту — описанию реализации рекурсии. При написании единицы измерения пользователь может обрамить скобками одну или несколько единиц измерения, между которыми стоят знаки умножения и деления. Выражение в скобках может содержать другое, вложенное выражение, обрамлённое скобками (например
"(м^2/ (с^2 * кг))"
). Возможность вложения одних выражений со скобками в другие и есть источник рекурсии. Перейдём к Pyparsing.Вначале напишем выражение, не обращая внимание, что у нас есть рекурсия:
unit_expr = Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
Optional
содержит ту часть строки, которая может присутствовать, а может отсутствовать.OneOrMore
(переводится как «один или больше») содержит ту часть строки, которая должна встретиться в тексте не менее одного раза.OneOrMore
содержит два «слагаемых»: сначала мы ищем знак умножения и деления, потом единицу измерения или вложенное выражение.В том виде, как сейчас, оставлять
unit_expr
нельзя: слева и справа от знака равенства естьunit_expr
, что однозначно свидетельствует о рекурсии. Решается эта проблема очень просто: надо поменять знак присваивания на <<, а в строке передunit_expr
добавить присваивание специального классаForward()
:unit_expr = Forward() unit_expr << Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
Таким образом, при написании парсера нет необходимости заранее предвидеть рекурсию. Сначала пишите выражение так, как будто в нём не будет рекурсии, а когда увидите, что она появилась, просто замените знак = наForward()
.Проверим:
bprint(unit_expr.parseString("(Н*м/с^2)").asList()) # Получим: [('Н',), '*', ('м',), '/', ('с', 2.0)]
Парсинг общего выражения для единицы измерения.
У нас остался последний шаг: общее выражение для единицы измерения. На pyparsing:
parse_unit = (unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))
Обратите внимание, что выражение имеет вид
(a | b) + (c | d)
. Скобки здесь обязательны и имеют ту же роль, что и в математике. Используя скобки, мы хотим указать, что вначале надо проверить, что первое слагаемое —unit_expr
илиsingle_unit
, а второе слагаемое — необязательное выражение. Если скобки убрать, то получится, чтоparse_unit
– этоunit_expr
илиsingle_unit
+ необязательное выражение, что не совсем то, что мы задумывали. Те же рассуждения применимы и к выражению внутриOptional()
.Черновой вариант парсера. Коррекция кодировки результата.
Итак, мы написали черновой вариант парсера:
from pyparsing import * rus_alphas = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ' ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.') int_num = Word(nums) pm_sign = Optional(Suppress("+") | Literal("-")) float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0])) single_unit = (ph_unit + Optional(Suppress('^') + float_num)).setParseAction(lambda t: tuple(t.asList())) unit_expr = Forward() unit_expr << Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")") parse_unit = (unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))
Проверим:
print(s) # s = "Н*м^2/(кг*с^2)" — см. начало статьи. bprint(parse_unit.parseString(s).asList()) # Получим: [('Н',), '*', ('м', 2.0), '/', ('кг',), '*', ('с', 2.0)]
Группировка единиц измерения, обрамлённых скобками.
Мы уже близко к тому результату, который хотим получить. Первое, что нам нужно реализовать — группировка тех единиц измерения, которых пользователь обрамил скобками. Для этого в Pyparsing используется
Group()
, который мы применим кunit_expr
:unit_expr = Forward() unit_expr << Group(Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")"))
Посмотрим, что изменилось:
bprint(parse_unit.parseString(s).asList()) # Получим: [('Н',), '*', ('м', 2.0), '/', [('кг',), '*', ('с', 2.0)]]
Ставим степень 1 в тех кортежах, где степень отсутствует.
В некоторых кортежах после запятой ничего не стоит. Напомню, что кортеж соответствует единице измерения и имеет вид (единица измерения, степень). Вспомним, что мы можем давать имена определённым кусочкам результата работы парсера (описано в прошлой статье). В частности, назовём найденную единицу измерения как
'unit_name'
, а её степень как'unit_degree'
. ВsetParseAction()
напишем анонимную функциюlambda()
, которая будет ставить 1 там, где пользователь не указал степень единицы измерения). На pyparsing:single_unit = (ph_unit('unit_name') + Optional(Suppress('^') + float_num('unit_degree'))).setParseAction(lambda t: (t.unit_name, float(1) if t.unit_degree == "" else t.unit_degree))
Теперь весь наш парсер выдаёт следующий результат:
bprint(parse_unit.parseString(s).asList()) # Получим: [('Н', 1.0), '*', ('м', 2.0), '/', [('кг', 1.0), '*', ('с', 2.0)]]
В коде выше вместо
float(1)
можно было бы написать просто1.0
, но в Sage в таком случае получится не типfloat
, а собственный тип Sage для вещественных чисел.Убираем из результата парсера знаки * и /, раскрываем скобки.
Всё, что нам осталось сделать — это убрать в результате парсера знаки * и /, а также вложенные квадратные скобки. Если перед вложенным списком (т. е. перед [) стоит деление, знак степени у единиц измерения во вложенном списке надо поменять на противоположный. Для этого напишем отдельную функцию
transform_unit()
, которую будем использовать вsetParseAction()
дляparse_unit
:def transform_unit(unit_list, k=1): res = [] for v in unit_list: if isinstance(v, tuple): res.append(tuple((v[0], v[1]*k))) elif v == "/": k = -k elif isinstance(v, list): res += transform_unit(v, k=k) return(res) parse_unit = ((unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))).setParseAction(lambda t: transform_unit(t.asList()))
После этого наш парсер возвращает единицу измерения в нужном формате:
bprint(transform_unit(parse_unit.parseString(s).asList())) # Получим: [('Н', 1.0), ('м', 2.0), ('кг', -1.0), ('с', -2.0)]
Обратите внимание, что функция
transform_unit()
убирает вложенность. В процессе преобразования все скобки раскрываются. Если перед скобкой стоит знак деления, знак степени единиц измерения в скобках меняется на противоположный.Реализация проверки единиц измерения непосредственно в процессе парсинга.
Последнее, что было обещано сделать — внедрить раннюю проверку единиц измерения. Другими словами, как только парсер найдёт единицу измерения, он сразу проверит её по нашей базе данных.
В качестве базы данных будем использовать словарь Python:
unit_db = {'Длина':{'м':1, 'дм':1/10, 'см':1/100, 'мм':1/1000, 'км':1000, 'мкм':1/1000000}, 'Сила':{'Н':1}, 'Мощность':{'Вт':1, 'кВт':1000}, 'Время':{'с':1}, 'Масса':{'кг':1, 'г':0.001}}
Чтобы быстро проверить единицу измерения, хорошо было бы создать множество Python, поместив в него единицы измерения:
unit_set = set([t for vals in unit_db.values() for t in vals])
Напишем функцию
check_unit
, которая будет проверять единицу измерения, и вставим её вsetParseAction
дляph_unit
:def check_unit(unit_name): if not unit_name in unit_set: raise ValueError("Единица измерения указана неверно или отсутствует в базе данных: " + unit_name) return(unit_name) ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.').setParseAction(lambda t: check_unit(t.asList()[0]))
Вывод парсера не изменится, но, если попадётся единица измерения, которая отсутствует в базе данных или в науке, то пользователь получит сообщение об ошибке. Пример:
ph_unit.parseString("дюйм") # Получим сообщение об ошибке: Error in lines 1-1 Traceback (most recent call last): … File "", line 1, in <lambda> File "", line 3, in check_unit ValueError: Единица измерения указана неверно или отсутствует в базе данных: дюйм
Последняя строчка и есть наше сообщение пользователю об ошибке.
Полный код парсера. Заключение.
В заключение приведу полный код парсера. Не забудьте в строке импорта
"from pyparsing import *"
заменить * на использованные классы.from pyparsing import nums, alphas, Word, Literal, Optional, Combine, Forward, Group, Suppress, OneOrMore def bprint(obj): print(obj.__repr__().decode('string_escape')) # База данных единиц измерения unit_db = {'Длина':{'м':1, 'дм':1/10, 'см':1/100, 'мм':1/1000, 'км':1000, 'мкм':1/1000000}, 'Сила':{'Н':1}, 'Мощность':{'Вт':1, 'кВт':1000}, 'Время':{'с':1}, 'Масса':{'кг':1, 'г':0.001}} unit_set = set([t for vals in unit_db.values() for t in vals]) # Парсер для единицы измерения с проверкой её по базе данных rus_alphas = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ' def check_unit(unit_name): """ Проверка единицы измерения по базе данных. """ if not unit_name in unit_set: raise ValueError("Единица измерения указана неверно или отсутствует в базе данных: " + unit_name) return(unit_name) ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.').setParseAction(lambda t: check_unit(t.asList()[0])) # Парсер для степени int_num = Word(nums) pm_sign = Optional(Suppress("+") | Literal("-")) float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0])) # Парсер для единицы измерения со степенью single_unit = (ph_unit('unit_name') + Optional(Suppress('^') + float_num('unit_degree'))).setParseAction(lambda t: (t.unit_name, float(1) if t.unit_degree == "" else t.unit_degree)) # Парсер для выражения в скобках unit_expr = Forward() unit_expr << Group(Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")) # Парсер для общего выражения единицы измерения def transform_unit(unit_list, k=1): """ Функция раскрывает скобки в результате, выданном парсером, корректирует знак степени и убирает знаки * и / """ res = [] for v in unit_list: if isinstance(v, tuple): res.append(tuple((v[0], v[1]*k))) elif v == "/": k = -k elif isinstance(v, list): res += transform_unit(v, k=k) return(res) parse_unit = ((unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))).setParseAction(lambda t: transform_unit(t.asList())) #Проверка s = "Н*м^2/(кг*с^2)" bprint(parse_unit.parseString(s).asList())
Благодарю вас за терпение, с которым вы прочитали мою статью. Напомню, что код, представленный в этой статье, выложен на Sagemathcloud. Если вы не зарегистрированы на Хабре, вы можете прислать мне вопрос на почту или написать в ВК. В следующей статье я хочу познакомить вас с Sagemathcloud, показать, насколько сильно он может упростить вашу работу на Python. После этого я вернусь к теме парсинга на Pyparsing на качественно новом уровне.
Благодарю Дарью Фролову и Никиту Коновалова за помощь в проверке статьи перед её публикацией.
Как разобрать строку json в python?
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира- О компании
Загрузка…
.
Разобрать строку JSON в Python
Python — Разобрать строку JSON
Чтобы преобразовать строку JSON в объект Python, вы можете использовать встроенную библиотеку Python json . В пакете json есть функция load () для анализа строки JSON.
В этом руководстве мы узнаем, как анализировать строку JSON с помощью пакета json с помощью хорошо подробных примеров программ Python.
Синтаксис — json.loads ()
Следующий фрагмент кода описывает синтаксис для импорта пакета json и синтаксического анализа строки json с использованием json.загружает ().
# выписка на импорт импортировать json #parse json string pythonObj = json.loads (jsonStr)
, где
jsonStr
— это строка, содержащая данные JSON, аjson.loads ()
возвращает объект Python. В зависимости от структуры строки JSON тип возвращаемого объекта будет списком или словарем.На основе строки JSON json.loads () возвращает либо словарь Python, либо список Python.
Если строка JSON имеет следующий формат, то json.load () возвращает словарь Python.
{ключ: значение, ключ: значение, ключ: значение}
Если строка JSON имеет следующий формат, то json.loads () возвращает список словарей Python.
[{ключ: значение, ключ: значение}, {ключ: значение}, {ключ: значение}]
Пример 1: синтаксический анализ строки JSON в словарь Python
В этом примере давайте инициализируем строку с помощью действительные данные JSON. Мы будем использовать функцию json.loads () для анализа этой строки JSON.
Программа Python
импорт json # json строка jsonStr = '{"name": "Tesla", "age": 2, "city": "Нью-Йорк"}' # разобрать файл json pythonObj = json.загружает (jsonStr) #print тип объекта печать (тип (pythonObj)) # доступ к элементам в объекте name = pythonObj ['имя'] print (имя)
Вывод
Tesla Строка JSON в этом примере представляет собой один элемент с несколькими парами ключ: значение внутри. Следовательно, типом данных строки JSON, проанализированной функцией load (), является словарь. Используя этот объект словаря, вы можете получить доступ к значению для данного ключа, используя механизм индексации.Кроме того, вы можете выполнять все операции со словарем над этим объектом словаря Python.
Пример 2: Разбор строки JSON в список Python
В этом примере давайте инициализируем строку JSON с помощью массива элементов, и мы будем использовать функцию json.loads () для синтаксического анализа этой строки JSON в список Python.
Программа Python
импорт json # json строка jsonStr = '[{"name": "Тесла", "возраст": 2, "город": "Нью-Йорк"}, {"имя": "Тесла", "возраст": 2, "город": "Бостон "}] ' # разобрать файл json pythonObj = json.загружает (jsonStr) печать (тип (pythonObj)) print (введите (pythonObj [0])) city = pythonObj [1] ['город'] печать (город)
Выход
<класс 'dict'> Boston Строка JSON в этом примере представляет собой массив элементов. Следовательно, тип данных строки JSON, проанализированной функцией load (), является списком Python. И тип данных элементов в списке — словарь.
Пример 3: json.decoder.JSONDecodeError
Иногда вы можете столкнуться с json.decoder.JSONDecodeError. Эта ошибка вызывается функцией json.loads () при попытке загрузить недопустимую строку JSON.
Программа Python
импорт json jsonStr = '"name": "Tesla", "age": 2' pythonObj = json.loads (jsonStr) name = pythonObj ['имя'] print (name)
Вывод
Отслеживание (последний вызов последним): Файл "d: /workspace/python/example.py", строка 4, в
pythonObj = json.loads (jsonStr) Файл "C: \ Users \ PE \ AppData \ Local \ Programs \ Python \ Python37 \ lib \ json \ __ init__.ру », строка 348, в нагрузках вернуть _default_decoder.decode (s) Файл "C: \ Users \ PE \ AppData \ Local \ Programs \ Python \ Python37 \ lib \ json \ decoder.py", строка 340, в декодировании поднять JSONDecodeError ("Дополнительные данные", s, конец) json.decoder.JSONDecodeError: Дополнительные данные: строка 1 столбец 7 (char 6) Сводка
В этом руководстве примеров Python мы узнали, как анализировать строку JSON в Python с помощью хорошо подробных примеров программ.
Нравится то, что вы читаете! Добавьте эту страницу в закладки для быстрого доступа и поделитесь этой статьей со своими друзьями и коллегами.
.Анализ
— Как преобразовать строку с разделителями-запятыми в список в Python?
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира- О компании
.Строка
— стандартные строковые операции — документация Python 3.8.6
Исходный код: Lib / string.py
Строковые константы
В этом модуле определены следующие константы:
-
строка.
ascii_letters
Объединение
ascii_lowercase
иascii_uppercase
константы, описанные ниже. Это значение не зависит от языкового стандарта.
-
строка.
ascii_lowercase
Строчные буквы
'abcdefghijklmnopqrstuvwxyz'
. Это значение не
зависит от локали и не изменится.
-
строка.
ascii_uppercase
Заглавные буквы
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
. Это значение не
зависит от локали и не изменится.
-
строка.
цифр
Строка
'0123456789'
.
-
строка.
шестнадцатеричные цифры
Строка
'0123456789abcdefABCDEF'
.
-
строка.
восьмеричные цифры
Строка
'01234567'
.
-
строка.
пунктуация
Строка символов ASCII, считающихся знаками пунктуации
в локалиC
:! "# $% & '() * +, -._` {|} ~
.
-
строка.
для печати
Строка символов ASCII, которые считаются печатаемыми. Это
комбинацияцифр
,ascii_letters
,знаков препинания
,
ипробел
.
-
строка.
пустое пространство
Строка, содержащая все символы ASCII, которые считаются пробелами.
Сюда входят пробел символов, табуляция, перевод строки, возврат, перевод страницы и
вертикальная табуляция.
Пользовательское форматирование строки
Встроенный строковый класс предоставляет возможность выполнять сложные переменные.
замены и форматирование значений с помощью метода format ()
, описанного в
ПЕР 3101 . Класс Formatter
в модуле string
позволяет
вы можете создавать и настраивать свое собственное форматирование строк, используя те же
реализация как встроенный метод format ()
.
- класс
строка.
Форматер
Класс
Formatter
имеет следующие общедоступные методы:-
формат
( формат_строка , /, * args , ** kwargs ) Основной метод API. Требуется строка формата и
произвольный набор позиционных и ключевых аргументов.
Это просто оболочка, которая вызываетvformat ()
.
-
vformat
( формат_строка , args , kwargs ) Эта функция выполняет фактическую работу по форматированию.Он выставлен как
отдельная функция для случаев, когда вы хотите передать предопределенный
словарь аргументов, а не распаковывать и переупаковывать
словарь как отдельные аргументы с использованием* args
и** kwargs
синтаксис.vformat ()
выполняет работу по разбиению строки формата
в символьные данные и поля замены. Он вызывает различные
методы, описанные ниже.
Кроме того,
Formatter
определяет ряд методов, которые
предполагается заменить подклассами:-
синтаксический анализ
( формат_строка ) Перебирать строку format_string и возвращать итерацию кортежей
( литеральный_текст , имя_поля , спецификация_формата , преобразование ).Это используется
наvformat ()
, чтобы разбить строку на буквальный текст или
поля замены.Значения в кортеже концептуально представляют собой диапазон буквального текста.
за которым следует одно поле замены. Если нет буквального текста
(что может произойти, если два поля замены встречаются последовательно), тогда
literal_text будет строкой нулевой длины. Если нет замены
field, затем значения field_name , format_spec и преобразование
будетНет
.
-
get_field
( имя_поля , args , kwargs ) Учитывая field_name , возвращенное функцией
parse ()
(см. Выше), преобразуйте его в
форматируемый объект. Возвращает кортеж (obj, used_key). По умолчанию
версия принимает строки формы, определенной в PEP 3101 , например
«0 [имя]» или «label.title». args и kwargs переданы в
вформат ()
.Возвращаемое значение used_key имеет то же значение, что и
ключ параметр дляget_value ()
.
-
get_value
( ключ , args , kwargs ) Получить значение заданного поля. Аргумент ключа будет либо
целое число или строка. Если это целое число, оно представляет индекс
позиционный аргумент в args ; если это строка, то она представляет собой
названный аргумент в kwargs .Параметр args установлен в список позиционных аргументов для
vformat ()
, а параметр kwargs установлен в словарь
аргументы ключевого слова.Для составных имен полей эти функции вызываются только для первого
составляющая имени поля; последующие компоненты обрабатываются через
обычные операции с атрибутами и индексацией.Так, например, выражение поля «0.name» вызовет
get_value ()
для вызова с ключом , аргумент равен 0.Название
атрибут будет найден после того, какget_value ()
вернется путем вызова
встроенная функцияgetattr ()
.Если индекс или ключевое слово относится к элементу, который не существует
-
.