2011/05/29

Парсим логи при помощи генераторов - 2

Дано: сотни логов веб-сервера, разбросанные по разным директориям. Возможно, заархивированные.
Требуется: понять сколько байтов было передано :)

В питоне есть замечательная функция os.walk(), позволяющая блуждать по файловой системе:

import os

for path, dirlist, filelist in os.walk(topdir):
    # path : текущая директория
    # dirlist : список поддиректорий
    # filelist : список файлов
    ...

Для достижения цели потребуется создать несколько функций:
1. Функция-генератор, возвращающая список файлов по заданному шаблону:

import os
import fnmatch
 
def gen_find(filepat,top):
    for path, dirlist, filelist in os.walk(top):
        for name in fnmatch.filter(filelist, filepat):
            yield os.path.join(path, name)

Примеры использования:

pyfiles = gen_find('*.py', '/')
logs = gen_find('access-log*', '/usr/www/')

2. Функция-генератор, которая принимает список файлов, и, если среди них есть архивы, то возвращает распакованные файлы. Если файл не архив, то просто открывает его:

import gzip
import bz2
 
def gen_open(filenames):
    for name in filenames:
        if name.endswith(".gz"):
            yield gzip.open(name)
        elif name.endswith(".bz2"):
            yield bz2.BZ2File(name)
        else:
            yield open(name)

3. Функция (разумеется генератор), которая возвращает единую последовательность результатов, принимая при этом несколько последовательностей (в нашем случае передаем открытые файлы, а получаем последовательность строк):

def gen_cat(sources):
    for s in sources:
        for item in s:
            yield item

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

filenames = gen_find('access-log*', '/usr/www')
logfiles = gen_open(filenames)
loglines = gen_cat(logfiles)
bytecolumn = (line.rsplit(None, 1)[1] for line in loglines)
bytes = (int(x) for x in bytecolumn if x != '-')
print "Total", sum(bytes)

Особые эстеты могут свернуть этот исходник вплоть до 1 строчки, благодаря тому факту, что в питоне все функции - высшего порядка.

Парсим логи при помощи генераторов

Задача: понять по логам Apache сколько байтов мы передали.
NB: файлы мб большие (несколько гигов).

Формат логов примерно следуюший:

217.168.25.4 - - [28/May/2011:14:06:27 +0400] "GET / HTTP/1.0 200 8509

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

wwwlog = open("access-log")
total = 0
for line in wwwlog:
    bytestr = line.rsplit(None,1)[1]
    if bytestr != '-':
        total += int(bytestr)
print "Total", total

И, с генератором:

wwwlog = open("access-log")
bytecolumn = (line.rsplit(None,1)[1] for line in wwwlog)
bytes = (int(x) for x in bytecolumn if x != '-')
print "Total", sum(bytes)

Выглядит компактнее, не правда ли? В данном случае используется выражение-генератор, эдакий "цикл for наоборот", который выдает кортеж из результатов, суммирующийся далее.

2011/05/18

Скрипт для скачивания содержимого по ссылкам

Тестовое задание в одной конторе (не будем называть пальцем):

Напишите скрипт, который будет считывать список URL из файла (одна строка - один URL), скачивать их не более чем в N потоков и сохранять каждую страницу в отдельный файл. N задается аргументом командной строки и по умолчанию равно 10. Имена результирующих файлов значения
не имеют.

import sys
import threading
import Queue
import urllib2

class DownloadThread(threading.Thread):
    def run(self):
        headers = {'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'}
        while urlsPool.qsize() > 0:
            logfile = open(str(urlsPool.qsize()), 'w')
            req = urllib2.Request(urlsPool.get(), None, headers)
            logfile.write(urllib2.urlopen(req).read())
            logfile.close()

if len(sys.argv) < 2:
    print 'Usage: downloader.py [-n <number>] FILE\n\
"-n <number>" - number of threads (default 10)'
    sys.exit(1)
if len(sys.argv) == 4 and sys.argv[1] == '-n':
    threads = int(sys.argv[2])
else:
    threads = 10
urlsPool = Queue.Queue(0)
for url in open(sys.argv[-1]):
    urlsPool.put(url)
for x in xrange(threads):
    DownloadThread().start()

Немного про итераторы в питоне

В рамках акции по избавлению своих программ на питоне от быдлокода, я, как бывший программист на Си, открыл для себя много занимательных вещей. Сегодня немного про итераторы - объекты, которые позволяют программисту перебирать значения. Технически это реализуется с помощью метода next, который при каждом вызове возвращает следующий элемент коллекции. Например, так прочитает содержимое файла бывший быдлопрогаммист на Си:

file = open('file.txt')
while True:
    line = file.readline()
    if not line: break
    print line

А так это сделает грамотный питонер (объект файла поддерживает итерационный протокол, который используется в цикле for):

for line in open('file.txt')
    print line

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

У словарей тоже есть итератор! Так можно выполнить обход всех ключей словаря:

D = {'a' : 1, 'b' : 2, 'c' : 3}
for key in D:
    print key, D[key]

Итераторы в строках тоже могут быть весьма полезны. Следующий код получает и выводит на экран каждый 2 символ строки S (ещё здесь используется срез с третьим параметром, про который многие забывают):

for x in S[::2]:
    print x

Соотвествующий быдлокод, который к тому же работает медленнее, т.к. при каждом проходе цикла осуществляется доступ к объекту строки:

for i in range(0, len(S), 2):
    print S[i]

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