2009-06-22 43 views
109

Có cách nào để xác định mã hóa chuỗi trong C# không?Xác định mã hóa của chuỗi trong C#

Giả sử, tôi có một chuỗi tên tệp, nhưng tôi không biết liệu nó có được mã hóa trong Unicode UTF-16 hoặc mã hóa mặc định của hệ thống hay không, làm cách nào để tìm hiểu?

+0

Bạn không thể "mã hóa" trong Unicode. Và không có cách nào để tự động xác định mã hóa của chuỗi _any_ đã cho, mà không có bất kỳ thông tin trước nào khác. – NicDumZ

+0

"Bạn không thể 'mã hóa' bằng Unicode" - nếu chúng tôi giải thích Unicode là UTF-16 (hoặc bất kỳ UTF * cụ thể nào khác), thì đó là cách hoàn toàn hợp lệ để viết mã-điểm dưới dạng chuỗi byte (= mã hóa). –

+1

làm thế nào bạn có thể viết các xấp xỉ như vậy? UTF-16 là một trong những cách có thể để * mã hóa * dữ liệu Unicode. Bạn không thể "mã hóa Unicode"; Unicode không phải là UTF- *; và UTF- * không phải là Unicode. Xin lỗi, nhưng nếu chúng tôi tiếp tục viết các xấp xỉ như vậy, các hành vi liên quan đến Unicode sẽ thay đổi như thế nào? Người mới bắt đầu sẽ luôn bị nhầm lẫn bởi quái vật Unicode tối và mọi thứ sẽ không bao giờ thay đổi. Hãy chính xác. – NicDumZ

Trả lời

28

Kiểm tra Utf8Checker là lớp đơn giản thực hiện chính xác điều này trong mã được quản lý thuần túy. http://utf8checker.codeplex.com

Lưu ý: như đã được chỉ ra "xác định mã hóa" chỉ có ý nghĩa đối với luồng byte. Nếu bạn có một chuỗi nó đã được mã hóa từ một người nào đó dọc theo cách đã biết hoặc đoán mã hóa để nhận được chuỗi ở nơi đầu tiên.

+0

Nếu chuỗi là giải mã không chính xác được thực hiện chỉ đơn giản là 8- bit Encoding và bạn có Mã hóa được sử dụng để giải mã nó, bạn _can_ thường nhận được các byte trở lại mà không có bất kỳ tham nhũng, mặc dù. – Nyerguds

29

Nó phụ thuộc vào nơi chuỗi 'đến từ'. Chuỗi .NET là Unicode (UTF-16). Cách duy nhất nó có thể khác nếu bạn, nói, đọc dữ liệu từ một cơ sở dữ liệu vào một mảng byte.

bài viết CodeProject Điều này có thể quan tâm: Detect Encoding for in- and outgoing text

Jon Skeet Strings in C# and .NET là một lời giải thích tuyệt vời của chuỗi NET.

+0

Nó đến từ một ứng dụng không phải là Unicode C++ .. Bài viết CodeProject có vẻ hơi phức tạp, tuy nhiên có vẻ như tôi muốn làm .. Cảm ơn .. – krebstar

11

Một tùy chọn khác, rất muộn trong sắp tới, xin lỗi:

http://www.architectshack.com/TextFileEncodingDetector.ashx

này nhỏ C# -only lớp sử dụng BOMs nếu có, cố gắng để tự động phát hiện mã hóa unicode có thể khác, và rơi trở lại nếu không ai trong số mã hóa Unicode là có thể hoặc có khả năng. Có vẻ như UTF8Checker được tham chiếu ở trên có chức năng tương tự, nhưng tôi nghĩ rằng điều này hơi rộng hơn trong phạm vi - thay vì chỉ UTF8, nó cũng kiểm tra các mã hóa Unicode có thể khác (UTF-16 LE hoặc BE) có thể bị thiếu BOM.

Hy vọng điều này sẽ giúp ai đó!

+0

Mã đẹp, nó giải quyết vấn đề phát hiện mã hóa của tôi :) –

14

Tôi biết điều này là hơi muộn - nhưng phải rõ ràng:

