[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"getwebinfo":3,"news-item-43":4},null,{"classListRes":-1,"articleRes":5},{"code":6,"msg":3,"data":7,"total":3},200,{"id":8,"dwo_html":9,"dwo_name":10,"dwo_image":11,"dwo_time":12,"dwo_brief":10,"dwo_keywords":13,"dwo_cid":14,"dwo_show":15,"dwo_uid":16,"dwo_status":17,"dwo_comment_count":18,"dwo_status_reason":3,"dwo_status_time":3,"status_text":19},43,"\u003Ch1 style=\"text-align: start;\">使用PHP解决QQ空间头像接口中文昵称乱码问题\u003C\u002Fh1>\u003Cp style=\"text-align: start;\">最近在使用QQ空间头像接口获取用户信息时，遇到了一个比较典型的中文乱码问题\u003C\u002Fp>\u003Cp style=\"text-align: start;\">接口地址类似：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-text\">https:\u002F\u002Fusers.qzone.qq.com\u002Ffcg-bin\u002Fcgi_get_portrait.fcg?uins=22741441\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">英文昵称返回正常：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-js\">portraitCallBack({\"756048855\":[\"http:\u002F\u002Fqlogo4.store.qq.com\u002Fqzone\u002F756048855\u002F756048855\u002F100\",7323,-1,0,0,0,\"Forever\",0]})\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">但中文昵称会变成乱码：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-js\">portraitCallBack({\"22741441\":[\"http:\u002F\u002Fqlogo2.store.qq.com\u002Fqzone\u002F22741441\u002F22741441\u002F100\",1982,-1,0,0,0,\"����\",0]})\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2 style=\"text-align: start;\">问题原因\u003C\u002Fh2>\u003Cp style=\"text-align: start;\">一开始很容易以为这是PHP输出编码问题，于是尝试：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-php\">mb_convert_encoding($response, 'UTF-8', 'GBK');\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">但如果接口原始返回里已经是 \u003Cspan style=\"color: rgb(185, 74, 72); background-color: rgb(243, 243, 243);\">\u003Ccode>����\u003C\u002Fcode>\u003C\u002Fspan> 或 \u003Cspan style=\"color: rgb(185, 74, 72); background-color: rgb(243, 243, 243);\">\u003Ccode>�\u003C\u002Fcode>\u003C\u002Fspan>，后端再转码也无法恢复\u003C\u002Fp>\u003Cp style=\"text-align: start;\">关键点在于：普通GET请求时，QQ接口返回的中文昵称已经在服务端或网关层被错误转换了。此时拿到的不是GBK原始字节，而是已经损坏的替换字符。\u003C\u002Fp>\u003Cp style=\"text-align: start;\">后来发现，只要请求时加上：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-http\">Content-Type: multipart\u002Fform-data\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">并且让GET请求携带一个非空body，就可以触发QQ接口返回GBK\u002FGB18030原始中文字节\u003C\u002Fp>\u003Cp style=\"text-align: start;\">也就是说，真正起作用的不是PHP本身，而是这个请求特征改变了QQ接口的服务端处理分支：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-text\">GET + multipart\u002Fform-data + 非空body\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">拿到GBK原始字节后，再用PHP转成UTF-8，中文昵称就正常了\u003C\u002Fp>\u003Ch2 style=\"text-align: start;\">示例一：GuzzleHTTP 版本\u003C\u002Fh2>\u003Ch2 style=\"text-align: start;\">安装GuzzleHTTP\u003C\u002Fh2>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-bash\">composer require guzzlehttp\u002Fguzzle\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">完整代码：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-php\">&lt;?php\ndeclare(strict_types=1);\n\nrequire __DIR__ . '\u002Fvendor\u002Fautoload.php';\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\RequestOptions;\n\nheader('Content-Type: application\u002Fjson; charset=utf-8');\n\n$qq = $_GET['qq'] ?? '756048855';\n\ntry {\n    if (!preg_match('\u002F^[1-9][0-9]{4,11}$\u002F', $qq)) {\n        throw new RuntimeException('Invalid QQ number', 400);\n    }\n\n    $result = [\n        'code' =&gt; 200,\n        'qq' =&gt; $qq,\n        'data' =&gt; getQqUserInfo($qq),\n        'time' =&gt; date('Y-m-d H:i:s'),\n    ];\n} catch (Throwable $e) {\n    $status = ($e-&gt;getCode() &gt;= 400 &amp;&amp; $e-&gt;getCode() &lt; 600) ? $e-&gt;getCode() : 500;\n    http_response_code($status);\n\n    $result = [\n        'code' =&gt; $status,\n        'message' =&gt; $e-&gt;getMessage(),\n        'time' =&gt; date('Y-m-d H:i:s'),\n    ];\n}\n\necho json_encode(\n    $result,\n    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT\n);\n\n\u002F**\n * 获取QQ用户基础信息\n * @param string $qq QQ号码\n * @return array\n * @throws JsonException\n * @throws RuntimeException\n *\u002F\nfunction getQqUserInfo(string $qq): array\n{\n    $response = fetchQqPortrait($qq);\n    $response = mb_convert_encoding($response, 'UTF-8', 'GB18030');\n    $json = unwrapJsonp($response);\n    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);\n    $item = $data[$qq] ?? [];\n\n    return [\n        'name' =&gt; $item[6] ?? '',\n        'mail' =&gt; \"{$qq}@qq.com\",\n        'avatar' =&gt; isset($item[0]) ? str_replace('http:\u002F\u002F', 'https:\u002F\u002F', $item[0]) : '',\n        'qzone' =&gt; \"https:\u002F\u002Fuser.qzone.qq.com\u002F{$qq}\",\n        'imgurl' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=40\",\n        'imgurl1' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=100\",\n        'imgurl2' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=140\",\n        'imgurl3' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=640\",\n    ];\n}\n\n\u002F**\n * 使用GuzzleHTTP请求QQ空间头像接口\n * @param string $qq QQ号码\n * @return string\n *\n * @throws RuntimeException\n *\u002F\nfunction fetchQqPortrait(string $qq): string\n{\n    $client = new Client([\n        'timeout' =&gt; 10,\n        'connect_timeout' =&gt; 5,\n        'http_errors' =&gt; false,\n        'allow_redirects' =&gt; ['max' =&gt; 3],\n        'headers' =&gt; [\n            'User-Agent' =&gt; 'Mozilla\u002F5.0',\n            'Referer' =&gt; 'https:\u002F\u002Fuser.qzone.qq.com\u002F',\n        ],\n    ]);\n\n    $url = \"https:\u002F\u002Fusers.qzone.qq.com\u002Ffcg-bin\u002Fcgi_get_portrait.fcg?uins={$qq}\";\n\n    $response = $client-&gt;request('GET', $url, [\n        'version' =&gt; '1.1',\n        RequestOptions::BODY =&gt; 'x',\n        RequestOptions::HEADERS =&gt; [\n            'Content-Type' =&gt; 'multipart\u002Fform-data; boundary=----qq-portrait-boundary',\n        ],\n    ]);\n\n    $statusCode = $response-&gt;getStatusCode();\n\n    if ($statusCode &lt; 200 || $statusCode &gt;= 300) {\n        throw new RuntimeException(\"Qzone API returned HTTP {$statusCode}\", 502);\n    }\n\n    return (string) $response-&gt;getBody();\n}\n\n\u002F**\n * 去除QQ接口返回值外层的JSONP回调函数\n * @param string $response QQ接口返回的JSONP字符串\n * @return string\n *\n * @throws RuntimeException\n *\u002F\nfunction unwrapJsonp(string $response): string\n{\n    if (!preg_match('\u002F^[^(]+\\((.*)\\)\\s*;?$\u002Fs', trim($response), $matches)) {\n        throw new RuntimeException('Invalid JSONP response', 502);\n    }\n\n    return $matches[1];\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2 style=\"text-align: start;\">示例二：PHP cURL 版本\u003C\u002Fh2>\u003Cp style=\"text-align: start;\">如果项目里不想引入GuzzleHTTP，也可以直接使用PHP原生cURL\u003C\u002Fp>\u003Cp style=\"text-align: start;\">完整代码：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-php\">&lt;?php\ndeclare(strict_types=1);\n\nheader('Content-Type: application\u002Fjson; charset=utf-8');\n\n$qq = $_GET['qq'] ?? '756048855';\n\ntry {\n    if (!preg_match('\u002F^[1-9][0-9]{4,11}$\u002F', $qq)) {\n        throw new RuntimeException('Invalid QQ number', 400);\n    }\n\n    $result = [\n        'code' =&gt; 200,\n        'qq' =&gt; $qq,\n        'data' =&gt; getQqUserInfoByCurl($qq),\n        'time' =&gt; date('Y-m-d H:i:s'),\n    ];\n} catch (Throwable $e) {\n    $status = ($e-&gt;getCode() &gt;= 400 &amp;&amp; $e-&gt;getCode() &lt; 600) ? $e-&gt;getCode() : 500;\n    http_response_code($status);\n\n    $result = [\n        'code' =&gt; $status,\n        'message' =&gt; $e-&gt;getMessage(),\n        'time' =&gt; date('Y-m-d H:i:s'),\n    ];\n}\n\necho json_encode(\n    $result,\n    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT\n);\n\n\u002F**\n * 通过PHP cURL获取QQ用户基础信息\n * @param string $qq QQ号码\n * @return array\n * @throws JsonException\n * @throws RuntimeException\n *\u002F\nfunction getQqUserInfoByCurl(string $qq): array\n{\n    $response = fetchQqPortraitByCurl($qq);\n    $response = mb_convert_encoding($response, 'UTF-8', 'GB18030');\n    $json = unwrapJsonp($response);\n    $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);\n\n    $item = $data[$qq] ?? [];\n\n    return [\n        'name' =&gt; $item[6] ?? '',\n        'mail' =&gt; \"{$qq}@qq.com\",\n        'avatar' =&gt; isset($item[0]) ? str_replace('http:\u002F\u002F', 'https:\u002F\u002F', $item[0]) : '',\n        'qzone' =&gt; \"https:\u002F\u002Fuser.qzone.qq.com\u002F{$qq}\",\n        'imgurl' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=40\",\n        'imgurl1' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=100\",\n        'imgurl2' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=140\",\n        'imgurl3' =&gt; \"https:\u002F\u002Fq1.qlogo.cn\u002Fg?b=qq&amp;nk={$qq}&amp;s=640\",\n    ];\n}\n\n\u002F**\n * 使用PHP cURL请求QQ空间头像接口\n * @param string $qq QQ号码\n * @return string\n *\n * @throws RuntimeException\n *\u002F\nfunction fetchQqPortraitByCurl(string $qq): string\n{\n    $url = \"https:\u002F\u002Fusers.qzone.qq.com\u002Ffcg-bin\u002Fcgi_get_portrait.fcg?uins={$qq}\";\n\n    $curl = curl_init($url);\n\n    curl_setopt_array($curl, [\n        CURLOPT_RETURNTRANSFER =&gt; true,\n        CURLOPT_FOLLOWLOCATION =&gt; true,\n        CURLOPT_MAXREDIRS =&gt; 3,\n        CURLOPT_CONNECTTIMEOUT =&gt; 5,\n        CURLOPT_TIMEOUT =&gt; 10,\n        CURLOPT_ENCODING =&gt; '',\n        CURLOPT_HTTP_VERSION =&gt; CURL_HTTP_VERSION_1_1,\n        CURLOPT_CUSTOMREQUEST =&gt; 'GET',\n        CURLOPT_POSTFIELDS =&gt; 'x',\n        CURLOPT_HTTPHEADER =&gt; [\n            'User-Agent: Mozilla\u002F5.0',\n            'Referer: https:\u002F\u002Fuser.qzone.qq.com\u002F',\n            'Content-Type: multipart\u002Fform-data; boundary=----qq-portrait-boundary',\n        ],\n    ]);\n\n    $response = curl_exec($curl);\n\n    if ($response === false) {\n        $error = curl_error($curl);\n        curl_close($curl);\n\n        throw new RuntimeException(\"Curl error: {$error}\", 502);\n    }\n\n    $statusCode = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);\n    curl_close($curl);\n\n    if ($statusCode &lt; 200 || $statusCode &gt;= 300) {\n        throw new RuntimeException(\"Qzone API returned HTTP {$statusCode}\", 502);\n    }\n\n    return $response;\n}\n\n\u002F**\n * 去除QQ接口返回值外层的JSONP回调函数\n * @param string $response QQ接口返回的JSONP字符串\n * @return string\n *\n * @throws RuntimeException\n *\u002F\nfunction unwrapJsonp(string $response): string\n{\n    if (!preg_match('\u002F^[^(]+\\((.*)\\)\\s*;?$\u002Fs', trim($response), $matches)) {\n        throw new RuntimeException('Invalid JSONP response', 502);\n    }\n\n    return $matches[1];\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2 style=\"text-align: start;\">为什么不使用 mb_detect_encoding\u003C\u002Fh2>\u003Cp style=\"text-align: start;\">这里不建议使用：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-php\">mb_detect_encoding($response, ['ASCII', 'UTF-8', 'GB2312', 'GBK', 'BIG5']);\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">原因是自动检测编码并不可靠，尤其接口返回内容里同时包含英文、URL、数字和少量中文时，很容易误判\u003C\u002Fp>\u003Cp style=\"text-align: start;\">这个QQ老接口在成功触发正确分支后，中文昵称按GBK\u002FGB18030处理即可：\u003C\u002Fp>\u003Cpre style=\"text-align: start;\">\u003Ccode class=\"language-lang-php\">$response = mb_convert_encoding($response, 'UTF-8', 'GB18030');\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp style=\"text-align: start;\">英文昵称本身是ASCII，按GB18030转UTF-8也不会有问题\u003C\u002Fp>\u003Ch2 style=\"text-align: start;\">注意事项\u003C\u002Fh2>\u003Cp style=\"text-align: start;\">这个方案依赖的是QQ接口当前的非标准行为：GET请求携带multipart\u002Fform-data和非空body\u003C\u002Fp>\u003Cp style=\"text-align: start;\">因此它有几个注意点：\u003C\u002Fp>\u003Col>\u003Cli style=\"text-align: start;\">未来QQ接口或网关调整后，这个方式可能失效\u003C\u002Fli>\u003Cli style=\"text-align: start;\">某些代理、CDN或HTTP客户端可能会丢弃GET body。\u003C\u002Fli>\u003Cli style=\"text-align: start;\">这个接口返回的是JSONP，不是标准JSON，需要先去掉 \u003Cspan style=\"color: rgb(185, 74, 72); background-color: rgb(243, 243, 243);\">\u003Ccode>portraitCallBack(...)\u003C\u002Fcode>\u003C\u002Fspan> 外层再解析\u003C\u002Fli>\u003Cli style=\"text-align: start;\">如果生产环境HTTPS证书报错，建议修复服务器CA证书配置，不建议直接关闭SSL校验\u003C\u002Fli>\u003C\u002Fol>\u003Cp style=\"text-align: start;\">如果要做得更稳，可以先普通请求一次；如果发现昵称包含 \u003Cspan style=\"color: rgb(185, 74, 72); background-color: rgb(243, 243, 243);\">\u003Ccode>�\u003C\u002Fcode>\u003C\u002Fspan>，再使用 multipart GET方式重试\u003C\u002Fp>\u003Ch2 style=\"text-align: start;\">总结\u003C\u002Fh2>\u003Cp style=\"text-align: start;\">这个乱码问题的本质不是PHP输出编码问题，而是接口在普通GET请求下返回的数据已经损坏\u003C\u002Fp>\u003Cp style=\"text-align: start;\">解决思路是：\u003C\u002Fp>\u003Col>\u003Cli style=\"text-align: start;\">请求QQ接口\u003C\u002Fli>\u003Cli style=\"text-align: start;\">发送 \u003Cspan style=\"color: rgb(185, 74, 72); background-color: rgb(243, 243, 243);\">\u003Ccode>GET + multipart\u002Fform-data + 非空 body\u003C\u002Fcode>\u003C\u002Fspan>\u003C\u002Fli>\u003Cli style=\"text-align: start;\">拿到GBK\u002FGB18030原始响应\u003C\u002Fli>\u003Cli style=\"text-align: start;\">使用 \u003Cspan style=\"color: rgb(185, 74, 72); background-color: rgb(243, 243, 243);\">\u003Ccode>mb_convert_encoding\u003C\u002Fcode>\u003C\u002Fspan> 转成UTF-8\u003C\u002Fli>\u003Cli style=\"text-align: start;\">去掉JSONP外层并解析数据\u003C\u002Fli>\u003Cli style=\"text-align: start;\">返回标准JSON\u003C\u002Fli>\u003C\u002Fol>\u003Cp style=\"text-align: start;\">最终就可以稳定拿到中文昵称、QQ头像和空间地址等信息\u003C\u002Fp>\u003Cp>\u003Cbr>\u003C\u002Fp>","使用PHP解决QQ空间头像接口中文昵称乱码问题","https:\u002F\u002Fopenapi.dwo.cc\u002FPublic\u002FUploads\u002FImages\u002F8208e0aa522c3e34cc37acabbacadf10.png","2026-07-02 06:02:26","QQ,乱码,获取头像,GuzzleHTTP",6,"1",16,1,0,"已上架"]