Еще немного о кодировках Google

в 22:53, , рубрики: Google API, кодировка, разработка, метки: ,

В этом топике уже поднимался вопрос о кодировках сервисов Google. Однако там речь шла о некорректности текстов соглашений. Я же в одном из своих проектов столкнулся с проблемами кодировок при работе с одним из API Google. Пикантность ситуации в том, что проблема возникла при работе с недокументированным API, и «палиться» в службе поддержки очень не хотелось. Поиск в сети решений не дал (варианты с «повторять пока не заработает» не принимались как серьезные). Как же мне удалось найти выход и решить всё самому?
Сначала о проекте:
В свободное время разрабатываю переводчик для мобильных телефонов, платформы J2ME, Blackberry и Android. В какой то момент на форумах, где идет обсуждение программы, ребята начали жаловаться на непонятный баг. В рандомном порядке вместо переведенного текста пользователи получали какие то «иероглифы». Появлялись они в одном случае из 5-10 переводов, а могли вовсе не беспокоить человека несколько дней. Географии определенной не было (жалобы были и со стран СНГ, и с Латинской Америки, и с Азии, и с Европы). Единственное что объединяло — это модель телефонов. В приложении встроен логгер, и можно отправить его содержимое мне на почту одним нажатием кнопки. Я внес небольшие правки, и туда начались писаться результаты перевода. Иногда ребята присылали логи, но понять в чем дело так и не удавалось.
Знакомимся с багом:
Так бы проблема и не была решена, пока мне в руки не попался Samsung C3510 Corby. Установив на него приложение, я обнаружил что там в 100 случаев из 100 перевод приходит в «иероглифах». Ок, проблемы с кириллицей дело известное. Каково же было мое удивление, когда даже перевод с английского на французский привел к такому же результату. А вот это уже необычно.
Так что же за чертовщина там происходит:
Изрядно поиздевавшись с переводом, я отправил письмо и стал смотреть его уже на ПК.
Интересными оказались некоторые моменты:
-спецсимволы (двоеточие, скобки и так далее) приходили правильно;
-кириллица приходила не верно;
-латыница приходила тоже не верно;
-установка кодировки UTF-8 в заголовки подключения ничего не дала;
-установка кодировки в тело POST-запроса так же не решила проблему;
-установка User-Agent'a не влияет на результат;
Напрашивался вывод что используется нестандартная для сервисов кодировка, к тому же она не ASCII -based, так как английские в таким случае должны были бы быть в нормальном виде. К тому же баг как то привязан к конкретной модели телефона.
Как же решить:
Количество кодировок в телефонах изначально небольшая (UTF-8, ISO 8859-1 и еще парочка, если повезет), поэтому пришлось написать «ручное» декодирование массива байт в текст нужной кодировки. Тестовое приложение переводило «Привет мир», и в цикле перебирало все кодировки, принтя в консоль полученный текст. CP1251, ISO-8859-7 и так далее естественно не оправдали ожидание, а вот корректный текст был получен (как оказалось, этот комментарий был пророческим) с кодировкой KOI8-RU. На остальных телефонах срабатывает стандартная UTF-8.

