C#: 解码 Quoted-Printable 编码的类?

29
是否有现成的C#类可以将 Quoted-Printable 编码转换为 String?请点击上面的链接获取有关编码的更多信息。
以下是摘自上述链接的内容,供您参考:
任何8位字节值都可以用3个字符进行编码,即“=”后跟两个十六进制数字(0-9或A-F),表示该字节的数值。例如,US-ASCII换页符字符(十进制值12)可以表示为“= 0C”,而US-ASCII等号符(十进制值61)表示为“= 3D”。除可打印的ASCII字符或行尾字符外,所有字符都必须以这种方式编码。
所有可打印的ASCII字符(十进制值在33和126之间)均可代表其本身,除了“=”(十进制值61)。
ASCII制表符和空格字符,十进制值9和32,可以表示为它们自己,除非这些字符出现在一行的末尾。如果其中一个字符出现在行尾,则必须将其编码为“= 09”(制表符)或“= 20”(空格)。
如果要编码的数据包含有意义的换行符,则必须将其编码为ASCII CR LF序列,而不是原始字节值。相反,如果字节值13和10具有终止行以外的其他含义,则必须将它们编码为=0D和=0A。
引用打印编码数据的行不能超过76个字符。为了满足此要求而不更改已编码的文本,可以根据需要添加软行断点。软换行符由编码行末端的“=”组成,在解码文本中不会导致换行。

1
我刚刚在这里发布了一个关于UTF8解码的简单答案:http://stackoverflow.com/questions/37540244/how-to-convert-quoted-print-string/37540375#37540375 - ib11
15个回答

21

框架库中有用于此操作的功能,但似乎没有很好地暴露出来。实现在内部类System.Net.Mime.QuotedPrintableStream中。该类定义了一个名为DecodeBytes的方法,可以完成你想要的操作。该方法似乎仅被一个方法使用,用于解码MIME头。该方法也是内部的,但在一些地方会被直接调用,例如Attachment.Name设置器。以下是示例:

using System;
using System.Net.Mail;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Attachment attachment = Attachment.CreateAttachmentFromString("", "=?iso-8859-1?Q?=A1Hola,_se=F1or!?=");
            Console.WriteLine(attachment.Name);
        }
    }
}

输出结果为:

¡Hola,_señor!

您可能需要进行一些测试,以确保回车符等被正确处理,尽管在我进行的快速测试中它们似乎是可以的。然而,除非您的用例与解码 MIME 标头字符串很接近,否则依赖于这种功能可能不明智,因为它可能会被库所做的任何更改破坏。您最好编写自己的 quoted-printable 解码器。


6
这并未将 "=3D" 等内容转换,而且 codeproject 版本在解���时失败。 - Martin Murphy
1
框架2中存在一个错误,它不能处理加密字符串中的下划线。下划线代表旧解析中的空格。在版本4中修复了这个问题。然而,这种方法还有另一个小问题,如果字符集信息不是字符串开头,它就无法解密。加密字符串形如:**ID: 12345 Arpège **。 - Dino Liu
1
Attachment类也可以使用这种格式进行编码。创建一个空字符串对象,设置Name,然后使用ToString()获取编码后的名称。不幸的是,输出结果还包括MIME类型和字符集,例如"text/plain; name=\"=?utf-8?B?SWN ... nUg?=\"; charset=us-ascii",需要将其剥离。此外,它遵循RFC2074并将输入分成多个部分,以使编码后的单词长度不超过75个字符。 - Matt
不考虑76个字符的续行符。 - user2924019

18

我扩展了Martin Murphy的解决方案,希望它在任何情况下都能起作用。

private static string DecodeQuotedPrintables(string input, string charSet)
{           
    if (string.IsNullOrEmpty(charSet))
    {
        var charSetOccurences = new Regex(@"=\?.*\?Q\?", RegexOptions.IgnoreCase);
        var charSetMatches = charSetOccurences.Matches(input);
        foreach (Match match in charSetMatches)
        {
            charSet = match.Groups[0].Value.Replace("=?", "").Replace("?Q?", "");
            input = input.Replace(match.Groups[0].Value, "").Replace("?=", "");
        }
    }

    Encoding enc = new ASCIIEncoding();
    if (!string.IsNullOrEmpty(charSet))
    {
        try
        {
            enc = Encoding.GetEncoding(charSet);
        }
        catch
        {
            enc = new ASCIIEncoding();
        }
    }

    //decode iso-8859-[0-9]
    var occurences = new Regex(@"=[0-9A-Z]{2}", RegexOptions.Multiline);
    var matches = occurences.Matches(input);
    foreach (Match match in matches)
    {
        try
        {
            byte[] b = new byte[] { byte.Parse(match.Groups[0].Value.Substring(1), System.Globalization.NumberStyles.AllowHexSpecifier) };
            char[] hexChar = enc.GetChars(b);
            input = input.Replace(match.Groups[0].Value, hexChar[0].ToString());
        }
        catch { }
    }

    //decode base64String (utf-8?B?)
    occurences = new Regex(@"\?utf-8\?B\?.*\?", RegexOptions.IgnoreCase);
    matches = occurences.Matches(input);
    foreach (Match match in matches)
    {
        byte[] b = Convert.FromBase64String(match.Groups[0].Value.Replace("?utf-8?B?", "").Replace("?UTF-8?B?", "").Replace("?", ""));
        string temp = Encoding.UTF8.GetString(b);
        input = input.Replace(match.Groups[0].Value, temp);
    }

    input = input.Replace("=\r\n", "");
    return input;
}

