30

对于前端同学来说其实做的更多的事情就是把数据整合好,按照UI同学的设计通过后端同学给的数据展示在网页中,这也就导致了很多人认为前端很简单,没有做什么工作也没有什么后端复杂的业务逻辑。

其实不然前端要做的工作有很多,就比如今天要说的,如何做到数据的反爬,笔者最近也接到了相同的任务,公司的数据频繁被爬虫爬走,出现这个情况之后,然后就开始调研如何才能实现前端业务数据的反爬工作。刚刚开始接到这个需求的时候,不知道该如何处理这件事,只能硬着头皮接下了这个任务。

接到任务之后就开始了各种Google,笔者觉得如果想要做到反爬就要先知道什么是爬虫以及爬虫是如何作业。这就好比我们要足够的了解对手才能知道如何去防御。

爬虫是通过发送http请求获取获取到响应后的内容,通过按照一定的规则,自动爬去网页信息,之后保存数据的过程。

笔者在刚刚开始的时候也写了一个小小的爬虫实践了一下,大概就是发送一个get请求,然后通过类似于DOM操作的东西,找到网页中所需要的数据,然后进行存储。

分析58案例

在调研过程中发现很多博客都是针对爬取58同城的网页数据进行了分析,于是就点进去看了一下。58是使用的字体对数据进行加密的。所见的和所实际展示的内容是不一致的。看上去很是高端的样子。

image

这是什么情况?于是查看了一下当前的元素的CSS样式,可以注意到这样的元素使用了奇怪的font-family:fangchan-secret(房产-加密)字体样式,如果我们关闭这个strongbox样式,停用这个字体,页面上就会如实的显示乱码了。

其实可以看的出来58同城是使用了font-family字体进行一次加密处理,笔者在Network中找了很久也没有找到这个有关这段的字体文件。。。啊嘞?那么字体是哪来的。。。于是笔者就去查看了一下font-family@font-face这个CSS的相关文献。

原来@font-face不单单可以接收一个文件的地址,还可以使用base64作为src中的参数。于是在58同城的页面中查看源码,果真和我想的一样,58同城没有通过拉取字体库资源处理,而是在页面最开始创建时通过JavaScript脚本动态添加入到页面中。

image

大概知道58的骚操作之后就开始研究下一步的东西,如何实现文字所见和实际不同的。其实对于每个汉字和字符来都对应了一个Unicode编码,从第一张图片中不难看出查看源码时&#x9476这些就是Unicode编码,浏览器通过Unicode在字体库中找到对应的文字。这个就和平时使用的字体图标库道理差不多吧(个人觉得。。。哈哈哈)。

打开百度字体编辑网站打开一个以.ttf格式的字体文件。

image

清楚的可以看到字体包里面的每一个文字,以$开头的则是unicode的缩写了,做了一些处理,否则浏览器会直接解析成对应的字符。

接下来再回头分析58同城网页时如何操作的,看见的是5则查看源码时看到的则是其他的unicode编码,这个unicode对应的时另外的生僻的汉字。

先放下这一段,为了更好的理解,把矛头指向阿里图标库使用过阿里图标库的同学应该不是很陌生,阿里图标库会把svg文件转换成字体,通过下载引入到我们的项目中,完成图标的展示。针对不同的字体会生成新的unicode编码,然而这些新的unicode则不会与现有字体库中的unicode编码冲突。

就此可以想出.ttf文件中的每个一字体都是一个svg文件,我们只需要通过技术手段把字体包转换成svg获取到svg中每个字体的绘制参数,替换掉原有文字的unicode替换成生僻字的编码不就可以了吗?说干就干。

程序设计

由于字体文件太多,所以需要在程序运行前要对这些字体文件进行处理操作,转换成svg这个过程需要很长的时间,根本就不可能每次接到请求的时候就做这件事情,所以为了能够在请求过程中快速处理,必须在程序运行前就要把字体包转换成svg

但是遇到一个问题,并不是所有文字全部都需要加密的,而是某些特定的字符需要加密,所以,为了保证这个操作,在数据库中写入当前需要加密的字符,在转换成svg之后去读取svg文件中的标签,把标签中的绘制路径的属性和一些必要参数,根据对应字符存储到数据库当中。

image

每次接收到数据请求时直接去读取数据库中的数据,然后生成svg文件,再把生成好的svg文件进行处理成base64发送给前端,完成展示。

解析字体文件

遇到的第一个问题就是如何解析字体文件,由于笔者对于其他语言不太熟悉,所以只能使用node,通过对npm仓库的搜索找到了一个相关库ttf2svg

首先安装这个库:

npm install --save-dev ttf2svg

