Регулярные выражения в C++11 и парсинг логов Nginx
25 мая 2015
Ранее мы уже выясняли, что C++ никогда не умрет, и знать низкоуровневые вещи приходится, даже если фултайм пишешь на Scala. Поэтому я решил уделять некоторое время пописыванию небольших программок на C/C++. Тем более, что с тех пор, когда я активно этим делом увлекался, прошло уже лет семь-восемь и многое сильно изменилось. Так, например, в стандартной библиотеке C++ появились регулярные выражения, пример работы с которыми и приводится в этом посте.
Рассмотрим следующую проблему. Есть сервер, на котором поднят Nginx. Этот Nginx раздает какие-то mp3-файлы. Стоит задача определить, какие файлы и сколько раз были скачаны. Возможное решение на C++:
#include <iostream>
#include <map>
#include <regex>
#include <algorithm>
int main(int argc, char** argv) {
const std::regex re("\"GET (?:http://.+?/)?(.+?\\.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++ строки можно объявлять так:
#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 в формате вроде такого:
HTTP/1.1" 200 73544326 "-" "Mozilla/5.0 (Linux; U; en-us; BeyondPod)"
На выходе получаем статистику, какой файл сколько раз был скачан целиком. Некоторые клиенты качают файл частями, некоторые весь сразу. Кто-то слушает первые пять минут через браузер, потом скачивает файл в свой iPhone. Поэтому точно определить количество скачиваний непросто. В приведенном примере берется суммарное количество байт, переданных по каждому файлу в отдельности, и делится на максимальное количество байт, переданных в рамках одного запроса, в предположении, что это размер всего файла. На практике при достаточно большом количестве скачиваний (несколько тысяч для каждого файла) получаются довольно правдоподобные цифры. На глазок погрешность при таком подходе не превышает 10%.
Памятку по регулярным выражениям вы найдете здесь. В остальном код, как мне кажется, очень просто и не нуждается в дополнительных пояснениях.
Что интересно, если собрать программу gcc 4.8.2, который идет в Ubuntu 14.04 LTS по дэфолту:
… то мы не получим никаких ошибок, однако программа работать не будет. При создании std::regex будет бросаться исключение std::regex_error. Если заменить аргумент конструктора на какую-то более простую строку, исключение пропадет, но ни одна строчка из логов не будет соответствовать регулярному выражению, даже самому простому. Кажется, я где-то час пытался разобраться, пока наконец не понял, что в gcc 4.8 просто еще нет поддержки регулярных выражений. Хоть бы варнинг при компиляции показали! В gcc 4.9.2, говорят, поддержка есть, но вроде как она сильно завязана на конкретный тип (char) и в общем случае приводит к багам.
У clang в этом смысле все намного лучше:
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:
use strict;
use warnings;
use 5.018;
my %stat;
my $re = qr#GET (?:http://.+?/)?(.+?\.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 в своих проектах и довольны ли скоростью?
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.