Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni

08.05.2024

Особливості HTTP клієнтів на iOS

У Swift є практично монопольна бібліотека для HTTP запитів. Це Alamofire. Їй вже майже 10 років (тобто вона пережила всю еволюцію мови Swift з її суттєвими змінами.) Звісно, запити можна робити й стандартною бібліотекою — ба більше, Alamofire тільки її загортає.

Взагалі, як я розумію, стандартна бібліотека це єдиний розумний шлях, бо у доступі до HTTP з мобільних пристроїв є свої нюанси. Щоб оптимізувати використання ресурсів, ми делегуємо роботу операційній системі, а потім тільки обробляємо результат. Це суттєво відрізняється від звичного підходу, де операційна система надає TCP, а шар HTTP реалізований нашою програмою чи її залежностями.

Наприклад: є особливий метод download для завантаження великого файлу прямо на диск (ми отримуємо шлях до файлу.) Це не просто синтаксична зручність: наша програма не отримує зміст файлу та не змушена виділяти на нього памʼять. Коли файл завантажений, можна, наприклад, читати його по частинах.

Або: на iOS можна замовити завантаження в фоні. Йдеться про те, що наш застосунок може взагалі бути закритим та не палити процесор: операційна система сама виконає завантаження. Єдине, що фонове завантаження розраховане на один чи декілька файлів; з сотнями файлів, навіть маленьких, можна очікувати необмежених затримок через особливості планування.

Як на мене, то перекласти обробку HTTP на ОС це винахідливе рішення. Причому Apple справно підтримує нові стандарти: підтримка HTTP/3 зʼявилася ще у 2021.


07.05.2024

Завантаження відео з сайтів

Чи випадала тобі задача завантажити відео з інтернету? Мені чомусь багато разів, першу статтю про це писав ще у 2008… Ось кілька порад.

Для Youtube та багатьох інших сайтів є yt-dlp. Якщо стягнути з командного рядка вас влаштовує, то, може, більше нічого й не потрібно. Але якщо, наприклад, хочемо завантажувати відео прямо у мобільному застосунку?

Правда в тому, що якщо відео можна дивитися без реєстрації, то й стягнути його теж вдасться. (Та й з реєстрацією теж… тільки після авторизації.) Але, звісно, відеохостинги вдаються до мір, щоб це ускладнити.

Втім, будь-який хостинг буде використовувати звичайний компонент <video> для відтворення відео. А значить, відеопотік знаходиться за адресою, та нам залишається тільки її дізнатись. Колись відео захищали аплетами Flash та особливими кодеками, але цей час давно минув.

Де взяти адресу відеопотоку? Бажано, без використання справжнього браузера, бо це складно та повільно. В найпростішому варіанті, її можна обчислити з адреси сторінки, проте зазвичай адреси файлів розбігаються зі сторінками в першу чергу з технічних причин. Інколи адреса відео збігається з адресою картинки з метаданих — тільки з різними розширеннями.

Також часто адреса відеопотоку отримується з виклику API. Його можна підглянути в браузері на вкладці “Мережа”. (Взагалі, інструменти розробника в браузері це перше, за що ми беремося.) Тут можемо виявити наступне питання: як авторизуватись? API зовсім без авторизації — рідкість. Але, знову ніякої магії немає: або мусимо відтворити куки, або отримати та передати API токен. Все це можна знайти, підгледівши запити, які робить справжня сторінка.

Нарешті, інколи сервер робить додаткові перевірки: User-Agent досить типова, а також Referer. Тут доведеться поекспериментувати. Для реверс-інжинірінгу API дуже корисний RapidAPI - в ньому легко будувати запити та випробувати альтернативи. Набагато зручніше ніж curl/wget.


06.05.2024

Автокатегоризація постів: підготовка

Як напівтренування, напівкорисне діло захотілося автоматично категоризувати пости в каналі. Автоматично тому, що теми для постів зʼявляються незаплановано, та самому цікаво, в який бік мене несе. Найбільш очевидним алгоритмом є K-means, можна з нього почати, а потім може поекспериментувати з іншими підходами.

Аналіз тексту починається з того, щоб отримати той текст в чистому вигляді. В мене всі пости в Markdown; найпростіше тут взяти парсер Goldmark, який я вже використовую в телеграм-боті, та приписати до нього рендерер, що видає чисто текст без всякої розмітки.

Далі мусимо перетворити кожен пост на послідовність слів, тобто токенізувати. Тут ніби нічого важкого. Застряг я на наступному кроці — стемінгу. Стемінг то відкидання від слова змінної частини, щоб алгоритм не плутався між “слово” та “словом” або “застрягнути” та “застряг”. В кожній мові стемінг свій; до того ж немає “стандартного” стемінгу, бо він не призводить слово до базової форми (це надто складно), а навмання відрізає закінчення. Тут потрібна бібліотека саме для української мови.

Поки що знайшов чудовий ресурс про обробку текстів українською мовою. Також колекцію стемерів. Далі буде.


05.05.2024

IAM в AWS та Google: різні речі

Нещодавно зробив коротке занурення в Google Cloud (бо я резидент AWS.) Коли працюєш з Terraform, може створитися уявлення, що всі хмари однакові, тільки ресурси мають різні назви. Виявилося навпаки, що система зі знайомою назвою IAM (“Керування особами та доступом”) працює фундаментально інакше.

В AWS IAM я звик, що права складаються з політик, а в політиці міститься перелік ресурсів та дозволених операцій. Мені завжди це здавалося логічним; хіба що є проблема адресації ресурсів. Проте документація по IAM просто чудова та з нею, крок за кроком, можна все зібрати.

…А в Google IAM я до кінця не зрозумів, але ніби права мають модель графа авторизації. По-перше, ролей тут немає, тільки користувачі або “сервісні” користувачі. А по-друге, дозволи призначаються звʼязком користувача та ресурсу. Звʼязок цей містить “роль”, тобто визначає дозволені операції.

В теорії логічно, але на практиці, наприклад, складніше зрозуміти, які саме дозволи має той чи інший користувач. Політики, яку можна подивитись, немає. Натомість є цілий інтерфейс для пошуку звʼязків, який видасть перелік звʼязків табличкою.

Таке я не люблю. Політика в AWS виражає намір, який інформує її зміст. В Google по переліку звʼязків можна побачити, що існує, але не зрозуміти, чому. На щастя, якщо є Terraform, то намір можна викласти там.


04.05.2024

Будні проєкту на Ruby та Go

В екосистемі Ruby мова Go має конкретне місце: на ній можна написати найбільш навантажену частину проєкту. Я давно вже рекомендую цей підхід та вважаю, що саме мова Go є найкращою “швидкою” мовою для рубістів, в першу чергу через свою простоту.

Але “переписування на Go” безповоротно змінює проєкт. Тепер для кожної нової фічі в нас є вибір: писати на Ruby, або на Go. Чим більше працюєш з Go, тим далі критерій навантаженості зсувається від, умовно, найвибагливішого запита до API, до будь-чого що має потенціал стати “гарячим шляхом” в проєкті. Чим більше коду Go написано, тим більше нового коду буде зручно написати на Go.

Чи я хочу сказати, що доля такого проєкту — бути переписаним на Go повністю, то це зовсім не так. Бо в Ruby та Rails є купа того, чого на Go немає — починаючи, наприклад, з інтеграційного тестування. Виходить, доведеться завжди балансувати.

Мені, особисто, дуже подобається писати все на Go. Тому я компенсую це пошуком актуальних переваг з боку Ruby. Наприклад, потрібно було написати задачу, що повторюється за графіком. Саму задачу мені дуже легко написати на Go. Але це була б AWS Lambda. А графік запуску довелося б робити на AWS EventBridge. Та ще все це конфігурувати. Та підтримувати.

А в Ruby вже є готова система задач - Sidekiq. Для неї достатньо додати рядок конфігурації на кшталт cron та графік готовий. Чи значить це що всі заплановані задачі краще робити на Ruby? Знову ні — якби задача була частішою, або обробляла багато даних, то зручність вже не в пріоритеті.

Одним словом, нема такого, що все чітко: тут Ruby, тут Go, нічого не перетинається. Вибір мови стає повсякденним питанням з вагомими наслідками.


03.05.2024

Винятки в мовах програмування

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

