【问题标题】:How to determine if a String contains invalid encoded characters如何确定字符串是否包含无效的编码字符
【发布时间】:2010-10-27 14:14:23
【问题描述】:

使用场景

我们已经实现了一个网络服务,我们的网络前端开发人员在内部使用(通过 php api)来显示产品数据。在网站上,用户输入一些东西(即查询字符串)。在内部,网站通过 api 调用服务。

注意:我们使用的是restlet,而不是tomcat

原来的问题

Firefox 3.0.10 似乎尊重浏览器中选择的编码,并根据选择的编码对 url 进行编码。这确实会导致 ISO-8859-1 和 UTF-8 的查询字符串不同。

我们的网站转发来自用户的输入并且不转换它(它应该),因此它可以通过使用包含德语变音符号的查询字符串调用 web 服务的 api 来调用服务。

即对于看起来像

的查询部分
    ...v=abcädef

如果选择“ISO-8859-1”,则发送的查询部分如下所示

...v=abc%E4def

但如果选择“UTF-8”,则发送的查询部分看起来像

...v=abc%C3%A4def

所需的解决方案

当我们控制服务时,因为我们已经实现了它,所以我们想在服务器端检查调用是否包含非 utf-8 字符,如果是,则以 4xx http 状态响应

详细的当前解决方案

检查每个字符 ( == string.substring(i,i+1) )

  1. 如果 character.getBytes()[0] 为 '?' 等于 63
  2. 如果 Character.getType(character.charAt(0)) 返回 OTHER_SYMBOL

代码

protected List< String > getNonUnicodeCharacters( String s ) {
  final List< String > result = new ArrayList< String >();
  for ( int i = 0 , n = s.length() ; i < n ; i++ ) {
    final String character = s.substring( i , i + 1 );
    final boolean isOtherSymbol = 
      ( int ) Character.OTHER_SYMBOL
       == Character.getType( character.charAt( 0 ) );
    final boolean isNonUnicode = isOtherSymbol 
      && character.getBytes()[ 0 ] == ( byte ) 63;
    if ( isNonUnicode )
      result.add( character );
  }
  return result;
}

问题

这会捕获所有无效(非 utf 编码)字符吗? 你们有没有更好(更简单)的解决方案?

注意:我用以下代码检查了 URLDecoder

final String[] test = new String[]{
  "v=abc%E4def",
  "v=abc%C3%A4def"
};
for ( int i = 0 , n = test.length ; i < n ; i++ ) {
    System.out.println( java.net.URLDecoder.decode(test[i],"UTF-8") );
    System.out.println( java.net.URLDecoder.decode(test[i],"ISO-8859-1") );
}

打印出来:

v=abc?def
v=abcädef
v=abcädef
v=abcädef

它确实不会抛出 IllegalArgumentException sigh

