Выбор кодировки для текстового режима

Для записываемых текстовых файлов кодировку следует выбирать исходя из особенностей текста или подстраиваясь под требования внешних программ, для которых предназначены генерируемые файлы. Если внешних требований нет, можно придерживаться правила всегда использовать ANSI для простых текстов с числами, английскими буквами и пунктуацией (таблица 128 таких международных символов приведена в разделе Сравнение строк). При работе с национальными языками или спецсимволами используйте UTF-8 или Unicode, т.е. соответственно:

int u8 = FileOpen("utf8.txt"FILE_WRITE | FILE_TXT | FILE_ANSI0CP_UTF8);
int u0 = FileOpen("unicode.txt"FILE_WRITE | FILE_TXT | FILE_UNICODE);

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

Чтение собственных файлов не должно составлять проблему, потому что здесь достаточно указать при чтении те же настройки кодировки, что были сделаны при записи. Однако текстовые файлы могут поступать из разных источников. Их кодировка может быть неизвестна или меняться без предварительного уведомления. Поэтому встает вопрос, что делать, если часть файлов может поставляться в виде однобайтовых строк (ANSI), часть — в виде "широких" двухбайтовых (Unicode), а часть — в кодировке UTF-8.

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

MQL5 не позволяет полностью автоматически детектировать и применять правильные кодировки, однако существует один наиболее универсальный режим для считывать разнообразных текстовых файлов. Для этого нужно задать следующие входные параметры функции FileOpen:

int h = FileOpen(filenameFILE_READ | FILE_TXT | FILE_ANSI0CP_UTF8);

Здесь на нас работает несколько факторов.

Во-первых, кодировка UTF-8 прозрачно пропускает упомянутые 128 символов в любой кодировке ANSI (т.е. они передаются "один в один").

Во-вторых, она является наиболее популярной в протоколах Интернет.

В-третьих, в MQL5 встроен дополнительный анализ на форматирование текста в двухбайтовом Unicode, который позволяет при необходимости автоматически переключить режим работы файла в FILE_UNICODE, невзирая на заданные параметры. Дело в том, что файлы в формате Unicode обычно предваряются специальной парой символов: 0xFFFE или наоборот 0xFEFF. Эта последовательность называется меткой порядка байтов (Byte Order Mark, BOM). Она нужна из-за того, что, как мы знаем, байты могут храниться внутри чисел в разном порядке на разных платформах (об этом говорилось в разделе Управление порядком байтов в целых числах).

Формат FILE_UNICODE использует 2-байтовое целое число (код) на каждый символ, поэтому порядок байтов становится важен, в отличие от прочих кодировок. BOM, соответствующий порядку байт для Windows, — это последовательность 0xFFFE. Если ядро MQL5 находит эту метку в начале текстового файла, его чтение автоматически переключается в режиме Unicode.

Давайте посмотрим, как различные настройки режимов работают с текстовыми файлами разных кодировок. Для этого подготовлен скрипт FileText.mq5 и несколько текстовых файлов с одним и тем же содержимым, но в разных кодировках (в скобках указан размер в байтах):

  • ansi1252.txt (50) — европейская кодировка 1252 (будет отображаться целиком без искажений в Windows с европейским языком);
  • unicode1.txt (102) — двухбайтовый Unicode, в начале идет присущий Windows BOM 0xFFFE;
  • unicode2.txt (100) — двухбайтовый Unicode без BOM (в принципе, BOM является опциональным);
  • unicode3.txt (102) — двухбайтовый Unicode, в начале идет BOM, присущий Unix, 0xFEFF;
  • utf8.txt (54) — кодировка UTF-8.

В функции OnStart мы считываем эти файлы в циклах при разных настройках FileOpen. Обратите внимание, что за счет использования FileHandle (рассмотренного в предыдущем разделе) нам не приходится заботиться о закрытии файлов: все происходит автоматически внутри каждой итерации.

