通过 $_SERVER['HTTP_USER_AGENT'] 和 $_SERVER['HTTP_ACCEPT_LANGUAGE'] 可以获取浏览器标识和用户偏好语言。但原始字符串解析复杂、易出错,现代项目推荐使用成熟解析库。本文提供原生解析代码、常见陷阱、及推荐库对比。
<?php
/**
* 简单浏览器检测 (仅供演示,生产勿用)
* 缺点:UA 字符串极其复杂、易伪造、新浏览器不识别、维护成本高
*/
function detectBrowserSimple(string $ua = ''): string {
$ua = $ua ?: ($_SERVER['HTTP_USER_AGENT'] ?? '');
$ua = strtolower($ua);
// 顺序很重要:更具体的先判断
if (str_contains($ua, 'edg/')) return 'Microsoft Edge';
if (str_contains($ua, 'chrome/') && !str_contains($ua, 'chromium')) return 'Google Chrome';
if (str_contains($ua, 'firefox/')) return 'Mozilla Firefox';
if (str_contains($ua, 'safari/') && !str_contains($ua, 'chrome')) return 'Apple Safari';
if (str_contains($ua, 'opera/') || str_contains($ua, 'opr/')) return 'Opera';
if (str_contains($ua, 'msie ') || str_contains($ua, 'trident/')) return 'Internet Explorer';
if (str_contains($ua, 'curl/')) return 'cURL';
if (str_contains($ua, 'bot') || str_contains($ua, 'spider') || str_contains($ua, 'crawler')) return 'Bot/Crawler';
return 'Unknown';
}
/**
* 简单语言检测
* HTTP_ACCEPT_LANGUAGE 格式:zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
*/
function detectLanguageSimple(string $header = ''): string {
$header = $header ?: ($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
// 解析 q 值排序
$langs = [];
foreach (explode(',', $header) as $part) {
$part = trim($part);
if (preg_match('/^([a-z-]+)(?:;q=([0-9.]+))?$/i', $part, $m)) {
$langs[$m[1]] = (float)($m[2] ?? 1.0);
}
}
arsort($langs);
$top = array_key_first($langs) ?? 'en';
// 归一化常见代码
$map = [
'zh-cn' => 'zh-CN', 'zh-hans' => 'zh-CN', 'zh-sg' => 'zh-CN',
'zh-tw' => 'zh-TW', 'zh-hk' => 'zh-TW', 'zh-mo' => 'zh-TW', 'zh-hant' => 'zh-TW',
'en-us' => 'en-US', 'en-gb' => 'en-GB',
];
return $map[strtolower($top)] ?? $top;
}
// 使用
echo "Browser: " . detectBrowserSimple() . "\n";
echo "Language: " . detectLanguageSimple() . "\n";
?>
| 库 | 安裝 | 特點 | 適用場景 |
|---|---|---|---|
| ua-parser/uap-php | composer require ua-parser/uap-php | 官方 ua-parser 實現、正則庫自動更新、支援設備/作業系統/瀏覽器三維度 | 通用專案、統計分析、設備適配 |
| jenssegers/agent | composer require jenssegers/agent | Laravel 風格、基於 ua-parser、提供 isMobile()/isDesktop() 等便利方法 | Laravel 專案、視圖邏輯判斷 |
| piwik/device-detector | composer require device-detector/device-detector | Matomo 同款、支援客戶端提示 (Client Hints)、解析速度快、緩存機制 | 高流量、需解析 Client Hints、詳細設備資訊 |
| donatj/phpuseragentparser | composer require donatj/phpuseragentparser | 輕量、無依賴、純 PHP、解析快 | 微服務、無 Composer 環境、嵌入式 |
<?php
require 'vendor/autoload.php';
use UAParser\Parser;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$parser = Parser::create(); // 內建 regexes.yaml
$result = $parser->parse($ua);
echo "瀏覽器: {$result->ua->family} {$result->ua->major}.{$result->ua->minor}.{$result->ua->patch}\n";
echo "作業系統: {$result->os->family} {$result->os->major}.{$result->os->minor}\n";
echo "設備: {$result->device->family} ({$result->device->brand})\n";
echo "是否行動裝置: " . ($result->device->isMobile() ? '是' : '否') . "\n";
echo "是否機器人: " . ($result->ua->family === 'Bot' ? '是' : '否') . "\n";
/*
輸出範例:
瀏覽器: Chrome 120.0.6099.109
作業系統: Windows 10
設備: Other ()
是否行動裝置: 否
是否機器人: 否
*/
?>
<?php
require 'vendor/autoload.php';
use DeviceDetector\DeviceDetector;
use DeviceDetector\Cache\DoctrineCache;
use Doctrine\Common\Cache\ArrayCache;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$headers = getallheaders(); // 獲取 Client Hints 標頭
$dd = new DeviceDetector($ua, $headers);
// 設定快取 (生產環境建議 Redis/Memcached)
$dd->setCache(new DoctrineCache(new ArrayCache()));
$dd->parse();
if ($dd->isBot()) {
echo "Bot: {$dd->getBotName()}\n";
} else {
echo "客戶端: {$dd->getClient()->getName()} {$dd->getClient()->getVersion()}\n";
echo "引擎: {$dd->getClient()->getEngine()}\n";
echo "作業系統: {$dd->getOs()->getName()} {$dd->getOs()->getVersion()}\n";
echo "設備: {$dd->getDeviceName()} ({$dd->getBrand()})\n";
echo "型號: {$dd->getModel()}\n";
echo "是否行動: " . ($dd->isMobile() ? '是' : '否') . "\n";
echo "是否桌面: " . ($dd->isDesktop() ? '是' : '否') . "\n";
echo "是否平板: " . ($dd->isTablet() ? '是' : '否') . "\n";
}
?>
<?php
/**
* 标准语言协商:根據 HTTP_ACCEPT_LANGUAGE 選擇最佳匹配
* @param array $supported 支援的語言代碼陣列,如 ['zh-TW', 'zh-CN', 'en-US', 'en']
* @return string 最佳匹配語言
*/
function negotiateLanguage(array $supported = ['zh-TW', 'en-US']): string {
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en-US';
$accept = [];
foreach (explode(',', $header) as $part) {
$part = trim($part);
if (preg_match('/^([a-zA-Z-]+)(?:;q=([0-9.]+))?$/', $part, $m)) {
$lang = strtolower($m[1]);
$q = (float)($m[2] ?? 1.0);
$accept[$lang] = $q;
}
}
arsort($accept);
// 標準化支援語言
$supportedNorm = array_combine(
array_map('strtolower', $supported),
$supported
);
// 精確匹配
foreach ($accept as $lang => $q) {
if (isset($supportedNorm[$lang])) return $supportedNorm[$lang];
}
// 前綴匹配 (zh-CN 匹配 zh)
foreach ($accept as $lang => $q) {
$prefix = explode('-', $lang)[0];
foreach ($supportedNorm as $supLang => $orig) {
if (str_starts_with(strtolower($supLang), $prefix . '-')) {
return $orig;
}
}
}
// 預設返回第一個支援語言
return $supported[0];
}
// 使用
$locale = negotiateLanguage(['zh-TW', 'zh-CN', 'en-US', 'ja-JP']);
echo "選擇語言: $locale\n";
// 設定應用語言
putenv("LANG=$locale");
setlocale(LC_ALL, $locale . '.UTF-8');
bindtextdomain('messages', __DIR__ . '/locale');
textdomain('messages');
?>
Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform 等標頭。解析庫已支援。regexes.yaml 較大,首次載入慢,生產環境必須快取 (APCu/Redis/檔案)zh-TW (語言-地區),但瀏覽器可能發送 zh-tw、zh_TW,需標準化比對判斷瀏覽器與語言:
str_contains + HTTP_ACCEPT_LANGUAGE 解析即可