Всем привет! Подруга попросила меня помочь с написанием примера парсинга (загрузки и обработки) цен AliExpress на perl версии 5. Ну а я решил не ограничиваться Perl, и реализовал образцы кода также на Python, Ruby и, конечно же, скрипт на Bash.
Суть скрипта экстремально проста. Что удивительно, я думал в AliExpress цена окажется скрытой за динамически подгружаемым JS, но это не так! Исследование страницы товаров обыкновенным curl показало, что цена отображается даже для "plaintext/reference" браузера в сферическом вакууме. Не нужно даже подменять user-agent или работать с HTTPS. Что-ж, это всё упрощает. И это нам и нужно!
Bash на Linux
Для начала, так как я хотел убедиться в работоспособности примера, я написал скрипт на Bash. Его вам и приведу. Для него нужно установить wget, если его у вас нет (для финальной версии скрипта я выбрал его, хотя вы легко можете заменить его на curl): sudo apt install wget
. Остальной софт, а именно: awk, cat, grep, echo
у вас стоит по умолчанию с бОльшой степенью вероятности. Если у вас есть желание запустить код данного примера на других платформах (не-Linux), можете посмотреть в сторону вроде как вышедшей Ubuntu под Windows 10, полноценных виртуальных машин, или "адаптера" cygwin.
#!/bin/bash
# The following script parses aliexpress product's price with bash.
# Required: wget; cat; grep; awk (everything except wget should be installed by default)
# The following scripts takes URL of a product.
# Run like: ./script.sh "https://ru.aliexpress.com/item/SAMSUNG-USB-Flash-Drive-Disk16G-32G-64G-128-USB-3-0-Metal-Super-Mini-Pen-Drive/32595548093.html"
echo "The price of the item is:"
wget -O a.html -q "$1"; cat a.html | grep j-sku-price | awk -F">" '{ print $2 }' | awk -F"<" '{ print $1 }'
rm a.html
Можно сразу сохранить этот код под любым именем (например parse.sh
), сделать его исполняемым (sudo chmod +x parse.sh
), и запускать с аргументом в виде ссылки на товар, цену которого вы хотите узнать. Например, так: ./script.sh "https://ru.aliexpress.com/item/SAMSUNG-USB-Flash-Drive-Disk16G-32G-64G-128-USB-3-0-Metal-Super-Mini-Pen-Drive/32595548093.html"
. Разберём по этапам, что происходит.
Изначально у меня возникали проблемы с попыткой обработать поток данных веб-страницы, приходящей по запросу curl, поэтому я сделал мини-буфер в виде файла под названием a.html. Вначале wget скачивает туда файл, затем происходит его обработка, вывод на экран, и удаление данного файла.
Разберём подробнее.
- Echo - команда, выводящая текст после неё на экран. Всё, что указано до echo, закомментировано (в bash комментарий обозначается как #), а значит будет проигнонировано интерпретатором при запуске.
- Wget - консольный загрузчик веб-страниц. Аргумент -O означает Output, то есть файл, куда сохранять укаазнную далее ссылку. Флаг -q означает "Quiet", т.е. загрузка "молча", без выдачи процесса загрузки в терминал. $1 - это первый аргумент, передаваемый скрипту при запуске. Именно он и обеспечивает работу с введённой вами после ./script.sh ссылкой.
- Точка с запятой - конструкция в Bash, означающая следующее по порядку действие, которое будет выполнено только по завершении предыдущего. В нашем случае - cat a.html - вывести в стандартный вывод файл a.html. Стандартным выводом был бы наш экран, если бы не
- | - оператор конвеерных операций в Bash. Таким образом мы подаём вывод программы cat на вход программы grep, соединяя их в единый "конвеер". Grep, в свою очередь, это программа, которая ищет строку с указанным нами текстом. Здесь я выбрал для примера тег (точнее, его id) j-sku-price - именно его и ищет и выдаёт строку целиком grep.
- Awk в данном примере очень похож на grep (хотя в реальности awk значительно более мощный "зверь"). Здесь мы передаём выход grep программе awk, которая умеет работать с переменными, находя определенный разделитель (-F = "Field Separator", то есть "разделитель полей"). Не пугайтесь, у awk очень специфический синтаксис. Всё, что вам нужно знать о нём в данном примере кроме флага F - это конструкция print $2. Она означает, что мы передадим стандартному выводу всё то, что найдём после разделителя полей. То есть ищем в строке-переменной, уже ранее отсеянной grep, символ ">" (это как раз последний символ тэга < s p a n > в нашем случае), и передаём стандартному выводу всё, что после него ($1 - до него).
- Но у нас ещё остался мусор - закрывающий тэг < / s p a n >! Снова призовём awk, соединяя его в конвеер | и выводя, на этот раз, всё вплоть до тега "<". Отсюда и
| awk -F"<" '{ print $1 }'
. Конечно, для данной операции можно призвать и другой софт - tr, cut, но я привык работать с awk и он достаточно эффективен. - Результат действий конвеера программ wget, cat, grep и дважды awk, будет выведен на экран. Напоследок, у нас остался буферный файл a.html - удалим его, т.к. он больше не нужен -
rm a.html
.
В качестве домашнего задания попробуйте заменить wget на curl, а затем превратите этот скрипт в PHP-скрипт (curl присуствует и работает в PHP абсолютно так же).
Надеюсь, я понятно объяснил, и вы смогли сохранить и запустить данный скрипт, а также он сработал :) Вот вам исходный код на pastebin, и давайте двигаться дальше:
Perl на любой платформе
Не смотря на то, что код здесь и далее должен по идее заработать одинаково просто и хорошо на любой платформе, само написание и запуск\тестирование я проводил на Linux (Ubuntu). Думаю ни для кого не секрет что работа с утилитами для разработчика (каламбур ;) проще и удобнее всего. Здесь и далее речь идёт о perl версии 5, хотя для более поздних разница не должна быть супер-большой.
Для начала, я использовал HTML::TagParser. Да, фанаты Perl могут закидать меня тапками, ведь есть мощный WWW::Mechanize и Mojo::DOM, а также другие труЪ-coder ways работы с парсингом веб-страниц. Но, во-первых, скажите мне, зачем пригонять комбайн для сборки зерна, если вам нужно всего 100 грамм? :) Во-вторых, примеры на них получаются в целом объемнее, а по понятности примерно такие же. В-третьих, я с пары-тройки пинков не сумел заставить простейший референс-пример с ними делать то, что мне нужно, а с HTML::TagParser получилось сразу.
Если вы ранее не работали с Perl, через :: тут обозначаются внешние подключаемые библиотеки, которые можно собрать самому (труЪ-way) или взять из каталога cpan одной командой. Для начала, убедитесь, что у вас установлен perl и cpan: sudo apt install cpan
.
Теперь нам надо скачать две зависимости для работы, как я уже сказал выше, это HTML::TagParser, и URI::Fetch - программа для загрузки страниц по URI, которая имеется у первого в зависимостях. Поэтому, сделаем:
sudo cpan URI::Fetch
sudo cpan HTML::TagParser
Если вы впервые запускаете cpan, нужно согласится со всем, что он у вас спросит (yes, yes, yes) и затем, весьма вероятно, выполнить команду повторно. Всё, "боевая машина" perl для работы со страничками AliExpress настроена, поехали! :)
# You have to install HTML::TagParser and also URI::Fetch
# For example do: sudo cpan URI::Fetch
# And then also do: sudo cpan HTML::TagParser
# Save this script as perlparse.pl and run perl ./perlparse.pl
# Good luck ;)
# Written 25.03.2017 by Security XIII
use HTML::TagParser;
my $url = "https://ru.aliexpress.com/item/6-12Colors-Non-toxic-Crayon-edible-baby-drawing-Supplies-ring-toy-Easy-to-erase-educational-toys/32753968959.html";
my $tag = "j-sku-discount-price";
my $html = HTML::TagParser->new( $url );
my @list = $html->getElementById( $tag );
print @list[0]->innerText;
Разберём подробнее.
- Perl # также воспринимает как комментарий.
- Use - подключаем нужную библиотеку (в данном случае HTML::TagParser). Зависимости подключатся сами, так что всё ОК. Обратите внимание, в Perl все значимые строки (кроме комментариев и пустых строк, соответственно), заканчиваются точкой с запятой.
- my $url - в этот раз, в отличии от bash, где мы передавали ссылку на товар в интерактивном режиме при запуске скрипта, давайте сохраним ссылку в переменную. Впрочем, ничего не мешает взять её из интерактивного режима.
- my $tag = "j-sku-discount-price"; - в этот раз поработаем с другим тэгом - discount price (цена со скидкой), т.к. на указанный товар как раз происходит распродажа.
- В переменную $html сохраним результаты работы TagParser'а. Соответственно, самому TagParser'у мы передаём переменную, содержащую URL страницы для загрузки.
- В массив @list сохраним все тэги, ID которых соответствует ранее назначенной переменной $tag.
- Так как нас интересует только первое вхождение, то выведем на экран первый элемент массива @list (это [0] элемент, помните, вы же труЪ-погромист). Вывод в stoud (т.е. в данном случае на экран) в perl это print, тут ничего необычного. Последнее, innerText - это текст, содержащийся внутри тэга с найденным ID. Именно он нам и нужен - это же perl, тут так удобно - не нужны никакие awk и прочие ухищрения.
Если вы хотите вывести все элементы полученного массива, а не один, как в примере (в случае, когда у вас несколько тегов будут соответствовать заданному примеру), то придётся заменить строку print на кострукцию посложнее.
foreach my $elem ( @list ) {
my $tagname = $elem->tagName;
my $attr = $elem->attributes;
my $text = $elem->innerText;
print "<$tagname";
foreach my $key ( sort keys %$attr ) {
print " $key=\"$attr->{$key}\"";
}
if ( $text eq "" ) {
print " />\n";
} else {
print ">$text</$tagname>\n";
}
}
Если коротко, то здесь для каждого элемента в списке @list собираются его имя, атрибуты, текст, а затем всё это выводится с корректными переносами строк на экран.
Python на любой платформе
В случае с Python (используемая версия - 2.7) нам, с одной стороны, повезло - нужные библиотеки (urllib и lxml) уже входят в стандартную поставку python, но с другой стороны - работа с тегами тут будет менее изящной. Что-ж, потерпим :)
# Python program to grab AliExpress price of an item.
# Written by SecurityXIII on 25.03.2017
import urllib
from lxml.html import fromstring
url = 'https://ru.aliexpress.com/item/6-12Colors-Non-toxic-Crayon-edible-baby-drawing-Supplies-ring-toy-Easy-to-erase-educational-toys/32753968959.html'
id = 'j-sku-discount-price'
content = urllib.urlopen(url).read()
doc = fromstring(content)
print doc.get_element_by_id(id).text_content()
Разберём подробнее.
- Python тоже понимает (читай - пропускает) комментарии, начинающиеся с #.
- import в python - то же самое, что use в perl - подключение внешних библиотек.
- from ... import ... - из библиотеки lxml.html нам нужна только функция fromstring, вот её и импортируем. Удобно? А то!
- Переменные url и id - думаю, тут всё уже понятно, дополнительно останавливаться не будем.
- В переменную content сохраняется результаты работы urllib, которая выполняет метод urlopen (загрузку страницы по указанному URL) и функцию read по отношению к ней. ТруЪ-погромисты, исправьте меня, я точно тут что-то не так сказал.
- В переменную doc сохраним content, но только обработанный как строковая переменная (та самая функция fromstring из библиотеки lxml.html).
- Выводим на экран текстовое содержимое тега (text_content()), найденному по id (get_element_by_id(id)) внутри переменной doc.
Ну вот и всё. Проще? Думаю, примерно так же. А вот по красоте, perl, пожалуй, пока выигрывает.
Ruby на любой платформе
Для самых стойких и хардкорных ТруЪ-погромистов остался этот раздел. Ну, если вдруг у вас не установлен ruby, давайте sudo apt install ruby
. Работаем с версией 2.3.1. Для ruby все советуют делать парсеры при помощи nokogiri. Ну он есть в ruby gem, так что sudo gem install nokogiri
.
# Ruby AliExpress price fetch example
# Requires nokogiri (sudo gem install nokogiri)
# Save and run as ./ruby.rb
# Written 25.03.2017 by Security XIII
require 'open-uri'
require 'nokogiri'
url = 'http://ru.aliexpress.com/item/6-12Colors-Non-toxic-Crayon-edible-baby-drawing-Supplies-ring-toy-Easy-to-erase-educational-toys/32753968959.html'
id = 'j-sku-discount-price'
doc = Nokogiri::HTML(open(url))
puts doc.css("span[id=#{id}]").inner_text
Разберём подробнее.
- Решёточка # это комментарий, если вы ещё не привыкли. Тут тоже работает. :)
- require те же use и import - подключение используемых библиотек. В нашем случае это open-uri (нужна для загрузки страницы по URL) а также наш парсер nokogiri.
- С переменными url и id, думаю, объяснять ничего не надо.
- В переменную doc помещаем результаты парсинга Nokogiri, которому их в свою очередь передаст open-uri через функцию open.
- Тут чутка замороченнее. Выводим на экран (puts) внутреннюю часть тега (inner_text). doc.css - это обработка в CSS-стиле нашей ранее подготовленной переменной doc, содержащей спарсенную nokogiri страницу. А вот в скобках мы ищем тег span с указанными id. Но так как нам нужно подставить в стровую переменную (видите, span в двойных кавычках?) другую переменную? В Ruby это делается как #{string} (в нашем случае id). Важно, чтобы строковая переменная была в двойных кавычках. В другом случае не сработает!
Что думаете о Ruby? Он, кстати, второй кандидат по популярности, применяемый именно в сервер-сайд сегменте (точнее, наверное, ROR - Ruby on Rails), после PHP.
Сравнение - бонус 1
ЯП | Переменные | Библиотеки | Вывод на экран |
---|---|---|---|
Bash | i = $z | . /path/to/functions.sh | echo |
Perl | $i = $z | use | |
Python | i = z | import | |
Ruby | i = z | require | puts |
Сравнение - бонус 2
Я решил также сделать анализ скорости работы данного кода в разных ЯП. Конечно, код не претендует на идеальность или оптимизированность, но ведь условия-то приблизительно равные, так что почему бы и нет? :)
Зафигачил по-быстрому вот такой скрипт:
#!/bin/bash
for ((i=1;i<=$1;i++))
do
echo "TEST NUMBER $i of $1"
/usr/bin/time -f "%E, %U, %S" ./parse.sh "http://ru.aliexpress.com/item/6-12Colors-Non-toxic-Crayon-edible-baby-drawing-Supplies-ring-toy-Easy-to-erase-educational-toys/32753968959.html" > /dev/null
/usr/bin/time -f "%E, %U, %S" perl parse.pl > /dev/null
/usr/bin/time -f "%E, %U, %S" python parse.py > /dev/null
/usr/bin/time -f "%E, %U, %S" ruby parse.rb > /dev/null
done
Для того чтобы он корректно отработал, ваши файлы с кодом должны называться parse.sh, parse.pl, parse.py, parse.rb
соответственно. Запускаем как ./all.sh 100, где 100 - количество желаемых итераций.
Затем я сделал просто: скопировал вывод терминала в LibreOffice Calc. Конечно же вручную мы считать ничего не будем, для начала, надо написать адреса каких-нибудь повторяющихся ячеек. У меня это, к примеру, B2, B7, B12 и далее. Впишите в отдельный столбец справа в каждую ячейку адреса одинаковой переменной. Теперь выберите эти 3 адреса и потяние, пока Calc не сгенерирует адресацию до самого конца вашей "выгрузки" (у меня это 100 переменных, соответственно, адрес последней получился B497). Повторяем операцию для колонок C и D а также остальных ЯП (у меня получилось всего 12 раз).
Теперь нужно воспользоваться удобной функцией LibreOffice - взять адреса ячеек из других ячеек и посчитать среди них среднее арифметическое. Вот формула:
=AVERAGE(INDIRECT(H1:H100))
Удобно? Очень :) А дальше просто копируем получившийся результат в 2 соседние ячейки. С новой строки нужно снова ввести правильную адресацию и снова 2 раза скопировать. Всего проделываем эту процедуру 4 раза. Аналитика готова:
ЯП | real | user | system |
---|---|---|---|
Bash | 0.9 | 0 | 0 |
Perl | 1.4 | 0.4 | 0.04 |
Python | 1.34 | 0.15 | 0.01 |
Ruby | 1.38 | 0.31 | 0.03 |
К сожалению, /usr/bin/time почему-то по умолчанию не выдаёт время после 2 знака от запятой (точнее, я не знаю как настроить точность его выдачи), а системный time это делает, но зато он не умеет сортировать переменные, поэтому пришлось пользоватся /usr/bin/time. И отсюда же нули в результатах баша, там на самом деле не нули, а задержки на уровне 0.015-0.020s.
Я был очень сильно удивлён, но, согласно аналитике, Bash оказался самым быстрым интерпретируемым языком. Конечно, надо отдавать себе отчёт, что остальные проги запускаются именно интерпретатором bash, который добавляет свои задержки - но, как выяснилось, они не такие уж и большие. Среди трёх языков по скорости победил Python, Perl и Ruby же находятся примерно на одном уровне.
Завершение учебника
Вместо красивого финала, даю ссылочку на папку со всеми исходниками на pastebin.
Пишите в комментариях, какой язык программирования вам нравится больше (да, баш это не ЯП, но я тут обобщил)? Мне, например, больше по душе, когда переменные всё-же эксплуатируются с $, а не просто plaintext'ом. Поэтому мой выбор это Bash и Perl :) К тому же, Perl логичнее для работы с сетью, хотя Ruby тоже себя в этом хорошо показывает. А Python побеждает по универсальности и отсутствию необходимости устанавливать зависимости ;)
Кстати, как вам учебник? Не напрягает небольшое количество скриншотов и большое количество информации? Пишите в комментах!
С вами, как всегда,
Айтишник-Линуксоид из Ростова-на-Дону
Den Ivanov aka SXIII
На Bash можно сделать куда лучше:
cat products.list | parallel curl 2> /dev/null | grep j-sku-price | awk -F">" '{ print $2 }' | awk -F"<" '{ print $1 }' | sed 's/ //g; s/,/./g'
Правда порядок не сохраняется, нужно подумать, как поправить.
...окончательно понимаю, как я безнадежно отстал от всего этого...))))..
=)))))))