void OnStart()
{
   Print("=====> UTF-8");
   for(int i = 0i < ArraySize(texts); ++i)
   {
      FileHandle fh(FileOpen(texts[i], FILE_READ | FILE_TXT | FILE_ANSI0CP_UTF8));
      Print(texts[i], " -> "FileReadString(~fh));
   }
   
   Print("=====> Unicode");
   for(int i = 0i < ArraySize(texts); ++i)
   {
      FileHandle fh(FileOpen(texts[i], FILE_READ | FILE_TXT | FILE_UNICODE));
      Print(texts[i], " -> "FileReadString(~fh));
   }
   
   Print("=====> ANSI/1252");
   for(int i = 0i < ArraySize(texts); ++i)
   {
      FileHandle fh(FileOpen(texts[i], FILE_READ | FILE_TXT | FILE_ANSI01252));
      Print(texts[i], " -> "FileReadString(~fh));
   }
}

Функция FileReadString читает из файла строку, мы рассмотрим её в разделе о записи и чтении переменных.

Вот пример журнала с результатами выполнения скрипта:

=====> UTF-8
MQL5Book/ansi1252.txt -> This is a text with special characters: �? / � / �
MQL5Book/unicode1.txt -> This is a text with special characters: ±Σ / £ / ¥
MQL5Book/unicode2.txt -> T
MQL5Book/unicode3.txt -> ��
MQL5Book/utf8.txt -> This is a text with special characters: ±Σ / £ / ¥
=====> Unicode
MQL5Book/ansi1252.txt -> 桔獩椠⁳⁡整瑸眠瑩⁨灳捥慩档牡捡整獲›㾱⼠ꌠ⼠ꔠ
MQL5Book/unicode1.txt -> This is a text with special characters: ±Σ / £ / ¥
MQL5Book/unicode2.txt -> This is a text with special characters: ±Σ / £ / ¥
MQL5Book/unicode3.txt -> 吀栀椀猀 椀猀 愀 琀攀砀琀 眀椀琀栀 猀瀀攀挀椀愀氀 挀栀愀爀愀挀琀攀爀猀㨀 넀
MQL5Book/utf8.txt -> 桔獩椠⁳⁡整瑸眠瑩⁨灳捥慩档牡捡整獲›뇂ꏎ⼠술₣ ꗂ
=====> ANSI/1252
MQL5Book/ansi1252.txt -> This is a text with special characters: ±? / £ / ¥
MQL5Book/unicode1.txt -> This is a text with special characters: ±Σ / £ / ¥
MQL5Book/unicode2.txt -> T
MQL5Book/unicode3.txt -> þÿ
MQL5Book/utf8.txt -> This is a text with special characters: Â±Î£ / Â£ / Â¥

Файл unicode1.txt всегда читается правильно, потому что в нем есть BOM 0xFFFE, и система игнорирует настройки в исходном коде. Однако, если метка отсутствует или имеет обратный порядок байтов, это автоопределение не работает. Также при установке FILE_UNICODE мы теряем способность читать однобайтовые тексты и UTF-8.

В результате, более устойчивым к вариациям форматирования следует считать вышеупомянутое сочетание FILE_ANSI и CP_UTF8. Выбор специфической национальной кодовой страницы рекомендуется только при явном требовании.

Несмотря на существенную помощь для программиста со стороны API при работе с файлами в текстовом режиме, мы можем при необходимости отказаться от режима FILE_TXT или FILE_CSV, и открыть по сути текстовый файл в двоичном режиме FILE_BINARY. Это переложит все сложности по парсингу текста и определению кодировки на плечи программиста, зато позволит поддержать другие нестандартные форматы. Но основной момент здесь в том, что текст можно считывать и записывать в файл, открытый в двоичном режиме. А вот обратное, в общем случае, невозможно. Бинарный файл с произвольными данными (то есть, если в него не были записаны исключительно строки), открытый в текстовом режиме, будет, скорее всего, интерпретироваться как текстовая "абракадабра". Если вам требуется записать бинарные данные в текстовый файл, воспользуйтесь предварительно функцией CryptEncode и кодировкой CRYPT_BASE64.