Для тех кто любит технические детали

    /**
    *****j2me реализация******
     **/
    public static String detectEncoding() {
        try {
            String sentence = "Привет Мир";

            String qq = encodeSequence(sentence);

            HttpConnection net = (HttpConnection) Connector.open(query,
                    Connector.READ_WRITE, true);

            net.setRequestMethod(HttpConnection.POST);

            OutputStream output = net.openOutputStream();

            output.write(("sl=ru&tl=en&client=t&text=" + qq).getBytes());

            output.close();

            int resp = net.getResponseCode();

            if (resp == HttpConnection.HTTP_OK) {

                InputStream is = net.openInputStream();

                ByteArrayOutputStream out = new ByteArrayOutputStream();

                int b = 1;

                while ((b = is.read()) >= 0) {

                    out.write(b);
                }

                out.flush();

                is.close();

                net.close();

                byte[] buff = out.toByteArray();

                String enc = detectEncoding(buff, sentence);

                if (!enc.equals("")) {

                    return enc;
                }

            } else {

                net.close();

                throw new Exception("Invalid ResponseCode: " + resp);
            }
        } catch (Exception e) {

            e.printStackTrace();
        }

        return "UTF-8";
    }
    public static String[] charsets = new String[]{"WINDOWS-1251", "KOI8-R", "WINDOWS-1257", "ISO-8859-1", "ISO-8859-2", "UTF-8", "UNICODE"};
    protected static char[] iso8859_1map = "u0402u0403u201au201eu201eu2026u2020u2021u20acu2030u0409u2039u040au040cu040bu040fu0452u2018u2019u201cu201du2022u2013u2014u2122u0459u203au045au045cu045bu045f u040eu045eu0408u00a4u0490u00a6u00a7u0401u00a9u0404u00abu00acu00adu00aeu0407u00b0Zu00b1u0406u0456u0491u00b5u00b6u00b7u0451u2116u0454u00bbu0458u0405u0455u0457u0410u0411u0412u0413u0414u0415u0416u0417u0418u0419u041au041bu041cu041du041eu041fu0420u0421u0422u0423u0424u0425u0426u0427u0428u0429u042cu042bu042au042du042eu042fu0430u0431u0432u0433u0434u0435u0436u0437u0438u0439u043au043bu043cu043du043eu043fu0440u0441u0442u0443u0444u0445u0446u0447u0448u0449u044au044bu044cu044du044eu044f".toCharArray();
    protected static char[] cp1251map = "u0402u0403u201Au0453u201Eu2026u2020u2021u20ACu2030u0409u2039u040Au040Cu040Bu040Fu0452u2018u2019u201Cu201Du2022u2013u2014uFFFDu2122u0459u203Au045Au045Cu045Bu045Fu00A0u040Eu045Eu0408u00A4u0490u00A6u00A7u0401u00A9u0404u00ABu00ACu00ADu00AEu0407u00B0u00B1u0406u0456u0491u00B5u00B6u00B7u0451u2116u0454u00BBu0458u0405u0455u0457u0410u0411u0412u0413u0414u0415u0416u0417u0418u0419u041Au041Bu041Cu041Du041Eu041Fu0420u0421u0422u0423u0424u0425u0426u0427u0428u0429u042Au042Bu042Cu042Du042Eu042Fu0430u0431u0432u0433u0434u0435u0436u0437u0438u0439u043Au043Bu043Cu043Du043Eu043Fu0440u0441u0442u0443u0444u0445u0446u0447u0448u0449u044Au044Bu044Cu044Du044Eu044F"
            .toCharArray();
    protected static char[] cp1257map = "u20ACu201Au201Eu2026u2020u2021u2030u2039250u02C7270u2018u2019u201Cu201Du2022u2013u2014u2122u203A257u02DB240242243244246247330251u0156253254255256306260261262263264265266267370271u0157273274275276346u0104u012Eu0100u0106304305u0118u0112u010C311u0179u0116u0122u0136u012Au013Bu0160u0143u0145323u014C325326327u0172u0141u015Au016A334u017Bu017D337u0105u012Fu0101u0107344345u0119u0113u010D351u017Au0117u0123u0137u012Bu013Cu0161u0144u0146363u014D365366367u0173u0142u015Bu016B374u017Cu017Eu02D9"
            .toCharArray();
    protected static char[] iso8859_2map = "200201202203204205206207210211212213214215216217220221222223224225226227230231232233234235236237240u0104u02D8u0141244u013Du015A247250u0160u015Eu0164u0179255u017Du017B260u0105u02DBu0142264u013Eu015Bu02C7270u0161u015Fu0165u017Au02DDu017Eu017Cu0154301302u0102304u0139u0106307u010C311u0118313u011A315316u010Eu0110u0143u0147323324u0150326327u0158u016E332u0170334335u0162337u0155341342u0103344u013Au0107347u010D351u0119353u011B355356u010Fu0111u0144u0148363364u0151366367u0159u016F372u0171374375u0163u02D9"
            .toCharArray();
    protected static char[] koi8rmap = "u2500u2502u250Cu2510u2514u2518u251Cu2524u252Cu2534u253Cu2580u2584u2588u258Cu2590u2591u2592u2593u2320u25A0u2219u221Au2248u2264u2265u00A0u2321u00B0u00B2u00B7u00F7u2550u2551u2552u0451u2553u2554u2555u2556u2557u2558u2559u255Au255Bu255Cu255Du255Eu255Fu2560u2561u0401u2562u2563u2564u2565u2566u2567u2568u2569u256Au256Bu256Cu00A9u044Eu0430u0431u0446u0434u0435u0444u0433u0445u0438u0439u043Au043Bu043Cu043Du043Eu043Fu044Fu0440u0441u0442u0443u0436u0432u044Cu044Bu0437u0448u044Du0449u0447u044Au042Eu0410u0411u0426u0414u0415u0424u0413u0425u0418u0419u041Au041Bu041Cu041Du041Eu041Fu042Fu0420u0421u0422u0423u0416u0412u042Cu042Bu0417u0428u042Du0429u0427u042A"
            .toCharArray();

    public static String detectEncoding(byte[] bytes, String exemple) {

        for (int i = 0; i < charsets.length; i++) {

            String ss = byteArrayToString(bytes, charsets[i]);

            if (ss.indexOf(exemple) != -1) {

                return charsets[i];
            }

        }

        return "";
    }

    public static String byteArrayToString(byte[] bytes, String charSet) {


        String output;
        char[] map = null;

        if (charSet.equalsIgnoreCase("WINDOWS-1251")
                || charSet.equalsIgnoreCase("WINDOWS1251")
                || charSet.equalsIgnoreCase("WIN1251")
                || charSet.equalsIgnoreCase("CP1251")) {
            map = cp1251map;
        } else if (charSet.equalsIgnoreCase("KOI8-R")) {
            map = koi8rmap;
        } else if (charSet.equalsIgnoreCase("WINDOWS-1257")) {
            map = cp1257map;
        } else if (charSet.equalsIgnoreCase("ISO-8859-1")) {
            map = iso8859_1map;
        } else if (charSet.equalsIgnoreCase("ISO-8859-2")) {
            map = iso8859_2map;
        } else if (charSet.equalsIgnoreCase("UTF-8")) {

            try {

                return (decodeUTF8(bytes, false));
            } catch (Exception udfe) {
            }

            map = cp1251map;
        }


        if (map != null) {
            char[] chars = new char[bytes.length];
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[i] = (b >= 0) ? (char) b : map[b + 128];
            }
            output = new String(chars);
        } else {
            try {
                output = new String(bytes, charSet);
            } catch (UnsupportedEncodingException e) {
                output = new String(bytes);
            }
        }
        return output;
    }

    private static String decodeUTF8(byte[] data, boolean gracious)
            throws UTFDataFormatException {
        byte a, b, c;
        StringBuffer ret = new StringBuffer();

        for (int i = 0; i < data.length; i++) {
            try {
                a = data[i];
                if ((a & 0x80) == 0) {
                    ret.append((char) a);
                } else if ((a & 0xe0) == 0xc0) {
                    b = data[i + 1];
                    if ((b & 0xc0) == 0x80) {
                        ret.append((char) (((a & 0x1F) << 6) | (b & 0x3F)));
                        i++;
                    } else {
                        throw new UTFDataFormatException("Illegal 2-byte group");
                    }
                } else if ((a & 0xf0) == 0xe0) {
                    b = data[i + 1];
                    c = data[i + 2];
                    if (((b & 0xc0) == 0x80) && ((c & 0xc0) == 0x80)) {
                        ret.append((char) (((a & 0x0F) << 12)
                                | ((b & 0x3F) << 6) | (c & 0x3F)));
                        i += 2;
                    } else {
                        throw new UTFDataFormatException("Illegal 3-byte group");
                    }
                } else if (((a & 0xf0) == 0xf0) || ((a & 0xc0) == 0x80)) {
                    throw new UTFDataFormatException(
                            "Illegal first byte of a group");
                }
            } catch (UTFDataFormatException udfe) {
                if (gracious) {
                    ret.append("?");
                } else {
                    throw udfe;
                }
            } catch (ArrayIndexOutOfBoundsException aioobe) {
                if (gracious) {
                    ret.append("?");
                } else {
                    throw new UTFDataFormatException("Unexpected EOF");
                }
            }
        }
        data = null;

        return ret.toString();
    }

    /**
     * *
    * */

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

Автор: SergejKomlach

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js