В Ruby винятки — це механізм обробки помилок будь-якого рівня — від SyntaxError до специфічної помилки бізнес-логіки. Ну, тобто, за домовленістю на “однозначні” помилки можна просто повернути nil, а все, що не вкладається в nil, утворює виняток. Та, в Ruby дійсно можна спіймати навіть помилку синтаксису або вихід з програми. Зазвичай так роблять не навмисно, а тому, що ловлять пращур всіх помилок: Exception - про це в мене є дуже стара стаття.

В JavaScript якийсь час механізм винятків якось ігнорувався, тобто їх обробку рідко можна було побачити. Не в останню чергу тому, що вони не були сумісними з Promise та рівночасним кодом. Втім, з впровадженням синтаксису async/await блоком обробки винятку try ... catch можна охопити скільки завгодно асинхронної логіки, тому винятки знову повертаються в моду. Втім, в JavaScript немає чітко визначеної класифікації винятків. Навіть в TypeScript неможливо зазначити, якого типу ти очікуєш помилку.

В Go виняток, тобто паніка — це, за посібником, реакція на помилку розробника. Тобто таку, яку виправити може тільки зміна в програмі, а не в даних чи обставинах. Решта помилок повертається з функцій напряму. Фактично в більшості випадків ми просто емулюємо механізм винятків вручну та передаємо помилку назовні.

У Swift функція мусить явно оголосити, що вона створює винятки, модифікатором throws. Відповідно, функції, що її викликають, або обробляють винятки, або успадковують модифікатор. З типом Optional на винятки залишається не так багато потреб — зазвичай це помилки вводу/виводу. Але деякі помилки на кшталт “індекс за межами масиву” вважаються “помилками розробника” та їх спіймати взагалі неможливо - “сам винний!”. Тож паніки в Go це ще дружня поведінка!


02.05.2024

Kafka як барʼєр від стресу

Я вже писав як Kafka розʼєднує сервіси за навантаженням: тобто звільняє джерело подій від турбот про навантаження на споживача. Проте є й інший аспект турбот: надійність. З ненадійними споживачами доводиться перестраховуватись, щоб уникнути каскадних збоїв.

Та ось в чому річ. За останній рік Kafka була, без перебільшень, найбільш стабільним компонентом з тих, з якими мені доводиться працювати. Краще ніж “хмарні” амазонівські сервіси на кшталт SQS та Firehose. Тому Kafka виступає надійним буфером між сервісами, який завжди готовий прийняти події… та притримати їх стільки, скільки це потрібно.

Є декілька обʼєктивних причин такої надійності. В першу чергу, це простота - Kafka нічого не робить з даними. В ній немає схеми — а значить, схему не можна порушити. Скільки разів було таке, що міграції в базі розійшлися з клієнтом, та це зупинило запис? З Кафкою такого ніколи не буде.

Також в Кафки гарний принцип надмірності. Кафка зазвичай побудована кластером. Та клієнти знають не одну вхідну адресу сервера, а багато. При збої одного з серверів клієнти просто переходять на інший. Це стає до нагоди й при оновленнях ПЗ.

Якщо ви стикнулися з ситуацією, де отримувач даних ненадійний, та доводиться вводити повторні спроби та інші механізми захисту — подивіться на Кафку.


01.05.2024

Optional у Swift

Продовжуємо рубрику “аспекти дизайну мов програмування”. Сьогодні не було настрою робити щось по трекеру, тому просто виправляв скарги лінтера. Одна з частих скарг — на так званий “force unwrapping”. Про неї й хочу поговорити.

Відсутність значення у Swift реалізована механізмом Optional. Optional приймає або значення загорнутого типу, або nil. Це єдиний випадок, коли звернення до значення неприпустиме, ніякого більше “порожнього посилання” або “непризначеної змінної”. Та головне, що тип Optional обовʼязково потрібно “розгорнути”. Або безпечною перевіркою, або примусово — що може призвести до фатальної помилки.

Мене здивувало у Swift те, скільки методів в стандартній бібліотеці повертають Optional. Наприклад: методи Array.first, .last, .min, .max. Бо дійсно, якщо масив порожній, то жодних елементів в ньому не буде. (На жаль, операції з індексами позбавлені такої безпеки та можуть призвести до помилки.)

