Что нового

[Баг] IniRead() не воспринимает .INI в кодировке UTF-8 c сигнатурой

A

Alecsis1

Гость
Недавно в результате «пляски с бубном» обнаружил, что IniRead() спотыкается на .INI, сохранённых в кодировке UTF-8 c сигнатурой (т.н. «with BOM»).
Вместе с тем, UTF-8 без BOM и оба варианта UTF-16 (как с сигнатурой, так и без неё) обрабатываются нормально.
Код:
Код:
; IniRead() и .INI, сохранённые в Unicode
;
Opt('MustDeclareVars', True)

Local Const $csIni_UTF16  = 'ini_UTF-16.ini', _
            $csIni_UTF16b = 'ini_UTF-16b.ini', _
            $csIni_UTF8   = 'ini_UTF-8.ini', _
            $csIni_UTF8b  = 'ini_UTF-8b.ini'

Local $sValue

; Проба c кодировкой Unicode UTF-8 с сигнатурой
;
$sValue = IniRead($csIni_UTF8b, 'Main', 'Key1', 'Not found!')
ConsoleWrite($csIni_UTF8b & '  -- UTF-8 + BOM' & @CRLF)
ConsoleWrite('$sValue = "' & $sValue & '"' & @CRLF)

; Проба c кодировкой Unicode UTF-8 без сигнатуры (BOM)
;
ConsoleWrite($csIni_UTF8 & '   -- UTF-8 without BOM' & @CRLF)
$sValue = IniRead($csIni_UTF8, 'Main', 'Key1', 'Not found!')
ConsoleWrite('$sValue = "' & $sValue & '"' &  @CRLF)

; Проба c кодировкой Unicode UTF-16 с сигнатурой
;
ConsoleWrite($csIni_UTF16b & ' -- UTF-16 + BOM' & @CRLF)
$sValue = IniRead($csIni_UTF16b, 'Main', 'Key1', 'Not found!')
ConsoleWrite('$sValue = "' & $sValue & '"' &  @CRLF)

; Проба c кодировкой Unicode UTF-16 без сигнатуры (BOM)
;
ConsoleWrite($csIni_UTF16 & '  -- UTF-16 without BOM' & @CRLF)
$sValue = IniRead($csIni_UTF16, 'Main', 'Key1', 'Not found!')
ConsoleWrite('$sValue = "' & $sValue & '"' &  @CRLF)
Exit
Протокол «лабораторной работы»:
Код:
>"H:\DevStudio\AutoIt3\SciTE\AutoIt3Wrapper\AutoIt3Wrapper.exe" /run /prod /ErrorStdOut /in "F:\Alecsis\Prog\AutoIt\Debug\ini_test.au3" /UserParams    
+>14:23:35 Starting AutoIt3Wrapper v.2.1.2.9    Environment(Language:0409  Keyboard:00000409  OS:WIN_XP/Service Pack 3  CPU:X64 OS:X86)
>Running AU3Check (1.54.22.0)  from:H:\DevStudio\AutoIt3
+>14:23:35 AU3Check ended.rc:0
>Running:(3.3.8.1):H:\DevStudio\AutoIt3\autoit3.exe "F:\Alecsis\Prog\AutoIt\Debug\ini_test.au3"    
--> Press Ctrl+Alt+F5 to Restart or Ctrl+Break to Stop
ini_UTF-8b.ini  -- UTF-8 + BOM
$sValue = "Not found!"
ini_UTF-8.ini   -- UTF-8 without BOM
$sValue = "value #1"
ini_UTF-16b.ini -- UTF-16 + BOM
$sValue = "value #1"
ini_UTF-16.ini  -- UTF-16 without BOM
$sValue = "value #1"
+>14:23:35 AutoIt3.exe ended.rc:0
>Exit code: 0    Time: 0.659
P.S. №1 Кстати, IniReadSection() и IniReadSectionNames() ведут себя аналогично; остальные Ini…() проверять не стал, предполагая тот же эффект.
P.S. №2 Примеры .INI в разных кодировках см. во вложении.
 

AZJIO

Меценат
Меценат
Сообщения
2,874
Репутация
1,194
Alecsis1
1. ini_UTF-8.ini у вас содержит английский текст, это значит, что он не отличается от текста в кодировке ASCI, и соответственно при чтении воспринимается как ASCI и без проблем читается.
2. ini_UTF-16b.ini - Для этой кодировки есть 2 варианта "старшими байтами вперёд" и "младшими байтами вперёд", а разница заключается что символ запишется либо 005F либо 5F00. При чём это указано и в описании функции FileOpen и при выборе в меню Notepad++, поэтому я и не слышал про существование варианта "с BOM" для UTF16.
3. Если в качестве ключей или имён секций использовать русские буквы, чтобы реально задействовать кодировку, то только один из 4-х вернёт верно значение, о чём и сказано в справке.
 
