Регулярные выражения в C++11 и парсинг логов Nginx

25 мая 2015

Ранее мы уже выясняли, что C++ никогда не умрет, и знать низкоуровневые вещи приходится, даже если фултайм пишешь на Scala. Поэтому я решил уделять некоторое время пописыванию небольших программок на C/C++. Тем более, что с тех пор, когда я активно этим делом увлекался, прошло уже лет семь-восемь и многое сильно изменилось. Так, например, в стандартной библиотеке C++ появились регулярные выражения, пример работы с которыми и приводится в этом посте.

Рассмотрим следующую проблему. Есть сервер, на котором поднят Nginx. Этот Nginx раздает какие-то mp3-файлы. Стоит задача определить, какие файлы и сколько раз были скачаны. Возможное решение на C++:

#include <string>
#include <iostream>
#include <map>
#include <regex>
#include <algorithm>

int main(int argc, char** argv) {
  const std::regex re("\"GET (?:https?://.+?/)?(.+?\\.mp3) "
                      "HTTP/1\\.[01]\" \\d{3} (\\d+)");
  std::map<std::string, std::pair<long, long>> stat;
  for(std::string line; std::getline(std::cin, line); ) {
    std::smatch match;
    if(std::regex_search(line, match, re)) {
      const std::string fname = match[1];
      long size = atol(match[2].str().c_str());
      auto it = stat.find(fname);
      if(it == stat.end()) { // not found
        stat[fname] = std::pair<long, long>(size, size);
      } else {
        auto pair = stat[fname];
        long sum = pair.first;
        long max = pair.second;
        sum += size;
        max = std::max(max, size);
        stat[fname] = std::pair<long, long>(sum, max);
      }
    }
  }

  for(auto& it: stat) {
    auto& key = it.first;
    auto& value = it.second;
    long sum = value.first;
    long max = std::max(value.second, 1L); // avoid division by zero
    double downloads = (double)sum / (double)max;
    std::cout << "Key: " << key << " downloads: " << downloads
              << " (max size: " << max << ")" << std::endl;
  }
}

Кстати, в современном C++ строки можно объявлять так:

// g++ -Wall -std=c++1y test.cpp -o test
#include <iostream>

int main() {
  std::cout << R"(string with "quotes")" << std::endl;
  std::cout << R"ololo(string with )", etc)ololo" << std::endl;
}
/*
Output:

string with "quotes"
string with )", etc
*/

… что позволяет избежать лишнего экранирования кавычек, но, к сожалению, ломает подсветку синтаксиса в Vim и в плагине CodeColorer, используемом в этом блоге. А вот в CLion подсветка не ломается.

В stdin программа ожидает получить логи Nginx в формате вроде такого:

12.34.56.78 - - [31/Jan/2015:13:06:15 -0500] "GET /path/to/some/file.mp3
 HTTP/1.1" 200 73544326 "-" "Mozilla/5.0 (Linux; U; en-us; BeyondPod)"

На выходе получаем статистику, какой файл сколько раз был скачан целиком. Некоторые клиенты качают файл частями, некоторые весь сразу. Кто-то слушает первые пять минут через браузер, потом скачивает файл в свой iPhone. Поэтому точно определить количество скачиваний непросто. В приведенном примере берется суммарное количество байт, переданных по каждому файлу в отдельности, и делится на максимальное количество байт, переданных в рамках одного запроса, в предположении, что это размер всего файла. На практике при достаточно большом количестве скачиваний (несколько тысяч для каждого файла) получаются довольно правдоподобные цифры. На глазок погрешность при таком подходе не превышает 10%.

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

Что интересно, если собрать программу gcc 4.8.2, который идет в Ubuntu 14.04 LTS по дэфолту:

g++ -Wall -std=c++1y -O2 parse.cpp -o parse_gcc

… то мы не получим никаких ошибок, однако программа работать не будет. При создании std::regex будет бросаться исключение std::regex_error. Если заменить аргумент конструктора на какую-то более простую строку, исключение пропадет, но ни одна строчка из логов не будет соответствовать регулярному выражению, даже самому простому. Кажется, я где-то час пытался разобраться, пока наконец не понял, что в gcc 4.8 просто еще нет поддержки регулярных выражений. Хоть бы варнинг при компиляции показали! В gcc 4.9.2, говорят, поддержка есть, но вроде как она сильно завязана на конкретный тип (char) и в общем случае приводит к багам.

У clang в этом смысле все намного лучше:

sudo apt-get install libc++-dev
clang++-3.5 -Wall -std=c++1y -stdlib=libc++ -O2 parse.cpp -o parse
zcat -f /var/log/nginx/access.log* | ./parse

На моем компьютере программа успешно отрабатывает за 8.5 секунд при одном большом распакованном файле из 378к строк на входе, по 0.022 мс на строку. Если использовать unordered_map, программа справляется с задачей за 8 секунд, но требует написания дополнительного кода для вывода результатов в отсортированном порядке. Примечательно, что аналогичный скрипт на Perl:

#!/usr/bin/env perl

use strict;
use warnings;
use 5.018;

my %stat;
my $re = qr#GET (?:https?://.+?/)?(.+?\.mp3) HTTP/1\.[01]" \d{3} (\d+)#;

while(my $line = <STDIN>) {
  my($fname, $size) = $line =~ $re;
  next unless $fname;
  if($stat{$fname}) {
    my $sum = $stat{$fname}[0];
    my $max = $stat{$fname}[1];
    $sum += $size;
    $max = $max > $size ? $max : $size;
    $stat{$fname} = [$sum, $max];
  } else {
    $stat{$fname} = [$size, $size];
  }
}

for my $key (keys %stat) {
  my $max = $stat{$key}[1];
  $max = 1 if $max <= 0;
  say "$key => ".($stat{$key}[0]/$max)." (max: $max)";
}

… справляется с той же задачей за 1.5 секунды. При этом я не особо преуспел в оптимизации кода на C++ до такого же уровня путем повторного использования объектов или замены значений на ссылки. И хотя я допускаю, что могу чего-то не знать, скажем, о буферизации std::cin, в реальных проектах вам может захотеться поэкспериментировать с libpcre. Эта либа может оказаться намного быстрее текущей реализации регулярных выражений в стандартной библиотеке C++.

Документация к <regex>: http://www.cplusplus.com/reference/regex/.

А пользуетесь ли вы std::regex в своих проектах и довольны ли скоростью?

Метки: , .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: