File.listFiles () меняет имена Unicode с JDK 6 (проблемы нормализации Unicode) - PullRequest
36 голосов
/ 31 августа 2010

Я борюсь со странной проблемой кодировки имен файлов при перечислении содержимого каталогов в Java 6 как в OS X, так и в Linux: File.listFiles() и связанные методы, похоже, возвращают имена файлов в другой кодировке, чем остальная система.

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

Вот программа дляпродемонстрировать.Он создает файл с именем Unicode, затем печатает версии с кодировкой URL версий имен файлов, полученных из непосредственно созданного файла, и тот же файл, если он указан в родительском каталоге (этот код следует запустить впустой каталог).Результаты показывают различную кодировку, возвращаемую методом File.listFiles().

String fileName = "Trîcky Nåme";
File file = new File(fileName);
file.createNewFile();
System.out.println("File name: " + URLEncoder.encode(file.getName(), "UTF-8"));

// Get parent (current) dir and list file contents
File parentDir = file.getAbsoluteFile().getParentFile();
File[] children = parentDir.listFiles();
for (File child: children) {
    System.out.println("Listed name: " + URLEncoder.encode(child.getName(), "UTF-8"));
}

Вот что я получаю, когда запускаю этот тестовый код на своих системах.Обратите внимание на символьные представления %CC против %C3.

OS X Snow Leopard:

File name: Tri%CC%82cky+Na%CC%8Ame
Listed name: Tr%C3%AEcky+N%C3%A5me

$ java -version
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)

KUbuntu Linux (работает на виртуальной машине в той же системе OS X):

File name: Tri%CC%82cky+Na%CC%8Ame
Listed name: Tr%C3%AEcky+N%C3%A5me

$ java -version
java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1)
OpenJDK Client VM (build 16.0-b13, mixed mode, sharing)

Я пробовал различные способы взлома, чтобы согласовать строки, включая установку системного свойства file.encoding и различных переменных среды LC_CTYPE и LANG.Ничего не помогает, и я не хочу прибегать к таким взломам.

В отличие от этого (несколько связанного?) Вопроса , я могу читать данные из перечисленных файлов, несмотря на нечетные имена

Ответы [ 6 ]

16 голосов
/ 01 сентября 2010

Используя Unicode, существует более одного допустимого способа представления одной и той же буквы. Символы, которые вы используете в своем Tricky Name, - это «латинская строчная буква i с кружком» и «латинская строчная буква a с кольцом над».

Вы говорите: «Обратите внимание на %CC против %C3 символьных представлений», но при ближайшем рассмотрении вы видите последовательности

i 0xCC 0x82 vs. 0xC3 0xAE
a 0xCC 0x8A vs. 0xC3 0xA5

То есть первая буква i, за которой следует 0xCC82, которая представляет собой кодировку UTF-8 символа Unicode \u0302 «комбинирующий акцент на акцент», а вторая - UTF-8 для \u00EE "латинская строчная буква i с обхватом". Аналогично для другой пары, первая - это буква a, за которой следует 0xCC8A - символ «объединяющее кольцо над», а вторая - «латинская строчная буква а с кольцом над». Оба они являются допустимыми кодировками UTF-8 действительных символьных строк Unicode, но одна находится в «составленном», а другая в «разложенном» формате.

Тома OS X HFS Plus хранят строки (например, имена файлов) как «полностью разложенные». Файловая система Unix действительно хранится в соответствии с тем, как драйвер файловой системы выбирает ее для хранения. Вы не можете делать какие-либо общие утверждения для разных типов файловых систем.

См. Статью в Википедии о Эквивалентность Unicode для общего обсуждения составных и разложенных форм, в которых конкретно упоминается OS X.

Информацию о преобразовании форм см. В технических вопросах и ответах Apple QA1235 (в Objective-C, к сожалению).

A недавняя ветка электронной почты в списке рассылки Apple java-dev может вам чем-то помочь.

По сути, вам нужно нормализовать разложенную форму в составленную форму, прежде чем вы сможете сравнивать строки.

2 голосов
/ 19 сентября 2015

Решение, извлеченное из вопроса:

Спасибо Стивену П., который поставил меня на правильный путь.

Исправление во-первых, для нетерпеливых. Если вы компилируете с Java 6, вы можете использовать класс java.text.Normalizer , чтобы нормализовать строки в обычную форму по вашему выбору, например,

// Normalize to "Normalization Form Canonical Decomposition" (NFD)
protected String normalizeUnicode(String str) {
    Normalizer.Form form = Normalizer.Form.NFD;
    if (!Normalizer.isNormalized(str, form)) {
        return Normalizer.normalize(str, form);
    }
    return str;
}

Поскольку java.text.Normalizer доступен только в Java 6 и более поздних версиях, если вам нужно скомпилировать с Java 5, вам, возможно, придется прибегнуть к реализации sun.text.Normalizer и что-то вроде этого взлом на основе отражения также Как работает эта нормализующая функция?

Одного этого достаточно, чтобы решить, что я не буду поддерживать компиляцию моего проекта с Java 5: |

