Правильно читать текстовый файл utf-16 в строку без внешних библиотек?

Я использую StackOverflow с самого начала, и иногда у меня возникало искушение публиковать вопросы, но я всегда либо разбирался в них сам, либо находил ответы, опубликованные в конце концов... до сих пор. Кажется, это должно быть довольно просто, но я часами безрезультатно бродил по Интернету, поэтому перехожу сюда:

У меня есть довольно стандартный текстовый файл utf-16 со смесью английских и китайских символов. Я хотел бы, чтобы эти символы заканчивались строкой (технически, wstring). Я видел много ответов на связанные вопросы (здесь и в других местах), но они либо пытаются решить гораздо более сложную проблему чтения произвольных файлов без знания кодировки, либо преобразования между кодировками, либо просто обычно смущен тем, что «Unicode» является диапазоном кодировок. Я знаю источник текстового файла, который я пытаюсь прочитать, это всегда будет UTF16, у него есть спецификация и все такое, и он может оставаться таким.

Я использовал решение, описанное здесь, которое работал для текстовых файлов, которые были полностью английскими, но после обнаружения определенных символов он переставал читать файл. Единственное другое предложение, которое я нашел, заключалось в том, чтобы использовать ICU, что, вероятно, сработает, но я действительно не хотел бы включать целый большой библиотеку в приложении для распространения, просто чтобы прочитать один текстовый файл в одном месте. Однако меня не волнует системная независимость - мне это нужно только для компиляции и работы в Windows. Решение, которое не полагалось бы на этот факт, было бы, конечно, красивее, но я был бы так же счастлив за решение, которое использовало бы stl, полагаясь на предположения об архитектуре Windows, или даже решения, включающие функции win32 или ATL; Я просто не хочу включать еще одну большую стороннюю библиотеку, такую ​​как ICU. Мне все еще не повезло, если я не хочу переделать все это самостоятельно?

редактировать: я застрял в использовании VS2008 для этого конкретного проекта, поэтому код C++ 11, к сожалению, не поможет.

изменить 2: я понял, что код у меня был раньше заимствовал, не терпел неудачу с неанглийскими символами, как я думал. Скорее, он не работает с определенными символами в моем тестовом документе, в том числе ':' (ПОЛНОШИРИННАЯ ТОЛЧКА, U+FF1A) и ')' (ПОЛНОШИРИННАЯ ПРАВАЯ СКОБКА, U+FF09). Опубликованное решение bames53 также в основном работает, но те же персонажи ставят его в тупик?

редактировать 3 (и ответ!): исходный код, который я использовал, в основном работал - как bames53 помог мне обнаружить, что ifstream просто нужно было открыть в двоичном режиме, чтобы он работал.


person neminem    schedule 08.05.2012    source источник
comment
Пожалуйста, покажите нам код. Какой фактический API вы вызываете? Читать файл? фред? читать?   -  person bmargulies    schedule 08.05.2012
comment
Не должно быть проблем, если вы действительно уверены, что текст UTF16. Насколько мне известно, китайский обычно заканчивается строкой MBCS, что является совершенно другим зверем.   -  person Mahmoud Al-Qudsi    schedule 08.05.2012
comment
_wfopen может открывать/переводить кодировку UTF-16, которую затем можно прочитать в строку с помощью fread msdn.microsoft.com/fr-fr/library/yeby3zcb%28v=vs.80%29.aspx   -  person Benj    schedule 08.05.2012
comment
Я не вижу причин, по которым код, на который вы ссылаетесь, не должен работать. Он читает файл байтов и приводит его к типу wchar_t* для инициализации wstring. Единственное, что я бы проверил, это то, что файл открыт в двоичном режиме, но я бы не ожидал, что ошибка покажет ваш симптом.   -  person Mark Ransom    schedule 09.05.2012
comment
@MarkRansom См. мой ответ на сообщение bames53: теперь у меня есть лучшее представление о том, какой странный симптом заключается в том, что код, который мы ранее использовали, отображался: определенные определенные символы юникода остановили его чтение до того, как он прочитал весь файл. Однако недостаточно эксперта по юникоду, чтобы догадаться, почему.   -  person neminem    schedule 09.05.2012
comment
@bmargulies (и тот, кто проголосовал за этот комментарий): я связался с кодом, который я использовал ранее, который был stl (ifstream/stringstream). Однако я не привязан к конкретному API, пока у меня есть к нему доступ.   -  person neminem    schedule 09.05.2012
comment
@MahmoudAl-Qudsi: я чертовски уверен, что это UTF16. Или, по крайней мере, я почти уверен, что это текстовый файл, который очень похож на UTF16 и определенно не является MBCS. Например, я не могу доказать, что это не UCS-2 (до сегодняшнего дня я ничего не знал об этой кодировке или ее отличиях от UTF16).   -  person neminem    schedule 09.05.2012


Ответы (3)


