[Erlang]Тролльдетектор для rsdn
От: Mr.Cat  
Дата: 12.07.09 16:50
Оценка: 614 (36) +1 :))) :))) :))) :))) :))) :))) :))) :)))
Сегодня мы напишем детектор троллей для нашего форума. Почему в decl? Потому что на эрланге.
Обнаруживать троллей мы будем простым и эффективгым способом: будем строить граф интересующей нас ветки. Вершинами графа будут юзеры, а ребра (соответствующего веса и толщины) будут связывать тех юзеров, которые что-то друг другу писали. По такому графу нетрудно догадаться, кто есть кто в ветке.

Для вытягивания тем мы воспользуемся веб-сервисом rsdn (http://rsdn.ru/ws/janusAT.asmx). К сожалению, в wsdl сервиса (http://rsdn.ru/ws/janusAT.asmx?WSDL) значится левый порт (8888), поэтому wsdl мы сохраним на диск и уберем из него ошибочный порт, вот так: http://gist.github.com/145674.

В качестве soap-клиента будем использовать оный из yaws, про который можно прочитать пару строк здесь: http://yaws.hyber.org/soap_intro.yaws. Таким образом, из эрланговых либ нам понадобятся:
1. yaws
2. erlsom
3. ibrowse (к сожалению, веб-сервис не желает работать, когда в качестве http-клиента используется оный из http).
Из тулзов:
1. graphviz
2. imagemagick

Первым делом, сгенерируем hrl со структурами для веб-сервиса: http://gist.github.com/145677.
Далее напишем функцию, которая достанет все сообщения ветки по id этой самой ветки (вообще, сервис вроде бы достает ветку по id сообщения в ней, но id ветки совпадает с id первого сообщения в ней):
get_topic(Topic) ->
    ibrowse:start(),
    JanusWsdl = yaws_soap_lib:initModel("janusAT.wsdl"),
    {ok, _,
     [#'p:GetTopicByMessageResponse'{
       'GetTopicByMessageResult'=
       #'p:TopicResponse' {
         'Messages'=
         #'p:ArrayOfJanusMessageInfo'{'JanusMessageInfo' = Messages}}}]} =
    yaws_soap_lib:call(JanusWsdl, "GetTopicByMessage",
               [#'p:TopicRequest'{ userName = "username",
                           password = "password",
                           messageIds = #'p:ArrayOfInt'{int = [Topic]}}]),
    Messages.

Эта штука возвращает список структур 'p:JanusMessageInfo'. Далее, пользуясь тем, что веб-сервис отдает сообшения отсортированными по времени, посчитаем количество ответов, которые пользователи отправляли друг другу.
links(Messages)->
    links(Messages, dict:new(), dict:new()).
links([Message|Tail], UserDict, LinkDict) ->
    MessageId = Message#'p:JanusMessageInfo'.messageId,
    MessageNick = Message#'p:JanusMessageInfo'.userNick,
    NewUserDict = dict:store(MessageId, MessageNick, UserDict),
    ParentId = Message#'p:JanusMessageInfo'.parentId,
    case ParentId of
    0 -> links(Tail, NewUserDict, LinkDict);
    _ -> ParentNick = dict:fetch(ParentId, UserDict),
         links(Tail, NewUserDict,
            dict:update_counter(case MessageNick < ParentNick of
                        true -> {MessageNick, ParentNick};
                        false -> {ParentNick, MessageNick}
                    end, 1, LinkDict))
    end;
links([], _, LinkDict) ->
    dict:to_list(LinkDict).

Эта функция для некоторой гипотетической ветки возвращает список такого вот рода туплов:
[{{"DarkGray","eao197"},3},
 {{"Ikemefula","thesz"},2},
 {{"VGn","thesz"},4},
 {{"jazzer","lomeo"},1},
 {{"VladD2","thesz"},2},
 {{"DarkGray","thesz"},4},
 {{"StevenIvanov","thesz"},2},
 {{"Calabon","thesz"},1},
 {{"Ikemefula","eao197"},2},
 {{"eao197","thesz"},1},
 {{"Ikemefula","VladD2"},1},
 {{"jazzer","thesz"},10}]

Попрошу заметить, что все имена вымышленные и любые их совпадения с никами реальных людей случайны.
Теперь этот список преобразуем в граф для graphviz:
links2dot(Links) ->
    links2dot(Links, "}\n").
links2dot([{{Nick1, Nick2}, Weight}|Tail], Acc) ->
    links2dot(Tail, [io_lib:format("\"~s\" -- \"~s\" [weight=~B, label=~B, style=\"setlinewidth(~B)\"];\n", [Nick1, Nick2, Weight, Weight, linewidth(Weight)]) | Acc]);
links2dot([], Acc) ->
    lists:flatten(["graph Links {\noverlap=false;\n" | Acc]).

linewidth(Weight) ->
    if
    Weight < 3 -> 1;
    Weight < 10 -> 2;
    Weight < 50 -> 4;
    true -> 8
    end.

Этот граф остается записать в файл и сконвертить в картинку. Для выявления троллей, на мой взгляд, больше всего подходит neato. Для нашей гипотетической ветки мы получим вот такой граф:

Итак, наиболее вероятно, что в данной ветке троллем является thesz. Другие участники дискуссии практически не общаются друг с другом — только лишь с thesz-ом. Также заметно, что основным оппонентом thesz-а является jazzer. Но поскольку ему (кроме thesz-а) практически никто не отвечает — можно предположить, что jazzer представляет мнение большинства, тогда как thesz играет роль провокатора.

В качестве последнего штриха сделаем вот что. Будем строить несколько графов, представляющих состояния ветки в разные моменты времени. А потом сделаем gif.
link_progress(Topic) ->
    link_progress([links2dot(links(M)) || M <- heads(get_topic(Topic))], 0).
link_progress([Dot|Tail], N) ->
    file:write_file(lists:flatten(io_lib:format("~4.10.0B.dot", [N])), Dot),
    link_progress(Tail, N+1);
link_progress([], _) ->
    ok.

heads(List)->
    heads([], List).
heads([], [Head|Tail]) ->
    heads([[Head]], Tail);
heads([List|_]=Acc, [Head|Tail]) ->
    heads([[Head|List]|Acc], Tail);
heads(Acc, []) ->
    lists:reverse(lists:map(fun lists:reverse/1, Acc)).

Функция link_progress наполнит текущий каталог кучкой .dot файлов, которые мы сперва преобразуем в картинки:
for dot in *.dot; do neato -Tpng -o${dot}.png $dot; done

Потом первую (пустую) картинку заресайзим до размера, который должен быть у гифа. Вообще, это надо делать автоматически, а не вручную, но мне влом.
сonvert 0000.dot.png -resize 775x375\! 0000.dot.png

И теперь сделаем гиф из картинок с графами:
convert -delay 100 -loop 0 -dispose previous -dispose background *.png thread.gif

И получится у нас вот такая штука:

Такие дела. Как видите, мы написали очень полезную тулзу: она поможет модераторам более эффективно раздавать предупреждения, банить пользователей и применять другие меры наказания. Достаточно одного взгляда на граф "горячей" ветки, чтобы понять кто в ней троллит — и принять соответствующие меры. Ура, товарищи. Банхаммер, вперед!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.