Все задачи

Такой непростой tsv

20 Nov 2014

Программисту понадобилось быстренько отфильтровать и переформатировать один лог. Исходный файл содержит по одной json-записи в строке, в итоге должен получиться tab separated файл из двух колонок.

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

Получился такой скрипт:

#!/usr/bin/perl

use strict;
use warnings;

use JSON;

while(<>){
    my $data = eval{decode_json($_)};
    next unless $data;
    next unless is_interesting_id($data->{id}) && is_valid_int($data->{sum});
    print join("\t", $data->{id}, $data->{sum});
    print "\n";
}

sub is_interesting_id
{
    return $_[0] =~ /^5665[0-9]{7,13}$/ ? 1 : 0;
}

sub is_valid_int
{
    return $_[0] =~ /^[0-9]+$/ ? 1 : 0;
}

Все хорошо? Нет! На выходе некоторые строчки пустые, а некоторые содержат только одно значение вместо положенных двух.

Подсказка

Показать

Пример подозрительного результата:

566512345671    123
566512345672    123

566512345673
        123
566512345674    123

Но как? Ведь там же валидация…

Подсказка-2

Показать

А вот пример подозрительных данных:

{"id":"566512345671", "sum": 123}
{"id":"566512345672", "sum": "123\n"}
{"id":"566512345673\n", "sum": "123"}
{"id":"566512345674", "sum": "123"}
{"id":"5665123

Но как эти данные проходят проверку?

Разоблачение

Показать

Проблема в том, что доллар $ в регулярных выражениях матчится либо в конце строки, либо перед символом \n в конце строки. К примеру, строка "123\n" успешно сопоставляется с регулярным выражением /^[0-9]+$/.

Чтобы в регулярном выражении поймать настоящий конец строки, стоит использовать \z:

sub is_interesting_id
{
    return $_[0] =~ /^5665[0-9]{7,13}\z/ ? 1 : 0;
}

Также см. perldoc perlre (раздел Assertions).