Автор
A

Alecsis1

Гость
AZJIO
1. Насчёт ACSII спора нет, а вот с кодами символов >= 0x80 совсем другой расклад:
Код:
; IniRead() и .INI в разных кодировках (продолжение):
; теперь «подпытный кролик» — кириллица в именах секций и ключей
;
Opt('MustDeclareVars', True)

Local Const $csIni_UTF16  = 'iniRu_UTF-16.ini', _
            $csIni_UTF16b = 'iniRu_UTF-16b.ini', _
            $csIni_UTF8   = 'iniRu_UTF-8.ini', _
            $csIni_UTF8b  = 'iniRu_UTF-8b.ini', _
            $csIni_CP1251 = 'iniRu_CP1251.ini', _
            $csSection    = 'ЭЮЯ', _
            $csKey        = 'Ключ1'

Local $sValue

i_Output($csIni_UTF8b,  $csSection, $csKey, 'UTF-8 с сигнатурой')
i_Output($csIni_UTF8,   $csSection, $csKey, 'UTF-8 без сигнатуры')
i_Output($csIni_UTF16,  $csSection, $csKey, 'UTF-16 Low-endian с сигнатурой')
i_Output($csIni_UTF16b, $csSection, $csKey, 'UTF-16 Low-endian без сигнатуры')
i_Output($csIni_CP1251, $csSection, $csKey, 'ANSI 1251')

Exit

; Форматирование и вывод на консоль ==> в отдельную функцию,
;
Func i_Output($sIni, $sSect, $sKey, $sInfo)
  Local $csFormat = 'Кодировка %s\n.INI= %s\nСекция= "%s", ключ= "%s", значение= "%s"\n----\n'
  ConsoleWrite(StringFormat($csFormat, $sInfo, $sIni, $sSect, $sKey, _
               IniRead($sIni, $sSect, $sKey, '[Not found]')))
EndFunc
И вот что имеем:
Код:
>"H:\DevStudio\AutoIt3\SciTE\AutoIt3Wrapper\AutoIt3Wrapper.exe" /run /prod /ErrorStdOut /in "F:\Alecsis\Prog\AutoIt\Debug\iniRu_Test.au3" /UserParams    
+>12:57:34 Starting AutoIt3Wrapper v.2.1.2.9    Environment(Language:0409  Keyboard:00000409  OS:WIN_XP/Service Pack 3  CPU:X64 OS:X86)
>Running AU3Check (1.54.22.0)  from:H:\DevStudio\AutoIt3
+>12:57:34 AU3Check ended.rc:0
>Running:(3.3.8.1):H:\DevStudio\AutoIt3\autoit3.exe "F:\Alecsis\Prog\AutoIt\Debug\iniRu_Test.au3"    
--> Press Ctrl+Alt+F5 to Restart or Ctrl+Break to Stop
Кодировка UTF-8 с сигнатурой
.INI= iniRu_UTF-8b.ini
Секция= "ЭЮЯ", ключ= "Ключ1", значение= "[Not found]"
----
Кодировка UTF-8 без сигнатуры
.INI= iniRu_UTF-8.ini
Секция= "ЭЮЯ", ключ= "Ключ1", значение= "[Not found]"
----
Кодировка UTF-16 Low-endian с сигнатурой
.INI= iniRu_UTF-16.ini
Секция= "ЭЮЯ", ключ= "Ключ1", значение= "Значение_1"
----
Кодировка UTF-16 Low-endian без сигнатуры
.INI= iniRu_UTF-16b.ini
Секция= "ЭЮЯ", ключ= "Ключ1", значение= "Значение_1"
----
Кодировка ANSI 1251
.INI= iniRu_CP1251.ini
Секция= "ЭЮЯ", ключ= "Ключ1", значение= "Значение_1"
----
+>12:57:34 AutoIt3.exe ended.rc:0
>Exit code: 0    Time: 0.710
Как видим, с UTF-16 и ANSI-1251 всё кошерно, а вот с UTF-8 тотальный пролёт мимо кассы что с сигнатурой, что без неё.