2
非常感谢,它解决了我的问题,这就是我花了很多小时寻找的东西。 - Farid-ur-Rahman
2
在这行代码中不支持等号和回车符号:"如果你相信真理=美,那么数学无疑是哲学中最美丽的分支。" - Dan W
这个方法必须进行重构。我添加了工作副本但不是最终版本。"=[0-9A-Z]{2}"不能总是被解码。例如,当输入为"3+7=10"时。 - Igor Semkiv
嘿,感谢你的代码!我现在遇到了一些问题,如果你能看一下这里,我会非常感激。 - Shaul Behr
该代码不适用于UTF-8字符,因为它尝试单独解码每个字节而不是作为一组。请参见Demented Devil的评论(发布为“solution”)。 - Kevin North
显示剩余2条评论

9

我很快地写了这篇文章。

    public static string DecodeQuotedPrintables(string input)
    {
        var occurences = new Regex(@"=[0-9A-H]{2}", RegexOptions.Multiline);
        var matches = occurences.Matches(input);
        var uniqueMatches = new HashSet<string>(matches);
        foreach (string match in uniqueMatches)
        {
            char hexChar= (char) Convert.ToInt32(match.Substring(1), 16);
            input =input.Replace(match, hexChar.ToString());
        }
        return input.Replace("=\r\n", "");
    }       

需要更改uniqueMatches行以正确处理“MatchCollection”,但除此之外,这个工作得很好。谢谢! - Nick Gotch
QuotedPrintables 也有 76 字符限制,使用行续标记(=)。这种方法没有考虑到这一点。 - user2924019

6

我正在寻找一种动态解决方案,花了2天时间尝试不同的解决方案。此解决方案将支持日语字符和其他标准字符集。

private static string Decode(string input, string bodycharset) {
        var i = 0;
        var output = new List<byte>();
        while (i < input.Length) {
            if (input[i] == '=' && input[i + 1] == '\r' && input[i + 2] == '\n') {
                //Skip
                i += 3;
            } else if (input[i] == '=') {
                string sHex = input;
                sHex = sHex.Substring(i + 1, 2);
                int hex = Convert.ToInt32(sHex, 16);
                byte b = Convert.ToByte(hex);
                output.Add(b);
                i += 3;
            } else {
                output.Add((byte)input[i]);
                i++;
            }
        }


        if (String.IsNullOrEmpty(bodycharset))
            return Encoding.UTF8.GetString(output.ToArray());
        else {
            if (String.Compare(bodycharset, "ISO-2022-JP", true) == 0)
                return Encoding.GetEncoding("Shift_JIS").GetString(output.ToArray());
            else
                return Encoding.GetEncoding(bodycharset).GetString(output.ToArray());
        }

    }

然后,您可以使用以下方式调用该函数:
Decode("=E3=82=AB=E3=82=B9=E3", "utf-8")

这篇文章最初是在这里找到的。


