Курилка

Как в «Курилке» обновляется информация

Информация в статье была актуальна до марта 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">.

Нужно:

  1. Конвертировать HTML-файлы из Windows-1251 в UTF-8, сохранив их другой каталог.
  2. Заменить тег
    <meta http-equiv="Content-Type" content="text/html; CHARSET=Windows-1251">
    на
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">.
  3. Добавить перед закрывающим тегом </body> код Яндекс.Метрики и код кнопки хостера.

Причём сделать это нужно не один раз, а поддерживать синхронное состояние оригинальных и сконвертированных файлов так, чтобы конвертировались только новые и обновлённые файлы.

Решения

Инструменты

  1. Компилятор языка Си.
  2. Утилита nmake.
  3. Cygwin с Perl на борту.
  4. Любимый текстовый редактор.

Решение №1: cmd + nmake + Perl + конвертер на Си

  1. Простенький конвертер HTML из Windows-1251 (или 866) в UTF-8, написанный на Си. Читает стандартный ввод, конвертирует строку байт в 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;
    }
    
  2. Список файлов для конвертации вида
    404.shtml
    index.html
    somedir/index.html
    somedir/file1.html
    somedir/file2.html
    
  3. Небольшой командный файл, который создаёт временный make-файл, который затем передаётся на обработку nmake. Назовём его patch.cmd.
    @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
    
  4. Скрипт на Perl, который читает построчно файл, имя которого указано в командной строке, и выводит строки на стандартный вывод. Добавляет HTML-коды кнопки хостера и Метрики перед закрывающим тегом </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-файлы и нужно ли его генерировать заново.

Решение №2: тоже, что и №1, но без nmake

  1. patch.cmd:

    @echo off
    
    title %~nx0: Patch'n for online...
    perl.exe _misc/online.pl
    
  2. Фрагмент online.pl — всего одна функция. Остальной код — получение списка файлов, времени его изменения и др. опущены. Функция принимает аргументы:

    1. Каталог, куда сохранять изменённые файлы,
    2. HTML-код, который нужно добавить перед </body>,
    3. Список файлов.

    Возвращает количество обработанных файлов.

    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, чтобы найти нужную функцию.

Решение №3: тоже, что и №2, но без внешнего конвертера

Добавили функцию 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.