将扩展 ASCII 或 Unicode 转换为等效的 7 位 ASCII (<128),包括特殊字符

Convert Extended ASCII or Unicode to 7-bit ASCII (<128) equivalent including special characters

如何将 Java 中的字符从扩展 ASCII 或 Unicode 转换为它们的 7 位 ASCII 等效字符,包括特殊字符,如打开 ( 0x93) 和关闭 ( 0x94) 引用到一个简单的双引号 (" 0x22) 例如。或者类似地破折号 ( 0x96) 到连字符减号 (- 0x2D)。我发现 Stack Overflow questions 与此类似,但答案似乎只处理重音并忽略特殊字符。

例如我希望“Caffè – Peña”转换为"Caffe - Pena"

但是当我使用 java.text.Normalizer:

String sample = "“Caffè – Peña”";
System.out.println(Normalizer.normalize(sample, Normalizer.Form.NFD)
                         .replaceAll("\p{InCombiningDiacriticalMarks}", ""));

输出是

“Caffe – Pena”

为了阐明我的需求,我正在与使用 EBCDIC 编码的 IBM i Db2 数据库进行交互。例如,如果用户粘贴从 Word 或 Outlook 复制的字符串,我指定的字符等字符将转换为 SUB(EBCDIC 中的 0x3F,ASCII 中的 0x1A)。这会导致很多不必要的头痛。我正在寻找一种方法来清理字符串,以便尽可能少地丢失信息。

评论者说您的问题是“主观的”(不是 opinion-based 的意思,而是每个人的具体要求与其他人的略有不同)或定义不明确或本质上不可能。 .. 在技术上是正确的。

但是你正在寻找可以改善这种情况的实际做法,这也是完全有效的。

平衡实施难度与结果准确性的最佳点是将您已经找到的内容与 less-negative 评论者的建议结合在一起:

  • 使用标准规范化程序处理变音符号和其他“标准规范化”字符。
  • 使用您自己的映射处理其他所有内容(可能包括 Unicode General_Category property,但最终可能需要包括您自己的 hand-picked 将特定字符替换为其他特定字符)。

以上可能涵盖“所有”未来案例,具体取决于数据的来源。或者足够接近您可以实现并完成它的所有内容。如果你想增加一些健壮性,并且会在一段时间内维护这个过程,那么你也可以列出你想要在清理结果中允许的所有字符,然后设置某种异常或日志记录机制,让您(或您的继任者)在出现新的未处理案例时发现它们,然后可用于改进映射的自定义部分。

您可以按照另一位评论者的建议,只使用 String.replace() 来替换引号字符,随着时间的推移,您可能会增加有问题的字符列表。

您还可以使用更通用的函数来替换或忽略任何无法编码的字符。例如:

    private String removeUnrepresentableChars(final String _str, final String _encoding) throws CharacterCodingException, UnsupportedEncodingException {
        final CharsetEncoder enccoder = Charset.forName(_encoding).newEncoder();
        enccoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
        ByteBuffer encoded = enccoder.encode(CharBuffer.wrap(_str));
        return new String(encoded.array(), _encoding);
    }

    private String replaceUnrepresentableChars(final String _str, final String _encoding, final String _replacement) throws CharacterCodingException, UnsupportedEncodingException {
        final CharsetEncoder encoder = Charset.forName(_encoding).newEncoder();
        encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
        encoder.replaceWith(_replacement.getBytes(_encoding));
        ByteBuffer encoded = encoder.encode(CharBuffer.wrap(_str));
        return new String(encoded.array(), _encoding);
    }

例如,您可以用 _encoding 的“IBM-037”来调用它们。

但是,如果您的objective是希望丢失尽可能少的信息,您应该评估数据是否可以以 UTF-8 (CCSID 1208) 格式存储。这可以很好地处理智能引号和其他“特殊字符”。根据您的数据库和应用程序结构,这样的更改实施起来可能非常小,也可能非常大且有风险!但是要进行无损翻译,唯一的办法就是使用 unicode 风格,而 UTF-8 是最明智的。

经过一番挖掘,我找到了基于 this answer using org.apache.lucene.analysis.ASCIIFoldingFilter

的解决方案

我能找到的所有示例都使用方法的静态版本 foldToASCII as in this project:

private static String getFoldedString(String text) {
    char[] textChar = text.toCharArray();
    char[] output = new char[textChar.length * 4];
    int outputPos = ASCIIFoldingFilter.foldToASCII(textChar, 0, output, 0, textChar.length);
    text = new String(output, 0, outputPos);
    return text;
}

但是那个静态方法有一个注释说

This API is for internal purposes only and might change in incompatible ways in the next release.

所以经过反复试验,我想出了这个避免使用静态方法的版本:

public static String getFoldedString(String text) throws IOException {
    String output = "";
    try (Analyzer analyzer = CustomAnalyzer.builder()
              .withTokenizer(KeywordTokenizerFactory.class)
              .addTokenFilter(ASCIIFoldingFilterFactory.class)
              .build()) {
        try (TokenStream ts = analyzer.tokenStream(null, new StringReader(text))) {
            CharTermAttribute charTermAtt = ts.addAttribute(CharTermAttribute.class);
            ts.reset();
            if (ts.incrementToken()) output = charTermAtt.toString();
            ts.end();
        }
    }
    return output;
}

类似于我提供的答案 here

这正是我所寻找的,并将字符转换为其 ASCII 7 位等效版本。

然而,通过进一步的研究我发现因为我主要处理 Windows-1252 编码并且由于 jt400 处理 ASCII <-> EBCDIC (CCSID 37) 转换的方式,如果 ASCII 字符串转换为 EBCDIC 并返回 ACSII,唯一丢失的字符是 0x800x9f。受到 lucene's foldToASCII 处理方式的启发,我整理了以下仅处理这些情况的方法:

public static String replaceInvalidChars(String text) {
    char input[] = text.toCharArray();
    int length = input.length;
    char output[] = new char[length * 6];
    int outputPos = 0;
    for (int pos = 0; pos < length; pos++) {
        final char c = input[pos];
        if (c < '\u0080') {
            output[outputPos++] = c;
        } else {
            switch (c) {
                case '\u20ac':  //€ 0x80
                    output[outputPos++] = 'E';
                    output[outputPos++] = 'U';
                    output[outputPos++] = 'R';
                    break;
                case '\u201a':  //‚ 0x82
                    output[outputPos++] = '\'';
                    break;
                case '\u0192':  //ƒ 0x83
                    output[outputPos++] = 'f';
                    break;
                case '\u201e':  //„ 0x84
                    output[outputPos++] = '"';
                    break;
                case '\u2026':  //… 0x85
                    output[outputPos++] = '.';
                    output[outputPos++] = '.';
                    output[outputPos++] = '.';
                    break;
                case '\u2020':  //† 0x86
                    output[outputPos++] = '?';
                    break;
                case '\u2021':  //‡ 0x87
                    output[outputPos++] = '?';
                    break;
                case '\u02c6':  //ˆ 0x88
                    output[outputPos++] = '^';
                    break;
                case '\u2030':  //‰ 0x89
                    output[outputPos++] = 'p';
                    output[outputPos++] = 'e';
                    output[outputPos++] = 'r';
                    output[outputPos++] = 'm';
                    output[outputPos++] = 'i';
                    output[outputPos++] = 'l';

                    break;
                case '\u0160':  //Š 0x8a
                    output[outputPos++] = 'S';
                    break;
                case '\u2039':  //‹ 0x8b
                    output[outputPos++] = '\'';
                    break;
                case '\u0152':  //Œ 0x8c
                    output[outputPos++] = 'O';
                    output[outputPos++] = 'E';
                    break;
                case '\u017d':  //Ž 0x8e
                    output[outputPos++] = 'Z';
                    break;
                case '\u2018':  //‘ 0x91
                    output[outputPos++] = '\'';
                    break;
                case '\u2019':  //’ 0x92
                    output[outputPos++] = '\'';
                    break;
                case '\u201c':  //“ 0x93
                    output[outputPos++] = '"';
                    break;
                case '\u201d':  //” 0x94
                    output[outputPos++] = '"';
                    break;
                case '\u2022':  //• 0x95
                    output[outputPos++] = '-';
                    break;
                case '\u2013':  //– 0x96
                    output[outputPos++] = '-';
                    break;
                case '\u2014':  //— 0x97
                    output[outputPos++] = '-';
                    break;
                case '\u02dc':  //˜ 0x98
                    output[outputPos++] = '~';
                    break;
                case '\u2122':  //™ 0x99
                    output[outputPos++] = '(';
                    output[outputPos++] = 'T';
                    output[outputPos++] = 'M';
                    output[outputPos++] = ')';
                    break;
                case '\u0161':  //š 0x9a
                    output[outputPos++] = 's';
                    break;
                case '\u203a':  //› 0x9b
                    output[outputPos++] = '\'';
                    break;
                case '\u0153':  //œ 0x9c
                    output[outputPos++] = 'o';
                    output[outputPos++] = 'e';
                    break;
                case '\u017e':  //ž 0x9e
                    output[outputPos++] = 'z';
                    break;
                case '\u0178':  //Ÿ 0x9f
                    output[outputPos++] = 'Y';
                    break;
                default:
                    output[outputPos++] = c;
                    break;
            }
        }
    }
    
    return new String(Arrays.copyOf(output, outputPos));
}

因为事实证明我真正的问题是 Windows-1252 到 Latin-1 (ISO-8859-1) 的翻译,这里是 supporting material 显示 Windows -1252 到上述方法中使用的 Unicode 翻译,最终得到 Latin-1 编码。