Кратко о себе: Web-разработчик. Пишу на PHP, Python, JavaScript. Знаю Ruby и Go, со студенческих времён более-менее помню C и asm. Специализируюсь на ускорении загрузки сайтов и разработке ботов для Telegram. Linuxоид (использую Debian+LXDE). Сторонник IndieWeb, slow lifer.
Часто при разработке ботов для Telegram требуется учитывать информацию о часовом поясе пользователя. К сожалению, получить её через Bot API невозможно, поэтому предусматривать в боте команду, с помощью которой пользователь сможет указать, какой часовой пояс он использует. И тут возникает вопрос, как лучше это сделать, чтобы пользователю было удобно. Если бот ориентирован только на российских пользователей, вопрос решается просто: выводим список всех часовых поясов России, пользователь выбирает нужный, и бот сохраняет выбор в базу данных. Но как быть, если пользователи могут быть из других стран? Выводить огромный список из более чем сотни часовых поясов — крайне неудобно. Требовать от пользователя вручную написать название его часового пояса в формате Europe/Moscow или хотя бы MSK — тоже не самое лучшее решение. Можно предложить указать смещение в формате ±ЧЧ:ММ, но тогда нельзя будет учесть переход на летнее время.
В поисках более удобного решения я наткнулся на модули tzwhere и geopy. Первый позволяет определить часовой пояс для того или иного местоположения, заданного с помощью координат. Второй — получить эти самые координаты по названию города, причём позволяет это делать через Google Maps API, API Яндекс.Карт и ещё почти десяток сервисов. Я решил использовать Nominatim (это сервис Open Street Maps), так как он не требует получения tokenа, в отличие от Яндекс и Google, и при этом понимает названия городов и на английском языке, и на русском. В итоге код определения часового пояса выглядит примерно так:
import geopy
from tzwhere import tzwhere
import datetime
import pytz
def get_timezone(bot,chat_id,city):
geo = geopy.geocoders.Nominatim(user_agent="SuperMon_Bot")
location = geo.geocode(city) # преобразуе
if location is None:
bot.send_message(chat_id,"Не удалось найти такой город. Попробуйте написать его название латиницей или указать более крупный город поблизости.")
else:
tzw = tzwhere.tzwhere()
timezone_str = tzw.tzNameAt(location.latitude,location.longitude) # получаем название часового пояса
tz = pytz.timezone(timezone_str)
tz_info = datetime.datetime.now(tz=tz).strftime("%z") # получаем смещение часового пояса
tz_info = tz_info[0:3]+":"+tz_info[3:] # приводим к формату ±ЧЧ:ММ
bot.send_message(chat_id,"Часовой пояс установлен в %s (%s от GMT)." % (timezone_str,tz_info))
# здесь должно быть сохранение выбранной строки в БД
return timezone_str
Приведение даты к нужному часовому поясу перед выводом пользователю делается либо с помощью метода tz.localize(dt) (для так называемых naive date, то есть не содержащих информацию о часовом поясе), либо с помощью метода dt.astimezone(tz), где dt — объект класса datetime, а tz — timezone.
Часто при поисковой оптимизации сайта возникает задача избавиться от URL, кончающихся на ?, например, http://4xpro/profblog/?. С точки зрения поисковых систем такие адреса воспринимаются как дубли. На первый взгляд, это кажется простой задачей: нужно прописать в .htaccess правило для mod_rewrite. Но при попытке это сделать вас ждёт неприятный сюрприз: этот знак вопроса не входит ни в ту часть URL, которая проверяется по RewriteRule, ни в переменную %{QUERY_STRING}, которую можно проверить с помощью RewriteCond. К счастью, решение всё же есть: использовать %{THE_REQUEST}, куда Apache помещает полную строку HTTP-запроса. Тогда получаем вот такое условие:
RewriteCond %{THE_REQUEST} \?
RewriteCond %{QUERY_STRING} ^$
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L,QSD] Здесь первая строка проверяет на то, что знак вопроса вообще присутствует в запросе, вторая — на то, что QUERY_STRING пуста (то есть после знака вопроса ничего нет), и третья выполняет редирект на URL без этого знака (за это отвечает флаг QSD).
Ещё одним источником дублей является index.php. Если сделать редирект, прописав его напрямую в RewriteRule (что-то вроде RewriteRule ^/index.php$ https://%{HTTP_HOST}/ [R=301]), можем получить зацикливание. Поэтому нужно проверить, что он есть в запросе явно. Делается это с помощью %{REQUEST_URI}:
RewriteCond %{REQUEST_URI} ^/index.php$
RewriteRule ^(.*)$ https://%{HTTP_HOST}/ [R=301,L,QSA]
Иногда бывает нужно вынести адрес ссылки в скобки за её текстом, то есть преобразовать её из вида
<a href="http://example.com">Текст</a>
в формат
Текст (http://example.com)
Такое, например, полезно при выдаче данных в RSS, который впоследствии импортируется в Twitter. Делается это достаточно легко с помощью такого регулярного выражения: $text = preg_replace('|<a\W[^>]*?href=[\'"]([^>]+?)[\'"][^>]*?>(.*?)</a>|u','$2 ($1)',$text);
При разработке мультиязычного бота для Telegram возникает вопрос, на каком языке отвечать пользователю в начале диалога. Оказывается, узнать это достаточно просто: в сообщениях (объект Message) есть поле from. Оно представляет собой объект User, где имеется поле language_code, в котором и лежит код языка, выставленный у пользователя в настройках. Итоговый код на PHP будет выглядеть примерно так: $updates = $bot->getUpdates();
foreach ($updates as $item) {
$language = $item['message']['from']['language_code'];
if ($language==='ru') $response = 'Привет!';
else $response = 'Hello!';
$bot->sendMessage($response,$item['message']['chat']['id']);
}
Я уже писал о том, как отслеживать медленные запросы с помощью MySQL. Но не всегда причина бывает в базе данных. Поэтому имеет смысл применять и другой способ — использовать логи Web-серверов. В современных версиях Apache и NGinx есть возможность выводить в лог время выполнения запроса с точностью до миллисекунд. В NGinx это делается так: сначала объявляем формат лога с помощью директивы log_format и придумываем ему имя, например, logtimed. Возьмём за основу формат по умолчанию, и добавим к нему время выполнения и длину запроса. А потом объявим использование этого формата в директиве access_log: log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
' $request_time $request_length';
access_log /var/log/nginx/access.log logtimed Директива может использоваться на любом уровне (http, server, location), но в документации сказано, что следует использовать тот же, на котором прописана директива root.
В Apache требуется включить mod_log_config. Далее всё делается аналогично, только директивы называются LogFormat и CustomLog, а вывод времени запроса можно задать двумя способами — либо как %D (в микросекундах), либо как %msT (с версии 2.4.13) — в миллисекундах, либо как %T — в сеундах: LogFormat "%h %l %u %t \"%r\" %>s %b %D" logtimed
CustomLog /var/log/apache2/access.log logtimed Эти директивы можно использовать либо в глобальной конфигурации, либо на уровне VirtualHost. Надеюсь, эта информация поможет выявить медленные запросы и сделать ваши сайты и сервисы быстрее.
Версия протокола TLS 1.3 позволяет загружать сайты быстрее, чем TLS 1.2, за счёт того, что на этапе установки шифрованного соединения нужен только один обмен данными (round-trip) вместо двух. Поэтому по возможности следует включить её поддержку. Для этого нужно, чтобы на сервере была установлена библиотека openssl версии 1.1.1 или выше. Тогда в nginx это можно сделать с помощью директивы: ssl_protocols TLSv1.2 TLSv1.3; Указывать её нужно в секции server, там же, где пишется listen 443 ssl http2; В Apache поддерживаемые протоколы указываются директивой SSLProtocol. Она расположена в файле настроек модуля ssl (в Ubuntu это /etc/apache2/mods-available/ssl.conf), также можно указать и для отдельных виртуальных хостов. Для включения TLS 1.3 прописываем так: SSLProtocol -all +TLSv1.2 +TLSv1.3 Проверить поддержку версии 1.3 можно с помощью TLS checker или с помощью более продвинутого сервиса SSL server test от SSL Labs.
Как известно, в PHP есть функция date_default_timezone_set, которая устанавливает часовой пояс, который будет использоваться по умолчанию при работе с датами. На вход она принимает строки вида "Europe/Moscow", "Africa/Algiers". Полный список этих строк есть в мануале, но каждый раз переписывать его оттуда (или хотя бы выбирать те, которые используются среди целевой аудитории сайта) — напрасная трата сил и времени. Кроме того, если нужно хранить в базе часовой пояс каждого пользователя, то лучше сохранять его как число, а не как строку. Оказалось, что в PHP предусмотрена функция для решения этой проблемы. Называется она DateTimeZone::listIdentifiers. По умолчанию выводит вообще все имеющиеся идентификаторы временных зон. Но можно получить список часовых поясов для отдельного региона или даже нескольких, передав их в качестве первого параметра:
DateTimeZone::listIdentifiers(DateTimeZone::EUROPE | DateTimeZone::ASIA); Также можно установить в первом параметре значение DateTimeZone::PER_COUNTRY и в качестве второго передать двухбуквенный код страны. Важно: буквы в коде должны быть заглавными:
DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY,'RU'); Дальше всё просто: получить список идентификаторов, пронумеровать значения и вывести в select, а дальше сохранить номер выбранного значения в базе вместо строки.
Недавно решил написать клиент для протокола IndieAuth. Для этого потребовалось сделать возможность находить на произвольной Web-странице все ссылки с атрибутом rel="me", причём ссылки могут быть как обычные (тег a), так и скрытые (тег link).
Первое, что приходит в голову, — регулярные выражения. Но для разбора произвольного HTML, который может быть свёрстан не по стандартам и иметь произвольную кодировку, это плохое и ненадёжное решение.
Пришлось вспомнить про такое расширение как PHP DOM. Оно позволяет работать с XML и HTML с помощью API, аналогичной той, которая есть для работы с DOM в броузерном JavaScript, а также делать запросы с помощью XPath. Вот им-то я и решил воспользоваться. Итак, для начала создаём объект DOMDocument и загружаем в него HTML. Вместо обычного load, предназначенного для работы с правильно отформатированным XML, используем loadHTMLFile: Смотреть пример кода
Выносите подключение шрифтов (директива @font-face) из внешних CSS в тег style в HTML до их подключения. Это незначительно увеличит объём кода страницы, зато позволит уменьшить длину критических цепочек. Если изначально броузер сначала скачивал CSS, разбирал его и уже потом начинал загружать шрифты, то теперь загрузка шрифта и файла CSS будет идти одновременно. Кроме того, нужно учитывать, что популярные шрифты уже могут быть установлены у пользователя локально. В частности, это касается шрифтов Roboto, которые изначально предустановлены в Android. Поэтому в src прописываем сначала директиву local, а уже потом url: src: local("Roboto"),url("fonts/Roboto-Medium.ttf"); Не забывайте также указать свойство font-display: swap для шрифтов, которыми выводится основной контент и font-display: optional — для шрифтов вспомогательных элементов, например, значков из FontAwesome.
Явно задавайте размеры для блоков, в которых будут помещаться элементы с других сайтов: виджеты соцсетей, баннеры, Google-карты и тому подобное. В этом случае контент сайта не будет «ездить» после окончания их подгрузки, из-за чего он субъективно будет казаться более быстрым с точки зрения пользователя. Это улучшает показатель CLS (content layout shift) в PageSpeed Insights.
Одним из основных источников замедления сайта являются вспомогательные элементы типа онлайн-консультантов, форм заказа обратного звонка, уведомления обо всяких акциях и т.п., которые реализуются с помощью сторонних сервисов. Во многих случаях их загрузку имеет смысл отложить. Для этого берём тот код, который вы получили от сервиса, убираем из него тег <script> и закрывающий тег </script>, и оборачиваем с помощью функции setTimeout. Должно получиться что-то такое: <script>
setTimeout(function() { код_сервиса }, время);
</script> Где время задаётся в милисекундах. Для улучшения показателей Page Speed Insights желательно использовать задержку не менее 5000 мс.
В Интернет-магазинах очень любят использовать слайдеры — блоки, в которых несколько картинок циклически сменяют друг друга. Идея хорошая, но есть одно но: в начальный момент, пока JavaScript для отображения слайдера не загрузился, на какой-то момент на экране появляются все изображения сразу. Потом скрипт слайдера рассчитывает высоту и скрывает лишнее, из-за чего получается, что контент под слайдером «ездит» по экрану. (А если пришёл пользователь с отключенным JavaScript, то все изображения так и останутся видны.) Избежать этого достаточно просто. Предположим, у нас есть такой код слайдера: <div class="slider">
<a href="/page1.htm"><img src="banner1.png" alt="Картинка 1" /></a>
<a href="/page2.htm"><img src="banner2.png" alt="Картинка 2" /></a>
<a href="/page3.htm"><img src="banner3.png" alt="Картинка 3" /></a>
</div> Прописываем в CSS такой код: .slider a { display:none }
.slider a:first-child { display: block } В этом случае все картинки, кроме самой первой, просто скрываются до момента, когда загрузится и выполнится код слайдера. Кроме того, из-за того, что второе и третье изображение скрыты, броузер откладыает их загрузку, а вместо этого загружает другие элементы страницы, которые на начальном этапе нужнее.
Если на сайте есть iframe с вспомогательным тяжёлым контентом, например, ролики с YouTube, карты и тому подобные элементы, имеет смысл сделать так: пропишите в свойство src about:blank (или код какой-нибудь заглушки в srcdoc), а реальную загрузку сделайте тогда, когда пользователь докрутит страницу до нужного объекта. Пример такого кода: <iframe src="about:blank" id="map"></iframe>
<script>
let map2 = document.getElementById("map");
let observer_map = new IntersectionObserver(function(entries) {
if(entries[0].isIntersecting === true) {
map2.src="https://yandex.ru/map-widget/v1/?***"; // URL карты
observer_map.disconnect();
}
}, { threshold: [0.1] });
observer_map.observe(map2);
</script> Подробнее о том, как работает этот код, читайте в заметке про отслеживание попадания элемента в зону видимости.
Используйте эти несколько простых трюков, и ваш показатель PageSpeed Insights вырастет, а сайты станут быстрее и приятнее для использования!
У начинающих верстальщиков часто возникает вопрос, как отцентрировать текст в блоке по горизонтали и вертикали. С горизонталью всё достаточно просто: используем свойство text-align. А вот для вертикали раньше приходилось прибегать ко всяческим ухищрениям. Но с появлением flex-верстки всё стало намного проще. Зададим в CSS класс, для которого пропишем выравнивание по центру по главной и вспомогательной оси: .centered { display: flex; justify-content: center; align-items: center; text-align: center } И просто применим его к нужному блоку: <div class="centered">Немного текста <br /> и дополнительного <br />содержимого</div> Посмотреть пример
Здесь можно задать мне вопрос или спросить совета по любой теме, затронутой в блогах или на форуме.
После того, как я отвечу, вопрос и ответ появятся в соответствующем разделе.
Но не забываем, что я — сторонник slow life, поэтому каких-либо сроков ответов не обещаю.
Самые интересные вопросы станут основой для новых тем на форуме или записей в блоге.
Сразу предупреждаю: глупости, провокации, троллинг и тому подобное летит прямо в /dev/null.