CS 通识:编码算法

179 阅读4分钟

URL 编码

因为很多服务器只识别 ASCII 编码的字符,如果 URL 中包含中文等非 ASCII 字符就会产生乱码。出于兼容性考虑,就需要将编码转换为统一的格式再进行传输。

URL 编码规则

  • 以下字符保持不变:[A, Z][a, z][0, 9]-_.*
  • 其他字符先转换为 UTF-8 编码,然后对每个字节以 %XX 表示。如:”中“在 UTF-8 编码中 16 进制展示为 0xe4b8ad,编码后 %E4%B8%AD (URL 编码转换的结果总是大写)。

编码后的字符序列只包含 [A, Z][a, z][0, 9]-_.*%

Java 实现

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.io.UnsupportedEncodingException;

public class Main {
    public static void main(String[] args) {
        String encoded = null;
        try {
            // 编码
            encoded = URLEncoder.encode("中 文 !", StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        System.out.println("编码后:" + encoded); /* %E4%B8%AD+%E6%96%87+%21 */

        String decoded = null;
        try {
            // 解码
            decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        System.out.println("解码后:" + decoded); /* 中文! */
    }
}

和标准的 URL 编码稍有不同,URLEncoder 把空格字符编码成 +,而现在的 URL 编码标准要求空格被编码为 %20,不过,服务器都可以处理这两种情况。

Base64 编码

Base64 编码的目的是把二进制数据变成文本格式的数据,这样在很多文本中就可以处理二进制数据。如:电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用 Base64 对二进制文件进行编码,然后以文本的形式传送。

Base64 编码规则

  1. 把 3 byte(= 24 bit) 的二进制数据按 6 bit 一组,划分为 4 组,用 4 个 int 整数分别表示这 4 组数据;
  2. 然后查表,把 int 整数用索引对应到字符,得到编码后的字符串。

因为 6 bit 组成的整数的范围总是 [0, 63],所以,能用 64 个字符表示:

  • 字符 [A, Z] 对应索引 [0, 25];
  • 字符 [a, z] 对应索引 [26, 51];
  • 字符 [0, 9] 对应索引 [52, 61];
  • 最后两个索引 62、63 分别用字符 +/ 表示。

Base64 编码可以把任意长度的二进制数据变为纯文本,且只包含 [A, Z][a, z][0, 9]+/= 这些字符。

因为将 3 byte 的数据经过 Base64 编码后得到 4 byte 数据,所以说,它把原始数据的长度增加了 1/3。如果把 Base64 的 64 个字符编码表换成 32、48 或 58 个,就可以使用 Base32、Base48 和 Base58 编码。字符越少,编码的效率就会越低。

提问:如果要编码的数据不是 3 的倍数该怎么办?针对这种情况,需要在末尾补充零(即 Ox00),使其成为 3 的倍数再进行编码。编码后,在结尾加几个 = 表示补充了几个 0x00,解码的时候,去掉末尾补充的一个或两个 0x00 即可。

这个补充的 0 不会和字符 A 冲突吗?疑惑ing ...

针对 URL 的 Base64 编码

因为标准的 Base64 编码会出现 +/=,所以不适合把 Base64 编码后的字符串放到 URL 中。在针对 URL 的 Base64 编码中,把 + 变成 -/ 变成 _

Java 实现

import java.util.Arrays;
import java.util.Base64;

public class Main {
    public static void main(String[] args) {
        /* 在 Java 中,二进制数据就是 byte[] 数组 */
        byte[] input = new byte[] { 1, 2, 3, 4, 5, 6 }; 

        // 编码
        String encoded = Base64.getEncoder().encodeToString(input);
        System.out.println(encoded); /* AQIDBAUG */

        // 解码
        byte[] decoded = Base64.getDecoder().decode(encoded);
        System.out.println(Arrays.toString(decoded)); /* [1, 2, 3, 4, 5, 6] */
    }
}

/*
默认情况下,编码结果字符序列长度为 4 的倍数(编码前补充了 0x00,编码后自然多了 =)。
可以用 withoutPadding() 去掉填充的 =。
注:以上两种情况对解码不会有任何影响,因为编码后的长度加上 = 总是 4 的倍数,所以即使不加 = 也可以计算出原始输入的 byte[]。
*/
class Main2 {
    public static void main(String[] args) {
        byte[] input = new byte[] { 1, 2, 3, 4 };
        /* 编码1 */
        String encoded = Base64.getEncoder().encodeToString(input);
        /* 编码2 */
        String encoded2 = Base64.getEncoder().withoutPadding().encodeToString(input);
        System.out.println(encoded + "\n" + encoded2); 
        /* Output:
        AQIDBA== 
        AQIDBA
        */

        byte[] decoded = Base64.getDecoder().decode(encoded);
        byte[] decoded2 = Base64.getDecoder().decode(encoded2);
        System.out.println(Arrays.toString(decoded) + "\n" + Arrays.toString(decoded2));
    }
}

/*
使用 getUrlEncoder() 获得针对 URL 的 Base64 编码数据,对应使用 getUrlDecoder() 解码
*/
class Main3 {
    public static void main(String[] args) {
        byte[] input = new byte[] { 1, 2, 127, 0 };

        String encoded = Base64.getEncoder().encodeToString(input);
        System.out.println(encoded); /* AQJ/AA== */
        String encoded2 = Base64.getUrlEncoder().encodeToString(input);
        System.out.println(encoded2); /* AQJ_AA== */

        byte[] decoded = Base64.getUrlDecoder().decode(encoded2);
        System.out.println(Arrays.toString(decoded));
    }
}

参考