PHP的URL编码解码与原理、自定义实现

PHP实现URL编码与解码时,参考的是RFC 1738与RFC 3986:

RFC 1738: http://www.faqs.org/rfcs/rfc1738.html

RFC 3986: http://www.faqs.org/rfcs/rfc3986.html

其中rawurlencode()方法在PHP5.3.0之前,会依据RFC 1738,将波浪线~视为不安全字符,编码成%7E。

在PHP5.3.0之后则符合RFC 3986标准,将~号作为无限制字符,根据URL所处部位,可以不进行编码(通常也如此)。在PHP5.3.4之后,~则完全不编码。

rawurlencode主要的依据还是RFC 3986,其中不进行编码的字符包括:

数字,大小写字母,点号,下划线,减号,波浪号 (即: 0-9A-Za-z._-~)

其它都采用8位(一个字节)的%XX形式来编码

urlencode()方法,则采用非标准的技术,其中不进行编码的字符包括:

数字,大小写字母,点号,下划线,减号 (即: 0-9A-Za-z._-)

空格编码成+号

其它都采用8位(一个字节)的%XX形式来编码

可以看到,与urlencode()与rawurlencode()不同在于对波浪线空格的处理。相同点在于对数字、大小写字母、减号 - ,点号 . ,下划线 _ 都不进行编码。

PHP文档中对urlencode()及rawurlencode()的说明:

urlencode ( string $str ) : string — 编码 URL 字符串

返回值

返回字符串,此字符串中除了 -_. 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数,空格则编码为加号(+)。

此编码与 WWW 表单 POST 数据的编码方式是一样的,

同时与 application/x-www-form-urlencoded 的媒体类型编码方式一样。

由于历史原因,此编码在将空格编码为加号(+)方面与 RFC3986 编码(参见 rawurlencode())不同。

rawurlencode ( string $str ) : string — 按照 RFC 3986 对 URL 进行编码

返回值

返回字符串,此字符串中除了 -_. 之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数。

这是在 » RFC 3986 中描述的编码,是为了保护原义字符以免其被解释为特殊的 URL 定界符,同时保护 URL 格式以免其被传输媒体(像一些邮件系统)使用字符转换时弄乱。

实例:

echo urlencode("中文(zh_CN) : 2020.11~2020.12--%20+Days[!$*',%#?]") . nl2br(PHP_EOL);

在PHP文件编码为GBK时,显示结果:

%D6%D0%CE%C4%28zh_CN%29+%3A+2020.11%7E2020.12--%2520%2BDays%5B%21%24%2A%27%2C%25%23%3F%5D

urlencode()在GBK编码下,可以看到:

中文两字被编码成 %D6%D0%CE%C4 这是它的GBK编码,每个字节(8位)前都加了%

空格被编码成 +

~被编码成 %7E

数字、字母、. _ - 号都不被编码

其它的特殊字符都编码为%XX形式

在PHP文件编码为UTF-8时,显示结果:

%E4%B8%AD%E6%96%87%28zh_CN%29+%3A+2020.11%7E2020.12--%2520%2BDays%5B%21%24%2A%27%2C%25%23%3F%5D

urlencode()的在UTF-8编码下,可以看到:

中文两字被编码成 %E4%B8%AD%E6%96%87 这是它的UTF-8编码,每个字节(8位)前都加了%

其它的与GBK编码结果一致。

echo rawurlencode("中文(zh_CN) : 2020.11~2020.12--%20+Days[!$*',%#?]") . nl2br(PHP_EOL);

在PHP文件编码为GBK时,显示结果:

%D6%D0%CE%C4%28zh_CN%29%20%3A%202020.11~2020.12--%2520%2BDays%5B%21%24%2A%27%2C%25%23%3F%5D

rawurlencode()在GBK编码下,可以看到:

中文两字被编码成 %D6%D0%CE%C4 这是它的GBK编码,每个字节(8位)前都加了%

空格被编码成 %20

数字、字母、. _ - ~ 号都不被编码

其它的特殊字符都编码为%XX形式

在PHP文件编码为UTF-8时,显示结果:

%E4%B8%AD%E6%96%87%28zh_CN%29%20%3A%202020.11~2020.12--%2520%2BDays%5B%21%24%2A%27%2C%25%23%3F%5D

rawurlencode()的在UTF-8编码下,可以看到:

中文两字被编码成 %E4%B8%AD%E6%96%87 这是它的UTF-8编码,每个字节(8位)前都加了%

其它的与GBK编码结果一致。

通过以上的例子,我们可以发现,urlencode()与rawurlencode()在PHP下自己也可以很方便实现,其原理就是:

不管文件编码是GBK还是UTF-8, 除了数字、大小写字母、._-号,以及特殊处理空格及波浪线,其它字符(单字节的ASCII码特殊字符,双字节的GBK或不定长字节的UTF-8)都是各字节的十六进制(两位)大写形式前加%(即%XX)

自定义实现urlencode()及rawurlencode()----兼容RFC 3986:

