检测自定义字体是否加载
我们可以通过加载自定义字体来实现字体反爬虫,那么如何检测字体是否正常加载呢?
有几个维度的检测:
检测字体是否加载成功
检测内容是否正常渲染
检测字体是否加载
最简单的方法是使用浏览器提供的FontFaceSet API来检测,其中要用到的是FontFaceSet.check()
FontFaceSet 的check()方法会返回是否在给定的字体列表中的所有字体已经被加载并可用。--MDN
示例:
document.fonts.check("12px courier");
document.fonts.check("12px MyFont","ß"); // 如果字体“MyFont”具有ß字符,则返回true。
但这个检测较为简陋,无法检测字体错误的情况(字体正常加载,但不是我们想要的,页面上显示乱码)
检测内容是否正常渲染
针对这种情况,我们可以使用一些比较骚的操作:
创建一个span标签
给标签添加特定字符
重置字体样式
分别测量在添
font-family
前后标签大小大小一致 ? 字体加载失败 : 字体加载成功
(这里要创建span
标签,如果是div
的话宽度默认占满,无法测量真实的大小)
使用代码表示就是:
/**
* 检测字体是否正常渲染
* @param {string} font 字体名称
* @param {Array<string>} phrase 待匹配的字符
* @return {boolean} 是否存在该字体
*/
export function checkFont(font, phrase) {
/**
* 可以使用上述的 FontFaceSet API 作简要的判断,如果字体都不存在,那么肯定渲染失败
*/
if (document.fonts) {
const fonts = [...document.fonts];
if (fonts.findIndex((f) => f.family === font) === -1) {
console.log('font dose not exist');
return false;
}
}
// 创建一个空的 span 标签来填充内容
let node = document.createElement("span");
// 因为可以同时检测多个字符,因此需要存储一下各个字符的大小
const sizes = [];
/**
* 计算单个字符的尺寸
* @param {string} char 待计算的字符
*/
function checkChar(char) {
// 重置字体
node.style.fontFamily = "sans-serif";
// 填充内容
node.innerHTML = char;
const size = {};
// 先保存未设置字体时的尺寸
size.withoutFont = {
width: node.offsetWidth,
height: node.offsetHeight,
};
// 设置待检测的字体
node.style.fontFamily = font + ", sans-serif";
// 保存设置字体后的尺寸
size.withFont = {
width: node.offsetWidth,
height: node.offsetHeight,
};
// 保存该字符设置字体/未设置字体时的尺寸,待字符计算完毕后统一计算
sizes.push(size);
}
// 隐藏该 span 标签
node.style.position = "absolute";
node.style.left = "-10000px";
node.style.top = "-10000px";
// 字体大小设置大些误差小(offsetWidth和offsetHeight会四舍五入)
node.style.fontSize = "300px";
// 重置字体样式
node.style.fontFamily = "sans-serif";
node.style.fontVariant = "normal";
node.style.fontStyle = "normal";
node.style.fontWeight = "normal";
node.style.letterSpacing = "0";
// 插入页面中才会有 offsetWidth 和 offsetHeight
document.body.appendChild(node);
// 计算每一个字符的尺寸
phrase.forEach(checkChar);
/**
* 1. 当 offsetWidth 和 offsetHeight 均不相同时说明字体存在
* 2. 有一个字符的尺寸相同就说明字体有误
*/
const res =
node &&
sizes.reduce(
(acc, cur) =>
acc &&
(cur.withFont.width !== cur.withoutFont.width ||
cur.withFont.height !== cur.withoutFont.height),
true
);
// 做一些善后工作
node.parentNode.removeChild(node);
node = null;
return res;
}
指数退避算法检测内容是否正常渲染
由于网络字体的加载需要时间,我们可通过简单地设置一个延时调用上述的checkFont
,但可能有更好的方式来实现:指数退避算法
(自己理解)简单来讲就是在不超过所设置的时间范围内,以指数级超市时间定期重试
示例:
向服务端发送请求
失败?等待1秒后重试
失败?等待2秒后重试
失败?等待4秒后重试
失败?等待8秒后重试
以此类推,直到达到超时时间
在检测字体是否正常加载时,我们也可以使用该算法来更好地检测
(也可使用FontFaceSet: loadingdone event来作为时间点检测)
const check = (phrase, options = { timeout: 8192 }) => {
function doCheck(t) {
let tid = setTimeout(() => {
clearTimeout(tid);
tid = null;
const res = checkFont("m-font", phrase);
if (res) {
// success!
}
if (!res && (t << 1 < options.timeout)) {
doCheck(t << 1);
}
if (!res && (t << 1) >= options.timeout) {
// parse error!
}
}, t);
}
// 我们可以以64ms为起始时间点
doCheck(2 << 5);
};
总结
可使用FontFaceSet API来做简单的字体是否加载判断,但无法检测字体是否正常渲染
不同字体下,大概率各个字符的大小是不一样的,可依次来检测字体是否正常渲染
结束!
参考
How can I detect when a font is downloaded via CSS's "unicode-range" descriptor?