- 环境要求
当前网上很少找到类似的样例,之所以写一个是想自己整理一下,说是反爬其实就是提高反爬成本,其实根本很难杜绝反爬,更何况当前的AI识别更加难防。其实这里是模仿了起点小说的小说加密反爬虽然可能有不同只是实现效果差不多。
PHP >= 5.6 ,node.js ,composer ,一份完整的字体(这里以微软雅黑做测试 msyh.ttf)
npm 安装一些插件
npm install -g ttf2svg
//用法:ttf2svg fontello.ttf fontello.svg
npm install -g svg2ttf
npm install -g ttf2woff
2. 定义基础字体库
首先选择出要定义的基础字体比如 数字,基础符号,和一些常用字
吧微软雅黑字体(msyh.ttf)转为svg格式 (msyh.svg)(上面插件 可以自行转svg)
/**
* 基础模板生成
*/
public function baseCreateSvg()
{
//基础字
$baseWord = self::getBaseWord();
$xml = simplexml_load_string(file_get_contents(ROOT_PATH . "fonts/msyh.svg"));
$xml_json = json_encode($xml);
$xml_arr = json_decode($xml_json, true);
//把svg转为xml
$unicodeXml = $this->unicodeXmlBase($xml_arr, null, $baseWord);
//把xml自动转义的&符号 $amp; 替换为 &
$unicodeXml = str_replace('&', '&', $unicodeXml);
//这里生成基础模板根据基础模板生成多个模板 因为从头筛选字体文件会很大所以生成小型基础模板
$fileFont = ROOT_PATH . "fonts/" . "msyh_base" . ".svg";
file_put_contents($fileFont, $unicodeXml);
var_dump("生成成功路径: " . $fileFont);
}
定义基础字方法
此处举例就没定义太多:
/**
* 基础字
*/
private static function getBaseWord()
{
return [
'0', 1, 2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'O', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!', '(', ')', '.', '/', ':', ';', '<', '>', '=', '\\', '&', '?', '*', ',', '万', '三', '上', '下', '不', '与', '专', '且', '世', '业'
];
}
定义基础模板生成
/**
* 基础模板生成
* @param $arr
* @param null $parentNode
* @param array $baseWord
* @return mixed
*/
private function unicodeXmlBase($arr, $parentNode = null, $baseWord = [])
{
//如果父节点为null,则创建root节点,否则就使用父节点
if ($parentNode === null) {
$simxml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><svg></svg>');
} else {
$simxml = $parentNode;
}
//遍历数组
foreach ($arr as $k => $v) {
//再将xml转换成数组的时候,xml节点的属性会变成子数组,键名是@attributes,
//在转回xml的时候,就需要特殊处理,将这个数组添加为属性,而不是节点
if ($k === '@attributes') {
if (isset($v['unicode'])) {
if (in_array($v['unicode'], $baseWord)) {
foreach ($v as $name => $value) {
if ($name === 'unicode') {
//注意这个‘&#x’中的‘&’,在保存为xml文件是,会被转移为&记得在导出文件后批量替换掉。
$value = "&#x" . ltrim(bin2hex(iconv('UTF-8', 'UCS-4', $value)), '0') . ';';
}
$simxml->addAttribute($name, $value);
}
}
} else {
foreach ($v as $name => $value) {
$simxml->addAttribute($name, $value);
}
}
} else {
if (is_numeric($k)) {
$k = 'glyph';
if (isset($v['@attributes']['unicode']) && !in_array($v['@attributes']['unicode'], $baseWord)) {
continue;
}
if (!isset($v['@attributes']['unicode'])) {
continue;
}
}
if ($k === 'hkern') {
continue;
}
if (is_array($v)) { //如果是数组的话则继续递归调用,并以该键值创建父节点
if ($k === "glyph" && !isset($v['@attributes'])) {
$this->unicodeXmlBase($v, $simxml, $baseWord);
} else {
$this->unicodeXmlBase($v, $simxml->addChild($k), $baseWord);
}
} else {
$simxml->addChild($k, $v);
}
}
}
//返回数据
return $simxml->saveXML();
}
3. 定义随机字体库生成
这里根据生成的基础模板生成的svg来生成随机字体
/**
* 随机字体库生成
* 模板XML修改随机unicode生成新的xml转svg
* svg转woff字体库
*/
public function createSvg()
{
$xml = simplexml_load_string(file_get_contents(ROOT_PATH . "fonts/msyh_base.svg"));
$xml_json = json_encode($xml);
$xml_arr = json_decode($xml_json, true);
//获取基础字
$baseWord = self::getBaseWord();
//产生随机数
$decimal = self::unique_rand(45897, 54896, count($baseWord));
//替换字
$newBaseWord = [];
foreach ($baseWord as $value) {
foreach ($decimal as $key => $val) {
$newBaseWord[$value] = $val;
unset($decimal[$key]);
break;
}
}
//把随机产生的unicode范围值累的编码转为xml
$unicodeXml = $this->unicodeXml($xml_arr, null, $newBaseWord);
//替换xml因为特殊字符产生的转义符号
$unicodeXml = str_replace('&', '&', $unicodeXml);
//这里生成随机N个随机unicode的svg文件
// $fileFont = ROOT_PATH . "fonts/" . md5(time()) . "svg";
$fileFont = ROOT_PATH . "fonts/" . "msyhb_test" . ".svg";
file_put_contents($fileFont, $unicodeXml);
foreach ($newBaseWord as $key => $value) {
$newBaseWord[$key] = "&#" . $value . ";";
}
//随机key
$key = md5("1234");
//缓存一天生成的随机字符串
Cache::set($key, $newBaseWord, 86400);
var_dump("生成成功路径: " . $fileFont);
}
生成16进制随机数
/**
* 生成十六进制随机数
* @param $min
* @param $max
* @param $num
* @return array|null
*/
private static function unique_rand($min, $max, $num)
{
$count = 0;
$return = array();
while ($count < $num) {
$return[] = mt_rand($min, $max);
$return = array_flip(array_flip($return));
$count = count($return);
}
shuffle($return);
return $return;
}
unicode范围值累的编码转为xml
/**
* array转xml
* @param $arr
* @param $decimal
* @param null $parentNode
* @return mixed
*/
private function unicodeXml($arr, $parentNode = null, &$newBaseWord = [])
{
//如果父节点为null,则创建root节点,否则就使用父节点
if ($parentNode === null) {
$simxml = new \SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><svg></svg>');
} else {
$simxml = $parentNode;
}
//遍历数组
foreach ($arr as $k => $v) {
//再将xml转换成数组的时候,xml节点的属性会变成子数组,键名是@attributes,
//在转回xml的时候,就需要特殊处理,将这个数组添加为属性,而不是节点
if ($k === '@attributes') {
foreach ($v as $name => $value) {
if ($name === 'unicode') {
//注意这个‘&#x’中的‘&’,在保存为xml文件是,会被转移为&记得在导出文件后批量替换掉。
if ($newBaseWord) {
foreach ($newBaseWord as $key => $val) {
if ((string)$key == $value) {
$hexadecimal = base_convert($val, 10, 16);
$value = "&#x" . ltrim($hexadecimal) . ';';
break;
}
}
} else {
$value = "&#x" . ltrim(bin2hex(iconv('UTF-8', 'UCS-4', $value)), '0') . ';';
}
}
if ($name === 'glyph-name') {
continue;
}
$simxml->addAttribute($name, $value);
}
} else {
if (is_numeric($k)) {
$k = 'glyph';
}
if (is_array($v)) { //如果是数组的话则继续递归调用,并以该键值创建父节点
if ($k === "glyph" && !isset($v['@attributes'])) {
$this->unicodeXml($v, $simxml, $newBaseWord);
} else {
$this->unicodeXml($v, $simxml->addChild($k), $newBaseWord);
}
} else {
$simxml->addChild($k, $v);
}
}
}
//返回数据
return $simxml->saveXML();
}
4. 调用内容生成随机字体内容
生成加密内容
public function index()
{
$fileContent = "需要的加码的内容";// file("test.txt");
$newContent = $this->formatContent($fileContent);
var_dump($newContent);
}
处理内容格式化
/**
* 格式化内容
* @param $fileContent
* @param $fontSize
* @return array
*/
private function formatContent($fileContent)
{
$newContent = [];
//字体大小
$fontSize = 18;
//索引
$zIndex = 1;
//输出总高度
$totalHeight = 1263;
//输出总宽度
$totalWidth = 893;
//初始页数
$page = 1;
//一页高度按行算 向下取整 (高度/(字体+大概间距)) N行
$onePageHeight = floor($totalHeight / ($fontSize + floor($fontSize / 2)));
//一页宽度按字算 向下取整 (宽度/(字体+大概间距)) N个字
$onePageWidth = floor($totalWidth / ($fontSize + 2));
$heightArray = [];
//处理行数
// var_dump("处理内容行前->:" . self::msectime());
if (is_array($fileContent)) {
foreach ($fileContent as $val) {
if (mb_strlen($val) > $onePageWidth) {
$newArray = self::utf8_str_split($val, $onePageWidth);
foreach ($newArray as $item) {
$heightArray[] = $item;
}
continue;
}
$heightArray[] = $val;
}
}
//分页与内容
$pageNum = 0;
foreach ($heightArray as $key => $content) {
if (($onePageHeight * ($page - 1)) == $key) {
//分页初始索引
$zIndex = 1;
//分页
$newContent['page' . $page++][] = array(
'content' => array(
'h' => $totalHeight,
'w' => $totalWidth,
),
'p' => array(
'z_index' => $zIndex++,
),
'style' => 'font-size: "' . $fontSize . '"',
);
$pageNum = $page - 1;
}
//处理内容
self::setNewContent($newContent, $content, $pageNum, $zIndex, mb_strlen($content), rand(5, 15));
}
return $newContent;
}
中文字拆分
/**
* 中字拆分
* @param $str
* @param int $split_len
* @return array|bool|mixed
*/
private static function utf8_str_split($str, $split_len = 1)
{
if (!preg_match('/^[0-9]+$/', $split_len) || $split_len < 1) {
return FALSE;
}
$len = mb_strlen($str, 'UTF-8');
if ($len <= $split_len) {
return array($str);
}
preg_match_all('/.{' . $split_len . '}|[^\x00]{1,' . $split_len . '}$/us', $str, $ar);
return $ar[0];
}
处理字符内容
/**
* 处理字符内容
* @param $newContent
* @param $content
* @param $pageNum
* @param $zIndex
* @param $strLen
* @param int $captureLen
*/
private static function setNewContent(&$newContent, $content, $pageNum, &$zIndex, $strLen, $captureLen = 10)
{
if ($strLen > $captureLen) {
$strSplit = self::utf8_str_split($content, $captureLen);
foreach ($strSplit as $value) {
$newContent['page' . $pageNum][] = array(
//替换字符串
'content' => self::wordToUnicode($value),
'p' => array(
'z_index' => $zIndex++,
),
);
}
} else {
$newContent['page' . $pageNum][] = array(
//替换字符串
'content' => self::wordToUnicode($content),
'p' => array(
'z_index' => $zIndex++,
),
);
}
}
字转unicode
/**
* 字转unicode
* @param $content
* @return
*/
private static function wordToUnicode($content)
{
//获取字体映射
$word = self::wordWarehouse();
//切割字符串
$splitContent = self::str_split_unicode($content);
$content = "";
//替换字
foreach ($splitContent as $value) {
if (isset($word[$value])) {
$content .= $word[$value];
continue;
}
$content .= $value;
}
return $content;
}
/**
* 字体转换库
*/
private static function wordWarehouse()
{
//获取随机key拿到缓存数据
$key = md5("1234");
$warehouse = Cache::get($key);
return $warehouse;
}
切割字符串
/**
* 切割字符串
* @param $str
* @param int $l
* @return array|array[]|false|string[]
*/
private static function str_split_unicode($str, $l = 0)
{
if ($l > 0) {
$ret = array();
$len = mb_strlen($str, "UTF-8");
for ($i = 0; $i < $len; $i += $l) {
$ret[] = mb_substr($str, $i, $l, "UTF-8");
}
return $ret;
}
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
5. shell 生成woff,ttf
#!/bin/bash
echo "this is test create *.ttf *.woff "
folder="./fonts"
if [ ! -d $folder ]; then
mkdir $folder
fi
if [ "$(ls -A $folder)" = "" ]; then
echo "目录为空"
exit 0
fi
for file_name in ${folder}/*.svg; do
temp_file=$(basename $file_name .svg)
fontpath=$folder/$temp_file
if [ -e $fontpath.svg ]; then
if [ ! -e $fontpath.woff ]; then
svg2ttf $fontpath.svg $fontpath.ttf
ttf2woff $fontpath.ttf $fontpath.woff
fi
echo $temp_file
fi
done
echo "执行完成"