Когда вы открываете файл для UTF-16, вы должны открывать его в двоичном режиме. Это связано с тем, что в текстовом режиме некоторые символы интерпретируются особым образом, в частности, 0x0d полностью отфильтровывается, а 0x1a отмечает конец файла. Есть некоторые символы UTF-16, у которых один из этих байтов будет половиной кода символа, и это испортит чтение файла. Это не ошибка, это преднамеренное поведение, и это единственная причина наличия отдельных текстовых и двоичных режимов.

По причине того, что 0x1a считается концом файла, см. этот сообщение в блоге Рэймонда Чена, в котором прослеживается история Ctrl-Z. Это в основном обратная совместимость вышла из-под контроля.

person Mark Ransom    schedule 09.05.2012

Насколько мне известно, решение C++11 (поддерживаемое на вашей платформе Visual Studio с 2010 года) будет таким:

#include <fstream>
#include <iostream>
#include <locale>
#include <codecvt>
int main()
{
    // open as a byte stream
    std::wifstream fin("text.txt", std::ios::binary);
    // apply BOM-sensitive UTF-16 facet
    fin.imbue(std::locale(fin.getloc(),
       new std::codecvt_utf16<wchar_t, 0x10ffff, std::consume_header>));
    // read     
    for(wchar_t c; fin.get(c); )
            std::cout << std::showbase << std::hex << c << '\n';
}
person Cubbi    schedule 08.05.2012
comment
На платформах с двухбайтовым wchar_t, таких как Windows, это преобразует UTF-16 в UCS-2. В частности, реализация VS2010 усекает символы за пределами BMP. - person bames53; 08.05.2012
comment
@ bames53 Действительно .. VS2010 правильно считывает эти символы в char32_t, но со строкой UCS4 в Windows мало что можно сделать. Вероятно, еще слишком рано избавляться от зависящих от компилятора вещей, таких как _O_U16TEXT. - person Cubbi; 08.05.2012
comment
Раздражает, я попробовал ваш фрагмент, и хотя сначала я подумал, что он не работает (когда я увидел, что он печатает целые числа, а не символы юникода), затем я заметил, что это то, что он должен был делать. Я заменил cout добавлением к wstring и увидел строку юникода, которую ожидал увидеть. Я говорю раздражающе, потому что до сих пор не думал, что важно упомянуть, что я застрял на vs2008 для этого конкретного проекта. (Я так отредактировал свой вопрос.) Это по-прежнему правильный ответ, если предположить, что вам разрешено использовать С++ 11. Или за исключением символов вне BMP, во всяком случае. - person neminem; 09.05.2012
comment
Вы знаете, как происходит запись в файл? Я пытаюсь: std::wofstream wofs(/utf16dump.txt); wofs.imbue(std::locale(wofs.getloc(), новый std::codecvt_utf16‹wchar_t, 0x10ffff, std::consume_header›)); wofs ‹‹ ws; и я получаю мусор - person NoSenseEtAl; 08.06.2012
comment
@NoSenseEtAl у меня работает, выдает UTF-16be по запросу (используя clang++/libcxx). Возможно, вам нужно было std::little_endian? - person Cubbi; 08.06.2012
comment
std::consume_header, похоже, не работает в VS2010 — спецификация потребляется, но порядок байтов не изменяется. Мне также пришлось явно использовать std::little_endian. - person Eugene; 08.05.2013
comment
Почему вы открываете файл в бинарном режиме? - person hkBattousai; 01.04.2016
comment
@hkBattousai, потому что я не хочу, чтобы чтение прерывалось, если оно столкнется с \x1a. Винда такая сумасшедшая. - person Cubbi; 01.04.2016
comment
Для читателей замените последнюю строку на std::wcout << c << '\n';, чтобы увидеть вывод символов Unicode. - person zar; 28.10.2019
comment
Обратите внимание, что в macOS мне пришлось явно установить std::little_endian вместо std::consume_header для файла в кодировке UTF-16 LE, который включал соответствующую спецификацию. В противном случае я бы получил вывод с прямым порядком байтов. - person bfx; 27.05.2020
comment
Версия MSVC говорит, что такое использование std::codecvt устарело в C++ 17, см. _CXX17_DEPRECATE_CODECVT_HEADER. Я не вижу здесь упоминания об этом: en.cppreference.com/w/cpp/locale /codecvt - person Chris Guzak; 30.06.2021
comment
@ChrisGuzak std::codecvt не устарел. Заголовок codecvt и его содержимое были - cppreference отмечает, что на en.cppreference.com/ w/cpp/ и отдельные страницы - person Cubbi; 16.07.2021

Редактировать:

Таким образом, похоже, проблема заключалась в том, что Windows рассматривает определенные последовательности магических байтов как конец файла в текстовом режиме. Это решается использованием двоичного режима для чтения файла std::ifstream fin("filename", std::ios::binary);, а затем копированием данных в wstring, как вы это уже делали.



Самым простым непереносимым решением было бы просто скопировать данные файла в массив wchar_t. Это основано на том факте, что wchar_t в Windows имеет размер 2 байта и использует кодировку UTF-16.


У вас будут некоторые трудности с преобразованием UTF-16 в специфичную для локали кодировку wchar_t полностью переносимым способом.

