четверг, 16 ноября 2017 г.

Парсим известную маркет-площадку. Лениво и из командной строки Windows

Захотелось тут автоматизировать одну задачу. Вот у нас есть название какой-нибудь штуки, скажем, TP-LINK TL-SF1005D. А нам нужно: несколько изображений, технические характеристики и самую низкую цену в регионе. И, конечно, не руками всё это искать и сохранять. А ещё хорошо бы из одного источника – по одному шаблону.

Источников сегодня полно, так что второй вопрос сразу решаем – берём одну из самых известных маркет-площадок и, по совместительству, базу данных товаров. Ну, сами понимаете, какую.
Хорошо, а как не делать ничего руками? Самое адекватное решение это, конечно, API.
Только вот цены за API у нашего "маркета" как раз неадекватные. И только для юр. лиц.
При этом информация доступна – бери не хочу. Даже особого программирования не нужно.

Веселья ради и опыта для сделаем парсинг на чистом cmd. Ну, почти.

Поскольку у нас есть только название, прежде всего нам нужна ссылка на страницу товара. Смотрим сайт и видим, что получить её можно, отправив поисковый запрос, типа такого:

wget "http://market.some/search?redirect=2&suggest=test123"

Сразу натыкаемся на несколько проблем.


Во-первых, suggest (угадывание) срабатывает из браузера, но не из wget по ряду причин. Во-вторых, мы не знаем синтаксиса запроса, а перебирать варианты долго. В третьих, после второго и далее wget-запроса с некоторой вероятностью нас блокируют и просят ввести капчу.
Это уже много работы, а мы ленивые.
Значит, надо подумать outside the box.
Раз нужно делать как можно меньше запросов к нашему market'у, мы можем запросить другой, не такой капризный сервер для получения прямой ссылки. Поисковик какой-нибудь. Но это опять парсинг и уже по другом шаблону. А попроще нельзя?
Можно! Ищем, есть ли у нашего маркета расширение для поиска из адресной строки браузера. Серверы-обработчики такого поиска, скорее всего, настроены на постоянную обработку автоматизированных запросов. А значит, капчи там нет, угадывание работает всегда, а сам ответ сервера гораздо меньше и проще.

Так и поступим:

wget "http://server.some/suggest-rich?&srv=market&part=TP-LINK+TL-SF1005D"


В ответ получим что-то вроде:

