[Из песочницы] Как я мониторил Avito по SMS

Как известно, на «Авито» периодически появляются товары очень хорошего качества и при этом очень дешевые. Но появляются они редко, висят там мало и исчезают быстро.

Поэтому возникла у меня идея: а не поискать ли сервис, который раз в несколько минут проверяет объявления, и если появилось что-то интересное для меня — оповещает об этом? При этом оповещать лучше всего по смс, а то почту я не всегда проверяю оперативно.

Гуглинг выдал несколько таких сервисов, «всего» от 3 руб за смс или от 4 руб в сутки.

В итоге, я решил написать такой сервис самостоятельно, но об этом дальше…
Для интереса я зарегистрировался на одном из сервисов. Вчера он проверял ссылки каждые 15 минут, и если что изменилось, слал уведомления на почту. Про смс же у них на сайте было вскользь упомянуто, что почта mail.ru умеет отправлять смски. По факту оказалось, что mail.ru умеет отправлять только на мегафон, а у меня совсем даже не он… А если надо билайн-мтс, то пожалуйста, сервис с удовольствием поможет, за отдельную денежку.

Замечу также, что я давно являюсь пользователем очень удобного и бесплатного сервиса, о котором давным-давно писали тут же на хабре, и который позволяет отправить письмо с определенной темой на определенный ящик, и содержание письма придет мне в виде смс. Хотел указать свой_ящик@sms.ru для писем сервиса, но не понял как поменять тему письма, без чего смс не получишь.

Кроме того, сегодня демо-период глисы закончился, и периодичность проверки стала 720 минут.

В общем, подумав что платить за, прошу прощения, «сервис» такого уровня это все равно что платить за винду воздух, я решил что проще всего потратить часа 3 своего ценного времени и соорудить подобный сервис самому, благо парсинг страницы «Авито» тривиальный и, как следует ниже, занял у меня ровно 1 строчку кода.

Я для этого скрипта использовал VPS-хостинг. Подойдет также WEB-хостинг, при условии наличия на нем перла, доступа «наружу» и планировщика. В крайнем случае, подойдет любой включенный в инет комп. Думаю, у многих есть что-то подобное.

На чем написан скрипт


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

Логика работы, кратко


— Запускаем скрипт каждые xxx минут;
— Скачиваем страничку с помощью wget;
— Храним скачанную в прошлый раз страничку, сравнивая ее с вновь скачанной, если какие-то объявления изменились/появились новые — отправляем смс об этом.

Вытаскиваемая из объявлений инфа — это:

1. URL объявления (который я использую как уникальный идентификатор объявления);
2. Название;
3. Цена.

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

Детальнее


Перед использованием проверить пути и имена для mailer и wget, убедиться что они у вас есть и работают. В частности, у меня в centos мейлер называется mutt, чаще встречается mail или sendmail с тем же синтаксисом. Может быть, вам надо заменить wget на /usr/local/bin/wget и т.п.

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

Запускать скрипт командой: ./avito.pl урл_страницы_с_объявлениями.

Замечу, что URL страницы должен быть в виде «списком с фото». Иначе говоря, в урле не должно быть никаких &view=list или &view=gallery.

Пример урла: www.avito.ru/moskva?q=%D1%80%D0%B5%D0%B7%D0%B8%D0%BD%D0%BE%D0%B2%D1%8B%D0%B9+%D1%81%D0%BB%D0%BE%D0%BD

Страница скачивается в файл с именем полученным из урла, с заменой всех левых символов на подчеркивания, наподобие такого:

https___www.avito.ru_moskva_q__D1_80_D0_B5_D0_B7_D0_B8_D0_BD_D0_BE_D0_B2_D1_8B_D0_B9__D1_81_D0_BB_D0_BE_D0_BD

Оно должно быть уникальным, поддерживаться и в линуксе, и в винде и при этом быть достаточно читаемым.

Если такой файл уже есть, скрипт пытается вытащить из него объявления. Если объявлений в файле не найдено, скрипт вызывает wget, при этом перезаписывая файл. Если объявления найдены, файл сохраняется с суффиксом -1:

https___www.avito.ru_moskva_q__D1_80_D0_B5_D0_B7_D0_B8_D0_BD_D0_BE_D0_B2_D1_8B_D0_B9__D1_81_D0_BB_D0_BE_D0_BD-1

