743567274 发表于 2024-4-10 14:58

css字体反扒的尝试记录小说下载

首先,不知道发表在哪个区,如果有错,请版主移动一下,谢谢版主~~



前言

https://static.52pojie.cn/static/image/hrline/2.gif

本片帖子的基础是前几天在论坛上看到了一篇关于这个的帖子:https://www.52pojie.cn/thread-1910333-1-1.html
这里我也at一下帖子作者:@jing99

网站:aHR0cHM6Ly93d3cuemhpaHUuY29tL21hcmtldC9wYWlkX2NvbHVtbi8xNzMwNjA3ODEwMjI2Njg4MDAwL3NlY3Rpb24vMTczMDE4MTE0ODk2ODMyNTEyMA==


我对于英文特别不敏感,代码很多不会写。所以以下的代码中,如果有问题,请见谅~
我可能比大家想象中的还要小白,所以此篇帖子我会站在比我更小白的角度,尽量让大家看得明白

并且我很喜欢互相交流学习,如果需要交流学习,请私信~~~

本篇分析以学习为目的,禁止触犯法律法规。
浏览器:edge

感谢当前的AI浪潮吧,让编程基础又降低了很多门槛。此分析帖子的很多代码都是全靠AI,我自己只是稍微修改了一下~

分析

https://static.52pojie.cn/static/image/hrline/2.gif
打开网址,F12。发现网页直接跳转了~
那么,就新建一个标签页,先F12,设置,禁用JS
再次粘贴网址,跳转。对比一下网页的文字和html源代码中的文字。
https://img.z4a.net/images/2024/04/10/2.png
下一步确认一下文字动态还是静态
按照上面的方法,再次新建一个标签页,重新打开网址,发现是两次源代码的文字是不一样的。确认是动态加载!
https://img.z4a.net/images/2024/04/10/3.png
https://img.z4a.net/images/2024/04/10/4.png
并且字体为base64编码,提取出来,保存为TTF文件!
这里推荐一个查字体的网站:https://www.bejson.com/ui/font/
打开网站后,上传字体文件。
https://img.z4a.net/images/2024/04/10/5.png
分析到这里,大致的思路已经出来了~
读取网页源代码->提取base64编码的字体文件->取出来字体文件中的字符图片以及真实的Unicode字符->识别图片中的文字->按照规则进行文章的文字替换


读取网页源代码
https://static.52pojie.cn/static/image/hrline/2.gif
https://img.z4a.net/images/2024/04/10/6.png

提取base64编码的字体文件
https://static.52pojie.cn/static/image/hrline/2.gif
https://img.z4a.net/images/2024/04/10/7.png
代码来自于AI

取出来字体文件中的字符图片以及真实的Unicode字符
https://static.52pojie.cn/static/image/hrline/2.gif
https://img.z4a.net/images/2024/04/10/8.png
https://img.z4a.net/images/2024/04/10/9.png
现在的目的达到了,接下来就是识别这个字符的字,然后把文章的"要"替换为"发"
现在软件可以通过uni8981查询到这个字是"要",但是软件是不知道这个字代表的是"发"
所以,我的思路就是使用图像识别


识别图片中的文字
https://static.52pojie.cn/static/image/hrline/2.gif
以下非黑色字体为在实现过程中的曲折,没有很细致,可以跳过
既然python是pip调包侠
那么,用哪个包呢?因为之前没有使用过类似的包,只要靠搜索引擎了


初识:Tesseract
这是一个由谷歌开发的文字识别库,好像挺强大的,但是win系统需要安装一个exe程序。安装好之后可能是设置问题,一直没有识别出来。是TM的一个都没有识别出来~
查文档,正如开篇所讲,确实英文不好。所以放弃~~~


初识:easyocr
这个就不介绍了,PIP过程中,下载的包很大。当然,识别库都需要下载,理解;
遇到的问题就是某些可以识别,某些不能识别。当然,对于一个github上一万多个star的库来说,肯定是我的问题。文档还是一如既往的英语~放弃


后来遇到了CnOCR。使用这个的主要原因是因为有中文文档,最主要的是他有一个在线测试的网站,可以把你的图片上传上去测试~就试了一下。
https://img.z4a.net/images/2024/04/10/10.png
没问题就决定使用这个库了。接下来一顿pip
https://img.z4a.net/images/2024/04/10/11.png
果然,厉害了我的GUO(其实后面他给的网址没了,其实不是因为V*N的原因)。。。我就知道过程没有那么顺利~
仔细查看官方文档后,发现需要下载检测模型和识别模型。因为网络原因不能下载,那么就需要手动下载后放入本地指定目录。干吧
https://img.z4a.net/images/2024/04/10/12.png
已经提取出来了。然后就是把识别到的文字和真实的文字放一起,就可以下一步了~
https://img.z4a.net/images/2024/04/10/13.png

按照规则进行文章的文字替换
https://static.52pojie.cn/static/image/hrline/2.gif
https://img.z4a.net/images/2024/04/10/14.png
然后进行原文章对比校验
不出意外的话,还是出意外了~
https://img.z4a.net/images/2024/04/10/15.png
这里其实找了以下原因,是因为字符重复替换的问题