["tp-link tl-sf1005d"],
["[{"img": "images.server.some/img_id30773513443.jpg", "text": "TP-LINK TL-SF1005D", "link": "market.some/product/1581115?hid", "prices": [{"max": 100500, "min": 1050}, "type": "model"}]]]


Крутотенюшка! И ссылка, и изображение, и минимальная цена, и в машиночитаемом формате. Какой смысл был API закрывать?

Итак, мы нежданно получили почти всё, что нам было нужно.
Ссылку получим через grep (родные виндовские find и findstr оказались не слишком хороши), как-то так:

FOR /F "tokens=*" %%N IN ('grep -Po "(?<=product\\\/)(\S{1,16})(?=\?hid)" temp') DO SET LINK=%%N

И скачаем нужную нам страницу с данными:

wget "%LINK%/specifications" -O modelsheet

Вроде как уже всё. Но "есть нюансы":
1. У нас полноценная современная html-страница. А значит, около мегабайта текста с кошмарным форматированием. К тому же, grep из gnuwin32 в мультистроковом режиме не работает;
2. У нас по-прежнему капча на повторных запросах;
3. Мы кириллические юзеры. В странице UTF, в cmd CP866. Переключиться на 65001 можно, но передача параметров на кириллице даже в grep не срабатывает.

Чтобы было меньше капчи, добавим в wget некоторый закос под браузер:
wget --user-agent "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko" --no-http-keep-alive --load-cookies "ya_cook.txt" "%LINK%/specifications" -O modelsheet


Начнём с простого. Вынем из нашей страницы минимальную цену:

FOR /F "tokens=*" %%N IN ('grep -Po "\d{1,16}(?=(\s|\S){1,16}lowPrice)" modelsheet') DO SET /A PRICE=%%N


Можно сразу сделать её ещё ниже, во имя победы в конкурентной борьбе:

SET /A PRICE=%PRICE%-((%RANDOM%%%10)*10+100)


А теперь фото. Их там много, в том числе и не наши, а нам нужно 2-3 штуки. Поэтому берём первые штук шесть:

FOR /F "tokens=*" %%F in ('grep -Po "images(\S{1,70})original.jpg" modelsheet') DO


и сразу их качаем. Сервер картинок тоже отдельный и не капчит. Но задержку между запросами лучше всё же оставить:

(
    wget "%%F" -O "!PHCOUNT!.jpg"
    SET /A PHCOUNT=!PHCOUNT!+1
    IF !PHCOUNT! GTR 6 GOTO ENDPHDWNLD
    timeout 1
)


Для красоты и SEO можем переименовать наши фото. Видов спереди, "панорамно" и сбоку будет вполне достаточно.
Первая картинка обычно всегда основная:

ren 1.jpg %MANUFACTURER%_%MODEL%_main.jpg

Для получения данных об изображениях в командной строке используем бесплатную MediaInfo CLI.
Остальные фото можно обработать по такой логике: следующая после основной картинка будет "панорамной", если она такого же или близкого размера к главному фото. Первая узкая (до 300 пикселей) картинка это, скорее всего, вид сбоку:

FOR /F "tokens=* skip=1" %%F IN ('dir /b *.jpg') DO (
    FOR /F "tokens=*" %%S IN ('MediaInfo.x64-sse2.exe --INFORM^=Image^;%%Width%% %%F') DO (
        SET TMPSIZE=%%S
    )
    IF !TMPSIZE! LSS 299 REN %%F !MANUFACTURER!_!MODEL!__side.jpg
)
REM Find wide photo by width, side photo excluded, renamed photos excluded
FOR /F "tokens=* skip=1" %%Y IN ('dir /b ?.jpg') DO (
    FOR /F "tokens=*" %%S IN ('MediaInfo.x64-sse2.exe --INFORM^=Image^;%%Width%% %%Y') DO (
        SET TMPSIZE=%%S
    )
    IF !TMPSIZE! LEQ !FRONTSIZE! REN %%Y !MANUFACTURER!_!MODEL!_wide.jpg && GOTO ENDPHRN
)
:ENDPHRN


А теперь самое интересное. Вынимаем кириллические характеристики в UTF из кошмарно форматированного html с примесью скриптов, BASE64 и кучи лишней информации.
К счастью, компьютеры не оперируют нечёткой логикой, и как бы страшно не выглядела оцифрованная информация, у неё есть чёткая структура. Вынуть характеристики из нашего market'а нам помогут регулярные выражения. Да-да, те самые ещё более неудобочитаемые и с виду совершенно бессмысленные символы. На самом деле, всё становится гораздо проще, когда используешь онлайн-чекеры (мне лично нравится regex101.com). Вот наша регулярка:

((?<=<span class=\"n-product-spec__name-inner\">)(([a-zA-Z0-9А-Яа-я-:\"\.,;\/()]|\s){1,64})(?=<))|((?<=<span class=\"n-product-spec__value-inner\">)(([a-zA-Z0-9А-Яа-я-:\"\.,;\+()]|\s){1,80})(?=<))|((<h2 class="title title_size_18">)(([a-zA-Z0-9А-Яа-я-:\"\.,;()]|\s){1,64})(</h2>))


Да, я сам её боюсь. Но если по кускам, то

((?<=<span class=\"n-product-spec__name-inner\">) - нужная нам строка начинается с определённого тега, при этом сам тег исключаем

(([a-zA-Z0-9А-Яа-я-:\"\.,;\/()]|\s){1,64}) - нужная нам строка содержит символы: a-zA-Z0-9А-Яа-я-:\"\.,;\/(), то есть алфавиты русский и английский, цифры и служебные символы -:".,;/(), при этом "\" - знак экранирования, ИЛИ (знак "|") пробельные символы (обозначаются "\s") в количестве {1,64}

(?=<)) - нужная строка заканчивается символом "<", но в строку его не включаем

И ничего страшного :)

Правда, gnuwin32 grep UTF-параметры на вход никак не осиливает и в мультистроковом режиме тоже не работает. Поэтому пришлось набросать быстренько прогу на C# исходник на Github сама программа (бесплатно и без смс, как всегда).

Делаем просто:
cwr modelsheet regex.txt out.txt


Можно сразу отформатировать в html список для красивости:

FOR /F "tokens=*" %%S in (out.txt) DO (
    ECHO "%%S" | FIND "h2"
    SET /A STRPTR=!ERRORLEVEL!
    CHCP 65001
    IF !STRPTR!==0 IF !COUNT! NEQ 0 SET OUTSTR=!OUTSTR!^</dl^>
    IF !STRPTR!==0 SET OUTSTR=!OUTSTR!%%S
    IF !STRPTR!==0 SET OUTSTR=!OUTSTR!^<dl class="prspec"^> && SET /A COUNT=0
    SET /A PAR=!COUNT!%%2
    IF !STRPTR! NEQ 0 IF !PAR! NEQ 0 SET OUTSTR=!OUTSTR!^<dd class="prspec"^>
    IF !STRPTR! NEQ 0 IF !PAR! NEQ 0 SET OUTSTR=!OUTSTR!%%S
    IF !STRPTR! NEQ 0 IF !PAR! NEQ 0 SET OUTSTR=!OUTSTR!^</dd^>
    IF !STRPTR! NEQ 0 IF !PAR! EQU 0 SET OUTSTR=!OUTSTR!^<dt class="prspec"^>
    IF !STRPTR! NEQ 0 IF !PAR! EQU 0 SET OUTSTR=!OUTSTR!%%S
    IF !STRPTR! NEQ 0 IF !PAR! EQU 0 SET OUTSTR=!OUTSTR!^</dt^>
    IF !STRPTR! NEQ 0 SET /A COUNT=!COUNT!+1
    SET /A STRCOUNT=!STRCOUNT!-1
    IF !STRCOUNT! EQU 0 SET OUTSTR=!OUTSTR!^</dl^>^" && ECHO !OUTSTR!>>!OUTFLNM! && GOTO FORMEND
    CHCP 866
)
:FORMEND


Вот мы и получили всё, что хотели.
Как видим, не сильно сложно. Самое смешное во всём этом даже не cmd, а то, что если купить какой-нибудь готовый парсер, то делать придётся практически то же самое. Всё равно ковырять страницу, выискивать теги, запрашивать ссылки. А если у нашего маркета что-то поменяется, то нет гарантии, что платный парсер с закрытым кодом не перестанет работать. В нашем же случае изменения внести проще простого, даже компилятор не нужен.

Защита от парсинга на market'ах, в общем, номинальная. Открыли бы API с ограниченными запросами, проблем было бы меньше для них же.

Хочется загрузить полученную информацию куда-нибудь в базу данных или на сайт? Да не вопрос. Переведём в xml, csv, sql и вообще что угодно. Но об этом – в следующий раз.

Да пребудет с вами Regex, да обойдёт вас капча!

Комментариев нет:

Отправить комментарий