Một chuỗi không thực sự có mã hóa ... trong .NET là một chuỗi là một bộ sưu tập của các đối tượng char. Về cơ bản, nếu nó là một chuỗi, nó đã được giải mã.

Tuy nhiên nếu bạn đang đọc nội dung của tệp, được tạo thành từ byte và muốn chuyển đổi thành chuỗi, thì phải sử dụng mã hóa của tệp.

.NET bao gồm các lớp mã hóa và giải mã cho: ASCII, UTF7, UTF8, UTF32 và hơn thế nữa.

Hầu hết các mã hóa này chứa các dấu thứ tự byte nhất định có thể được sử dụng để phân biệt loại mã hóa nào được sử dụng.

.NET class System.IO.StreamReader có thể xác định mã hóa được sử dụng trong luồng, bằng cách đọc các dấu thứ tự byte đó;

Dưới đây là một ví dụ:

/// <summary> 
    /// return the detected encoding and the contents of the file. 
    /// </summary> 
    /// <param name="fileName"></param> 
    /// <param name="contents"></param> 
    /// <returns></returns> 
    public static Encoding DetectEncoding(String fileName, out String contents) 
    { 
     // open the file with the stream-reader: 
     using (StreamReader reader = new StreamReader(fileName, true)) 
     { 
      // read the contents of the file into a string 
      contents = reader.ReadToEnd(); 

      // return the encoding. 
      return reader.CurrentEncoding; 
     } 
    } 
+2

Điều này sẽ không hoạt động để phát hiện UTF 16 mà không có BOM. Nó cũng sẽ không rơi vào bảng mã mặc định cục bộ của người dùng nếu nó không phát hiện bất kỳ mã hóa unicode nào. Bạn có thể sửa lỗi sau bằng cách thêm 'Encoding.Default' làm tham số StreamReader, nhưng sau đó mã sẽ không phát hiện UTF8 mà không có BOM. –

+1

@DanW: UTF-16 không có BOM thực sự đã từng thực hiện chưa? Tôi chưa bao giờ sử dụng nó; nó nhất định là một thảm họa để mở ra khá nhiều thứ. – Nyerguds

42

Mã dưới đây có các tính năng sau:

  1. Detection hoặc phát hiện cố gắng của UTF-7, UTF-8/16/32 (bom, không có bom, ít & lớn endian)
  2. Quay lại trang mã mặc định cục bộ nếu không tìm thấy mã hóa Unicode.
  3. Phát hiện (có xác suất cao) các tệp unicode với BOM/chữ ký bị thiếu
  4. Tìm kiếm charset = xyz và encoding = xyz bên trong tệp để giúp xác định mã hóa.
  5. Để lưu chế biến, bạn có thể 'nếm' tệp (số byte có thể xác định).
  6. Tệp văn bản mã hóa và giải mã được trả về.
  7. giải pháp thuần túy byte dựa trên hiệu quả

Như những người khác đã nói, không có giải pháp có thể được hoàn hảo (và chắc chắn người ta có thể không dễ dàng phân biệt giữa 8-bit mã hóa ASCII mở rộng khác nhau được sử dụng trên toàn thế giới), nhưng chúng tôi có thể nhận được 'đủ tốt' đặc biệt là nếu các nhà phát triển cũng trình bày cho người dùng một danh sách các mã hóa thay thế như ở đây: What is the most common encoding of each language?

một danh sách đầy đủ các bảng mã có thể được tìm thấy bằng Encoding.GetEncodings();