这里解释一下
如果原文本是:一二一二一二一二一二一二一二一二
你识别出来的字符是数组是[("一","二"),("二","一")]
那么替换两次,
第一次把一替换为二,这个时候源文本就全是二了
第二次把二替换为一,文本就全是一了。原理差不多就是这么个原理

https://img.z4a.net/images/2024/04/10/16.png
从断点中也可以看的出来,数组是从00开始替换;
到02的时候把文章的"不"替换为了"用"
到31的时候又把文章的"用"替换了"可"


到04的时候把个替换为了生
到30的时候又把"生"替换为了"和"


为了解决这个问题,我想出来了一个方案。
新建一个空数组,记录已经替换的位置。下次替换的时候如果此位置已经替换,则跳过不替换~(此代码依旧来自于AI)
replaced_positions = set()
    for old, new in result:
      for i, char in enumerate(conent):
            if i not in replaced_positions and char == old:
                conent = conent[:i] + new + conent
                replaced_positions.add(i)
            elif i in replaced_positions:
                continue
    with open('正文.txt','w',encoding='utf-8') as f:
      f.write(conent)


至此,运行。查看结果。
https://img.z4a.net/images/2024/04/10/17.png
https://img.z4a.net/images/2024/04/10/18.png

全部结束~
https://static.52pojie.cn/static/image/hrline/2.gif

fortytwo 发表于 2024-4-11 01:56

本帖最后由 fortytwo 于 2024-4-11 01:59 编辑

跟着你的思路复现了一下
easyocr这个库应该是基于已有的字体来训练的,对于知乎这种自定义的字体,支持不是很好。
其他ocr库不太清楚,我平时使用的ocr插件(utools的)正常识别没有问题。

我采用的是将ttf文件的字形画出来,然后给easyocr识别(也是一样的问题,个别字识别不出来),后面思路一致。

https://cdnjson.com/images/2024/04/11/1712771685493a92e7af6c8f38ac2.png

import base64
import re

import bs4
import easyocr
import requests
from fontTools.ttLib import TTFont
from PIL import Image, ImageDraw, ImageFont
import os


def ocr(folder_dir: str) -> dict:
    reader = easyocr.Reader(['ch_sim'], gpu=False)
    key_words = {}
    directory = folder_dir
    dir_list = os.listdir(directory)
    for file_name in dir_list:
      full_path = os.path.join(directory, file_name)
      # print(full_path)
      error_key_word = f'\\{file_name}'.encode('utf-8').decode('unicode_escape').replace('.png', '')
      try:
            success_key_word = reader.readtext(full_path, detail=False)
      except IndexError:
            success_key_word = '一'
      key_words.update({error_key_word: success_key_word})
    return key_words


def get_html():
    url = "https://www.zhihu.com/market/paid_column/1730607810226688000/section/1730181148968325120"

    payload = {}
    headers = {
      'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
      'accept-language': 'zh-CN,zh-TW;q=0.9,zh;q=0.8',
      'cache-control': 'no-cache',
      'pragma': 'no-cache',
      'sec-ch-ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
      'sec-ch-ua-mobile': '?0',
      'sec-ch-ua-platform': '"Windows"',
      'sec-fetch-dest': 'document',
      'sec-fetch-mode': 'navigate',
      'sec-fetch-site': 'none',
      'sec-fetch-user': '?1',
      'upgrade-insecure-requests': '1',
      'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
    }

    response = requests.request("GET", url, headers=headers, data=payload)

    return response.text


def html_analyze(html_body: str):
    html_obj = bs4.BeautifulSoup(html_body, features="html.parser")
    return '\n'.join()


def clear_directory(path):
    for filename in os.listdir(path):
      filepath = os.path.join(path, filename)
      try:
            if os.path.isfile(filepath):
                os.remove(filepath)
      except Exception as e:
            print(f"Error deleting {filepath}: {e}")


def export_glyph_images(ttf_file_path, output_folder, image_size=(200, 200), font_size=200):
    if not os.path.exists(output_folder):
      os.makedirs(output_folder)

    font = TTFont(ttf_file_path)
    cmap = font.getBestCmap()
    pil_font = ImageFont.truetype(ttf_file_path, font_size)

    for unicode_char, glyph_name in cmap.items():
      char = chr(unicode_char)
      img = Image.new("RGBA", image_size, color="white")
      draw = ImageDraw.Draw(img)

      # 绘制字符
      draw.text((0, 0), char, font=pil_font, fill="black", font_size=24, align="center")
      # 保存图片
      unicode_hex_str = f'u{unicode_char:04X}' + ".png"
      print(unicode_hex_str)
      image_file_path = os.path.join(output_folder, unicode_hex_str)
      img.save(image_file_path)

    font.close()


