Мета заняття: перевірити, що твоя адреса відкривається по HTTPS із дійсним сертифікатом, і зробити сторінку такою, щоб вона швидко відкривалася з телефона на мобільному інтернеті.
Замість <логін> підставляєш свій
логін виду bohdan.g9: ім’я, крапка, номер групи. Кутові дужки не пишуться.
У прикладах на цій сторінці скрізь стоїть bohdan.g9 — читай це як свій логін
і виправляй у кожній команді, яку копіюєш. Кабінет — сторінка, де проти кожного критерію
стоїть відмітка, зарахований він чи ні.
<домен> — назва, яку викладач називає на початку заняття.
Вона вже записана у твоєму файлі ~/app/project.yaml, розділ server,
поле url; пиши її точно так, як там, латинськими літерами. У прикладах на цій
сторінці замість неї скрізь стоїть klub.example. Перший крок практики —
прочитати справжню назву звідти, вигадувати нічого не треба. Якщо в тому полі досі стоїть __DOMAIN__, спитай викладача, перш ніж
рухатися далі: без адреси половина сьогоднішніх команд не спрацює.
Зверни увагу: кабінет поки що відкривається по http за
IP-адресою. Сертифіката на IP-адресу немає — чому так, пояснено в розділі 3 теорії, — тому
пароль від кабінету передається без шифрування. Не став туди пароль, який використовуєш
десь іще. Коли кабінет переїде на адресу з іменем і замком, викладач скаже нову адресу.
Твій сервіс уже має другу адресу — власний сабдомен, який відкривається по HTTPS.
Сьогодні ти перевіриш його з двох боків: подивишся сертифікат у браузері й прочитаєш той
самий сертифікат командою в терміналі, а також переконаєшся, що адреса на http
сама перекидає на https.
Друга частина — швидкість. Ти зміряєш, скільки важить твоя сторінка й за скільки
мілісекунд відповідає API. API — це адреси на твоєму сервері, за якими
програма віддає короткі дані для інших програм; оформленої сторінки для читання за такою
адресою немає. У тебе це /api/ping: за нею служба club-api
відповідає коротким повідомленням про те, що вона працює. Відповідь там маленька, тому на ній
зручно міряти час. Далі стиснеш власні картинки, зміряєш ще раз і запишеш обидва числа
в README.md. Вони лишаться у файлі, і до них можна буде повернутися на наступних
заняттях.
Досі ти відкривав свій сайт за адресою 91.219.61.4. Сьогодні в нього
з’являється друга адреса — словами. Щоб зрозуміти, що між ними спільного, потрібні чотири
поняття.
91.219.61.4 — чотири
групи цифр через крапку. Саме з такими числами працює мережа: у кожному запиті, який
надсилає браузер, стоїть IP-адреса, а не назва.bohdan.g9.klub.example сабдомен — це bohdan.g9. Одному доменові
можна зробити скільки завгодно сабдоменів, і всі вони можуть вести на той самий сервер. Так
зроблено і в нас: у кожного в групі своє ім’я, машина за ними одна.DNS зберігає пари «ім’я — IP-адреса». Список цих пар спільний на весь світ і лежить на окремих серверах; на твоєму комп’ютері його немає, тому по відповідь щоразу звертаються туди.
Порядок такий. Ти набираєш ім’я. Браузер питає в системи: «яка тут IP-адреса». Система, якщо
не пам’ятає відповіді з попереднього разу, питає в DNS-сервера — зазвичай це сервер
провайдера або шкільного роутера. У відповідь приходить 91.219.61.4. І лише
після цього браузер надсилає запит на цю IP-адресу.
Відповідь DNS зберігається на якийсь час — від кількох хвилин до кількох годин. Тому, коли адміністратор змінює запис, зміна доходить до всіх не миттєво: у когось ще лежить попередня відповідь.
Далі запит приходить на сервер, і там його приймає nginx — програма, яка
слухає порти 80 і 443 та вирішує, кому передати запит. Порт — номер, за яким операційна
система розрізняє, якій програмі віддати запит; докладніше про це в наступному розділі. Усі імена вашої групи ведуть на одну
машину, і розрізняє їх nginx за іменем, яке браузер написав у запиті. Тому за адресою
bohdan.g9.… відкривається тека /home/bohdan.g9/www, а не чиясь
чужа.
Богдан відкрив у браузері свій сабдомен — сторінка з’явилася. Потім він набрав
91.219.61.4/u/bohdan.g9/ — і побачив ту саму сторінку. Що з цього випливає?
Замок в адресному рядку з’являється, коли сторінка відкрита по HTTPS. Щоб зрозуміти, що він дає, спершу треба побачити, що відбувається без нього.
club-api — саме така служба.club-api теж має свій порт, і nginx передає запити на нього.Тепер про шкільний Wi-Fi. Між твоїм ноутбуком і сервером стоять точка доступу, шкільний роутер і обладнання провайдера. Коли сторінка відкрита по HTTP, кожна з цих машин бачить повний текст запиту. На занятті 07 ти зробив вхід із паролем — по HTTP цей пароль передається відкритим текстом, і його видно всім, хто має доступ до мережевого обладнання або просто підключив ноутбук до тієї самої відкритої мережі.
Коли сторінка відкрита по HTTPS, збоку видно тільки дві речі: ім’я сервера, з яким ти з’єднався (без нього сервер не зрозуміє, який сертифікат віддавати), і приблизний обсяг переданих даних. Адреса сторінки, заголовки, куки, вміст форми і пароль зашифровані.
Важливо не плутати два різні паролі. Пароль на Wi-Fi закриває доступ до самої мережі й нічого не робить із твоїм трафіком усередині неї. HTTPS шифрує вміст незалежно від того, чи є пароль на мережі й хто ще до неї підключений.
Марта сидить у кав’ярні на відкритому Wi-Fi без пароля, відкриває свій сервіс
по https:// і вводить пароль у форму входу. За сусіднім столиком людина
записує весь трафік цієї мережі. Що вона побачить?
Шифрування саме по собі не доводить, з ким ти розмовляєш. Якщо хтось у мережі відповість замість справжнього сервера, з’єднання теж буде зашифроване — тільки з ним. Тому потрібна друга частина: перевірка імені.
Браузер перевіряє три речі одночасно: ім’я в сертифікаті збігається з тим, що в адресному рядку; сьогоднішня дата всередині строку дії; ланцюжок підписів доходить до кореневого сертифіката зі списку. Якщо не сходиться хоч одне — замка не буде, а замість сторінки з’явиться попередження на весь екран.
Сертифікати Let’s Encrypt діють 90 днів і оновлюються автоматично. Прострочений сертифікат — одна з найчастіших причин, чому в цілком робочого сайту раптом зникає замок.
У нашому випадку сертифікат один на всю групу: він виданий на ім’я із зірочкою —
*.g9.klub.example для дев’ятої групи, *.g13.klub.example для
тринадцятої. Зірочка замінює рівно одну частину імені — ту, що стоїть на її місці, тобто твоє
ім’я перед номером групи. Тому для bohdan.g9.klub.example і
marta.g9.klub.example підходить один і той самий файл. Двох частин одразу зірочка
не замінює: під *.klub.example ім’я bohdan.g9.klub.example уже не
підпадає, бо перед доменом у ньому стоять дві частини, а зірочка покриває тільки одну. Коли на
практиці ти надрукуєш поле
subject свого сертифіката й побачиш там зірочку з номером своєї групи — це саме
той сертифікат.
Кирило набрав https://91.219.61.4/u/kyrylo.g13/ і побачив
попередження: «сертифікат не дійсний для цієї адреси». За іменем сабдомена той самий сайт
відкривається із замком. Чому так?
http сам перекидає на httpsЛюди набирають адресу без https:// — просто ім’я. Багато браузерів у такому
разі звертаються спершу на порт 80, тобто по HTTP. Якщо сервер віддасть там сторінку, її вміст
передається відкритим текстом, і замок не з’явиться, хоч сертифікат і є.
Тому в налаштуваннях nginx на порт 80 стоїть інша відповідь — перенаправлення.
200 OK означає «ось потрібна сторінка», і далі йде її вміст. Код
404 означає, що такого файлу на сервері немає.LocationLocation із новою адресою — браузер
читає його і одразу йде туди. Людина бачить тільки те, що в адресному рядку сама собою
з’явилася адреса з https.Є ще одна річ, яку легко зламати власноруч. Якщо сторінка відкрита по HTTPS, а всередині
неї стоїть посилання на файл по http://, браузер такий файл не завантажить і
напише Mixed Content у консолі — це вікно, куди браузер друкує повідомлення про
помилки на сторінці; відкривається воно клавішею F12, вкладка
Console. Картинки не буде, а замок стане з попередженням або зникне. Найчастіша причина —
повна адреса з цифрами, залишена в коді сторінки з перших занять:
<img src="http://91.219.61.4/u/bohdan.g9/foto.jpg">.
Виправляється це просто: посилання на власні файли пиши відносними —
<img src="foto.jpg">. Тоді браузер сам підставить ту адресу й той протокол,
з якого відкрита сторінка, і файл завантажиться по HTTPS.
Оля відкрила свій сайт по https://. Сторінка з’явилася, текст на
місці, але замість фото — порожній прямокутник, а в консолі (клавіша F12,
вкладка Console) написано Mixed Content. У коді стоїть
<img src="http://91.219.61.4/u/olia.g11/foto.jpg">. Що робити?
Коли ти відкриваєш сайт, браузер завантажує не один файл. Спершу приходить HTML, у ньому знайдені посилання на стилі, скрипти, картинки й шрифти — і кожен із цих файлів завантажується окремим запитом. Розмір сторінки — це сума всього.
Cache-Control каже, скільки цю копію можна тримати. У нас
для картинок, стилів і скриптів стоїть сім днів.Тепер числа. Мобільний інтернет у школі на перерві дає приблизно 2–5 Мбіт/с — це реальна швидкість, коли до тієї самої мережі одночасно підключені кілька десятків людей. Сторінка на 4 МБ за таких умов завантажується близько десяти секунд. Стільки ніхто не чекає: людина закриває вкладку раніше, ніж побачить, що ти зробив.
Майже вся вага — це картинки. Фото з телефона важить 3–6 МБ і має 4000 пікселів завширшки. На сторінці воно показується шириною 800 пікселів, тобто решта даних передається каналом і викидається браузером.
Звідси дві дії, які дають основний результат:
З кешем пов’язана помилка, яка трапляється часто. Якщо ти заміниш файл на сервері,
лишивши ту саму назву, людина, яка заходила вчора, ще сім днів бачитиме стару копію: браузер
навіть не питатиме сервер, чи файл змінився. Тому, коли міняєш картинку, міняй і її ім’я —
foto-2.webp замість foto.webp. Собі для перевірки достатньо
натиснути Ctrl+Shift+R
(на Mac Cmd+Shift+R):
це оновлення сторінки з повторним завантаженням усіх файлів, повз кеш.
Богдан стиснув foto.jpg і поклав на сервер новий файл із тією
самою назвою. У себе він бачить нову картинку — оновив сторінку через
Ctrl+Shift+R.
Друг, який заходив учора, відкриває сторінку й бачить стару. Чому?
Числа «до» і «після» щось означають лише тоді, коли обидва зміряні однаково. Тут потрібні три поняття.
Один вимір не означає нічого. Перший запит завжди довший за наступні: у ньому браузер ще встановлює з’єднання й перевіряє сертифікат. Тому міряють десять разів і беруть медіану.
Різницю між медіаною і середнім арифметичним видно на оцінках. Десять оцінок: дев’ять по 9–10 і одна двійка за пропущену роботу. Середнє арифметичне через цю двійку стане помітно меншим, а медіана дорівнюватиме дев’яти — і саме це число правильно описує, як ти зазвичай пишеш.
Друга умова — звідки міряти. 127.0.0.1 — це адреса, за якою машина
звертається сама до себе. Такий запит не виходить за межі сервера: він не проходить каналом до
відвідувача, не йде через шкільний Wi-Fi і обладнання провайдера, а на порту 80 ще й не
шифрується. Число вийде маленьке, але воно не покаже, скільки чекає людина з телефоном. Тому
автоперевірка міряє з сервера, але через публічну адресу — так само, як це робить браузер
відвідувача.
Артем зміряв час відповіді десять разів і отримав: 180, 190, 175, 900, 185,
178, 195, 182, 188, 176 мс. У README.md він написав «у середньому 255 мс».
Що тут не так?
Кроки 1–4 і 6–11 робляться в SSH-сесії на сервері. Крок 5 — у браузері; у кроці 8 сторінку теж доведеться відкрити в браузері й перевірити картинки. Якщо картинок у теці зовсім немає, знадобиться ще й домашній комп’ютер: файл кладуть на сервер по FTP саме з нього.
Налаштування nginx, тобто стиснення й кеш, вмикає адміністратор: це дії від імені
root — головного користувача сервера, якому дозволено змінювати системні
налаштування. В учнівських облікових записів таких прав немає, і це нормально. Твоя частина —
перевірити результат і зменшити те, що залежить від тебе.
ssh bohdan.g9@91.219.61.4
grep -A 2 '^server:' ~/app/project.yamlgrep -A 2 друкує знайдений рядок і два наступні за ним. Побачиш приблизно
таке:
server:
subdomain: bohdan.g9
url: "https://bohdan.g9.klub.example/"Якщо замість імені домену стоїть __DOMAIN__, далі не йди — спитай викладача
адресу й впиши її у project.yaml. Автоперевірка бере адресу саме звідси.
Тепер збережемо адресу у змінну, щоб не набирати її двадцять разів:
MY=$(grep -A 5 '^server:' ~/app/project.yaml | grep -m 1 'url:' | grep -o 'https://[^"]*' | sed 's:/*$::')
echo "$MY"Розберімо рядок по частинах. Перший grep бере рядок server: і
п’ять наступних за ним — тобто саме той розділ файлу, у якому лежить адреса. Другий бере з
них перший рядок зі словом url: (-m 1 означає «зупинись на першому
збігу»). Третій друкує не весь рядок, а лише ту частину, що збіглася з шаблоном:
https:// і все до першої лапки. Так у змінну потрапляє саме поле
url з розділу server, а не якесь інше посилання, що може стояти в
файлі вище. sed 's:/*$::' прибирає косі риски в кінці, щоб далі писати
$MY/api/ping і не отримати дві риски підряд. Усе разом у $(…)
означає «виконай це і поклади вивід у змінну».
echo має надрукувати твою адресу без косої риски в кінці. Порожній рядок
означає, що в project.yaml немає адреси, — вернись на початок кроку.
$MY буде порожня. Просто виконай ту саму
команду ще раз.getent hosts "${MY#https://}"${MY#https://} — це та сама змінна, але без початку https://:
решітка означає «прибери з початку рядка ось це». Виходить чисте ім’я, а саме його й хоче
getent.
getent hosts питає в системи IP-адресу для імені — тим самим шляхом, яким
це робить браузер. У відповідь має прийти рядок:
91.219.61.4 bohdan.g9.klub.exampleТи щойно побачив те, про що йшлося в теорії: ім’я і число вказують на ту саму машину — на ту, у якій ти зараз сидиш по SSH.
Якщо у відповідь порожньо, DNS цього імені ще не знає. Скажи викладачеві й поки що працюй далі за IP-адресою — решта вимірів від імені не залежить.
curl -sI "http://${MY#https://}/" | head -n 5curl — програма, яка робить той самий запит, що й браузер, і друкує
відповідь у термінал. -s прибирає смужку прогресу, -I просить
лише заголовки, без вмісту сторінки. Зверни увагу: адреса тут навмисно починається з
http://, тобто запит іде на порт 80.
Очікувана відповідь:
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://bohdan.g9.klub.example/Це і є те перенаправлення з теорії: код 301 і заголовок Location з новою
адресою. Якщо тут 200 OK, сервер віддав сторінку по відкритому HTTP —
критерій L10.3 не зарахується, скажи викладачеві.
А тепер пройдемо весь шлях так, як його проходить браузер:
curl -sIL "http://${MY#https://}/" | grep -E '^HTTP|^[Ll]ocation'-L означає «іди за перенаправленнями». grep -E лишає тільки
рядки з кодом відповіді й адресою переходу. Побачиш два кроки: спершу 301 із
Location, потім 200 уже по HTTPS.
openssl s_client -connect "${MY#https://}:443" -servername "${MY#https://}" </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -datesЩо тут відбувається. openssl s_client відкриває з’єднання на порт 443 і
друкує все, що сервер надіслав, зокрема сам сертифікат. -servername повідомляє
ім’я, яке ми питаємо, — без нього сервер не зрозуміє, чий сертифікат віддавати, бо імен на
машині багато. </dev/null означає «нічого не вводитиму з клавіатури,
закривай з’єднання одразу», інакше команда чекатиме на введення.
2>/dev/null ховає службові повідомлення. Далі текст іде до
openssl x509, який дістає з нього три речі: subject — для якого
імені виданий, issuer — хто підписав, dates — з якого й до якого
числа діє.
Приблизна відповідь:
subject=CN = *.g9.klub.example
issuer=C = US, O = Let's Encrypt, CN = R11
notBefore=Aug 14 07:22:10 2026 GMT
notAfter=Nov 12 07:22:09 2026 GMTЗірочка в subject — той самий спільний сертифікат на всю групу, а після неї
стоїть номер твоєї групи. У issuer замість R11 цілком може бути
R10, R12 чи інша схожа назва: Let’s Encrypt час від часу міняє свої
проміжні сертифікати, і це не ознака помилки. notAfter — дата, після якої
браузери почнуть показувати попередження; до неї сертифікат оновиться сам.
Тепер подивись увесь ланцюжок:
openssl s_client -connect "${MY#https://}:443" -servername "${MY#https://}" </dev/null 2>/dev/null \
| sed -n '/Certificate chain/,/^---/p'sed -n '/від/,/до/p' друкує шматок виводу між двома рядками. Побачиш пари
рядків: s: — чий це сертифікат (subject), i: — хто його підписав
(issuer). Рядок i: одного сертифіката збігається з рядком s:
наступного — так і виглядає ланцюжок. Останню ланку, кореневий сертифікат, сервер не
надсилає: вона вже є в браузері.
Відкрий свою адресу з https:// у браузері й натисни на значок замка зліва
від адреси.
У вікні сертифіката знадобляться три поля. Common Name (скорочено CN,
українською «загальна назва») — основне ім’я, для якого виданий сертифікат; це те саме, що
subject у виводі команди. Subject Alternative Name (SAN,
«додаткові імена») — список усіх імен, які цей сертифікат покриває; саме цей список
перевіряють сучасні браузери. Issuer — хто сертифікат підписав.
Знайди три речі й порівняй їх із тим, що надрукувала команда з кроку 4:
Значення мають збігтися: браузер і openssl дивляться на той самий файл.
Різниця лише в тому, що браузер робить це щоразу, коли ти відкриваєш сторінку, і мовчки.
Створи теку для маленьких допоміжних програм і файл у ній:
mkdir -p ~/app/tools
nano ~/app/tools/page-weight.pyВстав текст цілком:
#!/usr/bin/env python3
"""Скільки важить сторінка: сам HTML плюс файли, які він тягне за собою."""
import re
import sys
import urllib.request
from urllib.parse import urljoin
if len(sys.argv) < 2:
sys.exit("Вкажи адресу: page-weight.py https://bohdan.g9.klub.example/")
url = sys.argv[1]
def download(link):
request = urllib.request.Request(link, headers={"User-Agent": "page-weight"})
with urllib.request.urlopen(request, timeout=15) as answer:
return answer.read()
page = download(url)
text = page.decode("utf-8", "replace")
total = len(page)
print(f"{len(page):>9} Б сама сторінка")
files = re.findall(r'<(?:img|script|source)[^>]+src=["\']([^"\']+)', text, re.I)
files += re.findall(r'<link[^>]+href=["\']([^"\']+)', text, re.I)
seen = set()
for item in files:
if item.startswith(("data:", "#", "javascript:")):
continue
full = urljoin(url, item)
if full in seen:
continue
seen.add(full)
try:
size = len(download(full))
except Exception as error:
print(f"{'?':>9} {item} ({error})")
continue
total += size
print(f"{size:>9} Б {item}")
print("-" * 44)
print(f"{total:>9} Б разом = {total / 1024:.0f} КБ")Програма робить те саме, що браузер: завантажує сторінку, шукає в її тексті посилання на
картинки, скрипти й стилі, завантажує кожен файл і додає розміри. urljoin
добудовує повну адресу з відносного посилання, а seen не дає порахувати той
самий файл двічі. Картинки, які підставляються з CSS, вона не побачить — це чесне
обмеження, і про нього варто пам’ятати, якщо числа розійдуться з тим, що покаже браузер.
Запусти:
/opt/club/venv/bin/python ~/app/tools/page-weight.py "$MY/"На початку команди стоїть повний шлях /opt/club/venv/bin/python — це Python зі
спільного оточення. Оточення (англійською virtual environment, скорочено
venv) — окрема тека, у якій лежать Python і встановлені для нього бібліотеки. На нашому сервері таке оточення одне на
всіх і лежить у /opt/club/venv; бібліотеки в нього ставить адміністратор. Тому
шлях до Python пишемо повністю: у системному python3 потрібних бібліотек
немає.
Запиши число «разом» — це твоє «до». Найзручніше одразу вписати його в
чернетку в ~/app/README.md, бо через двадцять хвилин ти його не згадаєш.
Заразом подивись, що дає стиснення:
curl -s -o /dev/null -w 'без стиснення: %{size_download} Б\n' "$MY/"
curl -s --compressed -o /dev/null -w 'зі стисненням: %{size_download} Б\n' "$MY/"-o /dev/null означає «вміст нікуди не зберігай», -w — «надрукуй
ось таке після завершення», а %{size_download} — це скільки байтів сервер
реально надіслав. --compressed додає до запиту заголовок
Accept-Encoding: gzip, тобто повідомляє серверу, що браузер уміє розпаковувати.
Друге число має бути в кілька разів менше. Обидва теж запиши.
export LC_ALL=C
for i in $(seq 10); do curl -s -o /dev/null -w '%{time_total}\n' "$MY/api/ping"; done \
| sort -n | awk 'NR==5 || NR==6 {s+=$1} END {printf "медіана: %.0f мс\n", s/2*1000}'Розбір: seq 10 дає числа від 1 до 10, тож цикл робить десять запитів.
%{time_total} — повний час запиту в секундах. sort -n впорядковує
числа за зростанням. awk бере п’ятий і шостий рядки, рахує їхнє середнє й
переводить секунди в мілісекунди. Це та сама медіана з теорії.
Рядок export LC_ALL=C потрібен через десятковий роздільник. Якщо на сервері
стоять українські мовні налаштування, curl надрукує 0,182 з комою, а
sort і awk чекають на крапку й дадуть неправильний результат.
LC_ALL=C вмикає для команд просту англійську локаль із крапкою. Виконати цей
рядок досить один раз за сесію.
Запиши число. Для критерію L10.2 воно має бути меншим за
300 мс.
Якщо цікаво, на що саме йде час, є докладніший вимір:
curl -s -o /dev/null "$MY/api/ping" \
-w 'DNS %{time_namelookup} з’єднання %{time_connect} шифрування %{time_appconnect} перший байт %{time_starttransfer} разом %{time_total}\n'Числа йдуть наростаючим підсумком, у секундах. Різниця між time_connect і
time_appconnect — це узгодження шифрування й перевірка сертифіката; різниця між
time_appconnect і time_starttransfer — час, який твоя програма
витратила на відповідь. Так одразу видно, де саме затримка: у мережі чи в коді.
Спершу подивись, що взагалі лежить у теці сайту й скільки важить:
ls -lhS ~/www
du -sh ~/www-S сортує за розміром, найбільші зверху; -h друкує розміри в
зрозумілих одиницях. du -sh показує вагу всієї теки.
91.219.61.4, порт 21, шифрування «явний FTP
через TLS», логін і пароль ті самі, що для SSH. TLS — той самий спосіб шифрування, на якому
тримається HTTPS; без нього логін і пароль передаватимуться відкритим текстом. Числа «до» і
«після» тобі все одно потрібні, а перевіряти стиснення на порожній теці нема на чому.Перевір, що бібліотека для роботи з картинками є у спільному оточенні:
/opt/club/venv/bin/python -c "import PIL; print('Pillow', PIL.__version__)"Pillow — бібліотека Python, яка вміє відкривати картинки, змінювати
їхній розмір і зберігати в іншому форматі. Якщо у відповідь
ModuleNotFoundError, скажи викладачеві: бібліотеки в спільне оточення ставить
адміністратор, самому нічого встановлювати не треба.
Тепер сам скрипт:
nano ~/app/tools/optimize-images.py#!/usr/bin/env python3
"""Зменшує картинки в ~/www і зберігає копію у форматі WebP.
Оригінали лишаються на місці: скрипт їх не чіпає.
"""
from pathlib import Path
from PIL import Image
MAX_WIDTH = 1200 # ширини понад цю на сторінці все одно не видно
QUALITY = 80 # 80 — межа, після якої різниці для ока майже немає
FOLDER = Path.home() / "www"
for source in sorted(FOLDER.rglob("*")):
if source.suffix.lower() not in (".jpg", ".jpeg", ".png"):
continue
rel = str(source.relative_to(FOLDER))
was = source.stat().st_size
picture = Image.open(source)
picture = picture.convert("RGBA" if picture.mode in ("P", "LA", "RGBA") else "RGB")
if picture.width > MAX_WIDTH:
height = round(picture.height * MAX_WIDTH / picture.width)
picture = picture.resize((MAX_WIDTH, height), Image.LANCZOS)
result = source.with_suffix(".webp")
if result.exists():
print(f"{rel:<26} пропущено: {result.name} уже є")
continue
picture.save(result, "WEBP", quality=QUALITY, method=6)
became = result.stat().st_size
if became >= was:
result.unlink()
print(f"{rel:<26} {was // 1024:>6} КБ -> WebP вийшов не меншим, лишаю оригінал")
continue
print(f"{rel:<26} {was // 1024:>6} КБ -> "
f"{result.name:<26} {became // 1024:>6} КБ "
f"{picture.width}×{picture.height} пікселів")Рядок з convert приводить картинку до формату кольорів, який WebP точно
збереже: з прозорістю — RGBA, без прозорості — RGB.
rglob("*") обходить теку разом із підтеками, тому в списку можуть з’явитися й
файли з підтек. Щоб було видно, звідки кожен файл, у лівому стовпці друкується шлях від
~/www: foto.jpg або foto/dvir.jpg. resize зменшує зображення,
зберігаючи пропорції: висота рахується з тієї самої пропорції, що й ширина.
Дві перевірки в скрипті потрібні через те, що WebP не завжди виграє. Якщо поруч лежать
foto.jpg і foto.png, обидва дали б один файл foto.webp,
і другий мовчки затер би перший — тому скрипт пропускає картинку, для якої такий
.webp уже є, і пише про це. А маленькі PNG у WebP часто виходять більшими за
оригінал; у такому разі скрипт видаляє щойно створений файл і лишає оригінал. Якщо треба
перезібрати картинку заново, спершу видали старий .webp руками.
Останні числа в рядку — ширина й висота готової картинки в пікселях. Вони знадобляться в розмітці, тому не закривай вивід одразу.
/opt/club/venv/bin/python ~/app/tools/optimize-images.pyСкрипт надрукує по рядку на картинку: скільки було й скільки стало. Тепер треба, щоб сторінка брала нові файли. Спершу копія на випадок помилки, потім правка:
mkdir -p ~/backups
cp ~/www/index.html ~/backups/index-$(date +%F).html
nano ~/www/index.htmlmkdir -p створює теку, а якщо вона вже є — нічого не робить і не свариться.
Без цього рядка cp завершиться помилкою No such file or directory, і
копії не буде.
У кожному <img> заміни розширення на .webp і перевір, що
посилання відносне:
<img src="foto.webp" width="1200" height="800" alt="Головна сторінка мого сервісу">width і height — це справжні розміри картинки в пікселях. Візьми
ті два числа, які скрипт надрукував для цього файлу; 1200 і 800 тут
стоять просто як приклад. Браузер із цих чисел рахує пропорції ще до того, як картинка
завантажилася, і одразу лишає під неї місце потрібної висоти — тому текст під картинкою не
стрибає. Якщо вписати числа навмання, браузер лишить місце неправильної висоти, і текст усе
одно стрибне.
Щоб на вузькому екрані картинка не виходила за межі сторінки, у стилях має бути таке правило:
img { max-width: 100%; height: auto; }Без нього width="1200" задає ширину на екрані рівно 1200 пікселів, і на
телефоні сторінку доведеться прокручувати вбік. З ним картинка звужується до ширини екрана, а
height: auto тримає пропорції. Перевір, чи є це правило у <style>
твого index.html; якщо немає — додай.
alt — текст, який побачить людина, якщо картинка не завантажилася, і який
прочитає програма озвучування; його ти вже додавав на занятті 06.
Відкрий сторінку в браузері з Ctrl+Shift+R і переконайся, що картинки на місці. І лише після цього прибери старі файли:
rm ~/www/foto.jpgfoto.jpg тут — приклад. Видаляй ті файли, які скрипт справді перетворив: їхні
шляхи стоять у лівому стовпці його виводу. Якщо картинка лежала в підтеці, пиши шлях разом із
нею — rm ~/www/foto/dvir.jpg. Файли, для яких скрипт написав «лишаю оригінал»,
видаляти не можна: .webp для них немає. Файли з позначкою «пропущено» теж лиши на
місці — .webp із таким іменем зроблено з іншої картинки.
Спершу перевірка, потім видалення — у зворотному порядку легко лишитися без картинок зовсім.
Ті самі команди, що в кроках 6 і 7: умови виміру мають бути однакові, інакше числа «до» і «після» не порівняти. Вимірів чотири — вага сторінки, розмір без стиснення, розмір зі стисненням і медіана часу відповіді.
export LC_ALL=C
/opt/club/venv/bin/python ~/app/tools/page-weight.py "$MY/"
curl -s -o /dev/null -w 'без стиснення: %{size_download} Б\n' "$MY/"
curl -s --compressed -o /dev/null -w 'зі стисненням: %{size_download} Б\n' "$MY/"
for i in $(seq 10); do curl -s -o /dev/null -w '%{time_total}\n' "$MY/api/ping"; done \
| sort -n | awk 'NR==5 || NR==6 {s+=$1} END {printf "медіана: %.0f мс\n", s/2*1000}'Заразом перевір, що кеш на статику справді ввімкнений. Ім’я файлу підстав своє — те, яке
тепер стоїть у index.html; якщо картинка лежить у підтеці, додай і її:
"$MY/foto/dvir.webp".
curl -sI "$MY/foto.webp" | grep -i -E 'cache-control|expires|content-type'grep -i шукає без огляду на великі й малі літери. Має бути рядок
cache-control: max-age=604800 — 604800 секунд, тобто рівно сім днів, — і
expires із датою приблизно за тиждень. Це те, що вмикав адміністратор; тепер ти
бачиш ці заголовки у відповіді сервера.
Вага сторінки має стати помітно меншою. Якщо вона майже не змінилася, вернись до кроку 8:
найчастіше в index.html лишилося старе посилання на .jpg, і сторінка
завантажує обидва файли.
Час відповіді /api/ping сьогодні змінюватися й не повинен: картинки та розмітка
на роботу служби не впливають. Якщо в цьому рядку числа «до» і «після» вийшли майже однакові —
це правильний результат, так і запиши.
nano ~/app/README.mdЗнайди розділ ## Швидкість роботи — у заготовці там стоїть рядок курсивом
«Заняття 10: сюди записати числа…». Прибери цей рядок і встав на його місце свої
результати. Ось зразок — числа підстав свої:
## Швидкість роботи
Виміряно 6 вересня 2026, заняття 10. Адреса: https://bohdan.g9.klub.example/
| Що міряв | До | Після |
|---|---|---|
| Сторінка з усіма файлами | 1840 КБ | 214 КБ |
| `/api/ping`, медіана з 10 запитів | 178 мс | 176 мс |
Головна сторінка без стиснення — 34 КБ, зі стисненням gzip — 9 КБ.
Що зробив: зменшив три фото до 1200 пікселів завширшки, зберіг у WebP і
замінив посилання в `index.html`. Стиснення gzip і кеш на статику ввімкнув
адміністратор у налаштуваннях nginx. Час відповіді `/api/ping` майже не
змінився: сьогоднішні правки стосувалися картинок, а не коду служби.Одиниці пиши обов’язково — «214» без «КБ» нічого не означає. Пиши те, що справді надрукували команди: якщо якесь число майже не змінилося, так і запиши, це нормальний результат виміру.
Репозиторій — тека, за якою стежить git: він запам’ятовує кожну збережену
версію файлів у ній. У тебе це ~/app. Тека ~/www в репозиторій не
входить, тому копію сторінки в ~/app оновлюємо руками — так само, як на занятті
06:
cp ~/www/index.html ~/app/www-index.html
cd ~/app
git add README.md www-index.html tools/
git status --shortКоміт — одна збережена версія файлів разом із підписом, що саме
змінилося. git status --short покаже список того, що в цей коміт увійде.
Переконайся, що в ньому немає нічого зайвого — картинок із ~/www там бути не
повинно, вони не в репозиторії.
git commit -m "Швидкість: WebP замість JPEG, числа до і після в README"
git push origin masterКод служби сьогодні не змінювався, тому перезапускати club-api й запускати
deploy.sh не потрібно. Якщо все ж запустиш — нічого не зламається, просто
зміни в статиці від цього не залежать.
Чотири критерії заняття 10, усі чотири перевіряє програма. Перевірка запускається за
розкладом, тому відмітка в кабінеті з’являється не в ту саму секунду, коли ти виконав
команду. Публічну адресу програма бере з ~/app/project.yaml, розділ
server, поле url — якщо там неправильна адреса, не зарахується
нічого.
| Код | Критерій | Що це означає | Не зараховано — що робити |
|---|---|---|---|
| L10.1 | сторінка важить менше 500 КБ | Програма завантажує твою головну сторінку разом із картинками, стилями й скриптами,
на які вона посилається, і додає їхні розміри. Сума має бути меншою за 500 КБ.
Це приблизно та сама перевірка, яку ти робив скриптом
page-weight.py. |
Запусти page-weight.py і подивись на список: у ньому одразу видно
найважчий файл. Майже завжди це картинка, яку не стиснули або яка лишилася в
.jpg, бо посилання в index.html не оновили. Перевір, що в
сторінці немає посилань на старі файли:
grep -o 'src="[^"]*"' ~/www/index.html. Якщо найважчий файл — не картинка,
а сама сторінка, подивись, чи не вставлено в неї картинку прямо в текст. Таку вставку видно
по тому, що в src замість адреси файлу стоїть довгий рядок, який починається
з data:image: це закодована сама картинка, і сторінка важчає рівно на її
розмір. |
| L10.2 | API відповідає швидше ніж за 300 мс | Програма робить десять запитів на /api/ping з сервера, але через
публічну адресу — тобто через nginx і шифрування, як це робить браузер. З десяти
результатів береться медіана, і вона має бути меншою за 300 мс. |
Повтори вимір із кроку 7 і подивись докладний розклад часу (друга команда того ж
кроку). Якщо велика різниця між time_appconnect і
time_starttransfer, час іде в твоєму коді: подивись, чи не читає
/api/ping базу і чи не лишилося в ньому print із великим
виводом. Якщо код відповідає швидко, а загальний час великий, перевір, чи не
зупиняється служба й не запускається знову без кінця. Це видно в журналі — списку рядків,
які служба пише про свою роботу; читає його команда
journalctl --user -u club-api -n 30, де -u вказує службу, а
-n 30 — скільки останніх рядків показати. |
| L10.3 | сертифікат валідний, http веде на https | Програма перевіряє дві речі: що адреса на http відповідає кодом 301 із
заголовком Location на https, і що сертифікат на порту 443
дійсний — виданий на це ім’я, не прострочений, підписаний відомим центром
сертифікації. Налаштування робить адміністратор, твоя частина — правильна адреса в
project.yaml і перевірка. |
Повтори кроки 3 і 4 практики. Якщо на http приходить 200 замість 301
або сертифікат прострочений — це на боці сервера, скажи викладачеві. Якщо
openssl пише, що ім’я в сертифікаті не збігається, дивись, що записано в
полі url: там має бути твій сабдомен, а не IP-адреса й не чужий логін. |
| L10.4 | у README є цифри «до» і «після» | Програма читає ~/app/README.md і шукає в розділі про швидкість числа
«до» і «після» — розмір сторінки й час відповіді. Заготовковий рядок курсивом має бути
прибраний: поки він на місці, критерій не зараховується. |
Перевір, що в файлі: grep -A 12 'Швидкість роботи' ~/app/README.md.
Числа мають бути з одиницями («214 КБ», «176 мс»), обидва стовпці заповнені, а рядка
«Заняття 10: сюди записати числа…» уже немає. Якщо все на місці, а критерій не
зараховано — перевір, що ти зробив коміт і git push origin master. |
Основне. Покажи сервіс за новою адресою трьом людям, які його ще не бачили: комусь із рідних, другові поза групою, комусь із учителів. Дай посилання й нічого не пояснюй наперед — просто попроси спробувати. Не підказуй, поки людина дивиться на екран: щойно ти скажеш, куди натиснути, ти вже не побачиш, де вона спинилася б сама.
Записуй два види зауважень: чого людина не зрозуміла (де зупинилася, що шукала) і що
зламалося (де сторінка повелася не так). Створи файл ~/app/FEEDBACK.md:
# Що сказали люди
## 1. Сестра, телефон, мобільний інтернет
Пробувала: додати запис і подивитися список.
Зупинилася на: не зрозуміла, що робить кнопка «Зберегти» — думала, що вона надсилає щось мені.
Сказала дослівно: «а це кудись відправиться?»
Що з цим робити: перейменувати кнопку на «Додати до списку».
## 2. Дмитро з паралельного класу, ноутбук, шкільний Wi-Fi
Пробував: ...
Зупинився на: ...
Сказав дослівно: ...
Що з цим робити: ...Рядок «сказав дослівно» — найважливіший. Свій переказ ти через тиждень прочитаєш як «і так усе зрозуміло», а чужа фраза лишиться такою, як була. З цього файлу складеться список правок перед показом сервісу всім на занятті 12.
cd ~/app
git add FEEDBACK.md
git commit -m "FEEDBACK: зауваження трьох людей після показу сервісу"
git push origin masterБез git push файл лишиться тільки на сервері, і викладач його не побачить.
Друге. Відкрий свою адресу з телефона, вимкнувши Wi-Fi, — саме на
мобільному інтернеті. Заміряй секундоміром, скільки минає від натискання посилання до
моменту, коли сторінка повністю з’явилася. Додай це число окремим рядком у розділ
«Швидкість роботи» в README.md, з позначкою, що вимір із телефона: числа з
сервера й числа з телефона відрізняються, і обидва варто мати. Після правки README теж
збережи й надішли:
cd ~/app
git add README.md
git commit -m "README: час завантаження з телефона на мобільному інтернеті"
git push origin masterКритерій L10.4 дивиться на README, який лежить у надісланому репозиторії, тому
git push тут обов’язковий.
Спершу перевір, чи знає це ім’я DNS:
getent hosts bohdan.g9.klub.exampleПорожня відповідь означає, що запис ще не з’явився або ще не дійшов до цієї мережі. Друга
часта причина — одрук: зайва крапка, пропущена частина логіна, переплутані схожі літери в назві
домену. Порівняй те, що набрав, із полем url у project.yaml
посимвольно.
Якщо на сервері getent знаходить ім’я, а на телефоні сторінка не
відкривається, річ у мережі телефона: шкільний Wi-Fi має свій DNS-сервер, і новий запис міг
до нього ще не дійти. Перевір із мобільного інтернету — якщо там працює, лишається просто
зачекати.
Прочитай, що саме написано в попередженні: причина завжди вказана дрібним текстом.
ERR_CERT_COMMON_NAME_INVALID або «сертифікат не дійсний для цієї адреси» —
ім’я в адресному рядку не збігається з іменем у сертифікаті. Найчастіше це спроба відкрити
по https адресу з цифрами 91.219.61.4: за IP-адресою сертифіката
немає, і так і має бути. Відкривай за іменем.ERR_CERT_DATE_INVALID — або сертифікат прострочений, або на твоєму
комп’ютері збився годинник. Перевір спершу дату в себе: браузер порівнює строк дії саме з
нею. Якщо дата правильна, скажи викладачеві.ERR_CERT_AUTHORITY_INVALID — браузер не довіряє тому, хто підписав
сертифікат. У шкільних мережах таке буває, коли трафік проходить через обладнання, яке
підміняє сертифікати. Перевір із мобільного інтернету: якщо там замок нормальний, річ у
мережі, а не в твоєму сервері.У будь-якому разі не тисни «продовжити попри попередження» — спершу розберись, чому воно з’явилося.
Відкрий F12, вкладку Console. Рядок зі словами
Mixed Content означає, що сторінка відкрита по HTTPS, а файл у ній запитується
по http://, і браузер його заблокував. Знайди винні рядки:
grep -n 'http://' ~/www/index.html-n друкує номери рядків. Кожне посилання на власний файл заміни на відносне:
src="foto.webp" без адреси на початку. Посилання на чужі сайти теж краще писати
з https://, інакше буде те саме попередження.
Перевір по черзі три речі:
ls -lhS ~/www. Якщо поруч із
foto.jpg лежить foto.webp майже такого ж розміру, картинка,
найімовірніше, вже була невеликою — тоді основна вага деінде, дивись увесь список.grep -o 'src="[^"]*"' ~/www/index.html. Якщо там ще .jpg,
сторінка тягне старі картинки, скільки їх не стискай.foto.jpg лежить у
~/www і згадується в розмітці, вага сторінки включає і його, і новий
.webp.Якщо скрипт узагалі завершився помилкою ModuleNotFoundError: No module named 'PIL' —
бібліотеки в спільне оточення /opt/club/venv ставить адміністратор. Скажи
викладачеві, самому нічого встановлювати не треба.
Спочатку з’ясуй, де саме зникає час. Порівняй два виміри — через публічну адресу і прямо до служби:
PORT=$(grep -o -- '--port [0-9]*' ~/.config/systemd/user/club-api.service | grep -o '[0-9]*')
curl -s -o /dev/null -w 'публічна адреса: %{time_total} с\n' "$MY/api/ping"
curl -s -o /dev/null -w 'прямо до служби: %{time_total} с\n' "http://127.0.0.1:$PORT/api/ping"Перша команда дістає номер порту з файлу служби. Служби на сервері запускає й доглядає
systemd — частина операційної системи, яка стежить, щоб потрібні програми
працювали й запускалися знову після перезавантаження сервера. На кожну службу в неї є файл із налаштуваннями;
у твоєї це ~/.config/systemd/user/club-api.service. У цьому файлі записано, якою
саме командою запускати club-api, а в тій команді стоїть і номер порту — звідти
grep його й дістає. Правити цей файл сьогодні не треба.
Якщо друге число теж велике, затримка в
твоєму коді: подивись, чи не відкриває /api/ping базу на кожен запит, чи не
робить він важкого запиту без потреби і чи не лишилося в ньому print, який
друкує великий список у журнал. Якщо друге число маленьке, а перше велике, справа не в коді —
покажи обидва числа викладачеві.
Окремо перевір, що служба не перезапускається постійно:
systemctl --user status club-api. Рядок active (running) і час
роботи в кілька годин — усе гаразд. Якщо служба запустилася секунду тому, подивись останні
тридцять рядків її журналу: journalctl --user -u club-api -n 30. Там буде видно,
через що вона зупинилася минулого разу. Перший запит після запуску завжди найповільніший.