Все задачи

Бренд или не бренд?

24 Feb 2014

На сайте, который поддерживал программист, пользователи могли оставлять комментарии. В какой-то момент было решено помечать комментарии, содержащие упоминания брендов, как возможный спам.

Программист реализовал простую проверку “текст содержит слово из списка запрещенных”, изменения в коде получились совсем небольшими:

--- SomeModule.pm  (revision 12345)
+++ SomeModule.pm  (working copy)
@@ -6,6 +6,43 @@
+{
+my $brand_name_regexp = qr/
+\b(?:
+    autocad |
+    benetton |
+    cisco |
+    dodge |
+    ericsson |
+    forbes |
+    gorenje |
+    hasselblad |
+    ikea |
+    jaguar |
+    kyocera |
+    lego |
+    manfrotto |
+    nokian |
+    oracle |
+    rolex |
+    salamander |
+    swarovski |
+    toshiba |
+    ubiquam |
+    volkswagen |
+    wallmart |
+    yamamoto |
+    zippo
+)\b
+/xi;
+
+sub contains_brand_name 
+{
+    return $_[0] =~ $brand_name_regexp;
+}
+}
 
@@ -53,8 +90,10 @@
     push @posts, { 
         text => $_,
+        contains_brand_name => contains_brand_name($_), 
         word_count => scalar split(/\W+/),
     };
 }
 

И что же, вы думаете, случилось дальше?

Подсказка

Показать

Во-первых, был весьма высок процент ошибок типа false negative (текст был хороший, но помечался как содержащий слова из списка).

Подсказка-2

Показать

Во-вторых, если присмореться, в массиве @posts встречались вот такие нелепые хеши:

{
    '4' => undef,
    'text' => "отличный штатив, всем рекомендую",
    'contains_brand_name' => 'word_count'
}

Почему 'word_count' в contains_brand_name? Откуда ключ 4?

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

Показать

Источник проблем – функция contains_brand_name, а точнее – ее поведение в списковом контексте.

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

Конструирование же хеша предоставляет как раз списковый контекст, так что хеш

{ 
    text => $_,
    contains_brand_name => contains_brand_name($_), 
    word_count => scalar split(/\W+/),
}

превращается в

{ 
    text => $_,
    contains_brand_name => word_count => scalar split(/\W+/),
}

, а так как => – это всего лишь запятая, дополнительно заключающая свой левый операнд в кавычки, хеш получается таким:

{ 
    text => $_,
    contains_brand_name => 'word_count',
    scalar split(/\W+/) => undef,
}

Конечно, это не то, на что рассчитвал программист.

Кстати, если программист использует use warnings, то при выполнении кода увидит по крайней мере предупреждение Odd number of elements in anonymous hash.

Для исправление программы стоило бы поменять код функции contains_brand_name, и сопоставление с регулярным выражением выполнять всегда в скалярном контексте:

    return scalar $_[0] =~ $brand_name_regexp;