// Function to detect the encoding for UTF-7, UTF-8/16/32 (bom, no bom, little 
// & big endian), and local default codepage, and potentially other codepages. 
// 'taster' = number of bytes to check of the file (to save processing). Higher 
// value is slower, but more reliable (especially UTF-8 with special characters 
// later on may appear to be ASCII initially). If taster = 0, then taster 
// becomes the length of the file (for maximum reliability). 'text' is simply 
// the string with the discovered encoding applied to the file. 
public Encoding detectTextEncoding(string filename, out String text, int taster = 1000) 
{ 
    byte[] b = File.ReadAllBytes(filename); 

    //////////////// First check the low hanging fruit by checking if a 
    //////////////// BOM/signature exists (sourced from http://www.unicode.org/faq/utf_bom.html#bom4) 
    if (b.Length >= 4 && b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF) { text = Encoding.GetEncoding("utf-32BE").GetString(b, 4, b.Length - 4); return Encoding.GetEncoding("utf-32BE"); } // UTF-32, big-endian 
    else if (b.Length >= 4 && b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00) { text = Encoding.UTF32.GetString(b, 4, b.Length - 4); return Encoding.UTF32; } // UTF-32, little-endian 
    else if (b.Length >= 2 && b[0] == 0xFE && b[1] == 0xFF) { text = Encoding.BigEndianUnicode.GetString(b, 2, b.Length - 2); return Encoding.BigEndianUnicode; }  // UTF-16, big-endian 
    else if (b.Length >= 2 && b[0] == 0xFF && b[1] == 0xFE) { text = Encoding.Unicode.GetString(b, 2, b.Length - 2); return Encoding.Unicode; }    // UTF-16, little-endian 
    else if (b.Length >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF) { text = Encoding.UTF8.GetString(b, 3, b.Length - 3); return Encoding.UTF8; } // UTF-8 
    else if (b.Length >= 3 && b[0] == 0x2b && b[1] == 0x2f && b[2] == 0x76) { text = Encoding.UTF7.GetString(b,3,b.Length-3); return Encoding.UTF7; } // UTF-7 


    //////////// If the code reaches here, no BOM/signature was found, so now 
    //////////// we need to 'taste' the file to see if can manually discover 
    //////////// the encoding. A high taster value is desired for UTF-8 
    if (taster == 0 || taster > b.Length) taster = b.Length; // Taster size can't be bigger than the filesize obviously. 


    // Some text files are encoded in UTF8, but have no BOM/signature. Hence 
    // the below manually checks for a UTF8 pattern. This code is based off 
    // the top answer at: https://stackoverflow.com/questions/6555015/check-for-invalid-utf8 
    // For our purposes, an unnecessarily strict (and terser/slower) 
    // implementation is shown at: https://stackoverflow.com/questions/1031645/how-to-detect-utf-8-in-plain-c 
    // For the below, false positives should be exceedingly rare (and would 
    // be either slightly malformed UTF-8 (which would suit our purposes 
    // anyway) or 8-bit extended ASCII/UTF-16/32 at a vanishingly long shot). 
    int i = 0; 
    bool utf8 = false; 
    while (i < taster - 4) 
    { 
     if (b[i] <= 0x7F) { i += 1; continue; }  // If all characters are below 0x80, then it is valid UTF8, but UTF8 is not 'required' (and therefore the text is more desirable to be treated as the default codepage of the computer). Hence, there's no "utf8 = true;" code unlike the next three checks. 
     if (b[i] >= 0xC2 && b[i] <= 0xDF && b[i + 1] >= 0x80 && b[i + 1] < 0xC0) { i += 2; utf8 = true; continue; } 
     if (b[i] >= 0xE0 && b[i] <= 0xF0 && b[i + 1] >= 0x80 && b[i + 1] < 0xC0 && b[i + 2] >= 0x80 && b[i + 2] < 0xC0) { i += 3; utf8 = true; continue; } 
     if (b[i] >= 0xF0 && b[i] <= 0xF4 && b[i + 1] >= 0x80 && b[i + 1] < 0xC0 && b[i + 2] >= 0x80 && b[i + 2] < 0xC0 && b[i + 3] >= 0x80 && b[i + 3] < 0xC0) { i += 4; utf8 = true; continue; } 
     utf8 = false; break; 
    } 
    if (utf8 == true) { 
     text = Encoding.UTF8.GetString(b); 
     return Encoding.UTF8; 
    } 


    // The next check is a heuristic attempt to detect UTF-16 without a BOM. 
    // We simply look for zeroes in odd or even byte places, and if a certain 
    // threshold is reached, the code is 'probably' UF-16.   
    double threshold = 0.1; // proportion of chars step 2 which must be zeroed to be diagnosed as utf-16. 0.1 = 10% 
    int count = 0; 
    for (int n = 0; n < taster; n += 2) if (b[n] == 0) count++; 
    if (((double)count)/taster > threshold) { text = Encoding.BigEndianUnicode.GetString(b); return Encoding.BigEndianUnicode; } 
    count = 0; 
    for (int n = 1; n < taster; n += 2) if (b[n] == 0) count++; 
    if (((double)count)/taster > threshold) { text = Encoding.Unicode.GetString(b); return Encoding.Unicode; } // (little-endian) 


    // Finally, a long shot - let's see if we can find "charset=xyz" or 
    // "encoding=xyz" to identify the encoding: 
    for (int n = 0; n < taster-9; n++) 
    { 
     if (
      ((b[n + 0] == 'c' || b[n + 0] == 'C') && (b[n + 1] == 'h' || b[n + 1] == 'H') && (b[n + 2] == 'a' || b[n + 2] == 'A') && (b[n + 3] == 'r' || b[n + 3] == 'R') && (b[n + 4] == 's' || b[n + 4] == 'S') && (b[n + 5] == 'e' || b[n + 5] == 'E') && (b[n + 6] == 't' || b[n + 6] == 'T') && (b[n + 7] == '=')) || 
      ((b[n + 0] == 'e' || b[n + 0] == 'E') && (b[n + 1] == 'n' || b[n + 1] == 'N') && (b[n + 2] == 'c' || b[n + 2] == 'C') && (b[n + 3] == 'o' || b[n + 3] == 'O') && (b[n + 4] == 'd' || b[n + 4] == 'D') && (b[n + 5] == 'i' || b[n + 5] == 'I') && (b[n + 6] == 'n' || b[n + 6] == 'N') && (b[n + 7] == 'g' || b[n + 7] == 'G') && (b[n + 8] == '=')) 
      ) 
     { 
      if (b[n + 0] == 'c' || b[n + 0] == 'C') n += 8; else n += 9; 
      if (b[n] == '"' || b[n] == '\'') n++; 
      int oldn = n; 
      while (n < taster && (b[n] == '_' || b[n] == '-' || (b[n] >= '0' && b[n] <= '9') || (b[n] >= 'a' && b[n] <= 'z') || (b[n] >= 'A' && b[n] <= 'Z'))) 
      { n++; } 
      byte[] nb = new byte[n-oldn]; 
      Array.Copy(b, oldn, nb, 0, n-oldn); 
      try { 
       string internalEnc = Encoding.ASCII.GetString(nb); 
       text = Encoding.GetEncoding(internalEnc).GetString(b); 
       return Encoding.GetEncoding(internalEnc); 
      } 
      catch { break; } // If C# doesn't recognize the name of the encoding, break. 
     } 
    } 


    // If all else fails, the encoding is probably (though certainly not 
    // definitely) the user's local codepage! One might present to the user a 
    // list of alternative encodings as shown here: https://stackoverflow.com/questions/8509339/what-is-the-most-common-encoding-of-each-language 
    // A full list can be found using Encoding.GetEncodings(); 
    text = Encoding.Default.GetString(b); 
    return Encoding.Default; 
} 
+0