З наївним написанням умов це трохи дратує: я ж перевірив, що масив не порожній!

if !items.isEmpty {
  maxItem = items.max! // Примусово розгортаємо!
  // ... щось зробити з maxItem
}

В багатьох випадках таку непряму та тому небезпечну перевірку можна замінити на явну, безпечну:

if let maxItem = items.max {
  // ... щось зробити з maxItem
}

Можна помітити зворотний порядок гілок: я звик перевіряти if item == nil як особливий випадок, а потім вже працювати з ним; у Swift для того є альтернативна конструкція guard, яка розгортає тип без вкладення:

guard let maxItem = items.max else { return }
// ... щось зробити з maxItem

Взагалі мені дуже подобається такий підхід до відсутності значень, бо він виділяє рівно один механізм та не дозволяє просто його ігнорувати. Проте, якщо в Go надійна програма завжди здобрена перевірками if err != nil, то у Swift так само багато перевірок guard.


30.04.2024

ID в базах Apple

…А наробило мені проблем вчора те, що чомусь в обʼєктів в базах CoreData / SwiftData немає нормальних ID. Тобто, ідентифікатор є — має клас PersistentModelIdentifier. Але він не кодується ані в число, а ні в рядок. Спроба кодувати його в JSON видає ось таке:

{
  "implementation": {
    "isTemporary": false,
    "storeIdentifier": "345F4CA3-CCAE-49D8-8434-EFECA9056B68",
    "primaryKey": "p2640",
    "uriRepresentation": "x-coredata://345F4CA3-CCAE-49D8-8434-EFECA9056B68/Sample/p2640",
    "entityName": "Sample"
  }
}

Так, все це з одного ID. Бачите, теоретично, наприклад, поле urlRepresentation могло б використовуватись як рядкове кодування. Але його не дають повернути назад у PersistentModelIdentifier (або я не знайшов, як.) Ба більше, навіть отримати це значення я не можу — тільки через JSON. Звісно, весь цей JSON разом можна декодувати в ID, але він абсурдно надлишковий.

… В базах Apple ID не дуже потрібний, оскільки вони мають обʼєктну природу. Замість ID ми зберігаємо просто посилання на інші обʼєкти. Та це дійсно дуже круто — бо дозволяє не думати про завантаження асоціацій.

Але — деколи ID все ж потрібні (наприклад, для побудови власних URL чи для експорту.) Що я роблю: генерую для кожного обʼєкта UUID, який зберігається окремим полем. Використовується він тільки для експорту. Чому UUID, які я так не люблю? Бо є ймовірність збігів (якщо застосунок встановлений на двох пристроях); плюс для генерації UUID є стандартний клас. А швидкодія мене мало турбує, бо ці UUID не є первинним ключем.

(PS: якщо придивитись, то ID CoreData теж містить в себе UUID. Але, наскільки я розумію, це UUID пристрою, а локально обʼєкти мають чисельні ідентифікатори.)


29.04.2024

Експорт: JSON або CSV?

Повернуся до теми експорту. Мені був потрібний архівний експорт даних таймтрекера, щоб перейменувати застосунок; бо з боку даних перейменований застосунок це все одно що новий, та я б втратив два місяці записів. Нарешті з цим закінчив; проблем було більше уявних ніж справжніх, та чимало з них випливали з того, що я взяв за формат CSV.

Чому CSV? Насправді перший варіант використовував JSON, але він вийшов заскладний. Тоді я замість того, щоб спростити, звернувся до CSV. Цей формат приваблює своєю максимальною простотою: як для генерації, так і “на вигляд”. От тільки для машинного читання CSV неприємний. В ньому майже немає перевірки на помилки — рідкий документ не є коректним CSV. Немає й типів. Також, якщо дані містять хоч якусь вкладеність, то простота зникає.

Одним словом, я б в CSV відвантажував тільки дані суворо табличного формату там, де їх будуть завантажувати в табличний редактор. Журнал транзакцій — так. Книгу контактів — можливо. Але в загальному випадку JSON простіше для сприйняття навіть користувачами. Якщо вже це такі користувачі, що будуть копирсатися в експортах. До речі, з JSON завжди можна зробити CSV - навіть в утиліти jq для цього є оператор.