if __name__ == '__main__':
    # 字体缓存地址
    output_folder = r"xxxxxxx"

    clear_directory(output_folder)
    html_body = get_html()
    font_face_regex = re.findall(r'@font-face\s*{([^}]*)}', html_body)
    base64_string = re.findall('base64,(.*?)\);', font_face_regex)
    binary_data = base64.b64decode(base64_string)
    with open('zhihu.ttf', 'wb') as f:
      f.write(bytes(binary_data))
    # 指定TTF文件的路径
    ttf_file_path = "zhihu.ttf"
    # 调用函数导出字形图片
    export_glyph_images(ttf_file_path, output_folder)

    keywords = ocr(output_folder)
    content =

    filter_list = []
    for error_key, success_key in keywords.items():
      for index in range(len(content)):
            if content == error_key and index not in filter_list:
                content = success_key
                filter_list.append(index)

    with open('知乎盐选.txt', 'w', encoding='utf-8') as f:
      f.write(''.join(content))


QAQ~QL 发表于 2024-4-11 19:06

有幸前段时间做过这个项目,可以参考github的反扒项目 https://github.com/stars-rivers/font_transfer
其中字体的思路很好,先解析font,遍历所有字体,组成长文本,塞进CnOcr,切割识别数组并做好字符映射,再替换
殊途同归

贴上rpc代码吧
import copy
import json
import math

import cnocr
import flask
import numpy
from PIL import Image, ImageDraw, ImageFont
from flask import request
from fontTools.ttLib import TTFont

# pip install cx_Freeze
class FontTransfer(object):

    def __init__(self):
      self.font_size = 20# 字体文字的尺寸
      self.ocr = cnocr.CnOcr()

    def get_chars_from_font(self, font_path):
      ttf = TTFont(font_path)
      return {k: v for k, v in ttf['cmap'].getBestCmap().items() if ttf['glyf'].xMax}

    def draw_font_word(self, char_unicode, origin, board, font):
      draw = ImageDraw.ImageDraw(board)
      draw.text(tuple(origin), char_unicode, font=font, fill=0)

    def font_to_image(self, font_path):
      char_dict = self.get_chars_from_font(font_path)
      # 字体能被分成多少行多少列的正方形图片
      num = math.ceil(math.sqrt(len(char_dict)))
      # 自适应图片的大小
      image_size = num * (self.font_size + 4)

      font = ImageFont.truetype(font_path, self.font_size)

      # unicode_list = list(char_dict.values())
      unicode_list = list(char_dict.keys())
      board = Image.new('RGB', (image_size, image_size), (255, 255, 255))

      # 这个算法用于确定每个字型的坐标
      origin =
      i = 1
      j = 1
      for k in char_dict.keys():
            origin = (self.font_size + 4) * (j - 1) + 2
            origin = (self.font_size + 4) * (i - 1) + 2
            if j % num == 0:
                i += 1
                j = 0
            char_unicode = chr(k)
            self.draw_font_word(char_unicode, copy.copy(origin), board, font)
            j += 1
      return numpy.asarray(board), unicode_list

    def get_font_transfer_dict(self, font_path):
      img_array, unicode_list = self.font_to_image(font_path)
      string_list = []
      rs = self.ocr.ocr(img_array)
      # print(font_path, rs)
      for res in rs:
            string_list += res['text']
      return dict(zip(unicode_list, string_list))


ft = FontTransfer()

app = flask.Flask(__name__)
app.config['JSON_AS_ASCII'] = False


@app.route('/check', methods=['get'])
def check():
    path = request.values.get('path')
    if path is None:
      return "Err"
    try:
      res_dict = ft.get_font_transfer_dict(path)
      # 双引号json格式
      return json.dumps(res_dict, ensure_ascii=False)
    except Exception as e:
      return "Err " + str(e)


if __name__ == '__main__':
    app.run(debug=True, port=7898, host="0.0.0.0")

jing99 发表于 2024-4-10 15:05

学习了,谢谢大佬指点。作为新手,这次练习确实收获很多!!
感谢大佬花时间指点迷津

Soma77 发表于 2024-4-10 15:07

好奇是不是只能扒账号可以看到的部分

jenisonbai 发表于 2024-4-10 15:11

学习了~~

jing99 发表于 2024-4-10 15:15

Soma77 发表于 2024-4-10 15:07
好奇是不是只能扒账号可以看到的部分

是的,盐选现在恶心在需要下载 app 才能看全。我是修改了请求头可以爬全,你可以去看看

atoms 发表于 2024-4-10 15:58

厉害 但反正中间也需要ocr,那是不是可以直接截取原页面的图片,然后将图片直接丢进ocr呢?

齐恩 发表于 2024-4-10 16:07

斗智斗勇啊,牛逼

Soma77 发表于 2024-4-10 16:17

jing99 发表于 2024-4-10 15:15
是的,盐选现在恶心在需要下载 app 才能看全。我是修改了请求头可以爬全,你可以去看看

感谢,又学习到不少

jing99 发表于 2024-4-10 16:25

atoms 发表于 2024-4-10 15:58
厉害 但反正中间也需要ocr,那是不是可以直接截取原页面的图片,然后将图片直接丢进ocr呢?

这样 ocr 的工作量是不是很大呀,我自己的想法,只是疑问一下

42328669425 发表于 2024-4-10 16:31

{:17_1089:} 代码太多看不懂了坐等大佬们的成品
页: [1] 2 3 4 5
查看完整版本: css字体反扒的尝试记录小说下载