1
太好了!在UTF-8上正常工作,比字符串上的常量替换操作更有效率。流可能比List<byte>稍微高效一些,但不够易读,如果字符串不太长,这并不是一个很大的问题。(对于那些感兴趣的人,请参见https://dev59.com/d2865IYBdhLWcg3wCqHI)。我现在已经处理了很多Unicode翻译,我仍然支持在解码之前使用中间字节列表/数组/流;任何其他处理可变字节数的UTF-8编码的方式都非常复杂,不值得。 - Kevin North
System.FormatException: '无法识别任何可识别的数字。',位于 int hex = Convert.ToInt32(sHex, 16); - user2924019

6
如果您使用UTF-8编码解码Quoted-Printable,则需要注意,如果有连续的Quoted-Printable字符序列,则不能像其他人所示一样逐个解码每个Quoted-Printable序列。
例如,如果您有以下序列=E2=80=99,并使用UTF8逐个解码,则会得到三个“奇怪”的字符。相反,如果您构建一个由三个字节组成的数组,并使用UTF8编码转换这三个字节,则会得到一个单引号。
显然,如果您使用ASCII编码,则逐个解码没有问题,但是解码连续的字符序列意味着您的代码将在任何文本编码器中都能正常工作。
哦,别忘了=3D是一个特殊情况,这意味着您需要再解码一次...那是一个令人疯狂的陷阱!
希望这可以帮助您。

2
是的,这更接近Skeet在这里的答案。我们应该将Quoted Printable视为字节序列化的一种方式,而不是字符。首先将其解码为字节,然后将该字节数组解码为您的编码字符串--System.Text.Encoding.UTF8.GetString(byteArray)或其他编码方式。试图从小于128的字节中“挽救”字符是一个令人头疼的错误。 - ruffin

2
    private string quotedprintable(string data, string encoding)
    {
        data = data.Replace("=\r\n", "");
        for (int position = -1; (position = data.IndexOf("=")) != -1;)
        {
            string leftpart = data.Substring(0, position);
            System.Collections.ArrayList hex = new System.Collections.ArrayList();
            hex.Add(data.Substring(1 + position, 2));
            while (position + 3 < data.Length && data.Substring(position + 3, 1) == "=")
            {
                position = position + 3;
                hex.Add(data.Substring(1 + position, 2));
            }
            byte[] bytes = new byte[hex.Count];
            for (int i = 0; i < hex.Count; i++)
            {
                bytes[i] = System.Convert.ToByte(new string(((string)hex[i]).ToCharArray()), 16);
            }
            string equivalent = System.Text.Encoding.GetEncoding(encoding).GetString(bytes);
            string rightpart = data.Substring(position + 3);
            data = leftpart + equivalent + rightpart;
        }
        return data;
    }

this.Text = quotedprintable("=3D", "utf-8"); - iaceian
支持将连续的十六进制数合并为字节,然后转换为编码。 - iaceian
适用于多个连续的多字节字符。非常好! - jk7

1

这个可引用编码解码器工作得非常好!

public static byte[] FromHex(byte[] hexData)
    {
        if (hexData == null)
        {
            throw new ArgumentNullException("hexData");
        }

        if (hexData.Length < 2 || (hexData.Length / (double)2 != Math.Floor(hexData.Length / (double)2)))
        {
            throw new Exception("Illegal hex data, hex data must be in two bytes pairs, for example: 0F,FF,A3,... .");
        }

        MemoryStream retVal = new MemoryStream(hexData.Length / 2);
        // Loop hex value pairs
        for (int i = 0; i < hexData.Length; i += 2)
        {
            byte[] hexPairInDecimal = new byte[2];
            // We need to convert hex char to decimal number, for example F = 15
            for (int h = 0; h < 2; h++)
            {
                if (((char)hexData[i + h]) == '0')
                {
                    hexPairInDecimal[h] = 0;
                }
                else if (((char)hexData[i + h]) == '1')
                {
                    hexPairInDecimal[h] = 1;
                }
                else if (((char)hexData[i + h]) == '2')
                {
                    hexPairInDecimal[h] = 2;
                }
                else if (((char)hexData[i + h]) == '3')
                {
                    hexPairInDecimal[h] = 3;
                }
                else if (((char)hexData[i + h]) == '4')
                {
                    hexPairInDecimal[h] = 4;
                }
                else if (((char)hexData[i + h]) == '5')
                {
                    hexPairInDecimal[h] = 5;
                }
                else if (((char)hexData[i + h]) == '6')
                {
                    hexPairInDecimal[h] = 6;
                }
                else if (((char)hexData[i + h]) == '7')
                {
                    hexPairInDecimal[h] = 7;
                }
                else if (((char)hexData[i + h]) == '8')
                {
                    hexPairInDecimal[h] = 8;
                }
                else if (((char)hexData[i + h]) == '9')
                {
                    hexPairInDecimal[h] = 9;
                }
                else if (((char)hexData[i + h]) == 'A' || ((char)hexData[i + h]) == 'a')
                {
                    hexPairInDecimal[h] = 10;
                }
                else if (((char)hexData[i + h]) == 'B' || ((char)hexData[i + h]) == 'b')
                {
                    hexPairInDecimal[h] = 11;
                }
                else if (((char)hexData[i + h]) == 'C' || ((char)hexData[i + h]) == 'c')
                {
                    hexPairInDecimal[h] = 12;
                }
                else if (((char)hexData[i + h]) == 'D' || ((char)hexData[i + h]) == 'd')
                {
                    hexPairInDecimal[h] = 13;
                }
                else if (((char)hexData[i + h]) == 'E' || ((char)hexData[i + h]) == 'e')
                {
                    hexPairInDecimal[h] = 14;
                }
                else if (((char)hexData[i + h]) == 'F' || ((char)hexData[i + h]) == 'f')
                {
                    hexPairInDecimal[h] = 15;
                }
            }

            // Join hex 4 bit(left hex cahr) + 4bit(right hex char) in bytes 8 it
            retVal.WriteByte((byte)((hexPairInDecimal[0] << 4) | hexPairInDecimal[1]));
        }

        return retVal.ToArray();
    }
    public static byte[] QuotedPrintableDecode(byte[] data)
    {
        if (data == null)
        {
            throw new ArgumentNullException("data");
        }

        MemoryStream msRetVal = new MemoryStream();
        MemoryStream msSourceStream = new MemoryStream(data);

        int b = msSourceStream.ReadByte();
        while (b > -1)
        {
            // Encoded 8-bit byte(=XX) or soft line break(=CRLF)
            if (b == '=')
            {
                byte[] buffer = new byte[2];
                int nCount = msSourceStream.Read(buffer, 0, 2);
                if (nCount == 2)
                {
                    // Soft line break, line splitted, just skip CRLF
                    if (buffer[0] == '\r' && buffer[1] == '\n')
                    {
                    }
                    // This must be encoded 8-bit byte
                    else
                    {
                        try
                        {
                            msRetVal.Write(FromHex(buffer), 0, 1);
                        }
                        catch
                        {
                            // Illegal value after =, just leave it as is
                            msRetVal.WriteByte((byte)'=');
                            msRetVal.Write(buffer, 0, 2);
                        }
                    }
                }
                // Illegal =, just leave as it is
                else
                {
                    msRetVal.Write(buffer, 0, nCount);
                }
            }
            // Just write back all other bytes
            else
            {
                msRetVal.WriteByte((byte)b);
            }

            // Read next byte
            b = msSourceStream.ReadByte();
        }

        return msRetVal.ToArray();
    }

1
更好的解决方案
    private static string DecodeQuotedPrintables(string input, string charSet)
    {
        try
        {
            enc = Encoding.GetEncoding(CharSet);
        }
        catch
        {
            enc = new UTF8Encoding();
        }

        var occurences = new Regex(@"(=[0-9A-Z]{2}){1,}", RegexOptions.Multiline);
        var matches = occurences.Matches(input);

    foreach (Match match in matches)
    {
            try
            {
                byte[] b = new byte[match.Groups[0].Value.Length / 3];
                for (int i = 0; i < match.Groups[0].Value.Length / 3; i++)
                {
                    b[i] = byte.Parse(match.Groups[0].Value.Substring(i * 3 + 1, 2), System.Globalization.NumberStyles.AllowHexSpecifier);
                }
                char[] hexChar = enc.GetChars(b);
                input = input.Replace(match.Groups[0].Value, hexChar[0].ToString());
        }
            catch
            { ;}
        }
        input = input.Replace("=\r\n", "").Replace("=\n", "").Replace("?=", "");

        return input;
}

1

0
请注意: 在互联网上到处都是使用“input.Replace”解决方案,但它们仍然不正确。
如果您有一个已解码的符号,然后使用“replace”,则“input”中的所有符号都将被替换,然后所有后续的解码都将被破坏。
更正确的解决方案:
public static string DecodeQuotedPrintable(string input, string charSet)
    {

        Encoding enc;

        try
        {
            enc = Encoding.GetEncoding(charSet);
        }
        catch
        {
            enc = new UTF8Encoding();
        }

        input = input.Replace("=\r\n=", "=");
        input = input.Replace("=\r\n ", "\r\n ");
        input = input.Replace("= \r\n", " \r\n");
        var occurences = new Regex(@"(=[0-9A-Z]{2})", RegexOptions.Multiline); //{1,}
        var matches = occurences.Matches(input);

        foreach (Match match in matches)
        {
            try
            {
                byte[] b = new byte[match.Groups[0].Value.Length / 3];
                for (int i = 0; i < match.Groups[0].Value.Length / 3; i++)
                {
                    b[i] = byte.Parse(match.Groups[0].Value.Substring(i * 3 + 1, 2), System.Globalization.NumberStyles.AllowHexSpecifier);
                }
                char[] hexChar = enc.GetChars(b);
                input = input.Replace(match.Groups[0].Value, new String(hexChar));

            }
            catch
            { Console.WriteLine("QP dec err"); }
        }
        input = input.Replace("?=", ""); //.Replace("\r\n", "");

        return input;
    }

还有一个有趣的事实: 如果它是Unicode,它可以只是一个符号或一组十六进制匹配一个符号。例如 '和'。 简而言之,这个解决方案根本不起作用。 - Don Joe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接