urlencode vs rawurlencode?

420

如果我想使用变量创建一个URL,我有两种选择来编码字符串。 urlencode()rawurlencode()

它们的区别是什么,哪个更受推荐?


1
我真的很想看到选择一个而不是另一个的原因(例如可能遇到的问题),我(以及其他人)希望能够仅选择一个并且永远使用它,而不会有太多麻烦,因此我已经在这个问题上开始了悬赏。 - Kzqai
37
如果只能选择一个,选择rawurlencode。将空格编码为%20的系统很少会出现问题,而那些将空格编码为+的系统则更加普遍,可能会导致错误。 - Anomie
11个回答

350
这将取决于您的目的。如果与其他系统的互操作性很重要,那么似乎rawurlencode是正确的选择。唯一的例外是那些期望查询字符串遵循表单编码样式,将空格编码为+而不是%20的旧系统(在这种情况下,您需要使用urlencode)。
rawurlencode在PHP 5.3.0之前遵循RFC 1738,在之后遵循RFC 3986(请参阅https://www.php.net/manual/en/function.rawurlencode.php)。
引用:
返回一个字符串,其中所有非字母数字字符(除了-_.~)都被替换为一个百分号(%)后跟两个十六进制数字。这是在RFC 3986中描述的编码方式,用于保护文字字符不被解释为特殊的URL分隔符,并保护URL免受某些电子邮件系统等字符转换的传输媒体破坏。
关于RFC 3986与1738的说明。在PHP 5.3之前,rawurlencode根据RFC 1738对波浪符(~)进行编码。然而,从PHP 5.3开始,rawurlencode遵循RFC 3986,不需要对波浪符进行编码。

urlencode将空格编码为加号(而不是像rawurlencode中的%20那样)(参见https://www.php.net/manual/en/function.urlencode.php

返回一个字符串,其中除了-_.之外的所有非字母数字字符都被替换为一个百分号(%)后跟两个十六进制数字,并且空格被编码为加号(+)。它的编码方式与从WWW表单中发布的数据的编码方式相同,即与application/x-www-form-urlencoded媒体类型中的编码方式相同。这与RFC 3986编码(参见rawurlencode())不同之处在于,由于历史原因,空格被编码为加号(+)。

这对应于RFC 1866中对application/x-www-form-urlencoded的定义。

附加阅读:

您还可以参阅http://bytes.com/groups/php/5624-urlencode-vs-rawurlencode中的讨论。

此外,RFC 2396 也值得一看。RFC 2396 定义了有效的 URI 语法。我们感兴趣的主要部分是从 3.4 查询组件开始:
在查询组件中,字符 ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" 是保留字符。
正如你所看到的,+ 是查询字符串中的保留字符,因此需要按照 RFC 3986 进行编码(如 rawurlencode)。

85
在这种情况下,建议使用标准的 rawurlencode。urlencode 仅为了向后兼容而保留。 - Jonathan Fingland
2
非常感谢,这正是我所想的,我只是想在开始更新大量代码之前听取第二个意见。 - Gary Willoughby
4
我认为是 rawurlencode 不会将空格编码为加号而是编码为 %20。 - BigName
1
@Jonathan Fingland 你好,Jonathan。我刚刚注意到这个答案在谷歌上的urlencode搜索中排名很高。虽然从技术上讲它是正确的,但是它有点难以阅读,你认为你愿意重新格式化它以提高清晰度,使其成为对来到此页面的PHP程序员更有用的资源吗?如果你同意,我也愿意自己重新格式化它以提高清晰度。 - Kzqai
2
@Pindatjuh:你引用的那部分内容 唯一的例外是那些期望查询字符串遵循表单编码样式,将空格编码为+而不是%20的旧系统(在这种情况下,你需要使用urlencode) 意味着虽然rawurlencode对大多数情况来说是正确的,但有些系统期望空格被编码为+(加号)。对于这样的系统,urlencode是更好的选择。 - Jonathan Fingland
显示剩余2条评论

225

证明在 PHP 的源代码中。

我将带你快速了解如何在未来自己查找此类信息的过程。请耐心等待,会有很多 C 源代码,你可以浏览(我会解释的)。如果你想学习一些 C 语言,可以从我们的 SO wiki 开始

下载源代码(或使用 https://heap.space/ 在线浏览),在所有文件中搜索函数名称,你会发现类似于这样的内容:

PHP 5.3.6(写作时最新版本)在文件 url.c 中以其本地 C 代码描述了这两个函数。

RawUrlEncode()

PHP_FUNCTION(rawurlencode)
{
    char *in_str, *out_str;
    int in_str_len, out_str_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &in_str,
                              &in_str_len) == FAILURE) {
        return;
    }

    out_str = php_raw_url_encode(in_str, in_str_len, &out_str_len);
    RETURN_STRINGL(out_str, out_str_len, 0);
}

UrlEncode()

PHP_FUNCTION(urlencode)
{
    char *in_str, *out_str;
    int in_str_len, out_str_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &in_str,
                              &in_str_len) == FAILURE) {
        return;
    }

    out_str = php_url_encode(in_str, in_str_len, &out_str_len);
    RETURN_STRINGL(out_str, out_str_len, 0);
}