这里使用的基础字体库是微软雅黑这个文件在电脑中就可以找到。MicrosoftYaHei.ttf如果找不到的小伙伴可以自行百度一下。

读取字体包转svg代码如下:

import path from "path";
import fs from "fs";
import util from "util";

import ttf2svg from "ttf2svg";

//  每次启动前需要删除原有svg文件,以防改变了所需要加密的字体包,参数没有及时发生变化
import {removeDir} from "../util/fs";

//  读取文件
const readFile = util.promisify(fs.readFile);
//  写入文件
const writeFile = util.promisify(fs.writeFile);
//  创建文件夹
const mkdir = util.promisify(fs.mkdir);

const ttfToSvg = async () => {
    //  运行根目录
    const rootPath = process.cwd();
    //  .ttf文件所在目录
    const ttfUrl= path.join(rootPath,"static/ttf/MicrosoftYaHei.ttf");
    //  导出文件文件夹名称
    const saveSvgMkdirName = "ttfSvg";
    //  svg存储路径
    const saveSvgUrl = path.resolve(rootPath,`${saveSvgMkdirName}/MicrosoftYaHei.svg`);
    //  svg文件夹路径
    const svgDirUrl = path.resolve(rootPath,saveSvgMkdirName);
    //  读取ttf文件生成buffer
    const ttfBuffer = await readFile(ttfUrl);
    //  通过 ttf2svg 将buffer转换成svg
    const svgContent = ttf2svg(ttfBuffer);
    //  删除原有svg文件
    removeDir(svgDirUrl);
    //  创建存放svg文件夹
    await mkdir(svgDirUrl);
    //  写入svg
    await writeFile(saveSvgUrl,svgContent);
    //  返回存放svg的路径地址
    return saveSvgUrl;
};

util/fs.js

import fs from "fs";

export const removeDir = (path) => {
  let files = [];
  if( fs.existsSync(path) ) {
    files = fs.readdirSync(path);
    files.forEach((file,index) => {
      let curPath = path + "/" + file;
      fs.unlinkSync(curPath);
    });
    fs.rmdirSync(path);
  }
}

生成好所需要的svg文件,看看生成好的svg是不是我们所需要的呢?

image

看样子一切都在朝着好的方向发展,这个东西正是我们所需要的。接下来就是开始读取svg文件(这里就不同步数据库了,小伙伴们可以根据自己的需求进行同步处理),想要读取svg开始的时候还是蛮头疼的,不知道该如何去读取里面的内容。想了想之后觉得svgxml是差不多的,于是就尝试着使用读取xml文件的形式去读取svg文件,结果就真的成了。

这里使用xmldom来读取的svg文件:

npm install --save-dev xmldom

有关xmldom的一些文档大家可以自行百度一下,也没有太复杂。具体应用代码如下:

import fs from "fs";
import util from "util";

import {DOMParser} from "xmldom";

const readFile = util.promisify(fs.readFile);

const readSvg = async (svgPath) => {
    //  读取svg文件
    const svgContent = await readFile(svgPath);
    //  读取内容转换成utf8形式
    const svgHtml = Buffer.from(svgContent).toString("utf8");
    //  生成伪xml
    const doc = (new DOMParser()).parseFromString(svgHtml, 'application/xml');
    //  获取到第一个font标签
    const oFont = doc.getElementsByTagName("font")[0];
    //  获取到font下面的所有glyph标签,并转换成数组
    //  读取出来的是个伪数组需要转换
    const oGlyphs = Array.from(oFont.getElementsByTagName("glyph"));
    //  测试临时使用数组
    const arr = [];
    //    遍历oGlyphs所有标签
    oGlyphs.map((fontEle,index) => {
        //  svg对应的unicode
        const unicode = fontEle.getAttribute("unicode");
        //  svg绘制参数
        const d = fontEle.getAttribute("d");
        //  svg横向位置
        const horizAdvX = fontEle.getAttribute("horiz-adv-x");
        //  svg竖向位置
        const vertAdvY = fontEle.getAttribute("vert-adv-y");
        //  这里只是个方便测试做的判断
        if(index === 20 || index === 21 || index === 22) {
          arr.push({
            unicode,d,horizAdvX,vertAdvY
          });
        }
    })
    console.log(...arr);
};

执行完上述代码就完成,完全可以读取到里面的所有属性。这个时候忽然感觉已经看到的胜利的曙光有没有,哈哈哈。接下来就是最关键的一步了,如何把读取到的内容转换成是转换成base64编码。经过一番搜索之后,找到了svg2ttf这个仓库,简直没有太香啊。

安装相关依赖:

npm install --save-dev svg2ttf

具体实现如下:

import fs from "fs";
import util from "util";

import {DOMParser} from "xmldom";

