четверг, 5 сентября 2013 г.

Частота - как много в этом звуке!

Сегодня я хочу рассмотреть разные решения одной распространенной задачи. Формулировки бывают разные. Вот одна из них:
"У нас есть access.log веб-сервера. Найти 5 IP-адресов, от которых было больше всего запросов."


Возьмем access.log файл. У меня он придуманный:

cat assess.log
127.0.0.10 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.11 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 202 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.11 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 404 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.10 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 500 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.2 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 500 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.2 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 404 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 202 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.2 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 500 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.2 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 404 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.3 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.4 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 404 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.5 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.6 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 400 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.7 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.8 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 400 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.9 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.100 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.11 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 204 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.2 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 204 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.2 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.3 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 402 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.12 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.13 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 402 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.14 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.15 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 408 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
127.0.0.16 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 408 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"

Веселая диаграмма принятия решений веб-сервером: тут.

1. Несколько решений на Python:
1.1. Вариант с использованием встроенных типов:

import re

def frequency(n=5):
d = dict()
result = list()
with open('access.log', 'r') as f:
for line in f:
ip = re.match(r'^\d+.\d+.\d+.\d+', line).group(0)
if ip in d:
d[ip] += 1
else:
d[ip] = 1

for value in sorted(d.values(), reverse=True)[:n]:
for key in d:
if d[key] == value:
result.append((key, value))
del d[key]
break
return result

print (frequency())

Комментарий: Регулярное выражение приведено в качестве того, чтобы как-то разнообразить этот хаос. Версия выглядит не красиво и больше похожа на С-стайл с if-else'ами и кучей вложенных циклов.

1.2. Вариант с использованием библиотеки collections:

from collections import Counter
from pprint import pprint

def frequency(n=5):
with open('access.log', 'r') as f:
IPs = [line.split()[0] for line in f.readlines()]
return Counter(IPs).most_common(n)

pprint (frequency())

Замечание: выглядит красивенько (всего 3 строки), но вот readlines() будет хранить в себе список всех строк, что не очень круто, если наш лог слишком большой.

2. С помощью стандартных консольных средств:

awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5

      7 127.0.0.1
      6 127.0.0.2
      3 127.0.0.11
      2 127.0.0.3
      2 127.0.0.10

Кажется, этот вариант - самый универсальный и красивый.

PS: code-review приветствуется)

3 комментария:

  1. правильнее будет так:
    awk '{print $1}' access.log| sort | uniq -c | sort -rn | head -n 5

    ОтветитьУдалить
  2. PS
    Собственно, к чему клоню. Сравни вывод и посмотри на опции sort:

    # awk '{print $1}' /usr/local/apache/domlogs/cpxxxxxx.cpanel.tech-xxxxx.ru | sort | uniq -c | sort -r | head -n5
    9 92.62.154.144
    8 5.10.83.41
    8 109.87.146.37
    7 178.94.13.164
    6 5.10.83.94

    # awk '{print $1}' /usr/local/apache/domlogs/cpxxxxxx.cpanel.tech-xxxxx.ru | sort | uniq -c | sort -rn | head -n5
    50 37.59.54.136
    35 64.34.170.56
    28 195.2.204.135
    24 208.177.76.10
    22 208.115.113.92

    Почувствовала разницу? ;)

    ОтветитьУдалить
  3. а чем такое решение хуже?
    import re
    import operator


    def get_top_requester_ips(count=10):
    counter = {}
    f = open('access.log', 'r')
    for line in f:
    ip = re.match(r'^\d+.\d+.\d+.\d+', line).group(0)

    if ip in counter:
    counter[ip] += 1
    else:
    counter[ip] = 1

    return sorted(counter.iteritems(), key=operator.itemgetter(1), reverse=True)[:count]

    ОтветитьУдалить