Кратко о себе: Web-разработчик. Пишу на PHP, Python, JavaScript. Знаю Ruby и Go, со студенческих времён более-менее помню C и asm. Специализируюсь на ускорении загрузки сайтов и разработке ботов для Telegram. Linuxоид (использую Debian+LXDE). Сторонник IndieWeb, slow lifer.
Те, кто занимается разработкой на PHP уже давно и не использует frameworks, наверное, не раз сталкивались с необходимостью правильно выводить код HTTP-ответа с правильной версией протокола и текстовым описанием (например, Ok для 200, Not Found для 404 и т.д.). Раньше для этих целей приходилось писать множество проверок условий вида: if ($status==200) header($_SERVER['HTTP_PROTOCOL'].' 200 Ok');
elseif ($status==404) header($_SERVER['HTTP_PROTOCOL'].' 404 Not Found');
// elseif обработка других статусов. Задача эта совершенно рутинная, и возникает вопрос, неужели нельзя это сделать средствами самого языка PHP. Как выяснилось, ещё в версии 5.4 такая возможность появилась — это функция http_response_code, которой достаточно передать числовой код статуса ответа. Например, если вызвать http_response_code(404), то сервер автоматически сгенерирует заголовок HTTP/1.1 404 Not Found (если запрос был по протоколу HTTP 1.1). Также можно вызвать функцию без параметров, чтобы получить код, который был установлен до этого (по умолчанию он равен 200). Так что теперь выдача заголовка сводится к всего одной строчке кода! Кроме того, в той же версии появилась ещё одна полезная функция: header_register_callback. Эта функция позволяет устанавливать обработчик, который будет вызван перед отправкой HTTP-заголовков клиенту. В нём можно использовать функции headers_list для получения списка подготовленных заголовков и header_remove для удаления тех, которые требуется исключить из отправки.
При установке Windows 10 на ноутбуки с процессорами Intel последних поколений NVMe-накопителем часто возникает ситуация, когда установщик Windows не показывает ни одного раздела, куда можно произвести установку. Объясняется это тем, что тем, что в чипсетах для процессоров 11 и более поздних поколений для работы с NVMe используется технология VMD. Для её работы BIOS (точнее, UEFI) подменяет реальные контроллеры NVMe на контроллер RST VMD. Но драйвера для него в дистрибутиве Windows 10 отсутствуют, поэтому установщик и не видит накопителя. Решить эту ситуацию можно двумя способами. Первый — зайти в BIOS и отключить использование технологии VMD (в ноутбуках от неё всё равно пользы немного). Второй — это на другом компьютере скачать драйвер RST VMD либо с сайта Intel (но он сейчас скачивание недоступно без VPN), либо с сайта какого-либо производителя ноутбуков, например, Lenovo (драйвер от производителя не зависит и должен подойти к любым ноутбукам). Далее нужно запустить скачаный EXE-файл, выбрать Extract вместо Install и распаковать драйвера на флешку (можно на флешку с дистрибутивом самого Windows, можно на отдельную). Далее начать установку Windows 10, на этапе выбора раздела нажать кнопку "Загрузить" и там указать путь к драйверу на флешке. После этого установщик должен написать, что найдено устройство Intel RST VMD, загрузить драйвер и найти раздел (возможно, потребуется нажать кнопку "Обновить"). Дальше установка должна пройти без проблем.
Часто при разработке ботов для 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: Смотреть пример кода
Здесь можно задать мне вопрос или спросить совета по любой теме, затронутой в блогах или на форуме.
После того, как я отвечу, вопрос и ответ появятся в соответствующем разделе.
Но не забываем, что я — сторонник slow life, поэтому каких-либо сроков ответов не обещаю.
Самые интересные вопросы станут основой для новых тем на форуме или записей в блоге.
Сразу предупреждаю: глупости, провокации, троллинг и тому подобное летит прямо в /dev/null.