const readFile = util.promisify(fs.readFile);

const readSvg = async (svgPath) => {
    //  读取svg文件
    const svgContent = await readFile(svgPath);
    //  读取内容转换成utf8形式
    const svgHtml = Buffer.from(svgContent).toString("utf8");
    //  生成伪xml
    const doc = (new DOMParser()).parseFromString(svgHtml, 'application/xml');
    //  获取到第一个font标签
    const oFont = doc.getElementsByTagName("font")[0];
    //  获取到font下面的所有glyph标签,并转换成数组
    //  读取出来的是个伪数组需要转换
    const oGlyphs = Array.from(oFont.getElementsByTagName("glyph"));
    //  测试临时使用数组
    const arr = [];
    //    遍历oGlyphs所有标签
    oGlyphs.map((fontEle,index) => {
        //  svg对应的unicode
        const unicode = fontEle.getAttribute("unicode");
        //  svg绘制参数
        const d = fontEle.getAttribute("d");
        //  svg横向位置
        const horizAdvX = fontEle.getAttribute("horiz-adv-x");
        //  svg竖向位置
        const vertAdvY = fontEle.getAttribute("vert-adv-y");
        //  这里只是个方便测试做的判断
        if(index === 20 || index === 21 || index === 22) {
          arr.push({
            unicode,d,horizAdvX,vertAdvY
          });
        }
    })

    //  获取svg内容
    let svgStr = getSvgStr(arr);
    console.log(svgStr)
    //  把svg转换成ttf
    const ttf = svg2ttf(svgStr,{});
    //  把ttf转换成base64
    const base64 = Buffer.from(ttf.buffer).toString('base64');
    console.log(base64);
};

const getSvgStr = (arr) => {
    //  用与拼接的svg
    let str = "";
    //  临时替换文件,暂时性的,以后需要替换成所以unicode
    let _a = ["&#x5539","&#x5535","&#x555C"];
    //  生成svg内容
    arr.map((el,index) => {
        str += `<glyph glyph-name="${+new Date()}"
                      unicode="${_a[index]};"
                      d="${el.d}"
                      horiz-adv-x="${el.horizAdvX}"
                      vert-adv-y="${el.vertAdvY}"/>`;
    })
    //  返回svg形式的字符串
    return `<?xml version="1.0" standalone="no"?>
        <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
        <svg xmlns="http://www.w3.org/2000/svg">
        <defs>
          <font id="svgtofont" horiz-adv-x="2688" vert-adv-y="2688">
          <font-face font-family="Microsoft YaHei"
                      font-weight="400"
                      font-stretch="normal"
                      units-per-em="2048"
                      ascent="2167"
                      descent="-536"/>
            <missing-glyph />
            ${str}
          </font>
        </defs>
        </svg>`;
}

这里出了一些小问题,注意我们要把我们所生成的字体svg文件的font标签部分复制过来,作为参数如果不这样做的话生成的字体会出现位置偏移的现象。

所有工作准备就绪了,执行程序就可以得到应该给前端的base64编码了,这我也进行了测试。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<script>
function addStyle (base64,fontName){
  let oStyle = document.createElement("style");
  oStyle.innerText = `@font-face {
                        font-family: "${fontName}";
                        src: url(data:application/x-font-woff;charset=utf-8;base64,${base64});
                      }`;
  document.head.appendChild(oStyle);
};
const b = "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";
const n = "abc";
addStyle(b,n);
</script>
<body>
<div class="box">
  <div></div>
  <div></div>
  <div style="font-family: abc; color:red; font-size:50px;">&#x5539;&#x5535;&#x555C;567</div>
  <div></div>
  <div class="fiveBox">0123456789</div>
</div>
<script>

</script>
<style>
* {
  margin:0px;
  padding: 0px;
}
.box {
  width:100%;
  white-space: nowrap;
}
.box div {
  width:500px;
  height:50px;
  border:1px solid #ededed;
  /* float: left; */
  font-size: 20px;
  color: pink;
}
.box::after {
  content: "";
  display: block;
  clear: both;
}
</style>
</body>
</html>

以上就是我的测试代码,展示效果如下:

image

这样下来就和58同城的效果是一样的了,经过了几天的调研也算是有了初步的成果,也是有一些成就感的。

总结

总的来说在这个调研的过程中还是学到了很多的东西,比如阿里图标库是如何实现的,字体包里面都有什么等等等。。。虽然在这个过程中用了很多第三方的依赖,但是结果是好的。

文章比较潦草,感谢各位花费这么长时间阅读,文章中如果有什么错误,请在评论处指出,我会尽快做出改正。


Aaron
4k 声望6.1k 粉丝

Easy life, happy elimination of bugs.