QQ 作為國內主流即時通訊工具,其用戶昵稱與在線狀態獲取常用於客服系統、社交應用、營銷工具。騰訊未提供官方開放 API,開發者需通過逆向分析 Web 協議實現。本文深度解析三種主流方案原理、代碼實現、優缺點對比及風險控制。
原理:請求 https://wpa.qq.com/pa?p=1:QQ號:4 返回在線/離線圖片,兩種狀態圖片大小固定不同,通過 Content-Length 區分。
function getQqStatusV1(string $uin): bool {
$url = "https://wpa.qq.com/pa?p=1:{$uin}:4";
$content = @file_get_contents($url);
if ($content === false) return false; // 網絡錯誤視為離線
foreach ($http_response_header as $header) {
if (stripos($header, 'Content-Length:') === 0) {
$length = (int)trim(substr($header, 15));
// 經驗閾值:在線圖片 > 1000 bytes,離線較小
return $length > 1000;
}
}
return false;
}
缺點:騰訊可能隨時調整圖片大小、壓縮算法,閾值失效;依賴 allow_url_fopen;無法獲取昵稱。
原理:模擬 HTTP 請求 wpa.qq.com/pa?p=1:QQ號:1,解析響應中的 Location 重定向 URL,包含 online 或 offline 關鍵字。
function getQqStatusV2(string $uin): int {
// -1: 錯誤, 0: 離線, 1: 在線
$request = "GET /pa?p=1:{$uin}:1 HTTP/1.1\r\n";
$request .= "Host: wpa.qq.com\r\n";
$request .= "User-Agent: PHP_QQ_STATUS_CHECKER\r\n";
$request .= "Connection: Close\r\n\r\n";
$socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (!$socket) return -1;
if (!@socket_connect($socket, 'wpa.qq.com', 80)) {
socket_close($socket);
return -1;
}
socket_write($socket, $request);
$response = @socket_read($socket, 2048, PHP_BINARY_READ);
socket_close($socket);
if (!$response) return -1;
foreach (explode("\r\n", $response) as $line) {
if (stripos($line, 'Location:') === 0) {
if (stripos($line, 'online') !== false) return 1;
if (stripos($line, 'offline') !== false) return 0;
return -1;
}
}
return -1;
}
優點:不依賴 allow_url_fopen,可控超時,解析邏輯穩健。缺點:需 sockets 擴展,無法獲取昵稱。
原理:
https://wpa.qq.com/pa?p=2:QQ號:41,通過 Content-Length 固定值(如 1243)判斷在線。https://r.qzone.qq.com/cgi-bin/user/cgi_personal_card?uin=QQ號,返回 JSONP,提取 nickname 字段。class QqUserInfo {
private const STATUS_URL = 'https://wpa.qq.com/pa?p=2:%s:41&r=%d';
private const NICK_URL = 'https://r.qzone.qq.com/cgi-bin/user/cgi_personal_card?uin=%s';
private const ONLINE_CONTENT_LENGTH = 1243; // 經驗值,需定期驗證
private $timeout = 3; // 秒
/**
* 獲取在線狀態
* @return bool|null true在線 false離線 null失敗
*/
public function getStatus(string $uin): ?bool {
$url = sprintf(self::STATUS_URL, $uin, time());
$headers = @get_headers($url, 1);
if ($headers === false) return null;
$length = $headers['Content-Length'] ?? 0;
return (int)$length === self::ONLINE_CONTENT_LENGTH;
}
/**
* 獲取昵稱
* @return string|false
*/
public function getNickname(string $uin): string|false {
$url = sprintf(self::NICK_URL, $uin);
$content = @file_get_contents($url);
if ($content === false) return false;
// 響應格式:portraitCallBack({"nickname":"xxx",...});
if (preg_match('/"nickname"\s*:\s*"([^"]+)"/', $content, $matches)) {
return $matches[1];
}
return false;
}
/**
* 綜合獲取
* @return array ['nickname'=>string|false, 'online'=>bool|null, 'uin'=>string]
*/
public function getInfo(string $uin): array {
return [
'uin' => $uin,
'nickname' => $this->getNickname($uin),
'online' => $this->getStatus($uin),
];
}
}
// 使用示例
$qq = new QqUserInfo();
$info = $qq->getInfo('561272831');
var_dump($info);
/*
array(3) {
'uin' => '561272831'
'nickname' => '張三'
'online' => true
}
*/
function getQqInfoCached(string $uin): array {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "qq:info:{$uin}";
if ($cached = $redis->get($key)) {
return json_decode($cached, true);
}
$qq = new QqUserInfo();
$info = $qq->getInfo($uin);
// 緩存 10 分鐘,狀態變化允許延遲
$redis->setex($key, 600, json_encode($info));
return $info;
}
高併發場景下,部署多台服務器輪詢調用,或使用代理 IP 池分散請求來源。
Content-Length 閾值變化。⚠️ 重要:上述接口均為騰訊內部/未公開接口,**非官方開放 API**。使用前請評估:
- 是否違反《騰訊服務協議》、《計算機信息網絡國際聯網管理暫行規定》等法規。
- 是否構成非法獲取公民個人信息(昵稱屬個人信息)。
- 高頻調用可能被判定為攻擊行為,面臨 IP 封禁、法律追責風險。
建議:
- 優先引導用戶授權登錄(QQ 互聯 OAuth2.0),合法獲取 OpenID、昵稱、頭像。
- 若必須使用非官方接口,嚴格控制頻率、加緩存、做降級、記錄審計日志。
- 關鍵業務**不依賴**此類非穩定接口,僅作輔助展示。
| 方案 | 獲取狀態 | 獲取昵稱 | 穩定性 | 依賴 | 推薦度 |
|---|---|---|---|---|---|
| Content-Length 判斷 | ✓ | ✗ | 低 | allow_url_fopen | ⭐ |
| Socket Location 解析 | ✓ | ✗ | 中 | sockets 擴展 | ⭐⭐ |
| 綜合接口 (推薦) | ✓ | ✓ | 中 | allow_url_fopen / sockets | ⭐⭐⭐ |
| QQ 互聯 OAuth2.0 (官方) | ✗ | ✓ | 高 | 官方 SDK | ⭐⭐⭐⭐⭐ (合規首選) |
技術無罪,用途定性。**合規合法、緩存優先、降級兜底、監控告警** 是生產環境接入非官方接口的鐵律。