Далее страница скачивается заново, в ней проверяются следующие ситуации:

1. Если объявления в новой скачанной странице не найдены, скрипт просто завершается — старая страница остается с суффиксом -1. Это на тот случай, если вдруг сеть пропала или подвисла — прошлый список объявлений не потеряется.
2. Если скрипт запущен в первый раз (не найдена ранее скачанная страница), то инфа придет просто о количестве имеющихся объявлений:

Found 25 items, page www.avito.ru/moskva?q=%D1%80%D0%B5%D0%B7%D0%B8%D0%BD%D0%BE%D0%B2%D1%8B%D0%B9+%D1%81%D0%BB%D0%BE%D0%BD monitoring started


Если это сообщение пришло, значит система запустилась, это главным образом проверка того, что все заработало.

Поскольку смс должно быть чем короче, тем лучше, то все сообщения очень лаконичны.

3. Если появилось новое объявление, то инфа об этом добавится в текст будущей смс. Потом по всем объявлениям инфа придет в виде одной смс.
4. Если изменилась цена или наименование товара, то инфа придет в виде: старая_цена -> новая_цена наименование ссылка. Или новое_название ссылка.

Не знаю, может ли измениться название, но лишнюю проверку сделать было не жалко.

5. В консоль выводится отдельным текстом список того, что нашлось. Это сделано больше для отладки, потому как сегодня парсер работает, а завтра, когда они поменяют разметку, перестанет. Придется менять парсинг.

О парсинге и нюансах


Собственно, весь парсинг — в этой строчке:

