Информация в статье была актуальна до марта 2023 года.
Для просмотра страницы с подсветкой синтаксиса разрешите, пожалуйста, исполнение JavaScript.
Этот сайт — статичный, написан на обычном HTML в кодировке Windows-1251. Все обновления делаются локально, затем загружаются по FTP на хостинг. Всё было хорошо до весны 2013 года, когда Яндекс решил избавиться от «народных» сайтов и отдать их uCoz, у которого одно из обязательных требований к страницам сайта — кодировка UTF-8.
«И задумался.» © Саша Соколов.
У Яндекса требование было иным, кодировка сайта — Windows-1251. Её прекрасно поддерживает любимый текстовый редактор, файлы можно было просматривать локально и заливать на сайт без всякой конвертации. Но любимый текстовый редактор в последней версии 2008 года так и не научился работать с UTF-8, команда разработчиков принялась переносить его с Паскаля на другой язык, и до сих пор (зима 2013 года) они не сделали ни одной «беты». А достойной замены любимому редактору так и не нашлось.
В скорости после переезда на замену uCoz был найден другой бесплатный хостинг.
Новому хостеру было всё равно, какая кодировка у страниц сайта.
Лишь бы она была корректно указана в теге <meta>
.
Но в 2013 году считается кошерным использование кодировки UTF-8,
поэтому данное послабление не было принято во внимание. Тем более,
что задача к тому времени уже была решена.
Кроме того, uCoz не предоставляет такой услуги, как сбор статистики сайта, и предлагает воспользоваться сервисом Яндекс.Метрика. На каждую страницу необходимо добавлять код «Метрики», который время от времени может меняться. А на новом хостинге прибавилась необходимость добавлять на каждую страницу ещё и кнопку с рекламой хостера — автоматически вставленный текст ссылки не радовал красотой. От «Метрики» тоже было решено не отказываться.
Итак.
Имеем: Исходные файлы HTML в кодировке Windows-1251, в которых в контейнере
<head>
есть тег
<meta http-equiv="Content-Type" content="text/html; CHARSET=Windows-1251">
.
Нужно:
<meta http-equiv="Content-Type" content="text/html; CHARSET=Windows-1251">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
.
Причём сделать это нужно не один раз, а поддерживать синхронное состояние оригинальных и сконвертированных файлов так, чтобы конвертировались только новые и обновлённые файлы.
<meta>
с указанием кодировки Windows-1251,
заменяет на строку, содержащую тег с кодировкой UTF-8.
Выводит результат на стандартный вывод.
#include <stdio.h> #include <stdlib.h> #include <string.h> #define WIN32_LEAN_AND_MEAN #include <windows.h> #pragma comment(lib, "user32") #define BUFFER_SIZE (64 * 1024) static char help_text[] = "ah2utf8 -- converts ANSI/OEM stdin to UTF-8 stdout.\n" "Version 0.1, written by Hobo, compiled @ " __DATE__ "\n" "Usage: ah2utf8 [-<switch>...]\n" "<Switches>:\n" " -o: convert from OEM\n" " -s: no replace 'meta' string (see below), simply Skip\n" "\n" "Without '-s' switch lines contains text like\n" "'<meta http-equiv=\"Content-Type\" content=\"text/html; CHARSET=Windows-1251\">'\n" "will be replaced by\n" "'<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">'\n" "\n" "Usage examples:\n" "\tah2utf8 <w1251.txt >utf8.txt\n" "\tdir | ah2utf8 -o >utf8.txt\n"; extern int main(int argc, char *argv[]) { char *ibuf = NULL; char *obuf = NULL; WCHAR *wbuf = NULL; int cp = CP_ACP; int skip_meta = 0; int i, rc = 0; for (i = 1; i < argc; ++i) { if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h") || !strcmp(argv[i], "-?")) { fputs(help_text, stderr); rc = 0; goto stop; } if (!strcmp(argv[i], "-o")) cp = CP_OEMCP; if (!strcmp(argv[i], "-s")) skip_meta = 1; } if (!(ibuf = malloc(BUFFER_SIZE * sizeof(*ibuf)))) { fprintf(stderr, "Can't malloc() %d bytes for input buffer\n", BUFFER_SIZE * sizeof(*ibuf)); rc = 1; goto stop; } if (!(obuf = malloc(3 * BUFFER_SIZE * sizeof(*obuf)))) { fprintf(stderr, "Can't malloc() %d bytes for output buffer\n", 3 * BUFFER_SIZE * sizeof(*obuf)); rc = 1; goto stop; } if (!(wbuf = malloc(BUFFER_SIZE * sizeof(*wbuf)))) { fprintf(stderr, "Can't malloc() %d bytes for temp buffer\n", BUFFER_SIZE * sizeof(*wbuf)); rc = 1; goto stop; } while (gets(ibuf)) { if (CP_OEMCP == cp) OemToChar(ibuf, obuf); else strcpy(obuf, ibuf); CharLower(obuf); if (strstr(obuf, "<meta") && strstr(obuf, "content-type") && strstr(obuf, "charset")) { if (skip_meta) continue; strcpy(ibuf, "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">"); } MultiByteToWideChar(cp, 0, ibuf, -1, wbuf, BUFFER_SIZE); WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, obuf, 3 * BUFFER_SIZE * sizeof(*obuf), NULL, NULL); puts(obuf); } stop: if (ibuf) free(ibuf); if (obuf) free(obuf); if (wbuf) free(wbuf); return rc; }
404.shtml index.html somedir/index.html somedir/file1.html somedir/file2.html
@echo off set HOSTER=eomy set PERL=perl.exe set RECODER=^| ah2utf8 title %~nx0: for %HOSTER% echo Patching files for %HOSTER%... echo>%HOSTER%.mak # This file was generated by %~nx0 echo. >>%HOSTER%.mak echo>>%HOSTER%.mak all : \ for /f %%a in (_misc\html2patch.txt) do echo>>%HOSTER%.mak ^ _%HOSTER%/%%a \ echo. >>%HOSTER%.mak for /f %%a in (_misc\html2patch.txt) do ( echo>>%HOSTER%.mak _%HOSTER%/%%a : _misc/%HOSTER%.pl %0 %%a echo>>%HOSTER%.mak ^ ^ %PERL% _misc/eomy.pl %%a ^%RECODER% ^>_%HOSTER%/%%a echo. >>%HOSTER%.mak ) nmake -nologo -f %HOSTER%.mak if errorlevel 1 goto skip_del echo OK. del'n %HOSTER%.mak del %HOSTER%.mak :skip_del
</body>
. Пусть он будет называться
eomy.pl.
#!/usr/bin/perl -w
use strict;
#
# Код Yandex.Metrika
#
my $yacode = <<YANDEX_CODE;
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function (d, w, c) {
//
// Большинство кода Метрики опущено
//
})(document, window, "yandex_metrika_callbacks");
YANDEX_CODE
#
# Код кнопки хостера
#
my $eomybtn = join "",
'<!-- EOMY.NET button -->',
'<p style="text-align: center; margin-bottom: 0;">',
'<a href="http://www.eomy.net/">',
'<img src="http://www.eomy.net/eomy.net.gif" border="0" alt="EOMY.NET: бесплатный хостинг" title="EOMY.NET"/>',
'</a></p>', "\n";
while (<>) {
if (m/<\/body\b[^>]*>/i) {
print $eomybtn;
print $yacode;
}
print;
}
Теперь всё вместе. Командный файл читает список файлов и создаёт make-файл
с именем eomy.mak
, который затем передаёт nmake,
примерно такого содержания:
# This file was generated by patch.cmd all : \ index.html \ somedir/index.html \ somedir/file1.html \ somedir/file2.html \ _eomy/index.html : _misc/eomy.pl patch.cmd index.html perl.exe _misc/eomy.pl index.html | ah2utf8 >_eomy/index.html _eomy/somedir/index.html : _misc/eomy.pl patch.cmd somedir/index.html perl.exe _misc/eomy.pl somedir/index.html | ah2utf8 >_eomy/somedir/index.html _eomy/somedir/file1.html : _misc/eomy.pl patch.cmd somedir/file1.html perl.exe _misc/eomy.pl somedir/file1.html | ah2utf8 >_eomy/somedir/file1.html _eomy/somedir/file2.html : _misc/eomy.pl patch.cmd somedir/file2.html perl.exe _misc/eomy.pl somedir/file2.html | ah2utf8 >_eomy/somedir/file2.html
nmake сравнивает время изменения файлов и, если какой либо файл, указанный после двоеточия, новее целевого файла (до двоеточия), то запускает скрипт на Perl, передавая ему в качестве параметра исходный HTML. Скрипт добавляет свои строки в текст и передаёт текст на ввод конвертера ah2utf8, чей вывод направляется в целевой файл.
Вот такой вот UNIX-way. Всё работает. И достаточно быстро: около 250 файлов обрабатываются чуть меньше минуты. Смущало одно: на генерацию make-файла тратится, хоть и небольшое, но заметное время. Что немного раздражает при обработке, скажем, одного файла.
Появилась идея возложить функции nmake на Perl. И ещё одно соображение в пользу отказа от nmake: если к задаче прибавить ещё и генерацию файла sitemap.xml, то нужно иметь способ узнать, обновлены ли HTML-файлы и нужно ли его генерировать заново.
patch.cmd:
@echo off title %~nx0: Patch'n for online... perl.exe _misc/online.pl
Фрагмент online.pl — всего одна функция. Остальной код — получение списка файлов, времени его изменения и др. опущены. Функция принимает аргументы:
Возвращает количество обработанных файлов.
sub mk_patch { my $dir = shift; my $footer = shift; my @flist = @_; my $mytime = (stat $0)[9]; my $patch_cnt = 0; foreach (@flist) { my $srctime = (stat $_)[9] || 0; my $dsttime = (stat($dir . $_))[9] || 0; if ($mytime > $dsttime || $srctime > $dsttime) { # # Открываем канал с ah2utf8, которая будет выводить в целевой файл # my $outname = "| ah2utf8 > $dir$_"; open FHI, "< $_" or die "Can't open '< $_': $!"; open FHO, $outname or die "Can't open '$outname': $!"; while (<FHI>) { if (m/<meta\s/i && m/(<meta\s+.+charset=Windows-1251[^>]*>)/i) { s{\Q$1}{<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">}; } if (m/<\/body\b[^>]*>/i) { # # $footer содержит коды «Метрики» и кнопки хостера # print FHO $footer; } print FHO $_; } close FHO; close FHI; ++$patch_cnt; } } $patch_cnt; }
От необходимости использовать nmake избавились. Но тестовый прогон показал, что данный способ работает процентов на 25 медленнее первого решения. Видимо, создание процесса и открытие канала для ah2utf8 требует в Cygwin бóльших затрат, чем в случае чистого cmd.exe.
Отсюда вывод: нужно и конвертирование делать средствами Perl. Сразу придти
к такому выводу мешало недостаточное знание стандартных модулей.
Кроме того смущал символ подчёркивания в начале названия функции
Encode::decode()
. Оказалось, что достаточно было внимательнее
прочитать
perldoc > Core modules (E) > Encode,
чтобы найти нужную функцию.
Добавили функцию cp1251_2utf8()
, которая принимает
строку байт в Windows-1251 и возвращает строку символов в UTF-8
без установленного флага UTF8
(в отличие от Encode::decode()
):
sub cp1251_2utf8 { use Encode qw(from_to); my $string = shift; Encode::from_to($string, 'cp1251', 'utf8'); $string; }
Немного подправленная функция mk_patch()
:
sub mk_patch { my $dir = shift; my $footer = shift; my @flist = @_; my $mytime = (stat $0)[9]; my $patch_cnt = 0; foreach (@flist) { my $srctime = (stat $_)[9] || 0; my $dsttime = (stat($dir . $_))[9] || 0; if ($mytime > $dsttime || $srctime > $dsttime) { # # Выводим в файл сами, а не через фильтр # my $outname = "> $dir$_"; open FHI, "< $_" or die "Can't open '< $_': $!"; open FHO, $outname or die "Can't open '$outname': $!"; while (<FHI>) { if (m/<meta\s/i && m/(<meta\s+.+charset=Windows-1251[^>]*>)/i) { s{\Q$1}{<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">}; } if (m/<\/body\b[^>]*>/i) { # # $footer уже сконвертирован в UTF-8 перед вызовом функции # В нём находится коды «Метрики» и кнопки хостера # print FHO $footer; } print FHO cp1251_2utf8($_); } close FHO; close FHI; ++$patch_cnt; } } $patch_cnt; }
Обработка тех же файлов стала быстрее первого решения более, чем на порядок.
Следует признаться, что первоначально, по незнанию модуля Encode.pm,
функция cp1251_2utf8()
выглядела иначе и называлась иначе:
# # Upper 128 characters of Windows-1251 encoded as UTF-8 # Исходный текст инициализации массива был сгенерирован программой на Си # my @win1251_utf8 = ( "\xd0\x82", # 128: 'Ђ' "\xd0\x83", # 129: 'Ѓ' "\xe2\x80\x9a", # 130: '‚' # # Пропущено много элементов массива # "\xd1\x8d", # 253: 'э' "\xd1\x8e", # 254: 'ю' "\xd1\x8f", # 255: 'я' ); my @chars; sub win1251_2utf8 { @chars = split //, shift; foreach (@chars) { $_ = $win1251_utf8[ord($_) - 128] if ord($_) > 127; } join "", @chars; }
Тем не менее, даже с этой, достаточно грубой, функцией патч работал в разы быстрее, чем решение №1.