function my_urlencode($str,$raw=false,$ebcdic=true){
    $url = '';
    for($i=0;$i<strlen($str);$i++){
        $ascii = ord(substr($str,$i,1));
        
        if($ascii==0x2D || $ascii==0x2E || $ascii==0x5F || ($ascii>=0x30 && $ascii<=0x39) || ($ascii>=0x41 && $ascii<=0x5A) || ($ascii>=0x61 && $ascii<=0x7A) ){//_.-以及数字、字母不转化
            $url .= chr($ascii);
        }else{
            if($ascii==0x20 && !$raw){//空格在非raw情况下,转化为+
                $url .= '+';
            }else if($ascii==0x7e && $raw && $ebcdic){//波浪线在raw情况下不转化
                $url .= chr($ascii);
            }else{
                $url .= '%' . str_pad(strtoupper(dechex($ascii)),2,"0",STR_PAD_LEFT);
            }
        }
    }
    
    return $url;
}
echo my_urlencode("中文+_zh .%- 2020~") . nl2br(PHP_EOL);

PHP文件编码为GBK时下显示:%D6%D0%CE%C4%2B_zh+.%25-+2020%7E

PHP文件编码为UTF-8时下显示:%E4%B8%AD%E6%96%87%2B_zh+.%25-+2020%7E

echo my_urlencode("中文+_zh .%- 2020~",true) . nl2br(PHP_EOL);

PHP文件编码为GBK时下显示:%D6%D0%CE%C4%2B_zh%20.%25-%202020~

PHP文件编码为UTF-8时下显示:%E4%B8%AD%E6%96%87%2B_zh%20.%25-%202020~

echo my_urlencode("中文+_zh .%- 2020~",true,false) . nl2br(PHP_EOL);

PHP文件编码为GBK时下显示:%D6%D0%CE%C4%2B_zh%20.%25-%202020%7E

PHP文件编码为UTF-8时下显示:%E4%B8%AD%E6%96%87%2B_zh%20.%25-%202020%7E

自定义实现urldecode()及rawurldecode()

function my_urldecode($str,$raw=false){
    !$raw && $str = str_replace('+','%20',$str);//非raw的话需解码+号为空格
    
    $url = '';
    for($i=0;$i<strlen($str);$i++){
        $c = substr($str,$i,1);
        if($c=='%'){
            $url .= hex2bin(substr($str,$i+1,2));
            $i = $i+2;
            if($i>=strlen($str)) break;
        }else{//_.-~以及数字、字母
            $url .= $c;
        }
    }
    
    return $url;
}
echo my_urldecode('%D6%D0%20%7E+~') . nl2br(PHP_EOL);
echo my_urldecode('%D6%D0%20%7E+~',true) . nl2br(PHP_EOL);

PHP文件为GBK下显示:

中 ~ ~

中 ~+~

另外还可以对不是符合URL编码的字符串进行解码而不出错的情况,修改下:

function my_urldecode_ext($str,$raw=false){
    !$raw && $str = str_replace('+','%20',$str);//非raw的话需解码+号为空格
    
    $url = '';
    $len = strlen($str);
    for($i=0;$i<$len;$i++){
        
        $c = substr($str,$i,1);
        if($c=='%'){
            if($i+1<=$len){
                $c2 = substr($str,$i+1,1);
                if($i+2<=$len){
                    $c3 = substr($str,$i+2,1);
                    if((ord($c2)>=0x30 && ord($c2)<=0x39) || (ord($c2)>=0x41 && ord($c2)<=0x46) || (ord($c2)>=0x61 && ord($c2)<=0x66)){
                        if((ord($c3)>=0x30 && ord($c3)<=0x39) || (ord($c3)>=0x41 && ord($c3)<=0x46) || (ord($c3)>=0x61 && ord($c3)<=0x66)){
                            $url .= hex2bin($c2.$c3);
                            $i = $i+2;
                        }else{
                            $url .= hex2bin("0".$c2);
                            $i = $i+1;
                        }
                    }else{
                        $url .= '%';
                    }
                }else{
                    if((ord($c2)>=0x30 && ord($c2)<=0x39) || (ord($c2)>=0x41 && ord($c2)<=0x46) || (ord($c2)>=0x61 && ord($c2)<=0x66)){
                        $url .= hex2bin("0".$c2);
                        $i = $i+1;
                    }else{
                        $url .= '%';
                    }
                }
            }else{
                $url .= '%';
            }
            
            if($i>=strlen($str)) break;
        }else{//_.-~以及数字、字母
            $url .= $c;
        }
    }
    
    return $url;
}
echo my_urldecode_ext('%D6%D0%20%7E+~%x%%%7%E') . nl2br(PHP_EOL);
echo my_urldecode_ext('%D6%D0%20%7E+~%x%%%7%E',true) . nl2br(PHP_EOL);

PHP文件为GBK下显示:

中 ~ ~%x%%

中 ~+~%x%%

其中%x、%%、%7、%E 由于不符合URL编码后的规则,被解码为:%x、%%、chr(0x7)、chr(0xE),后两者为为非显示的控制字符。