while($text=~/<div class=\"description\"> <h3 class=\"title\"> <a href=\"(.*?)\".*?>\n(.*?)\n.*?<div class=\"about\">\n\s*(\S*)/gs)


Хотя, еще цена содержит пробел в виде nbsp, который я вырезаю другим regexp-ом:

$price=~s/&nbsp;//g


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

g — модификатор глобального поиска, который позволяет засунуть поиск внутрь условия while, выдавая каждый раз следующее объявление;
s — позволяет внутри одного регекспа проводить поиск в нескольких строчках (на «Авито» URL, наименование и цена располагаются на 4 строках, но это сейчас, пока они верстку не поменяли).

Также замечу, что для многострочного чтения файла в начале скрипта присваивается:

undef $/;


Это чтобы my $text=; прочитал в себя весь файл целиком.

Еще нюанс: я во все смски вставляю кликабельные урлы. У меня нормальный смартфон, который позволяет ткнуть в урл внутри смс и попасть на нужную страницу, очень удобно. Так вот, почему-то sms.ru портит такой невинный символ, как подчеркивание. Заменяя его на %C2%A7. Повлиять я на это не могу, зато могу заменить его на код подчеркивания, который доходит нормально, при этом урл становится кликабельным для sms.ru, оставаясь таким и для обычной почты: $text=~s/_/%5F/g;

Добавляем задание в планировщик

#crontab -e
*/20    *       *       *       *       cd /scripts/avito && ./avito.pl 'https://www.avito.ru/moskva?q=%D1%80%D0%B5%D0%B7%D0%B8%D0%BD%D0%BE%D0%B2%D1%8B%D0%B9+%D1%81%D0%BB%D0%BE%D0%BD'


Каждые 20 минут вызывать скрипт, проверяя страницу. Не забыть экранировать URL одинарными кавычками.

Таких заданий можно задать сколько угодно, они все будут работать независимо друг от друга.

Что я еще не сделал для промышленного варианта и что было бы легко доделать


1. Веб-морду для добавления/удаления юзеров и заданий. Хранение урлов, периодичности, ящика и телефона юзеров на sms.ru в базе mysql. Скрипт бы вызывался каждую минуту, проверял, по какому урлу выполняться и слал бы смс не на мой жестко забитый номер, а на тот, который задан пользователем.

Тогда можно было бы сдирать с юзеров по 8 рублей в день или что-то типа того. Может, заняться? Есть желающие за такую штуку денег заплатить?

2. Фильтр цен. Игнорировать цену выше или ниже заданной. Делается элементарно, еще одним if: next if($page_new{"price"}{$uri}>$max_price or $page_new{"price"}{$uri}<$min_price). Просто не нужно было.

3. По аналогии с «Авито», добавить авто.ру, irr и т.п. сайты.

Тоже элементарно, просто за тем while(...){...} дописать еще несколько while — каждому сайту по одному. Главное, чтобы внутри них заполнялись $page{"name"}{$uri} и $page{"price"}{$uri}.

По каждому сайту будет срабатывать свой while, остальные просто возвращать пустой результат.

Ну и собственно код скрипта

#!/usr/bin/perl

use strict;
undef $/;

my $url=$ARGV[0];
my $mailer="mutt";
my $wget="wget";

if($url eq ""){
    print "Usage: avito.pl <https://www.avito.ru/...url>";
    exit;
}

my $filename=$url;
$filename=~s#[^A-Za-z0-9\.]#_#g;
$url=~m#(^.*?://.*?)/#;
my $site=$1;
print "site:".$site."\n";

sub sendsms {
    my $text=shift;
    $text=~s/_/%5F/g;
    system("echo '$text' | $mailer -s 79xxxxxxxxx xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\@sms.ru");
}

sub parse_page {
    open(MYFILE,"<".shift);
    my $text=<MYFILE>;
    close(MYFILE);
    my %page;

    while($text=~/<div class=\"description\"> <h3 class=\"title\"> <a href=\"(.*?)\".*?>\n(.*?)\n.*?<div class=\"about\">\n\s*(\S*)/gs)
    {
        my $uri=$1;
        my $name=$2;
        my $price=$3;
        $uri=~s/^\s+|\s+$//g;
        $name=~s/^\s+|\s+$//g;
        $price=~s/^\s+|\s+$//g;
        $price=~s/&nbsp;//g;

        $page{"name"}{$uri}=$name;
        $page{"price"}{$uri}=$price;
    }
    return %page;
}

my %page_old=parse_page($filename);

if(scalar keys %{$page_old{"name"}}>0){
    system("cp $filename ${filename}-1");
}
else{
    %page_old=parse_page("${filename}-1");
}
system("$wget '$url' -O $filename");
my %page_new=parse_page($filename);

if(scalar keys %{$page_old{"name"}}>0){ # already have previous successful search
    if(scalar keys %{$page_new{"name"}}>0){ # both searches have been successful
        my $smstext="";
        foreach my $uri(keys %{$page_new{"name"}})
        {
            if(!defined($page_old{"price"}{$uri})){
                $smstext.="New: ".$page_new{"price"}{$uri}." ".$page_new{"name"}{$uri}." $site$uri\n ";
            }
            elsif($page_new{"price"}{$uri} ne $page_old{"price"}{$uri}){
                $smstext.="Price ".$page_old{"price"}{$uri}." -> ".$page_new{"price"}{$uri}." ".$page_new{"name"}{$uri}." $site$uri\n";
            }
            if(!defined($page_old{"name"}{$uri})){
                # already done for price
            }
            elsif($page_new{"name"}{$uri} ne $page_old{"name"}{$uri}){
                $smstext.="Name changed from ".$page_old{"name"}{$uri}." to ".$page_new{"name"}{$uri}." for $site$uri\n";
            }
        }
        if($smstext ne ""){
            sendsms($smstext);
        }
    }
    else{ # previous search is successful, but current one is failed
        # do nothing, probably a temporary problem
    }
}
else{ # is new search
    if(scalar keys %{$page_new{"name"}}<=0){ # both this and previous have been failed
        sendsms("Error, nothing found for page '$url'");
    }
    else{ # successful search and items found
        sendsms("Found ".(scalar keys %{$page_new{"name"}})." items, page '$url' monitoring started");
    }
}

foreach my $uri(keys %{$page_new{"name"}})
{
    print "uri: $uri, name: ".$page_new{"name"}{$uri}.", price: ".$page_new{"price"}{$uri}."\n";
    if($page_new{"price"}{$uri} eq $page_old{"price"}{$uri}){print "old price the same\n";}
    else{print "old price = ".$page_old{"price"}{$uri}."\n";}
    if($page_new{"name"}{$uri} eq $page_old{"name"}{$uri}){print "old name the same\n";}
    else{print "old name = ".$page_old{"name"}{$uri}."\n";}

}

© Habrahabr.ru