Skip to content

Python: трюки с генераторами Часть 4 Синтаксический разбор и обработка данных

Dmitry Ponyatov edited this page Oct 4, 2019 · 9 revisions

Python: трюки с генераторами

Часть 4 Синтаксический разбор и обработка данных

Проблема программирования


Журналы веб-сервера состоят из разных столбцов данных. Нужно отпарсить каждую строку в полезную структуру данных, которая позволяет нам легко просматривать различные поля.


81.107.39.38 - - [24/Feb/2008:00:08:59 -0600] "GET ..." 200 7587

преобразуется в объект с полями

host referrer user [datetime] "request" status bytes

Парсинг с Regex

  • Давайте прогоним строки через парсер регулярных выражений
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 Обработка бесконечных данных

Clone this wiki locally