2. По поводу сигнатуры (BOM = Byte Order Mark) UTF-16: она таки да существует, и первые 2 байта файла в этой кодировке всегда = 0xFEFF, что и есть тот самый та самая «BOM». В случае UTF-8 BOM = 0xEFBBBF.
См. также скриншоты для каждого варианта во вложении.

3. Вдогонку в качестве «бонуса» покорёжим регистр символов:
Код:
; IniRead() и .INI, сохранённые в разных кодировках (окончание):
; кириллица в именах секций и ключей + регистрозависимость
; UTF-8 и не пробуем, всё равно будет облом
;
Opt('MustDeclareVars', True)

Local Const $csIni_UTF16  = 'iniRu_UTF-16.ini', _
            $csIni_UTF16b = 'iniRu_UTF-16b.ini', _
            $csIni_CP1251 = 'iniRu_CP1251.ini', _
            $csSection    = 'ЭюЯ', _
            $csKey        = 'КлЮч1'

Local $sValue

i_Output($csIni_UTF16,  $csSection, $csKey, 'UTF-16 Low-endian с сигнатурой')
i_Output($csIni_UTF16b, $csSection, $csKey, 'UTF-16 Low-endian без сигнатуры')
i_Output($csIni_CP1251, $csSection, $csKey, 'ANSI 1251')

Exit

; Форматирование и вывод на консоль ==> в отдельную функцию,
;
Func i_Output($sIni, $sSect, $sKey, $sInfo)
  Local $csFormat = 'Кодировка %s\n.INI= %s\nСекция= "%s", ключ= "%s", значение= "%s"\n----\n'
  ConsoleWrite(StringFormat($csFormat, $sInfo, $sIni, $sSect, $sKey, _
               IniRead($sIni, $sSect, $sKey, '[Not found]')))
EndFunc
Протокол следственного эксперимента:
Код:
>"H:\DevStudio\AutoIt3\SciTE\AutoIt3Wrapper\AutoIt3Wrapper.exe" /run /prod /ErrorStdOut /in "F:\Alecsis\Prog\AutoIt\Debug\iniRu_Test2.au3" /UserParams    
+>14:46:01 Starting AutoIt3Wrapper v.2.1.2.9    Environment(Language:0409  Keyboard:00000409  OS:WIN_XP/Service Pack 3  CPU:X64 OS:X86)
>Running AU3Check (1.54.22.0)  from:H:\DevStudio\AutoIt3
+>14:46:01 AU3Check ended.rc:0
>Running:(3.3.8.1):H:\DevStudio\AutoIt3\autoit3.exe "F:\Alecsis\Prog\AutoIt\Debug\iniRu_Test2.au3"    
--> Press Ctrl+Alt+F5 to Restart or Ctrl+Break to Stop
Кодировка UTF-16 Low-endian с сигнатурой
.INI= iniRu_UTF-16.ini
Секция= "ЭюЯ", ключ= "КлЮч1", значение= "Значение_1"
----
Кодировка UTF-16 Low-endian без сигнатуры
.INI= iniRu_UTF-16b.ini
Секция= "ЭюЯ", ключ= "КлЮч1", значение= "Значение_1"
----
Кодировка ANSI 1251
.INI= iniRu_CP1251.ini
Секция= "ЭюЯ", ключ= "КлЮч1", значение= "[Not found]"
----
+>14:46:02 AutoIt3.exe ended.rc:0
>Exit code: 0    Time: 0.545
Резюме:
  • в кодировке ANSI-1251 имена секций/ключей регистрозависимы, в UTF-16 — нет;
  • нестыковка IniRead() с кодировкой UTF-8 всё же существует, причём в официальном хелпе v3.3.8.1 на эту тему ни слова

Вот, собсс-но, и всё… первоначальный вопрос остаётся открытым, а FileOpen(), имхо, здесь как-то не по теме.

P.S. Маловероятно, что кому-либо в здравом уме и трезвом состоянии придёт в голову так извратиться с именами секций/ключей, да ещё в UTF-8 , однако «осадочек остался» © ;)
:beer:
 

AZJIO

Меценат
Меценат
Сообщения
2,874
Репутация
1,194
Alecsis1
Я предложил добавить информацию о кодировке в ini-файле и в версию 3.3.9... была добавлена строка:
IniWrite
If you want to use an ini file with Unicode encoding, first create an .ini file by using the FileOpen() function with the mode parameter set to a Unicode parameter.
. То есть 3.3.8.1 работает также, но много началось изменяться начиная с версии 3.3.9..., потому что я настойчиво отписывал все недостатки.

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

