如何编码 Azure 存储表行键和分区键?

Posted

技术标签:

【中文标题】如何编码 Azure 存储表行键和分区键?【英文标题】:How can I encode Azure storage table row keys and partition keys? 【发布时间】:2014-02-04 08:25:47 【问题描述】:

我正在使用 Azure 存储表,并且我有数据进入包含斜杠的 RowKey。根据this MSDN page,PartitionKey 和 RowKey 都不允许使用以下字符:

正斜杠 (/) 字符

反斜杠()字符

数字符号 (#) 字符

问号 (?) 字符

从 U+0000 到 U+001F 的控制字符,包括:

水平制表符 (\t) 字符

换行符(\n)

回车 (\r) 字符

从 U+007F 到 U+009F 的控制字符

我见过一些人使用 URL 编码来解决这个问题。不幸的是,这可能会导致一些故障,例如能够插入但无法删除某些实体。我还看到有些人使用 base64 编码,但这也可能包含不允许的字符。

如何有效地编码我的 RowKey 而不会遇到不允许的字符或滚动我自己的编码?

【问题讨论】:

“但无法删除某些实体”为什么会这样? @usr 这是一个错误。不知道为什么,但我已经看到了很多关于它的报告。 另见:Azure Table Storage RowKey restricted Character Patterns? 【参考方案1】:

2020 年 8 月 18 日更新了(新?)Azure 搜索中的“+”字符问题。有关背景,请参见下面@mladenb 的 cmets。值得注意的是,引用的文档页面不排除“+”字符。

当 URL 采用 Base64 编码时,Azure 表存储键列中唯一无效的字符是正斜杠 ('/')。若要解决此问题,只需将正斜杠字符替换为 (1) 在 Azure 表存储键列中有效且 (2) 不是 Base64 字符的另一个字符。我发现的最常见的例子(在其他答案中被引用)是用下划线('_')替换正斜杠('/')。

private static String EncodeUrlInKey(String url)

    var keyBytes = System.Text.Encoding.UTF8.GetBytes(url);
    var base64 = System.Convert.ToBase64String(keyBytes);
    return base64.Replace('/','_').Replace('+','-');

解码时,只需撤消替换的字符(首先!),然后 Base64 解码生成的字符串。仅此而已。

private static String DecodeUrlInKey(String encodedKey)

    var base64 = encodedKey.Replace('-','+').Replace('_', '/');
    byte[] bytes = System.Convert.FromBase64String(base64);
    return System.Text.Encoding.UTF8.GetString(bytes);

有人建议其他 Base64 字符也需要编码。根据Azure Table Storage docs,情况并非如此。

【讨论】:

由于这个有很多观点,我只是想补充一点,只是从/到base58转换要简单得多 如果您已经在使用带有 base58 例程的库,则有效点。 OP希望避免“自己动手”,这个答案没有任何假设。如果你确实走这条路,最好记录下 base58 编码正在被使用,因为有不止一个。 我想我可能更愿意将您的 UTF8 keyBytes 变量作为 HEX 字符串转储,而不是在编码时一直使用 base64。这样,我就不必弄乱你提到的正斜杠了。 @ShawnEary - 当然,如果您不介意较长的键。 BitConverter 是一种方法。您可以在此处的答案中找到其他选项:***.com/questions/623104/byte-to-hex-string @MladenB - 感谢您的跟进。示例代码已更新。【参考方案2】:

我也遇到了同样的需求。

我对 Base64 编码不满意,因为它将人类可读的字符串变成了无法识别的字符串,并且无论它们是否遵守规则都会扩大字符串的大小(当绝大多数字符不非法时,这是一种损失需要转义的字符)。

这是一个使用“!”的编码器/解码器作为转义字符,与传统上使用反斜杠字符的方式大致相同。

public static class TableKeyEncoding

    // https://msdn.microsoft.com/library/azure/dd179338.aspx
    // 
    // The following characters are not allowed in values for the PartitionKey and RowKey properties:
    // The forward slash(/) character
    // The backslash(\) character
    // The number sign(#) character
    // The question mark (?) character
    // Control characters from U+0000 to U+001F, including:
    // The horizontal tab(\t) character
    // The linefeed(\n) character
    // The carriage return (\r) character
    // Control characters from U+007F to U+009F
    public static string Encode(string unsafeForUseAsAKey)
    
        StringBuilder safe = new StringBuilder();
        foreach (char c in unsafeForUseAsAKey)
        
            switch (c)
            
                case '/':
                    safe.Append("!f");
                    break;
                case '\\':
                    safe.Append("!b");
                    break;
                case '#':
                    safe.Append("!p");
                    break;
                case '?':
                    safe.Append("!q");
                    break;
                case '\t':
                    safe.Append("!t");
                    break;
                case '\n':
                    safe.Append("!n");
                    break;
                case '\r':
                    safe.Append("!r");
                    break;
                case '!':
                    safe.Append("!!");
                    break;
                default:
                    if (c <= 0x1f || (c >= 0x7f && c <= 0x9f))
                    
                        int charCode = c;
                        safe.Append("!x" + charCode.ToString("x2"));
                    
                    else
                    
                        safe.Append(c);
                    
                    break;
            
        
        return safe.ToString();
    

    public static string Decode(string key)
    
        StringBuilder decoded = new StringBuilder();
        int i = 0;
        while (i < key.Length)
        
            char c = key[i++];
            if (c != '!' || i == key.Length)
            
                // There's no escape character ('!'), or the escape should be ignored because it's the end of the array
                decoded.Append(c);
            
            else
            
                char escapeCode = key[i++];
                switch (escapeCode)
                
                    case 'f':
                        decoded.Append('/');
                        break;
                    case 'b':
                        decoded.Append('\\');
                        break;
                    case 'p':
                        decoded.Append('#');
                        break;
                    case 'q':
                        decoded.Append('?');
                        break;
                    case 't':
                        decoded.Append('\t');
                        break;
                    case 'n':
                        decoded.Append("\n");
                        break;
                    case 'r':
                        decoded.Append("\r");
                        break;
                    case '!':
                        decoded.Append('!');
                        break;
                    case 'x':
                        if (i + 2 <= key.Length)
                        
                            string charCodeString = key.Substring(i, 2);
                            int charCode;
                            if (int.TryParse(charCodeString, NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo, out charCode))
                            
                                decoded.Append((char)charCode);
                            
                            i += 2;
                        
                        break;
                    default:
                        decoded.Append('!');
                        break;
                
            
        
        return decoded.ToString();
    

由于在编写自己的编码器时应格外小心,因此我也为它编写了一些单元测试。

using Xunit;

namespace xUnit_Tests

    public class TableKeyEncodingTests
    
        const char Unicode0X1A = (char) 0x1a;


        public void RoundTripTest(string unencoded, string encoded)
        
            Assert.Equal(encoded, TableKeyEncoding.Encode(unencoded));
            Assert.Equal(unencoded, TableKeyEncoding.Decode(encoded));
        

        [Fact]
        public void RoundTrips()
        
            RoundTripTest("!\n", "!!!n");
            RoundTripTest("left" + Unicode0X1A + "right", "left!x1aright");
        


        // The following characters are not allowed in values for the PartitionKey and RowKey properties:
        // The forward slash(/) character
        // The backslash(\) character
        // The number sign(#) character
        // The question mark (?) character
        // Control characters from U+0000 to U+001F, including:
        // The horizontal tab(\t) character
        // The linefeed(\n) character
        // The carriage return (\r) character
        // Control characters from U+007F to U+009F
        [Fact]
        void EncodesAllForbiddenCharacters()
        
            List<char> forbiddenCharacters = "\\/#?\t\n\r".ToCharArray().ToList();
            forbiddenCharacters.AddRange(Enumerable.Range(0x00, 1+(0x1f-0x00)).Select(i => (char)i));
            forbiddenCharacters.AddRange(Enumerable.Range(0x7f, 1+(0x9f-0x7f)).Select(i => (char)i));
            string allForbiddenCharacters = String.Join("", forbiddenCharacters);
            string allForbiddenCharactersEncoded = TableKeyEncoding.Encode(allForbiddenCharacters);

            // Make sure decoding is same as encoding
            Assert.Equal(allForbiddenCharacters, TableKeyEncoding.Decode(allForbiddenCharactersEncoded));

            // Ensure encoding does not contain any forbidden characters
            Assert.Equal(0, allForbiddenCharacters.Count( c => allForbiddenCharactersEncoded.Contains(c) ));
        

    

【讨论】:

是的,我认为不失去键的可读性非常重要。令更多人感到惊讶的是,他们不这样做或 URLencode,然后修复任何无法使用的东西,而不是 Base64-encode 和修复,这似乎是这里的一般方法。 这里需要注意的是,Microsoft Azure Storage Explorer v0.8.3 无法通过 PartitionKey eq 查询对象,如果您有! PartitionKey 中的字符。在幕后,事情似乎工作正常,但由于某种原因,相等运算符不能正常工作。我将此报告为错误,但如果您使用此解决方案和特定字符,请记住这一点。 我怀疑这是因为自然键只在部分情况下才有意义,而可读性通常/通常不利于代理键。 '\t''\n''\r' 的代码应该是不必要的,因为这些字符无论如何都由范围检查处理。好的,与范围的编码相比,它增加了可读性。 Decode 方法中,如果十六进制值不正确,例如当编码字符串包含!xhh 时,我们忽略所有四个字符。当escapeCode 未知时(例如!h),我们应该有相同的行为,而不是将字符! 附加到解码的字符串。【参考方案3】:

URL 编码/解码功能怎么样。它负责处理'/''?''#' 字符。

string url = "http://www.google.com/search?q=Example";
string key = HttpUtility.UrlEncode(url);
string urlBack = HttpUtility.UrlDecode(key);

【讨论】:

【参考方案4】:

查看这些链接 https://www.rfc-editor.org/rfc/rfc4648#page-7 Code for decoding/encoding a modified base64 URL(另见第二个答案:https://***.com/a/1789179/1094268)

我自己也遇到了问题。这些是我现在使用的我自己的函数。我使用了我提到的第二个答案中的技巧,以及更改了与可能仍然出现的 azure 键不兼容的 +/

private static String EncodeSafeBase64(String toEncode)

    if (toEncode == null)
        throw new ArgumentNullException("toEncode");
    String base64String = Convert.ToBase64String(Encoding.UTF8.GetBytes(toEncode));
    StringBuilder safe = new StringBuilder();
    foreach (Char c in base64String)
    
        switch (c)
        
            case '+':
                safe.Append('-');
                break;
            case '/':
                safe.Append('_');
                break;
            default:
                safe.Append(c);
                break;
        
    
    return safe.ToString();


private static String DecodeSafeBase64(String toDecode)

    if (toDecode == null)
        throw new ArgumentNullException("toDecode");
    StringBuilder deSafe = new StringBuilder();
    foreach (Char c in toDecode)
    
        switch (c)
        
            case '-':
                deSafe.Append('+');
                break;
            case '_':
                deSafe.Append('/');
                break;
            default:
                deSafe.Append(c);
                break;
        
    
    return Encoding.UTF8.GetString(Convert.FromBase64String(deSafe.ToString()));

【讨论】:

根据 Azure 文档,“+”字符在 Azure 表键字段中不是无效的。 @JasonWeber 我最初制作这个已经有一段时间了,但我很确定我记得读过那是(或曾经是)一个未记录的异常。【参考方案5】:

如果只是斜线,您可以在写入表格时简单地将它们替换为另一个字符,例如“|”并在阅读时重新替换它们。

【讨论】:

【参考方案6】:

我所看到的是,虽然很多非字母数字字符在技术上是允许的,但它作为分区和行键并不能很好地工作。

我查看了这里和其他地方已经给出的答案并写了这个: https://github.com/JohanNorberg/AlphaNumeric

两个字母数字编码器。

如果你需要转义一个主要是字母数字的字符串,你可以使用这个:

AlphaNumeric.English.Encode(str);

如果您需要转义大部分不是字母数字的字符串,您可以使用:

AlphaNumeric.Data.EncodeString(str);

编码数据:

var base64 = Convert.ToBase64String(bytes);
var alphaNumericEncodedString = base64
            .Replace("0", "01")
            .Replace("+", "02")
            .Replace("/", "03")
            .Replace("=", "04");

但是,如果您想使用例如电子邮件地址作为行键,您只需要转义“@”和“.”。此代码将执行此操作:

        char[] validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ3456789".ToCharArray();
        char[] allChars = rawString.ToCharArray();
        StringBuilder builder = new StringBuilder(rawString.Length * 2);
        for(int i = 0; i < allChars.Length; i++)
        
            int c = allChars[i];
            if((c >= 51 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))
            
                builder.Append(allChars[i]);
             
            else
            
                int index = builder.Length;
                int count = 0;
                do
                
                    builder.Append(validChars[c % 59]);
                    c /= 59;
                    count++;
                 while (c > 0);

                if (count == 1) builder.Insert(index, '0');
                else if (count == 2) builder.Insert(index, '1');
                else if (count == 3) builder.Insert(index, '2');
                else throw new Exception("Base59 has invalid count, method must be wrong Count is: " + count);
            
        

        return builder.ToString(); 

【讨论】:

以上是关于如何编码 Azure 存储表行键和分区键?的主要内容,如果未能解决你的问题,请参考以下文章

Azure 流分析分区键列在表存储中重复

如何降低 Azure 表存储延迟

Azure 表存储查询性能

一个具有许多分区键的 Azure 表存储表与许多具有较少分区键的表相比如何?

Azure 表存储:按顺序排列

Azure 表:选择分区/行键的最佳实践