好的,那么这里有什么不同之处?

它们本质上分别调用了两个不同的内部函数:php_raw_url_encodephp_url_encode

所以去找那些函数吧!

让我们来看看php_raw_url_encode

PHPAPI char *php_raw_url_encode(char const *s, int len, int *new_length)
{
    register int x, y;
    unsigned char *str;

    str = (unsigned char *) safe_emalloc(3, len, 1);
    for (x = 0, y = 0; len--; x++, y++) {
        str[y] = (unsigned char) s[x];
#ifndef CHARSET_EBCDIC
        if ((str[y] < '0' && str[y] != '-' && str[y] != '.') ||
            (str[y] < 'A' && str[y] > '9') ||
            (str[y] > 'Z' && str[y] < 'a' && str[y] != '_') ||
            (str[y] > 'z' && str[y] != '~')) {
            str[y++] = '%';
            str[y++] = hexchars[(unsigned char) s[x] >> 4];
            str[y] = hexchars[(unsigned char) s[x] & 15];
#else /*CHARSET_EBCDIC*/
        if (!isalnum(str[y]) && strchr("_-.~", str[y]) != NULL) {
            str[y++] = '%';
            str[y++] = hexchars[os_toascii[(unsigned char) s[x]] >> 4];
            str[y] = hexchars[os_toascii[(unsigned char) s[x]] & 15];
#endif /*CHARSET_EBCDIC*/
        }
    }
    str[y] = '\0';
    if (new_length) {
        *new_length = y;
    }
    return ((char *) str);
}

当然,还有php_url_encode:

PHPAPI char *php_url_encode(char const *s, int len, int *new_length)
{
    register unsigned char c;
    unsigned char *to, *start;
    unsigned char const *from, *end;
    
    from = (unsigned char *)s;
    end = (unsigned char *)s + len;
    start = to = (unsigned char *) safe_emalloc(3, len, 1);

    while (from < end) {
        c = *from++;

        if (c == ' ') {
            *to++ = '+';
#ifndef CHARSET_EBCDIC
        } else if ((c < '0' && c != '-' && c != '.') ||
                   (c < 'A' && c > '9') ||
                   (c > 'Z' && c < 'a' && c != '_') ||
                   (c > 'z')) {
            to[0] = '%';
            to[1] = hexchars[c >> 4];
            to[2] = hexchars[c & 15];
            to += 3;
#else /*CHARSET_EBCDIC*/
        } else if (!isalnum(c) && strchr("_-.", c) == NULL) {
            /* Allow only alphanumeric chars and '_', '-', '.'; escape the rest */
            to[0] = '%';
            to[1] = hexchars[os_toascii[c] >> 4];
            to[2] = hexchars[os_toascii[c] & 15];
            to += 3;
#endif /*CHARSET_EBCDIC*/
        } else {
            *to++ = c;
        }
    }
    *to = 0;
    if (new_length) {
        *new_length = to - start;
    }
    return (char *) start;
}

在我继续之前,一个快速的知识点,EBCDIC是另一个字符集,类似于ASCII,但是完全竞争对手。PHP尝试同时处理两者。但是基本上,这意味着EBCDIC 0x4c字节不是ASCII中的L,而实际上是一个<。我相信你看到了这里的困惑。

如果Web服务器已定义,这两个功能都可以管理EBCDIC。

此外,它们都使用一组字符(字符串类型)hexchars查找一些值,该数组的描述如下:

/* rfc1738:

   ...The characters ";",
   "/", "?", ":", "@", "=" and "&" are the characters which may be
   reserved for special meaning within a scheme...

   ...Thus, only alphanumerics, the special characters "$-_.+!*'(),", and
   reserved characters used for their reserved purposes may be used
   unencoded within a URL...

   For added safety, we only leave -_. unencoded.
 */

static unsigned char hexchars[] = "0123456789ABCDEF";

除此之外,这些函数的功能真的很不同,我将用ASCII和EBCDIC来解释它们。

ASCII中的差异:

URLENCODE:

  • 计算输入字符串的开始/结束长度,分配内存
  • 遍历while循环,增加直到到达字符串的结尾
  • 获取当前字符
  • 如果字符等于ASCII Char 0x20(即“空格”),则在输出字符串中添加一个+符号。
  • 如果它不是空格,并且也不是字母数字(isalnum(c)),并且还不是_-.字符,则我们会将一个%符号输出到数组位置0,对hexchars数组进行查找以进行os_toascii数组(从Apache that translates char to hex code)的键值
  • 如果最终发现它不是空格,它是字母数字或_-.字符之一,则输出与它完全相同的内容。

RAWURLENCODE:

  • 为字符串分配内存
  • 根据函数调用中提供的长度进行迭代(不像URLENCODE函数那样计算)。

注意:许多程序员可能从未见过for循环以这种方式迭代,这有点hackish并且不是大多数for循环使用的标准约定,请注意,它分配xy,检查len是否达到0的退出条件,并增加xy。我知道,这不是你期望的,但它是有效的代码。

  • 将当前字符分配给str中的匹配字符位置。
  • 它检查当前字符是否是字母数字或_-.字符之一,如果不是,则执行与URLENCODE几乎相同的分配操作,其中它执行查找,但我们使用y++而不是to [1]进行不同的递增,这是因为字符串以不同的方式构建,但最终都达到相同的目标。
  • 循环完成并且长度已经消失后,实际上会终止该字符串,分配\0字节。
  • 它返回编码后的字符串。

差异:

  • UrlEncode检查空格并分配一个+号,RawURLEncode则不会。
  • UrlEncode不会向字符串分配\0字节,而RawUrlEncode会(这可能是无关紧要的)
  • 它们迭代方式不同,其中一个可能容易因格式错误的字符串而溢出,我仅仅建议这个问题,我实际上没有调查过。

它们基本上迭代方式不同,在ASCII 20的情况下会分配一个+号。

EBCDIC中的区别:

URLENCODE:

  • 与ASCII相同的迭代设置
  • 仍将“空格”字符转换为+符号。注意--我认为这需要在EBCDIC中编译,否则你会遇到错误?有人可以编辑并确认吗?
  • 它检查当前字符是否是小于0的字符,除了.-之外,小于char 9但大于char的例外。 A大于Z且小于a但不是_大于z(是的,EBCDIC处理起来有点混乱)。如果它匹配其中任何一个,则执行类似于ASCII版本中找到的查找操作(它只不过不需要在os_toascii中进行查找)。

RAWURLENCODE:

  • 与ASCII相同的迭代设置。
  • 与EBCDIC版本的URL编码描述中所述的检查相同,但是如果大于 z ,则从URL编码中排除 ~
  • 与ASCII RawUrlEncode相同的分配
  • 在返回之前仍将 \ 0 字节附加到字符串。

总结

  • 两者都使用相同的hexchars查找表
  • URIEncode不使用\ 0终止字符串,Raw使用。
  • 如果您正在使用EBCDIC,则建议使用RawUrlEncode,因为它可以管理UrlEncode不支持的 ~ 这是已报告的问题)。 值得注意的是,ASCII和EBCDIC 0x20都是空格。
  • 它们以不同的方式迭代,其中一种可能更快,另一种可能容易受到基于内存或字符串的攻击。
  • URIEncode将空格变成 + ,RawUrlEncode通过数组查找将空格变成%20

