Цей список складений з технологій (утиліт Linux), які я використаю для однієї життєвої ситуації - пошуку аренди квартири по риночній ціні.
Знайомим з поняттям "скрейпінг" мабуть уже зрозуміло, про що буде пост. Тим не менше, запрошую під кат.
OLX є, здається, найактивнішою площадкою для пошуку аренди квартири у Києві. Але просто зайти на OLX, задати фільтри (район, кількість кімнат) недостатньо. Справа в тім, що є 3 типи аренд:
- аренди від ріелторів - ціни на ці квартири майже завжди завищені, тому-що від ціни квартири залежить дохід ріелтора. Через завищену вартість (неринкову) афіші висять відносно довго
- аренди від хазяїв - ціни на квартири нижчі, щоб швидше знаходити арендаторів
- афіші від шахраїв - ну з цими мудаками краще не зв'язуватись, але від них нікуди не дінешся
Так ось, хотілось би знайти квартиру від хазяїна по риночній ціні. Але проблема - через 15-20 хв після розміщення об'яви можна уже не дзвонити. Через те, що ціна риночна, квартира бронюється в перші 5-10 хв. Так-що або тиснете F5 постійно на протязі дня, або пишете скрейпера.
Судячи з кількості переглядів на кожній афіші, скрейпери використовуються поголовно.
Наш скрейпер буде на Баші. Якщо хочете, можете писати на Пітоні або будь-чому іншому, але Баш має свій шарм (якщо немає роботи з виключними ситуаціями).
З Башу дуже легко викачати сторінку з інтернету:
#local content="$(cat /tmp/olxcurl)"
local content="$(curl -sL "https://www.olx.ua/nedvizhimost/kvartiry-komnaty/arenda-kvartir-komnat/kvartira/kiev/?search%5Bfilter_float_price%3Afrom%5D=3000&search%5Bfilter_float_price%3Ato%5D=5500&search%5Bdescription%5D=1&search%5Bprivate_business%5D=private&search%5Bdistrict_id%5D=1")"
Ми отримаємо сторінку в Баш змінній content
. Пояснення до параметрів:
-s
- працювати тихо, не показувати баннери і прогресбари-L
- переходити по редіректам, якщо сервер відповідає 300-302. Жаль що це не дефолт
Pup нам розпарсить HTML з сторінки і дозволить виділити саме ті теги, які містять цікаву інформацію: посиланняя на афішу, опис, ціну і дату афіши.
echo -en "$content" \
| pup 'td.offer > table > tbody'
Пояснення:
echo -en
виведе нашу змінну content максимально без перетворень- селектор
td.offer
виділить нам всі теги<td>
з класомoffer
. Список афіш в OLX побудовний на основі HTML таблиці, тому це чудовий старт - але OLX пішов далі, і кожну афішу також побудував на основі таблиці, тому нам треба викинути цей рівень абстракції також. Селектор
td.offer > table > tbody
виділить всі теги<tbody>
, у яких є парент тег<table>
, у яких є парент тег<td>
з класомoffer
В результаті ми отримаємо фрагмент HTML, в якому ми маємо все, що відноситься до афіш. Оскільки афіш буде кілька, то воно буде мати вигляд:
<tr> ... афіша 1 </tr>
<tr> ... афіша 2 </tr>
<tr> ... афіша 3 </tr>
...
Але pup
занадто примітивна утиліта. Вона не може робити щось складніше ніж grep
по тегам. Тому ми заставимо pup видати результат у JSON форматі, який і будемо далі місити.
| pup 'td.offer > table > tbody json{}' \
local jq_extract="
{
url: .children[0].children[0].children[0].href,
date: .children[1].children[0].children[0].children[1].text,
text: .children[0].children[0].children[0].children[0].alt
}
"
echo -en "$content" \
| pup 'td.offer > table > tbody json{}' \
| jq -c '.[]' \
| jq -c "$jq_extract" \
| while read entry; do
local DATE="$(echo "$entry" | jq -r ".date")"
local URL="$(echo "$entry" | jq -r ".url")"
local TEXT="$(echo "$entry" | jq -r ".text")"
Ми пишемо далі наш скрипт перетворення. HTML не дуже весело співвідноситься з JSON, тому доводиться писати такі довгі рядки як .children[1].children[0].children[0].children[1].text
, щоб дістати текст афіши. Але давайте все-таки поясню:
.children
вjq
означає вибрати значення по ключуchildren
в словнику (dictionary), який дається на вхід. А.children[0]
означає що результатом буде список, і непогано було би одразу ж розкрити цей список, взявши перший його елемент{ url: XXXX, date: YYYY }
- це конструктор нового словника вjq
, деXXXX
таYYYY
- будь-які комбінації селекторів вхідних даних. Цим самим, ми катавасію з тегів HTML перетворюємо у красивий список JSON з потрібними нам даними-с
- перетворити JSON в один рядок, для зручної обробки в Bash.[]
- це цікавий селектор, який означає таке: візьми на вхід коректний JSON список і перетвори його у потік JSON елементів. Цей потік перестане бути коректним JSON, але дозволить обробляти список поелементно в Баші. Наприклад,[ { "x" = 1 }, { "y" = 2 } ]
перетвориться зjq -c .[]
у
{ "x" = 1 }
{ "y" = 2 }
-r
- означає перевторити результат у рядок. Наприклад,"XXXX"
перетвориться уXXXX
. Іншими словами, забрати подвійні лапки з виводу
Тепер, коли ми маємо текст, URL і дату, можна почати відправляти нотіфікейшени на робочий стіл. Цим займеться утиліта notify-send
(у мене з пакету libnotify
)
notify-send "($DATE) $TEXT" "$URL"
А Nix, в свою чергу, дозволить нам чітку визначити де і як всі вище вказані утиліти дістати.
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p curl pup jq libnotify
Так, це shebang. Означає він таке: цей скрипт запускається через nix-shell
, nix-shell
в свою чергу запустить цей скрипт через Баш, але перед тим додасть пакети curl
, pup
, jq
, libnotify
до оточення (environment). Такий собі virtualenv
на льоту, на системному рівні. Тому, якби я вирішив скопіювати цей скрипт на іншу машину, то мені не потрібно було би додатково качати і ставити всі залежності скрипта - вони будуть ліниво закачані при першому старті.
Чи є альтернативи Nix? Так, звісно:
- можна поставити ці пакети напряму через
apt
,apt-get
,yum
,pacman
,emerge
і вотевер елс. Але видаляти пакети також доведеться вручну. Наприклад,pup
- далеко не найпотрібніша утиліта в вашому оточенні. - можна все збілдити у докер контейнер, хоча я не певний що вийде просто перекидувати
notify-send
повідомлень з контейнера в хост. Можливо доведеться замінити на відсилання вебхук повідомлення в Slack.
Хоч я і не привів тут весь скрипт, я показав як маючи знання тільки Баша і навколозв'язаних утиліт, можна писати нетривіальні програми.
Хоча демон в деталях. Обробка дат формату OLX в баш (щоб відправляти нотіфікейшени тільки на свіжі афіші) виявилась далеко не тривіальною і займає більше рядків коду ніж сума вищеприведених.
🌟