Điều này làm việc cho các tệp .eml (và có thể là tất cả các tệp khác) Cyrillic (từ tiêu đề bộ ký tự của thư) –

+0

Cảm ơn bạn! – supertopi

+0

UTF-7 không thể được giải mã một cách ngây thơ, thực sự; lời mở đầu đầy đủ của nó dài hơn và bao gồm hai bit của ký tự đầu tiên. Hệ thống .Net dường như không có hỗ trợ nào cả cho hệ thống mở đầu của UTF7. – Nyerguds

5

Giải pháp của tôi là sử dụng các chất liệu tích hợp với một số nhược điểm.

Tôi đã chọn chiến lược từ câu trả lời cho một câu hỏi tương tự khác trên stackoverflow nhưng tôi không thể tìm thấy nó ngay bây giờ.

Nó kiểm tra BOM đầu tiên bằng cách sử dụng logic tích hợp trong StreamReader, nếu có BOM, mã hóa sẽ là một cái gì đó khác hơn Encoding.Default và chúng ta nên tin tưởng kết quả đó.

Nếu không, nó sẽ kiểm tra xem chuỗi byte có phải là chuỗi UTF-8 hợp lệ hay không. nếu có, nó sẽ đoán UTF-8 là mã hóa, và nếu không, một lần nữa, mã hóa ASCII mặc định sẽ là kết quả.