免责声明:我多年没有接触过C语言,也很长时间没有看过EBCDIC。 如果我有错误,请让我知道。

建议的实现

基于所有这些,大多数情况下都应该使用rawurlencode。 如Jonathan Fingland的答案中所示,在大多数情况下坚持使用它。 它处理了URI组件的现代方案,而urlencode则以旧方式处理,其中+表示“空格”。

如果您正在尝试在旧格式和新格式之间进行转换,请确保您的代码不会出错,并将解码后的+符号转换为空格,例如通过意外双重编码或类似的与此空格/20%/+问题相关的“糟糕”场景。

如果您正在使用不偏好新格式的旧软件的较旧系统,请坚持使用urlencode,但是我认为%20实际上是向后兼容的,因为在旧标准下%20有效,只是不被偏爱。 如果您想尝试玩耍,请试试,告诉我们它对您有何作用。

基本上,除非您的EBCDIC系统真的不喜欢您,否则应坚持使用原始工具。 大多数程序员在2000年之后甚至1990年之后制造的任何系统上都不会遇到EBCDIC(这是推测,但在我看来仍然很可能)。


我从来没有担心过双重编码,因为我应该知道我所编码的内容,毕竟是我自己进行编码的。由于我使用兼容模式对接收到的所有内容进行解码,该模式知道如何将“+”视为空格,因此我同样从未遇到过您在此处试图警告的问题。我可以理解如果我们不知道某些东西的作用,那么查看源代码可能有所帮助,但我们从执行这两个函数中学到了什么,这一点我并不清楚。我知道我有偏见,但我不禁认为这有些过头了。不过还是要赞扬你的努力!=) - nickl-
2
+1,对于这部分内容:“我相信%20实际上是向后兼容的,因为在旧标准下%20可以工作,只是不被推荐使用。” - Gras Double
UrlEncode不会给字符串分配\0字节,这是不正确的。它只是以不同的方式完成。请参见*to = 0;。可以将其解释为将值零分配给to指向的位置。此时,to指向应该有空字节的位置。另外,0'\0'是相等的,只是表达相同事物的不同方式。 - mdfst13

