Есть ли в Java iconv с эквивалентом // TRANSLIT? - PullRequest
13 голосов
/ 27 апреля 2011

Есть ли способ добиться транслитерации символов между кодировками в Java? что-то похожее на команду unix (или похожую функцию php):

iconv -f UTF-8 -t ASCII//TRANSLIT < some_doc.txt  > new_doc.txt

желательно работать со строками, не имея ничего общего с файлами

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

Ответы [ 3 ]

11 голосов
/ 27 апреля 2011

Я не знаю ни одной библиотеки, которая делает именно то, что iconv намеревается сделать (что, кажется, не очень четко определено).Тем не менее, вы можете использовать «нормализация» в Java для таких вещей, как удаление акцентов из символов.Этот процесс хорошо определен стандартами Unicode.

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

/* Decompose original "accented" string to basic characters. */
String decomposed = Normalizer.normalize(accented, Normalizer.Form.NFKD);
/* Build a new String with only ASCII characters. */
StringBuilder buf = new StringBuilder();
for (int idx = 0; idx < decomposed.length(); ++idx) {
  char ch = decomposed.charAt(idx);
  if (ch < 128)
    buf.append(ch);
}
String filtered = buf.toString();

С помощью фильтрации, приведенной здесь, вы можете сделать некоторые строки нечитаемыми.Например, строка китайских символов будет полностью отфильтрована, потому что ни один из них не имеет представления ASCII (это больше похоже на iconv's //IGNORE).

В целом, было бы безопаснее создать собственную таблицу поискадопустимых замен символов или, по крайней мере, комбинации символов (акценты и вещи), которые можно удалить.Наилучшее решение зависит от диапазона вводимых символов, которые вы ожидаете обработать.

4 голосов
/ 03 апреля 2014

Давайте начнем с небольшого изменения ответа Эриксона и построим на нем больше //TRANSLIT функций:

Разложим символы, чтобы получить ASCII- String

public class Translit {

    private static final Charset US_ASCII = Charset.forName("US-ASCII");
    private static String toAscii(final String input) {
        final CharsetEncoder charsetEncoder = US_ASCII.newEncoder();
        final char[] decomposed = Normalizer.normalize(input, Normalizer.Form.NFKD).toCharArray();
        final StringBuilder sb = new StringBuilder(decomposed.length);

        for (int i = 0; i < decomposed.length; ) {
            final int codePoint = Character.codePointAt(decomposed, i);
            final int charCount = Character.charCount(codePoint);

            if(charsetEncoder.canEncode(CharBuffer.wrap(decomposed, i, charCount))) {
                sb.append(decomposed, i, charCount);
            }

            i += charCount;
        }
        return sb.toString();
    }


    public static void main(String[] args) {
        final String a = "Michèleäöüß";
        System.out.println(a + " => " + toAscii(a));
        System.out.println(a.toUpperCase() + " => " + toAscii(a.toUpperCase()));
    }
}

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

Функция безопасна для дополнительных кодовых точек (что является немного избыточным для ASCII в качестве цели, но может уменьшить головную боль, есливыбрана другая целевая кодировка).

Также обратите внимание, что возвращается обычная строка Java;если вам нужен ASCII- byte[], вам все равно нужно преобразовать его (но, как мы убедились, нет оскорбительных символов ...).

И вот как вы можете расширить его на большее количество наборов символов:

Заменить или разложить символы для получения String, кодируемого в поставляемом Charset

import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.Normalizer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Created for http://stackoverflow.com/a/22841035/1266906
 */
public class Translit {
    public static final Charset                  US_ASCII     = Charset.forName("US-ASCII");
    public static final Charset                  ISO_8859_1   = Charset.forName("ISO-8859-1");
    public static final Charset                  UTF_8        = Charset.forName("UTF-8");
    public static final HashMap<Integer, String> REPLACEMENTS = new ReplacementBuilder().put('„', '"')
                                                                                              .put('“', '"')
                                                                                              .put('”', '"')
                                                                                              .put('″', '"')
                                                                                              .put('€', "EUR")
                                                                                              .put('ß', "ss")
                                                                                              .put('•', '*')
                                                                                              .getMap();

    private static String toCharset(final String input, Charset charset) {
        return toCharset(input, charset, Collections.<Integer, String>emptyMap());
    }

    private static String toCharset(final String input,
                                    Charset charset,
                                    Map<? super Integer, ? extends String> replacements) {
        final CharsetEncoder charsetEncoder = charset.newEncoder();
        return toCharset(input, charsetEncoder, replacements);
    }

    private static String toCharset(String input,
                                    CharsetEncoder charsetEncoder,
                                    Map<? super Integer, ? extends String> replacements) {
        char[] data = input.toCharArray();
        final StringBuilder sb = new StringBuilder(data.length);

        for (int i = 0; i < data.length; ) {
            final int codePoint = Character.codePointAt(data, i);
            final int charCount = Character.charCount(codePoint);

            CharBuffer charBuffer = CharBuffer.wrap(data, i, charCount);
            if (charsetEncoder.canEncode(charBuffer)) {
                sb.append(data, i, charCount);
            } else if (replacements.containsKey(codePoint)) {
                sb.append(toCharset(replacements.get(codePoint), charsetEncoder, replacements));
            } else {
                // Only perform NFKD Normalization after ensuring the original character is invalid as this is a irreversible process
                final char[] decomposed = Normalizer.normalize(charBuffer, Normalizer.Form.NFKD).toCharArray();
                for (int j = 0; j < decomposed.length; ) {
                    int decomposedCodePoint = Character.codePointAt(decomposed, j);
                    int decomposedCharCount = Character.charCount(decomposedCodePoint);

                    if (charsetEncoder.canEncode(CharBuffer.wrap(decomposed, j, decomposedCharCount))) {
                        sb.append(decomposed, j, decomposedCharCount);
                    } else if (replacements.containsKey(decomposedCodePoint)) {
                        sb.append(toCharset(replacements.get(decomposedCodePoint), charsetEncoder, replacements));
                    }

                    j += decomposedCharCount;
                }
            }

            i += charCount;
        }
        return sb.toString();
    }


    public static void main(String[] args) {
        final String a = "Michèleäöü߀„“”″•";
        System.out.println(a + " => " + toCharset(a, US_ASCII));
        System.out.println(a + " => " + toCharset(a, ISO_8859_1));
        System.out.println(a + " => " + toCharset(a, UTF_8));

        System.out.println(a + " => " + toCharset(a, US_ASCII, REPLACEMENTS));
        System.out.println(a + " => " + toCharset(a, ISO_8859_1, REPLACEMENTS));
        System.out.println(a + " => " + toCharset(a, UTF_8, REPLACEMENTS));
    }

    public static class MapBuilder<K, V> {

        private final HashMap<K, V> map;

        public MapBuilder() {
            map = new HashMap<K, V>();
        }

        public MapBuilder<K, V> put(K key, V value) {
            map.put(key, value);
            return this;
        }

        public HashMap<K, V> getMap() {
            return map;
        }
    }

    public static class ReplacementBuilder extends MapBuilder<Integer, String> {
        public ReplacementBuilder() {
            super();
        }

        @Override
        public ReplacementBuilder put(Integer input, String replacement) {
            super.put(input, replacement);
            return this;
        }

        public ReplacementBuilder put(Integer input, char replacement) {
            return this.put(input, String.valueOf(replacement));
        }

        public ReplacementBuilder put(char input, String replacement) {
            return this.put((int) input, replacement);
        }

        public ReplacementBuilder put(char input, char replacement) {
            return this.put((int) input, String.valueOf(replacement));
        }
    }
}

Я бы настоятельно рекомендовал создать обширную таблицу замен, поскольку простой пример уже показывает, как вы иначеможет потерять нужную информацию, такую ​​как .Для ASCII эта реализация, конечно, немного медленнее, поскольку декомпозиция выполняется только по требованию, и теперь StringBuilder может потребоваться увеличение для хранения замен.

В GNU iconv используются замены, перечисленные в translit.def для выполнения //TRANSLIT -конверсии, и вы можете использовать метод, подобный этому, если вы хотите использовать его в качестве карты-замены:

Импортировать оригинальные //TRANSLIT -замены

private static Map<Integer, String> readReplacements() {
    HashMap<Integer, String> map = new HashMap<>();
    InputStream stream = Translit.class.getResourceAsStream("/translit.def");
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream, UTF_8));
    Pattern pattern = Pattern.compile("^([0-9A-Fa-f]+)\t(.?[^\t]*)\t#(.*)$");
    try {
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            if (line.charAt(0) != '#') {
                Matcher matcher = pattern.matcher(line);
                if (matcher.find()) {
                    map.put(Integer.valueOf(matcher.group(1), 16), matcher.group(2));
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return map;
}
4 голосов
/ 19 марта 2012

Одним из решений является выполнение execute iconv как внешнего процесса. Это наверняка обидит пуристов. Это зависит от наличия iconv в системе, но он работает и делает именно то, что вы хотите:

public static String utfToAscii(String input) throws IOException {
    Process p = Runtime.getRuntime().exec("iconv -f UTF-8 -t ASCII//TRANSLIT");
    BufferedWriter bwo = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
    BufferedReader bri = new BufferedReader(new InputStreamReader(p.getInputStream()));
    bwo.write(input,0,input.length());
    bwo.flush();
    bwo.close();
    String line  = null;
    StringBuilder stringBuilder = new StringBuilder();
    String ls = System.getProperty("line.separator");
    while( ( line = bri.readLine() ) != null ) {
        stringBuilder.append( line );
        stringBuilder.append( ls );
    }
    bri.close();
    try {
        p.waitFor();
    } catch ( InterruptedException e ) {
    }
    return stringBuilder.toString();
}
...