Вот функция преобразования Unicode, доступная в стандартной библиотеке C++ (хотя VS 10 и 11 реализуют только элементы 3, 4 и 5)

  1. codecvt<char32_t,char,mbstate_t>
  2. codecvt<char16_t,char,mbstate_t>
  3. codecvt_utf8
  4. codecvt_utf16
  5. codecvt_utf8_utf16
  6. c32rtomb/mbrtoc32
  7. c16rtomb/mbrtoc16

И что делает каждый

  1. Аспект codecvt, который всегда конвертирует между UTF-8 и UTF-32.
  2. конвертирует между UTF-8 и UTF-16
  3. конвертирует между UTF-8 и UCS-2 или UCS-4 в зависимости от размера целевого элемента (символы за пределами BMP, вероятно, усекаются)
  4. преобразует последовательность символов с использованием схемы кодирования UTF-16 и UCS-2 или UCS-4.
  5. конвертирует между UTF-8 и UTF-16
  6. Если определен макрос __STDC_UTF_32__, эти функции преобразуются между кодировкой символов текущей локали и UTF-32.
  7. Если определен макрос __STDC_UTF_16__, эти функции преобразуются между кодировкой символов текущей локали и UTF-16.

Если __STDC_ISO_10646__ определено, то преобразование напрямую с использованием codecvt_utf16<wchar_t> должно быть правильным, поскольку этот макрос указывает, что значения wchar_t во всех локалях соответствуют коротким именам уставов Unicode (и, таким образом, подразумевается, что wchar_t достаточно велик, чтобы содержать любое такое значение).

К сожалению, нет ничего определенного, что бы напрямую переходило от UTF-16 к wchar_t. Можно использовать UTF-16 -> UCS-4 -> mb (если __STDC_UTF_32__) -> wc, но вы потеряете все, что не представляется в многобайтовой кодировке локали. И, конечно же, несмотря ни на что, преобразование из UTF-16 в wchar_t потеряет все, что не может быть представлено в кодировке wchar_t локали.


Так что, вероятно, не стоит быть переносимым, и вместо этого вы можете просто прочитать данные в массив wchar_t или использовать какое-либо другое специальное средство Windows, такое как режим _O_U16TEXT для файлов.

Это должно собираться и работать где угодно, но делает кучу предположений, чтобы действительно работать:

#include <fstream>
#include <sstream>
#include <iostream>

int main ()
{
    std::stringstream ss;
    std::ifstream fin("filename");
    ss << fin.rdbuf(); // dump file contents into a stringstream
    std::string const &s = ss.str();
    if (s.size()%sizeof(wchar_t) != 0)
    {
        std::cerr << "file not the right size\n"; // must be even, two bytes per code unit
        return 1;
    }
    std::wstring ws;
    ws.resize(s.size()/sizeof(wchar_t));
    std::memcpy(&ws[0],s.c_str(),s.size()); // copy data into wstring
}

Вероятно, вам следует хотя бы добавить код для обработки endianess и «BOM». Кроме того, новые строки Windows не преобразуются автоматически, поэтому вам нужно сделать это вручную.

person bames53    schedule 08.05.2012
comment
Что ж, оказывается, ваш код помог мне отладить - он перестал читаться точно в том же месте в моем тестовом текстовом файле, что и код, на который я ссылался - (cfc.kizzx2.com/index.php/ — сделал. Китайский иероглиф, он перестал читать на первом экземпляре символа : (ПОЛНОШИРИННАЯ ДВОРОТА, U+FF1A). Удалив его, он останавливается на ) (ПОЛНОШИРИННАЯ ПРАВАЯ СКОБКА, U+FF09). Я чувствую тему... - person neminem; 09.05.2012
comment
@neminem Думаю, мне следовало более внимательно посмотреть на эту ссылку, она просто делает то же самое, что и я. Я предполагаю, что по какой-то причине реализация fstream в VS 2008 не любит читать байт 0xFF. Этот байт представляет собой «удалить». Попробуйте открыть файл в бинарном режиме std::ifstream fin("...",std::ios::binary); - person bames53; 09.05.2012
comment
О мой бог фреллинга. Я потратил больше дня, пытаясь понять это, и это было так очевидно? Я пробовал -другие- вещи, которые включали открытие файла в двоичном режиме, но я никогда не пробовал -оригинальное- решение, открывающее его только в двоичном режиме? Вы так много выигрываете. Вы должны отредактировать это в своем решении на случай, если другие люди наткнутся на этот вопрос позже (я не могу представить, что я единственный человек, у которого когда-либо была эта проблема) :). - person neminem; 09.05.2012
comment
Это не ошибка - смотрите мой ответ. - person Mark Ransom; 09.05.2012
comment
@MarkRansom Это имеет смысл, хотя я ожидал, что это повлияет на Windows только тогда, когда 0x0D и 0x0A появляются вместе. 0x1A выглядит как ошибка по замыслу, но поскольку ни один из этих элементов не стандартизирован, вероятно, лучше никогда и нигде не использовать текстовый режим. - person bames53; 09.05.2012