爬虫漫游指南
浏览器指纹
最近深入阅读了瑞数的源码,发现瑞数收集了很多浏览器的特征信息,这部分源码对了解浏览器具有很高的学习价值,为此整理了这些特征信息,并尽可能还原了可读可用的代码来获取这些信息。本文测试环境为Chrome、Firefox和IE。
基础信息
这里的基础信息,指直接能获取到的浏览器属性,如navigator.userAgent
这样最基础的信息。
navigator下的基础信息
- navigator.languages:浏览器的语言环境,一个数组,形如[“zh-CN”];
- navigator.hardwareConcurrency:当前浏览器环境所拥有的CPU核心数,准确的说是线程数,而非核数,比如6核12线程的i5-10500显示的值是12;
- navigator.cpuClass:同样是cpu信息,形如"x86",只在IE中发现了这一属性,Chrome和Firefox中均为undefined;
- navigator.oscpu:操作系统信息,形如"Windows NT 10.0; Win64; x64",只在Firefox中发现了这一属性,Chrome和IE中均为undefined;
- navigator.platform:用户的操作系统信息,比如Mac、Intel、Win32、Linux x86_64等;
- navigator.appName:测试中3个浏览器均为"Netscape";
- navigator.productSub:Chrome浏览器中该值为"20030107",IE无此属性;
- navigator.product:测试中3个浏览器均为"Gecko";
- navigator.webdriver:在早几年的版本中,只有Chrome的无头浏览器puppeteer中有这一属性,当时只要检测
navigator.webdriver == true
就可判断为模拟浏览器,正常使用的Chrome中是undefined,在后来的更新中正常启动的Chrome也有了这一属性,值为false; - navigator.getBattery(): 这一方法Chrome支持、IE不支持、Firefox默认禁用,但可以启用,这一方法的检测代码是
"getBattery" in "navigator"
;
window下的基础信息
前面的部分是开胃小菜,从这里开始要上硬菜了。
_$Rn = [
"AudioTrackList",
"defaultStatus",
"Object.setPrototypeOf",
"taobrowser_Event",
"webkitRequestFileSystem",
"onoperadetachedviewchange",
"Path2D.prototype.addPath",
"SourceBuffer.prototype.changeType",
"weatherBridge",
"chrome.csi",
"password_manager_enabled",
"document.body.x-ms-acceleratorkey",
"external.AddFavorite",
"SogouLoginUtils",
"SourceBuffer",
"showModalDialog",
"document.selection.typeDetail",
"SVGPatternElement.SVG_UNIT_TYPE_OBJECTBOUNDINGBOX",
"document.onselectionchange",
"document.body.style.backgroundBlendMode",
"document.documentElement.onresize",
"CanvasRenderingContext2D.prototype.webkitGetImageDataHD",
"UCWebExt",
"CDATASection.prototype.remove",
"BlobDownloadCallback",
"_WXJS",
"document.msCapsLockWarningOff",
"CSSCharsetRule",
"document.scrollingElement.style.fontVariantNumeric",
"Function.prototype.bind",
"chrome.app.InstallState",
"isNodeWhitespace",
"Object.seal",
"document.defaultCharset",
"__firefox__",
"onmessage",
"__sogou_secure_input",
"CloseEvent.prototype.initCloseEvent",
"getMatchedCSSRules",
"Notification",
"HTMLFrameSetElement.prototype.hasPointerCapture",
"document.body.onmouseenter",
"OffscreenCanvasRenderingContext2D",
"chrome",
"Object.prototype.__defineSetter__",
"document.fileCreatedDate",
"webkitAudioContext.prototype.close",
"GetPerfTests",
"MediaController",
"external.IsSearchProviderInstalled",
"TextTrackList.prototype.getTrackById",
"document.selection",
"document.body.style.lineBreak",
"document.body.style.textAlignLast",
"ScreenOrientation",
"document.body.style.minWidth",
"SpeechSynthesisUtterance",
"onerror",
"WebKitFlags",
"ReaderModeArticlePage",
"__opera",
"PerformancePaintTiming",
"performance",
"document.body.style.msTextSizeAdjust",
"document.body.onpage",
"SVGGraphicsElement.prototype.mozRequestPointerLock",
"ClickData",
"MediaEncryptedEvent",
"__$_qihoo360_$__",
"document.onmousemove",
"BeforeInstallPromptEvent.prototype.KEYUP",
"HTMLFrameSetElement.prototype.webkitRequestFullScreen",
"external"
]
也不多,73个,逐一分析的话肝不够用,直接上瑞数的检测源码了,稍微翻译了一下,可读性已经比原来的代码强一百倍了。
_$S7 = []
function HV22(a, b) {
try {
return a[b];
} catch (_e) {
return false;
}
}
function foo(_$XP, _$6Y) {
for (_$_$ = 0; _$_$ < _$6Y.length - 1; ++_$_$) {
_$XP = HV22(_$XP, _$6Y[_$_$])
if (!_$XP) {
return false;
}
}
try {
return HV22(_$XP, _$6Y[_$6Y.length - 1]) || _$6Y[_$6Y.length - 1] in _$XP || _$XP["hasOwnProperty"](_$6Y[_$6Y.length - 1]);
} catch (_e) {
return false;
}
}
for (_$ZN = 0; _$ZN < _$Rn.length; _$ZN++) {
var l = _$Rn[_$ZN].split(".");
var w = window;
result = foo(w, l);
_$S7.push(result ? 1 : 0);
}
_$S7.join("")
这坨代码会得到一个73位的01序列,每一位都都应上面数组里的每一个属性是否存在,是1非0,在3个浏览器中运行分别得到如下结果:
Chrome:
0110101101000010001110010000110010010001111110000110111111000110000101011
Firefox:
0010001100000010001110010000110010010001110010000110111111000110000101001
IE:
1110000000001011011000000010010011010100010011000100110101000011000001001
不要小看这串序列哦,每一位都是重要的信息,如Chrome中的第一位是0,即Chrome中没有window.AudioTrackList
这一对象,只有IE才有,所以IE对应序列的第一位是1,我在Chrome中给这一对象赋值,使序列的第一位变成了1,然后这个请求直接被瑞数封了。
想来也合理,你其他所有信息都显示你是Chrome,但你偏偏有个window.AudioTrackList,这河里吗?这布河里。
进阶信息
navigator下的进阶信息
还是让navigator先来,navigator.plugins下有颇多信息,根据mozilla早先的API文档,它会返回当前所使用的浏览器安装的所有插件,但在后来的版本中,出于隐私考虑,navigator.plugins 数组的枚举可能会被限制。
获取plugins信息的代码如下
var _$6Y = [];
var _$h2 = navigator;
var _$XP = _$h2["plugins"];
_$7y = _$XP;
if (!_$7y) {} else {
for (_$_$ = 0; _$_$ < _$XP.length; _$_$++) {
_$UZ = _$XP[_$_$];
_$Cg = [_$UZ["name"], _$UZ["description"], _$UZ["filename"], _$UZ["version"]].join(",");
_$6Y.push(_$Cg);
}
}
_$6Y
由于上述的隐私限制,plugins根本无法显示我安装的插件信息,我在装了一堆插件的Chrome和一个纯洁无瑕的Chrome上得到的值是一样的
["Chrome PDF Plugin,Portable Document Format,internal-pdf-viewer,", "Chrome PDF Viewer,,mhjfbmdgcfjbbpaeojofohoefgiehjai,", "Native Client,,internal-nacl-plugin,"]
但是这一属性仍能用于区分不同的浏览器,不同浏览器下跑这段代码必然是不同的,IE中肯定不会出现什么Chrome PDF Plugin
。
navigator下还有一个长的和plugins有点像的东西,叫mimeTypes,其中包含可被当前浏览器识别的MimeType对象的列表。MIME,就是媒体类型,Multipurpose Internet Mail Extensions。
代码也和上面的有些类似
var _$6Y = [];
var _$h2 = navigator;
var _$XP = _$h2["mimeTypes"];
_$7y = _$XP;
if (!_$7y) {} else {
for (_$_$ = 0; _$_$ < _$XP.length; _$_$++) {
_$UZ = _$XP[_$_$];
_$6Y.push([_$UZ["type"], _$UZ["suffixes"], _$UZ["description"]].join(","));
}
}
_$6Y
在我的测试环境中,Chrome包含4种,IE包含2种,Firefox包含0种。
window下的进阶信息
window.Intl.DateTimeFormat().resolvedOptions(),这个东西在不同浏览器下长的也不一样
var _$pb = ''
var _$M9 = window
var _$S7 = new _$M9["Intl"]["DateTimeFormat"]()["resolvedOptions"]();
[_$S7["calendar"] !== _$pb ? _$S7["calendar"] : '', _$S7["fractionalSecondDigits"] !== _$pb ? _$S7["fractionalSecondDigits"] : '', _$S7["locale"] !== _$pb ? _$S7["locale"] : '', _$S7["numberingSystem"] !== _$pb ? _$S7["numberingSystem"] : '', _$S7["timeZone"] !== _$pb ? _$S7["timeZone"] : '', _$S7["year"] !== _$pb ? _$S7["year"] : '', _$S7["month"] !== _$pb ? _$S7["month"] : '', _$S7.day !== _$pb ? _$S7.day : '']
IE中,timeZone是undefined,其它浏览器则是有值的"Asia/Shanghai"
,而locale
也长的不太一样,虽然都是表示中文,但我的Chrome显示是zh-CN
,IE则是zh-Hans-CN
。
老版IE专区
虽然不知道哪些可恶的用户还在用老版本的IE,但瑞数很“贴心”的对这部分也检测了。先贴一下代码
var _$hR = window.ActiveXObject;
_$I6 = [];
_$S7 = [
"ShockwaveFlash.ShockwaveFlash",
"AcroPDF.PDF",
"PDF.PdfCtrl",
"QuickTime.QuickTime",
"rmocx.RealPlayer G2 Control",
"rmocx.RealPlayer G2 Control.1",
"RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)",
"RealVideo.RealVideo(tm) ActiveX Control (32-bit)",
"RealPlayer",
"SWCtl.SWCtl",
"WMPlayer.OCX",
"AgControl.AgControl",
"Skype.Detection"
];
for (_$5z = 0; _$5z < _$S7.length; _$5z++) {
try {
new _$hR(_$S7[_$5z]);
_$I6.push(_$S7[_$5z]);
} catch (_$VC) {
console.log(_$VC)
}
}
_$I6
ActiveXObject,一个古董级的名字,在大部分前端眼里都直接和不支持三个字划等号。这个列表里的古董绝大部分在新版IE里都没得了,只有WMPlayer.OCX
还能new出来,Chrome和Firefox就更不谈了,只能得到一个空值。
一些骚气的操作
H5中的Audio/Video组件各自都有一个canPlayType方法,该方法用于判断浏览器是否能播放指定的音频/视频类型,返回值有如下几种:
- “probably” - 浏览器最可能支持该音频/视频类型
- “maybe” - 浏览器也许支持该音频/视频类型
- “” - (空字符串)浏览器不支持该音频/视频类型
所有浏览器都支持 canPlayType() 方法,但是在不同的浏览器下,能支持的文件类型是不同的。以audio组件为例:
function _$IJ(_$N0) {
var _$S7 = typeof _$N0 === "function" && (_$N0 + '')["indexOf"]("[native code]") !== -1;
return _$S7;
}
var _$vs = document;
var _$UZ = [];
var _$vE = "audio/ogg; codecs=\"vorbis\"|audio/wav; codecs=\"1\"|audio/mpeg;|audio/x-m4a;audio/aac;";
var _$7y = _$vs["createElement"]("audio");
_$kh = _$7y && _$7y["canPlayType"] && _$IJ(_$7y["canPlayType"]);
if (!_$kh) {} else {
_$pX = _$vE["split"]("|");
for (_$ZN = 0; _$ZN < _$pX.length; _$ZN++) {
_$UZ.push(_$7y["canPlayType"](_$pX[_$ZN]));
}
}
_$UZ
上述代码在Chrome中得到的返回值是[“probably”, “probably”, “probably”, “maybe”],而在Firefox中则稍有不同[ “probably”, “probably”, “maybe”, “maybe” ],IE则最垃圾["", “”, “maybe”, “maybe”],有两个空字符串。
如果这种方法还不够骚的话,那还有下面这种更骚的。
用try-catch获取抛出的异常,不同浏览器的异常报错是不一样的。
var _$6Y = [];
try {
eval("$_config__.detail__ += 1");
} catch (_$XP) {
_$6Y.push(_$XP["message"]);
_$6Y.push(_$XP["lineNumber"]);
_$6Y.push(_$XP["description"]);
_$6Y.push("fileName" in _$XP);
}
try {
new window["WebSocket"]("itsgonnafail");
} catch (_$XP) {
_$6Y.push(_$XP["message"]);
}
_$6Y
IE的报错信息是:
["“$_config__”未定义", undefined, "“$_config__”未定义", false, "SyntaxError"]
Firefox是:
[ "$_config__ is not defined", 1, undefined, true, "An invalid or illegal string was specified" ]
Chrome是:
["$_config__ is not defined", undefined, undefined, false, "Failed to construct 'WebSocket': The URL 'itsgonnafail' is invalid."]
除此之外,瑞数中还有一些针对canvas、webgl啥的高级操作,太麻烦了,就不写了。