39
echo rawurlencode('http://www.google.com/index.html?id=asd asd');
产生。
http%3A%2F%2Fwww.google.com%2Findex.html%3Fid%3Dasd%20asd

当...的时候

echo urlencode('http://www.google.com/index.html?id=asd asd');
产出。
http%3A%2F%2Fwww.google.com%2Findex.html%3Fid%3Dasd+asd

两者的差异在于asd%20asdasd+asd

urlencode与RFC 1738的不同之处在于将空格编码为+而不是%20


29

选择urlencoderawurlencode的一个实际原因是如果你要在另一个环境中使用结果,例如JavaScript。

在PHP中,urlencode('test 1')返回'test+1',而rawurlencode('test 1')返回'test%201'

但是,如果你需要使用decodeURI()函数在JavaScript中“解码”它,那么decodeURI("test+1")将给出"test+1",而decodeURI("test%201")将给出"test 1"

换句话说,在PHP中由urlencode编码的空格(" ")会被编码为加号("+"),但在JavaScript中用decodeURI解码时,不能正确地解码成空格。

在这种情况下,应该使用rawurlencode PHP函数。


这是一个不错的例子,虽然我更喜欢使用 json_encodeJSON.parse 来实现这个目的。 - Fabrício Matté

22

我认为在URL路径组件内,空格必须编码为%20

以下示例展示了正确使用rawurlencodeurlencode的方法:

echo "http://example.com"
    . "/category/" . rawurlencode("latest songs")
    . "/search?q=" . urlencode("lady gaga");

输出:

http://example.com/category/latest%20songs/search?q=lady+gaga
如果将路径和查询字符串组件反向编码会发生什么?以以下示例为例:
http://example.com/category/latest+songs/search?q=lady%20gaga
  • Web服务器将寻找目录latest+songs,而不是latest songs
  • 查询字符串参数q将包含lady gaga

2
查询字符串参数 q 将包含 lady gaga。否则它会包含什么?无论在 PHP 5.2+ 中使用 rawurlencode 还是 urlencode,查询参数 q 的值似乎都与传递给 $_GET 数组的值相同。但是,urlencode 编码为 application/x-www-form-urlencoded 格式,这是 GET 请求的默认格式,所以我采用了您的方法。+1 - Fabrício Matté
2
我想澄清一下,当在查询字符串中使用+%20时,它们都被解码为空格。 - Salman A

6

1. 两者有何不同?

唯一的区别在于空格的处理方式:

urlencode - 基于旧版本实现,将空格转换为 +

rawurlencode - 基于RFC 1738,将空格转换为 %20

区别的原因是 + 在url中被保留并且有效(未编码)。

2. 哪个更好?

我真的很想知道选择一个而不是另一个的原因... 我希望能够只选择一个并永远使用它而不会出现任何问题。

很公平,我有一个简单的策略,在做出这些决定时我会分享给你,希望能有所帮助。

我认为HTTP/1.1规范RFC 2616要求"容错应用程序"

客户端在解析状态行时应该容错,服务器在解析请求行时应该容错。

面对这些问题时,最好的策略是尽可能多地消费并生成符合标准的内容。

所以我的建议是使用rawurlencode生成符合RFC 1738标准的编码字符串,并使用urldecode向后兼容和适应您可能遇到的任何内容。

现在你可以相信我的话,但让我们来证明一下吧...

php > $url = <<<'EOD'
<<< > "Which, % of Alice's tasks saw $s @ earnings?"
<<< > EOD;
php > echo $url, PHP_EOL;
"Which, % of Alice's tasks saw $s @ earnings?"
php > echo urlencode($url), PHP_EOL;
%22Which%2C+%25+of+Alice%27s+tasks+saw+%24s+%40+earnings%3F%22
php > echo rawurlencode($url), PHP_EOL;
%22Which%2C%20%25%20of%20Alice%27s%20tasks%20saw%20%24s%20%40%20earnings%3F%22
php > echo rawurldecode(urlencode($url)), PHP_EOL;
"Which,+%+of+Alice's+tasks+saw+$s+@+earnings?"
php > // oops that's not right???
php > echo urldecode(rawurlencode($url)), PHP_EOL;
"Which, % of Alice's tasks saw $s @ earnings?"
php > // now that's more like it

看起来 PHP 正是考虑到这一点,尽管我从未遇到拒绝其中任何一种格式的人,但我想不出更好的默认策略可供采用,你呢?

愉快!


5

将空格编码为%20 vs. +

在大多数情况下,我看到使用rawurlencode()的最大原因是因为urlencode将文本中的空格编码为+(加号),而rawurlencode将其编码为常见的%20

echo urlencode("red shirt");
// red+shirt

echo rawurlencode("red shirt");
// red%20shirt

我特别注意到某些API端点接受编码文本查询,并期望使用%20表示空格,如果使用加号代替,可能会导致失败。显然,这将因API实现而异,因此您的结果可能会有所不同。


5

区别在于返回值,即:

urlencode()

返回一个字符串,在该字符串中所有非字母数字字符(除了-_.)都被替换为一个百分号(%)后跟着两个十六进制数字,空格则被编码为加号(+)。它的编码方式与WWW表单提交的编码方式相同,即application/x-www-form-urlencoded媒体类型的编码方式。这与RFC 1738编码(请参见rawurlencode())不同之处在于,由于历史原因,空格被编码为加号(+)。

rawurlencode()

返回一个字符串,其中所有非字母数字字符(除了-_.)都被替换为百分号(%)后跟两个十六进制数字。这是在 RFC 1738 中描述的编码,用于保护文字字符不被解释为特殊的 URL 分隔符,并保护 URL 免受某些电子邮件系统等传输媒体的字符转换破坏。

这两种方法非常相似,但后者(rawurlencode)将空格替换为“%”和两个十六进制数字,适用于编码密码或其他类似情况,其中“+”不合适,例如:

echo '<a href="ftp://user:', rawurlencode('foo @+%/'),
     '@ftp.example.com/x.txt">';
//Outputs <a href="ftp://user:foo%20%40%2B%25%2F@ftp.example.com/x.txt">

2
OP问如何知道应该使用哪个,以及何时使用。了解每个返回值对空格的影响并不能帮助OP做出决策,如果他不知道不同返回值的重要性。 - dotancohen

4

urlencode:与RFC 1738编码(请参见rawurlencode())不同的是,由于历史原因,空格被编码为加号(+)。


1

简单来说 * 对路径进行rawurlencode编码 - 路径是问号之前的部分 - 空格必须编码为%20 * 对查询字符串进行urlencode编码 - 查询字符串是问号之后的部分 - 空格最好编码为"+" = rawurlencode通常更兼容


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