Вот и другие интересные вещи, которые я узнал в этом грязном приключении.

  • Путаница вызвана тем, что имена файлов находятся в одной из двух форм нормализации, которые нельзя сравнивать напрямую: форму нормализации каноническая декомпозиция (NFD) или форму нормализации каноническая композиция (NFC). Первый обычно имеет буквы ASCII, за которыми следуют «модификаторы» для добавления акцентов и т. Д., В то время как последний имеет только расширенные символы без ведущего символа ACSCII. Прочитайте вики-страницу ссылки Стивена П. для лучшего объяснения.

  • Строковые литералы Unicode, подобные тем, которые содержатся в примере кода (и полученные по HTTP в моем реальном приложении), представлены в форме NFD, а имена файлов, возвращаемые методом File.listFiles(), являются NFC. Следующий мини-пример демонстрирует различия:

    String name = "Trîcky Nåme";
    System.out.println("Original name: " + URLEncoder.encode(name, "UTF-8"));
    System.out.println("NFC Normalized name: " + URLEncoder.encode(
        Normalizer.normalize(name, Normalizer.Form.NFC), "UTF-8"));
    System.out.println("NFD Normalized name: " + URLEncoder.encode(
        Normalizer.normalize(name, Normalizer.Form.NFD), "UTF-8"));
    

    Выход:

    Original name: Tri%CC%82cky+Na%CC%8Ame
    NFC Normalized name: Tr%C3%AEcky+N%C3%A5me
    NFD Normalized name: Tri%CC%82cky+Na%CC%8Ame
    
  • Если вы создаете объект File со строковым именем, метод File.getName() вернет имя в той форме, которую вы ему дали изначально . Однако если вы вызываете File методы, которые обнаруживают имена самостоятельно, они, похоже, возвращают имена в форме NFC. Это потенциально неприятный вопрос. Это конечно попало.

  • Согласно приведенной ниже цитате из документации Apple имена файлов хранятся в разложенном виде (NFD) в файловой системе HFS Plus:

    При работе в Mac OS вы обнаружите, что используете смесь предварительно составленного и разложенного Unicode. Например, HFS Plus преобразует все имена файлов в разложенный Unicode, в то время как клавиатуры Macintosh обычно создают предварительно составленный Unicode.

    Таким образом, метод File.listFiles() помогает (?) Преобразовать имена файлов в (предварительно) составленную (NFC) форму.

1 голос
/ 31 августа 2010

Я видел нечто подобное раньше.Люди, которые загружают файлы со своего Mac в веб-приложение, используют имена файлов с é.

a) В ОС это обычный символ e + «знак ´, примененный к предыдущему символу»

b) ВДля Windows это специальный символ: é

Оба Unicode.Итак ... я понимаю, что вы передаете опцию (b) File create и в какой-то момент Mac OS преобразует ее в опцию (a).Возможно, если вы обнаружите проблему двойного представительства в Интернете, вы сможете найти способ успешно справиться с обеими ситуациями.

Надеюсь, это поможет!

0 голосов
/ 05 марта 2014

Альтернативное решение - использовать новый API java.nio.Path вместо API java.io.File, который отлично работает.

0 голосов
/ 31 августа 2010

Я подозреваю, что вам просто нужно указать javac, какую кодировку использовать для компиляции файла .java, содержащего специальные символы, поскольку вы жестко закодировали его в исходном файле.В противном случае будет использоваться кодировка платформы по умолчанию, которая может вообще не соответствовать UTF-8.

Для этого можно использовать аргумент VM -encoding.

javac -encoding UTF-8 com/example/Foo.java

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

0 голосов
/ 31 августа 2010

В файловой системе Unix имя файла на самом деле является байтом с нулем в конце []. Таким образом, среда выполнения java должна выполнить преобразование из java.lang.String в byte [] во время операции createNewFile (). Преобразование символов в байты регулируется локалью. Я тестировал настройки LC_ALL до en_US.UTF-8 и en_US.ISO-8859-1 и получил согласованные результаты. Это с Sun (... Oracle) Java 1.6.0_20. Однако для LC_ALL=en_US.POSIX результат будет:

File name:   Tr%C3%AEcky+N%C3%A5me
Listed name: Tr%3Fcky+N%3Fme

3F - знак вопроса. Это говорит мне, что преобразование не было успешным для не-ASCII символа. Опять же, все как и ожидалось.

Но причина, по которой ваши две строки различаются, заключается в эквивалентности между символом \ u00EE (или C3 AE в UTF-8) и последовательностью i + \ u0302 (69 CC 82 в UTF-8). \ u0302 является объединяющим диакритическим знаком (комбинированным акцентом). Некоторая нормализация произошла во время создания файла. Я не уверен, что это делается во время выполнения Java или в ОС.

ПРИМЕЧАНИЕ. Я потратил некоторое время на то, чтобы выяснить это, поскольку фрагмент кода, который вы разместили, не имеет диакритического знака, но эквивалентного символа î (например, \u00ee). Вы должны были встроить escape-последовательность Unicode в строковый литерал (но это легко сказать потом ...).

...