static Encoding getEncoding(string path) { 
    var stream = new FileStream(path, FileMode.Open); 
    var reader = new StreamReader(stream, Encoding.Default, true); 
    reader.Read(); 

    if (reader.CurrentEncoding != Encoding.Default) { 
     reader.Close(); 
     return reader.CurrentEncoding; 
    } 

    stream.Position = 0; 

    reader = new StreamReader(stream, new UTF8Encoding(false, true)); 
    try { 
     reader.ReadToEnd(); 
     reader.Close(); 
     return Encoding.UTF8; 
    } 
    catch (Exception) { 
     reader.Close(); 
     return Encoding.Default; 
    } 
} 
1

Lưu ý: đây là thử nghiệm để xem cách mã hóa UTF-8 hoạt động trong nội bộ. The solution offered by vilicvane, để sử dụng đối tượng UTF8Encoding được khởi tạo để ném ngoại lệ về lỗi giải mã, đơn giản hơn rất nhiều và về cơ bản cũng giống như vậy.


Tôi đã viết đoạn mã này để phân biệt giữa UTF-8 và Windows-1252. Nó không nên được sử dụng cho các tập tin văn bản khổng lồ mặc dù, kể từ khi nó tải toàn bộ điều vào bộ nhớ và quét nó hoàn toàn. Tôi đã sử dụng nó cho các tệp phụ đề .srt, chỉ để có thể lưu chúng trở lại trong mã hóa mà chúng được tải.

Mã hóa được cung cấp cho hàm làm ref phải là mã hóa dự phòng 8 bit để sử dụng trong trường hợp tệp được phát hiện là không hợp lệ UTF-8; nói chung, trên các hệ thống Windows, đây sẽ là Windows-1252.Điều này không làm bất cứ điều gì ưa thích như kiểm tra thực tế hợp lệ ascii phạm vi mặc dù, và không phát hiện UTF-16 ngay cả trên dấu thứ tự byte.

Lý thuyết đằng sau việc phát hiện Bitwise có thể được tìm thấy ở đây: https://ianthehenry.com/2015/1/17/decoding-utf-8/

Về cơ bản, phạm vi bit của byte đầu tiên xác định có bao nhiêu sau khi nó là một phần của thực thể UTF-8. Những byte sau khi nó luôn nằm trong cùng một phạm vi bit.

/// <summary> 
///  Detects whether the encoding of the data is valid UTF-8 or ascii. If detection fails, the text is decoded using the given fallback encoding. 
///  Bit-wise mechanism for detecting valid UTF-8 based on https://ianthehenry.com/2015/1/17/decoding-utf-8/ 
///  Note that pure ascii detection should not be trusted: it might mean the file is meant to be UTF-8 or Windows-1252 but simply contains no special characters. 
/// </summary> 
/// <param name="docBytes">The bytes of the text document.</param> 
/// <param name="encoding">The default encoding to use as fallback if the text is detected not to be pure ascii or UTF-8 compliant. This ref parameter is changed to the detected encoding, or Windows-1252 if the given encoding parameter is null and the text is not valid UTF-8.</param> 
/// <returns>The contents of the read file</returns> 
public static String ReadFileAndGetEncoding(Byte[] docBytes, ref Encoding encoding) 
{ 
    if (encoding == null) 
     encoding = Encoding.GetEncoding(1252); 
    // BOM detection is not added in this example. Add it yourself if you feel like it. Should set the "encoding" param and return the decoded string. 
    //String file = DetectByBOM(docBytes, ref encoding); 
    //if (file != null) 
    // return file; 
    Boolean isPureAscii = true; 
    Boolean isUtf8Valid = true; 
    for (Int32 i = 0; i < docBytes.Length; i++) 
    { 
     Int32 skip = TestUtf8(docBytes, i); 
     if (skip != 0) 
     { 
      if (isPureAscii) 
       isPureAscii = false; 
      if (skip < 0) 
       isUtf8Valid = false; 
      else 
       i += skip; 
     } 
     // if already detected that it's not valid utf8, there's no sense in going on. 
     if (!isUtf8Valid) 
      break; 
    } 
    if (isPureAscii) 
     encoding = new ASCIIEncoding(); // pure 7-bit ascii. 
    else if (isUtf8Valid) 
     encoding = new UTF8Encoding(false); 
    // else, retain given fallback encoding. 
    return encoding.GetString(docBytes); 
} 