Можно использовать универсальный вариант IniVirtual. С помощью FileGetEncoding получить кодировку, открыть файл с помощью FileOpen, указав соответствующий тип кодировки и сохранять в той же кодировке. Тогда не придётся перекодировать кракозябры, сразу все проблемы решает, не зависимо от того, в какой кодировке был файл.
 

autoall

Новичок
Сообщения
21
Репутация
0
За эти 8 лет в плане чтения кириллицы из INI какие-то решения появились? А то я полазил по форуму и ничего толком не нашёл. В AutoIt 3.3.14.5 IniRead грузит крякозябли из UTF-8 для кириллического значения.

Попробовал открыть файл через FileOpen, принудительно указав UTF-8. Получил данные через FileRead и закинул их в _IniVirtual_Initial. Но _IniVirtual_Read работает криво - отдаёт только самый первый параметр в любой секции, а остальные не видит. Я понял, что вся проблема утыкается в $i = _ArraySearch($aKey, $sKey, 1, 0, 0, 2, 1, 0), причём массив $aKey создаётся нормальным и непонятно почему _ArraySearch для поиска секции отрабатывает нормально, а ниже, для ключа, уже не работает как надо - как это исправить непонятно. Как вариант можно на AutoIt 3.3.8.1 откатиться. Но... 2021 год же... как же ж так? )
 

Spray

Новичок
Сообщения
17
Репутация
2
Я пользуюсь самописным костылём:
Код:
func IniReadSectionUTF8(const $filename, const $section)
    local const $file = FileOpen($filename, $FO_UTF8_NOBOM)
    if @error then return SetError(1)
    FileSetPos($file, 0, 0)
    while FileReadLine($file) <> "[" & $section & "]"
        if @error then
            FileClose($filename)
            return SetError(2)
        endif
    wend
    local $result[1][2]
    $result[0][0] = 0
    local $line
    while True
        $line = FileReadLine($file)
        if @error or StringRegExp($line, "^\[.*\]$") then exitloop
        $line = StringSplit($line, "=", $STR_NOCOUNT)
        if @error then continueloop
        redim $result[UBound($result) + 1][2]
        $result[0][0] += 1
        $result[UBound($result) - 1][0] = $line[0]
        $result[UBound($result) - 1][1] = $line[1]
    wend
    FileClose($file)
    return $result
endfunc
 
Последнее редактирование:

autoall

Новичок
Сообщения
21
Репутация
0
Я в результате тоже костылик приделал в виде своей полностью совместимой функции _IniRead, которая определяет кодировку файла, открывает его в нужной кодировке, сохраняет содержимое во временный файл в "правильной" кодировке ANSI из которого уже стандартной функцией IniRead вытаскивается ключик. Это далеко от совершенства и оптимальности, но я из INI читаю только разово при запуске, поэтому не критично. Плюс я еще напароноил с кодами возвращаемых ошибок если что-то где-то когда-то пойдёт не так - это всё в лог сбрасывается и если что можно посмотреть и разобраться.

Код:
Func _IniRead($filename, $section, $key, $default)

  Local $filename_format = 0
  Local $hFile
  Local $data_ini = ""
  Local $filename_temp = "_temp.ini"
  Local $key_ini = ""

  SetError(0)

  $filename_format = FileGetEncoding($filename, 1)
  If (@error) Then
    SetError(1)
    Return
  EndIf

  If ($filename_format = 512) Then
    $key_ini = IniRead($filename, $section, $key, $default)
    If (@error) Then
      SetError(2)
      Return
    EndIf
  Else
    $hFile = FileOpen($filename, 0+$filename_format)
    If (@error) Then
      SetError(3)
      Return
    Else
      $data_ini = FileRead($hFile)
      If (@error) Then
        SetError(4)
        Return
      EndIf
    EndIf

    FileClose($hFile)
    If (@error) Then
      SetError(5)
      Return
    EndIf

    $hFile = FileOpen($filename_temp, 1+512)
    If (@error) Then
      SetError(6)
      Return
    EndIf

    FileWrite($hFile, $data_ini)
    If (@error) Then
      SetError(7)
      Return
    EndIf

    FileClose($hFile)
    If (@error) Then
      SetError(8)
      Return
    EndIf

    $key_ini = IniRead($filename_temp, $section, $key, $default)

    FileDelete($filename_temp)
    If (@error) Then
      SetError(9)
      Return
    EndIf
  EndIf

  Return ($key_ini)

EndFunc
 
Верх