В этом топике уже поднимался вопрос о кодировках сервисов 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