【问题讨论】:

    标签: java string unicode encoding


    【解决方案1】:

    这是我用来检查编码的:

    CharsetDecoder ebcdicDecoder = Charset.forName("IBM1047").newDecoder();
    ebcdicDecoder.onMalformedInput(CodingErrorAction.REPORT);
    ebcdicDecoder.onUnmappableCharacter(CodingErrorAction.REPORT);
    
    CharBuffer out = CharBuffer.wrap(new char[3200]);
    CoderResult result = ebcdicDecoder.decode(ByteBuffer.wrap(bytes), out, true);
    if (result.isError() || result.isOverflow() ||
        result.isUnderflow() || result.isMalformed() ||
        result.isUnmappable())
    {
        System.out.println("Cannot decode EBCDIC");
    }
    else
    {
        CoderResult result = ebcdicDecoder.flush(out);
        if (result.isOverflow())
           System.out.println("Cannot decode EBCDIC");
        if (result.isUnderflow())
            System.out.println("Ebcdic decoded succefully ");
    }
    

    编辑:更新了 Vouze 建议

    【讨论】:

    • 有意思,看看!
    • 如果您的数据位于 btye 数组而不是字符串中,则此方法有效。当然,这是您测试有效性的唯一方法,因此非常完美。
    • 警告:你应该调用 decoder.flush() 并且当没有发现错误时 result.isUnderflow() 为真。
    【解决方案2】:

    您可能会感兴趣以下正则表达式:

    http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/185624

    我在 ruby​​ 中使用它如下:

    module Encoding
        UTF8RGX = /\A(
            [\x09\x0A\x0D\x20-\x7E]            # ASCII
          | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
          |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
          | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
          |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
          |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
          | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
          |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
        )*\z/x unless defined? UTF8RGX
    
        def self.utf8_file?(fileName)
          count = 0
          File.open("#{fileName}").each do |l|
            count += 1
            unless utf8_string?(l)
              puts count.to_s + ": " + l
            end
          end
          return true
        end
    
        def self.utf8_string?(a_string)
          UTF8RGX === a_string
        end
    
    end
    

    【讨论】:

      【解决方案3】:

      将所有控制字符替换为空字符串

      value = value.replaceAll("\\p{Cntrl}", "");
      

      【讨论】:

      • 你拯救了我的一天。新的 Android Studio 使用 ctrl 不会显示一些日志
      【解决方案4】:

      您可能希望在请求中包含已知参数,例如“...&encTest=䀔,以安全地区分不同的编码。

      【讨论】:

        【解决方案5】:

        尝试在您可以触摸的任何地方始终使用 UTF-8 作为默认值。 (数据库、内存和 UI)

        单一字符集编码可以减少很多问题,实际上它可以提高您的网络服务器性能。编码/解码浪费了太多的处理能力和内存。

        【讨论】:

        • 虽然这是个好建议,但它并不能回答问题。考虑改为评论。
        【解决方案6】:

        如果发现无效字符,您可以使用配置为抛出异常的 CharsetDecoder:

         CharsetDecoder UTF8Decoder =
              Charset.forName("UTF8").newDecoder().onMalformedInput(CodingErrorAction.REPORT);
        

        CodingErrorAction.REPORT

        【讨论】:

        • 我尝试了这种方法,试图从 UTF-8 到 ISO-8859-1 再到 JISAutoDetect,但遗憾的是似乎没有抛出异常..(虽然 UTF-8 失败我只是测试 mString.indexOf('\ufffd') != -1 )
        • 我也添加了.onUnmappableCharacter(CodingErrorAction.REPORT),现在它似乎会为无效编码抛出异常。
        • 我希望得到 CharacterCodingException,但在我的情况下(CSV 文件,将 UCS-2 BE BOM 读取为 UTF8)没有发生错误,但文件被读取为 1 个字符的文件。并且将 UTF-8-BOM 读取为 UTF-8 也没有标记为错误,但内容仍然是乱码。所以这个技术很好,可以捕捉到一些编码错误,但也不是万无一失的。
        【解决方案7】:

        我一直在研究类似的“猜测编码”问题。最好的解决方案是了解编码。除此之外,您可以做出有根据的猜测来区分 UTF-8 和 ISO-8859-1。

        要回答如何检测字符串是否正确编码为 UTF-8 的一般问题,您可以验证以下内容:

        1. 没有字节是 0x00、0xC0、0xC1 或在 0xF5-0xFF 范围内。
        2. 尾字节 (0x80-0xBF) 之前总是有一个头字节 0xC2-0xF4 或另一个尾字节。
        3. 头字节应该正确预测尾字节的数量(例如,0xC2-0xDF 中的任何字节都应该紧跟 0x80-0xBF 范围内的一个字节)。

        如果一个字符串通过了所有这些测试,那么它可以被解释为有效的 UTF-8。这并不能保证它 UTF-8,但它是一个很好的预测器。

        ISO-8859-1 中的合法输入可能没有除行分隔符之外的控制字符(0x00-0x1F 和 0x80-0x9F)。看起来 ISO-8859-1 中也没有定义 0x7F。

        (我基于 UTF-8 和 ISO-8859-1 的维基百科页面。)

        【讨论】:

          【解决方案8】:

          我问了同样的问题,

          Handling Character Encoding in URI on Tomcat

          我最近找到了一个解决方案,它对我来说效果很好。你可能想试一试。这是你需要做的,

          1. 将 URI 编码保留为 Latin-1。在 Tomcat 上,将 URIEncoding="ISO-8859-1" 添加到 server.xml 中的连接器。
          2. 如果您必须手动进行 URL 解码,也可以使用 Latin1 作为字符集。
          3. 使用 fixEncoding() 函数修复编码。

          例如,从查询字符串中获取参数,

            String name = fixEncoding(request.getParameter("name"));
          

          您总是可以这样做。编码正确的字符串不会改变。

          附上代码。祝你好运!

           public static String fixEncoding(String latin1) {
            try {
             byte[] bytes = latin1.getBytes("ISO-8859-1");
             if (!validUTF8(bytes))
              return latin1;   
             return new String(bytes, "UTF-8");  
            } catch (UnsupportedEncodingException e) {
             // Impossible, throw unchecked
             throw new IllegalStateException("No Latin1 or UTF-8: " + e.getMessage());
            }
          
           }
          
           public static boolean validUTF8(byte[] input) {
            int i = 0;
            // Check for BOM
            if (input.length >= 3 && (input[0] & 0xFF) == 0xEF
              && (input[1] & 0xFF) == 0xBB & (input[2] & 0xFF) == 0xBF) {
             i = 3;
            }
          
            int end;
            for (int j = input.length; i < j; ++i) {
             int octet = input[i];
             if ((octet & 0x80) == 0) {
              continue; // ASCII
             }
          
             // Check for UTF-8 leading byte
             if ((octet & 0xE0) == 0xC0) {
              end = i + 1;
             } else if ((octet & 0xF0) == 0xE0) {
              end = i + 2;
             } else if ((octet & 0xF8) == 0xF0) {
              end = i + 3;
             } else {
              // Java only supports BMP so 3 is max
              return false;
             }
          
             while (i < end) {
              i++;
              octet = input[i];
              if ((octet & 0xC0) != 0x80) {
               // Not a valid trailing byte
               return false;
              }
             }
            }
            return true;
           }
          

          编辑:由于各种原因,您的方法不起作用。当出现编码错误时,你不能指望你从 Tomcat 得到什么。有时你会得到 � 或 ?。其他时候,你什么也得不到,getParameter() 返回 null。假设您可以检查“?”,您的查询字符串包含有效的“?”会发生什么? ?

          此外,您不应拒绝任何请求。这不是您的用户的错。正如我在最初的问题中提到的,浏览器可以将 URL 编码为 UTF-8 或 Latin-1。用户没有控制权。你需要接受两者。将您的 servlet 更改为 Latin-1 将保留所有字符,即使它们是错误的,以便我们有机会修复它或将其丢弃。

          我在这里发布的解决方案并不完美,但它是我们迄今为止找到的最好的解决方案。

          【讨论】:

          • 不错的一个!但我不得不反对您的评论“Java 仅支持 BMP”。 UTF-8 字节序列的四字节限制是由 Unicode 联盟强加的,它足以处理完整的字符范围 (U+0000..U+10FFFF),而不仅仅是 BMP。
          • 正确的评论应该是“我们只关心BMP”。我的印象是代理对在 Java 中效果不佳。
          • 好吧,我在 5 月份问过 ;-) 无论如何,上面的代码是做什么的?它会从iso转换为utf-8吗?我不想转换代码,只需检查编码是否正确,如果不正确则抛出错误。请再次查看我上面的解决方案并检查它是否正确,好吗?
          • 您的解决方案不起作用。如果使用了错误的编码,你会得到问号,而不是异常。只需使用我的函数 validUTF8()。如果是真的,最有可能是 UTF8。否则,它是拉丁语 1。您必须在服务器中的任何地方都使用 Latin-1 编码才能进行此检查。
          • 是的,正如我所说:1. 检查 character.getBytes()[0] 是否为 '?' 等于 63,2. 检查 Character.getType(character.charAt(0)) 是否返回 OTHER_SYMBOL .这确实对我有用。如果你能证明相反,请告诉我...
          【解决方案9】:

          您需要从一开始就设置字符编码。尝试发送正确的 Content-Type 标头,例如 Content-Type: text/html; charset=utf-8 修复正确的编码。 Web 服务的标准一致性refers to utf-8 and utf-16 as the proper encoding。检查您的响应标头。

          另外,在服务器端——在浏览器没有正确处理服务器发送的编码的情况下——通过分配一个新的字符串来强制编码。您还可以通过执行单个 each_byte & 0x80 检查编码的 utf-8 字符串中的每个字节,验证结果为非零。

          
          boolean utfEncoded = true;
          byte[] strBytes = queryString.getBytes();
          for (int i = 0; i < strBytes.length(); i++) {
              if ((strBytes[i] & 0x80) != 0) {
                  continue;
              } else {
                  /* treat the string as non utf encoded */
                  utfEncoded = false;
                  break;
              }
          }
          
          String realQueryString = utfEncoded ?
              queryString : new String(queryString.getBytes(), "iso-8859-1");
          

          另外,请发look on this article,希望对您有所帮助。

          【讨论】:

          • string.getBytes() 和 new String() 是一个应该避免的经典错误
          【解决方案10】:

          URLDecoder 将解码为给定的编码。这应该适当地标记错误。但是文档指出:

          此解码器可以通过两种可能的方式处理非法字符串。它可以单独留下非法字符,也可以抛出 IllegalArgumentException。解码器采用哪种方法留给实现。

          所以你应该尝试一下。另请注意(来自 decode() 方法文档):

          World Wide Web Consortium Recommendation 声明应使用 UTF-8。不这样做可能会引入不兼容性

          所以还有其他事情要考虑!

          编辑:Apache Commons URLDecode 声称会为错误的编码抛出适当的异常。

          【讨论】:

          • 我知道该建议,但是违反它的浏览器(这里是 Firefox 3.0.10)怎么办?只要是推荐而不是要求,您必须确保没有非法实体,不是吗?
          • 所以我会尝试使用 URLDecoder 解码并选择适当的编码。我有兴趣(!)看看 URLDecoder does 是否对非法编码的字符抛出异常(易于在浏览器/服务器环境之外测试)
          • Apache Commons 链接已失效。看起来 URLCodec 替换了 URLDecoder:commons.apache.org/proper/commons-codec/apidocs/org/apache/…
          猜你喜欢
          • 2012-12-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-05
          • 2013-06-25
          • 2019-06-11
          • 1970-01-01
          相关资源
          最近更新 更多