Skip to content

Instantly share code, notes, and snippets.

@danbst
Created July 19, 2018 06:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danbst/3459a7c068dfd8e440efbb8b71d60138 to your computer and use it in GitHub Desktop.
Save danbst/3459a7c068dfd8e440efbb8b71d60138 to your computer and use it in GitHub Desktop.
Bash. Curl. Pup. Jq. Notify-send. Nix

Цей список складений з технологій (утиліт Linux), які я використаю для однієї життєвої ситуації - пошуку аренди квартири по риночній ціні.

Знайомим з поняттям "скрейпінг" мабуть уже зрозуміло, про що буде пост. Тим не менше, запрошую під кат.

Постановка задачі

OLX є, здається, найактивнішою площадкою для пошуку аренди квартири у Києві. Але просто зайти на OLX, задати фільтри (район, кількість кімнат) недостатньо. Справа в тім, що є 3 типи аренд:

  • аренди від ріелторів - ціни на ці квартири майже завжди завищені, тому-що від ціни квартири залежить дохід ріелтора. Через завищену вартість (неринкову) афіші висять відносно довго
  • аренди від хазяїв - ціни на квартири нижчі, щоб швидше знаходити арендаторів
  • афіші від шахраїв - ну з цими мудаками краще не зв'язуватись, але від них нікуди не дінешся

Так ось, хотілось би знайти квартиру від хазяїна по риночній ціні. Але проблема - через 15-20 хв після розміщення об'яви можна уже не дзвонити. Через те, що ціна риночна, квартира бронюється в перші 5-10 хв. Так-що або тиснете F5 постійно на протязі дня, або пишете скрейпера.

Судячи з кількості переглядів на кожній афіші, скрейпери використовуються поголовно.

Bash

Наш скрейпер буде на Баші. Якщо хочете, можете писати на Пітоні або будь-чому іншому, але Баш має свій шарм (якщо немає роботи з виключними ситуаціями).

Curl

З Башу дуже легко викачати сторінку з інтернету:

#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

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{}' \

Jq

    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. Іншими словами, забрати подвійні лапки з виводу

Notify-send

Тепер, коли ми маємо текст, URL і дату, можна почати відправляти нотіфікейшени на робочий стіл. Цим займеться утиліта notify-send (у мене з пакету libnotify)

notify-send "($DATE) $TEXT" "$URL"

так воно виглядає у мене

Nix

А 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 в баш (щоб відправляти нотіфікейшени тільки на свіжі афіші) виявилась далеко не тривіальною і займає більше рядків коду ніж сума вищеприведених.

@alexanderad
Copy link

🌟

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment