// +---------------------------------------------------------------------- declare(strict_types=1); namespace ywxapp\library; /** * 搜索引擎蜘蛛识别 * * 两层判定: * 1. detect() —— 仅按 User-Agent 特征串识别(零阻塞,可被伪造) * 2. rdnsSuffixes() —— 返回该蜘蛛官方反向域名后缀,供 SpiderVerify 做 * 「PTR 反查 + A 正查」双向验证,识别伪造 UA 的假蜘蛛。 * * 新增蜘蛛:追加一行 标识 => [UA特征串列表, 显示名, 官方rDNS域名后缀列表]。 * rDNS 后缀留空数组表示「官方不提供 rDNS,无法校验」,此类只按 UA 计数。 * * @author ywxapp */ class SpiderDetect { /** * 蜘蛛特征表:key=入库标识,value=[UA 特征串(不区分大小写,命中任一即算), 中文显示名, 官方 rDNS 域名后缀] */ public const SPIDERS = [ 'baidu' => [['Baiduspider'], '百度', ['baidu.com', 'baidu.jp']], 'google' => [['Googlebot', 'Google-InspectionTool', 'AdsBot-Google', 'Mediapartners-Google'], '谷歌', ['googlebot.com', 'google.com']], 'bing' => [['bingbot', 'msnbot', 'BingPreview', 'adidxbot'], '必应', ['search.msn.com', 'msn.com']], 'sogou' => [['Sogou web spider', 'Sogou inst spider', 'Sogou Pic Spider', 'Sogou Spider'], '搜狗', ['sogou.com', 'sogou.com.cn']], 'so360' => [['360Spider', 'HaoSouSpider', 'qihoobot'], '360', ['360.cn', '360safe.com', 'qihoo.net']], 'bytedance' => [['Bytespider', 'ToutiaoSpider'], '字节跳动', ['bytedance.com', 'bytespider.com']], 'shenma' => [['YisouSpider'], '神马', ['sm.cn', 'yisou.com']], 'huawei' => [['PetalBot'], '华为花瓣', ['huawei.com']], 'yandex' => [['YandexBot', 'YandexImages', 'YandexMetricaBot'], 'Yandex', ['yandex.ru', 'yandex.net', 'yandex.com']], 'duckduckgo' => [['DuckDuckBot', 'DuckDuckGo-Favicons-Bot'], 'DuckDuckGo', ['duckduckgo.com']], 'yahoo' => [['Yahoo! Slurp'], '雅虎', ['yahoo.com', 'yahoo.net', 'crawl.yahoo.net']], 'apple' => [['Applebot'], '苹果', ['apple.com']], 'gptbot' => [['GPTBot', 'ChatGPT-User', 'OAI-SearchBot'], 'OpenAI', ['openai.com']], 'claudebot' => [['ClaudeBot', 'anthropic-ai'], 'Anthropic', ['anthropic.com']], 'semrush' => [['SemrushBot'], 'Semrush', ['semrush.com']], 'ahrefs' => [['AhrefsBot'], 'Ahrefs', ['ahrefs.com']], 'mj12' => [['MJ12bot'], 'Majestic', ['majestic12.co.uk', 'mj12bot.com']], 'facebook' => [['facebookexternalhit', 'FacebookBot'], 'Facebook', ['facebook.com', 'fbsv.net', 'tfbnw.net']], ]; /** * 识别 UA,命中返回蜘蛛标识(SPIDERS 的 key),未命中返回 null */ public static function detect(string $userAgent): ?string { if ($userAgent === '') { return null; } foreach (self::SPIDERS as $key => [$needles]) { foreach ($needles as $needle) { if (stripos($userAgent, $needle) !== false) { return $key; } } } return null; } /** * 蜘蛛标识 → 中文显示名(未知标识原样返回) */ public static function label(string $key): string { return self::SPIDERS[$key][1] ?? $key; } /** * 全部蜘蛛 标识=>显示名(后台下拉/报表用) */ public static function labels(): array { $out = []; foreach (self::SPIDERS as $key => [, $label]) { $out[$key] = $label; } return $out; } /** * 该蜘蛛的官方 rDNS 域名后缀列表(空数组 = 官方不提供 rDNS,无法做真伪校验) * * @param string $key 蜘蛛标识 * @return string[] */ public static function rdnsSuffixes(string $key): array { $suffixes = self::SPIDERS[$key][2] ?? []; return is_array($suffixes) ? $suffixes : []; } /** * 该蜘蛛是否支持 rDNS 真伪校验 */ public static function verifiable(string $key): bool { return self::rdnsSuffixes($key) !== []; } }