/// <summary> 
/// Tests if the bytes following the given offset are UTF-8 valid, and returns 
/// the extra amount of bytes to skip ahead to do the next read if it is 
/// (meaning, detecting a single-byte ascii character would return 0). 
/// If the text is not UTF-8 valid it returns -1. 
/// </summary> 
/// <param name="binFile">Byte array to test</param> 
/// <param name="offset">Offset in the byte array to test.</param> 
/// <returns>The amount of extra bytes to skip ahead for the next read, or -1 if the byte sequence wasn't valid UTF-8</returns> 
public static Int32 TestUtf8(Byte[] binFile, Int32 offset) 
{ 
    Byte current = binFile[offset]; 
    if ((current & 0x80) == 0) 
     return 0; // valid 7-bit ascii. Added length is 0 bytes. 
    else 
    { 
     Int32 len = binFile.Length; 
     Int32 fullmask = 0xC0; 
     Int32 testmask = 0; 
     for (Int32 addedlength = 1; addedlength < 6; addedlength++) 
     { 
      // This code adds shifted bits to get the desired full mask. 
      // If the full mask is [111]0 0000, then test mask will be [110]0 0000. Since this is 
      // effectively always the previous step in the iteration I just store it each time. 
      testmask = fullmask; 
      fullmask += (0x40 >> addedlength); 
      // Test bit mask for this level 
      if ((current & fullmask) == testmask) 
      { 
       // End of file. Might be cut off, but either way, deemed invalid. 
       if (offset + addedlength >= len) 
        return -1; 
       else 
       { 
        // Lookahead. Pattern of any following bytes is always 10xxxxxx 
        for (Int32 i = 1; i <= addedlength; i++) 
        { 
         // If it does not match the pattern for an added byte, it is deemed invalid. 
         if ((binFile[offset + i] & 0xC0) != 0x80) 
          return -1; 
        } 
        return addedlength; 
       } 
      } 
     } 
     // Value is greater than the start of a 6-byte utf8 sequence. Deemed invalid. 
     return -1; 
    } 
} 
+0

Cũng không có câu lệnh 'else' cuối cùng nào sau' if ((current & 0xE0) == 0xC0) {...} nếu if ((current & 0xF0) == 0xE0) {...} else if ((current & 0xF0) == 0xE0) {...} else if ((current & 0xF8) == 0xF0) {...} '. Tôi cho rằng trường hợp 'else' sẽ không hợp lệ utf8:' isUtf8Valid = false; '. Bạn có? – hal

+0

@hal Ah, đúng ... Tôi đã cập nhật mã của riêng mình với hệ thống tổng quát hơn (và nâng cao hơn) sử dụng vòng lặp lên tới 3 nhưng về mặt kỹ thuật có thể thay đổi thành vòng lặp hơn (thông số kỹ thuật không rõ ràng) có thể mở rộng UTF-8 lên đến 6 byte bổ sung mà tôi nghĩ, nhưng chỉ có 3 byte được sử dụng trong các bản triển khai hiện tại), vì vậy tôi đã không cập nhật mã này. – Nyerguds

+0

@hal Đã cập nhật nó lên giải pháp mới của tôi. Nguyên tắc vẫn như cũ, nhưng các mặt nạ bit được tạo ra và kiểm tra trong một vòng lặp chứ không phải tất cả được viết ra một cách rõ ràng trong mã. – Nyerguds

Các vấn đề liên quan