-
Notifications
You must be signed in to change notification settings - Fork 5
Python: трюки с генераторами Часть 4 Синтаксический разбор и обработка данных
Dmitry Ponyatov edited this page Oct 4, 2019
·
9 revisions
Журналы веб-сервера состоят из разных столбцов данных. Нужно отпарсить каждую строку в полезную структуру данных, которая позволяет нам легко просматривать различные поля.
81.107.39.38 - - [24/Feb/2008:00:08:59 -0600] "GET ..." 200 7587
преобразуется в объект с полями
host referrer user [datetime] "request" status bytes
- Давайте прогоним строки через парсер регулярных выражений
logpats = r'(\S+) (\S+) (\S+) \[(.*?)\] '\
r'"(\S+) (\S+) (\S+)" (\S+) (\S+)'
logpat = re.compile(logpats)
groups = (logpat.match(line) for line in loglines)
tuples = (g.groups() for g in groups if g)
- Это генерирует последовательность кортежей
('71.201.176.194', '-', '-', '26/Feb/2008:10:30:08 -0600', 'GET', '/ply/ply.html', 'HTTP/1.1', '200', '97238')
- Я вообще не люблю обработку данных на кортежах
- Во-первых, они иммутабельные -- поэтому вы не можете изменить данные
- Во-вторых, чтобы извлечь определенные поля, вы должны запомнить номер столбца, что раздражает, если столбцов много.
- В-третьих, существующий код ломается, если вы измените количество полей
- Давайте превратим кортежи в словари
colnames = ('host','referrer','user','datetime','method','request','proto','status','bytes')
log = (dict(zip(colnames, t)) for t in tuples)
- Это создает последовательность именованных полей
{ 'status' : '200',
'proto' : 'HTTP/1.1',
'referrer' : '-',
'request' : '/ply/ply.html',
'bytes' : '97238',
'datetime' : '24/Feb/2008:00:08:59 -0600',
'host' : '140.180.132.213',
'user' : '-',
'method' : 'GET'}
- Возможно, вы захотите отобразить определенные поля словаря с помощью функции преобразования (например,
int()
,float()
).
def field_map(dictseq, name, func):
for d in dictseq:
d[name] = func(d[name])
yield d
- Пример: преобразование нескольких значений полей
log = field_map(log, "status", int)
log = field_map(log, "bytes",
lambda s: int(s) if s !='-' else 0)
- Создает словари преобразованных значений
{ 'status' : 200,
...
'bytes' : 97238,
...
'method' : 'GET'}
- Опять же, это всего лишь один большой конвейер обработки
from pathlib import Path
lognames = Path('www').rglob('access-log*')
logfiles = gen_open(lognames)
loglines = gen_cat(logfiles)
groups = (logpat.match(line) for line in loglines)
tuples = (g.groups() for g in groups if g)
colnames = ('host','referrer','user','datetime','method','request','proto','status','bytes')
log = (dict(zip(colnames, t)) for t in tuples)
log = field_map(log,"bytes",
lambda s: int(s) if s != '-' else 0)
log = field_map(log,"status",int)
- По мере роста конвейера обработки некоторые его части могут быть полезными компонентами сами по себе.
- системо-зависимая часть: генерация потока строк логов из произвольных каталогов
- универсальная часть: разбор потока строк в синтаксисе логов Apache
- Ряд этапов конвейера может быть легко инкапсулирован обычной функцией Python
- Пример: несколько этапов конвейера внутри функции
from pathlib import Path
def lines_from_dir(filepat, dirname):
names = Path(dirname).rglob(filepat)
files = gen_open(names)
lines = gen_cat(files)
return lines
- Теперь это компонент общего назначения, который можно использовать как один элемент в других конвейерах.
- Пример: разбор лога Apache в словари
def apache_log(lines):
groups = (logpat.match(line) for line in lines)
tuples = (g.groups() for g in groups if g)
colnames = ('host','referrer','user','datetime','method','request','proto','status','bytes')
log = (dict(zip(colnames, t)) for t in tuples)
log = field_map(log, "bytes", lambda s: int(s) if s != '-' else 0)
log = field_map(log, "status", int)
return log
- Это просто
lines = lines_from_dir("access-log*","www")
log = apache_log(lines)
for r in log: print(r)
- Различные компоненты были разделены в соответствии с данными, которые они обрабатывают
- При создании компонентов конвейера важно сосредоточиться на входах и выходах
- Вы получите максимальную гибкость при использовании стандартизированного набора типов данных
- Проще ли иметь группу компонентов, которые все работают только со словарями, или иметь компоненты, которые требуют, чтобы входы/выходы были экземплярами различных пользовательских типов?
- Теперь, когда у нас есть журнал, давайте сделаем несколько запросов
- Найти множество всех документов, которые 404
stat404 = { r['request'] for r in log
if r['status'] == 404 }
- Распечатать все запросы, которые передают более мегабайта
large = (r for r in log if r['bytes'] > 1000000)
for r in large:
print(r['request'], r['bytes'])
- Найти самую большую передачу данных
print("%d %s" % max((r['bytes'],r['request']) for r in log))
- Собрать все уникальные IP-адреса хостов
hosts = { r['host'] for r in log }
- Найти количество скачиваний файла
sum(1 for r in log
if r['request'] == '/ply/ply-2.3.tar.gz')
- Найти, кто долбит
robots.txt
addrs = { r['host'] for r in log
if 'robots.txt' in r['request'] }
import socket
for addr in addrs:
try:
print(socket.gethostbyaddr(addr)[0])
except socket.herror:
print(addr)
- Мне нравится идея использования генераторных выражений в качестве конвейерного языка запросов.
- Вы можете писать простые фильтры, извлекать данные и т.д.
- Если вы передаете словари/объекты через конвейер, он становится достаточно мощным
- Похоже на написание SQL-запросов
Python: трюки с генераторами Часть 5 Обработка бесконечных данных