pythonp爬虫-fanqienovel字体反爬实战

发布时间:2025-04-29 21:45:20编辑:123阅读(21)

    字体反爬的处理需结合字体文件解析与动态映射技术,核心流程如下:

    一、识别字体反爬特征

    页面显示正常但源码为乱码:如价格、电话等关键数据在HTML源码中显示为特殊字符或Unicode编码。

    存在自定义字体引用:通过浏览器开发者工具检查CSS代码,查找@font-face规则及字体文件URL(如.woff、.ttf等格式)。

    二、获取字体文件

    静态文件下载:从网页源码或CSS中提取字体URL,用Python的requests库下载并保存为本地文件。

    动态Base64解码:若字体以Base64编码嵌入CSS(如src: url('data:font/woff;base64,...')),需解码后保存为字体文件。

    三、解析字体映射关系

    使用工具查看字形

    FontCreator:本地软件可查看字符编码与真实字形的对应关系。

    编程解析

    Python的fontTools库:将字体文件转为XML格式,提取<GlyphID>节点中的编码与字形名称的映射关系。

    四、构建字符替换字典

    固定映射关系:若网站使用静态字体(如某二手车平台),直接建立乱码字符与真实值的对应字典。


    目标网站:https://fanqienovel.com/

    打开其中一篇小说:https://fanqienovel.com/reader/7386518026868228670?enter_from=page

    image.png

    页面显示正常但源码为乱码,存在字体引用的情况,


    分析目标网站,思路:先获取小说的bookId, 再通过bookId获取所有章节,最后通过itemId获取章节的文本,最后再解决字体映射的关系,还原为正常文本即可。

    先获取bookid,分析页面:

    image.png

    可以看到里面有很多文字做了字体反爬,想要获取摘要,作者,书名,在线阅读人数,小说总字数等信息需要下载字体文件,做映射,即Unicode对应得名称,以及对应得值。

    下载字体文件

    image.png

    字体文件下载到本地url:https://lf6-awef.bytetos.com/obj/awesome-font/c/e26e946d8b2ccb7.woff2,然后打开在线字体解析网站,https://www.bejson.com/ui/font/,上传下载得字体文件,查看如下:

    image.png

    需要做一个字典映射表,即数字对应的中文,例子:

     font_mapping_dict = {

      "58665": "立", "58542": "象", "58449": "数", "58630": "四", "58596": "失", "58637": "满", "58499": "战",

      "58434": "远","58680": "格", "58597": "士", "58514": "音", "58420": "轻", "58510": "目", "58686": "条", 

      "58383": "呢",

    }

    然后通过book_id获取小说得所有章节itemid,API:https://fanqienovel.com/api/reader/directory/detail?bookId=7297316760233970688

    image.png

    最后在通过章节itemid获取每章的文本内容。

    image.png

    上面可以看到又是字体反爬,这里用到的字体文件跟上面bookid那里是不一样的,字体下载地址:https://lf6-awef.bytetos.com/obj/awesome-font/c/dc027189e0ba4cd-700.woff2

    image.png

    然后打开在线字体解析网站,https://www.bejson.com/ui/font/,上传下载得字体文件,查看如下:

    image.png

    可以看到上面的呢字是gid58383 ,下面的呢字是gid58682.

    这里也需要做一个字典映射表,即数字对应的中文,例子:

     font_mapping_dict = {

    '58437': '变', '58411': '通', '58451': '师', '58395': '立', '58369': '象', '58706': '数', '58705': '四', '58379': '失',

    '58567': '满', '58373': '战', '58448': '远', '58659': '格', '58434': '士', '58679': '音', '58432': '轻', '58689': '目',

    '58591': '条', '58682': '呢'

    }

    完整代码如下:

    import requests
    import json
    from lxml import etree
    
    headers = {
        "user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
        "cookie":"s_v_web_id=verify_ma13cihb_p99myzfb_IrsM_4lmT_8B1U_djKqHIGtHPee; novel_web_id=7498349345001260563; "
     "Hm_lvt_2667d29c8e792e6fa9182c20a3013175=1745845522,1745892606; HMACCOUNT=546E2263FBEA7F9C; "
     "csrf_session_id=10854bbdf5f185dee880b62b45e67142; Hm_lpvt_2667d29c8e792e6fa9182c20a3013175=1745893819; "
     "ttwid=1%7C7x2dcyuFdFxyomPguzqE4G7Kcs9HdSXHCMmg9ZUo2g8%7C1745893817%7C47cd4f24f82671a0eafdb84c9b97abf3e582fbb248989a7406e9f8c1caaf07b6",
    }
    
    def data_format(data):
        font_mapping_dict = {
            "58544": "0", "58703": "1", "58599": "2", "58628": "3", "58526": "4", "58614": "5", "58710": "6", "58684": "7",
            "58490": "8",
            "58484": "9", "58381": "a", "58652": "b", "58503": "c", "58422": "d", "58650": "e", "58427": "f", "58501": "g",
            "58554": "h",
            "58600": "i", "58384": "j", "58409": "k", "58598": "l", "58711": "m", "58653": "n", "58364": "o", "58453": "p",
            "58480": "q",
            "58546": "r", "58446": "s", "58421": "t", "58395": "u", "58548": "v", "58606": "w", "58555": "x", "58471": "y",
            "58553": "z",
            "58355": "的", "58662": "一", "58454": "是", "58647": "了", "58382": "我", "58641": "不", "58396": "人",
            "58687": "在",
            "58701": "他", "58560": "有", "58483": "这", "58619": "个", "58698": "上", "58451": "们", "58664": "来",
            "58447": "到",
            "58411": "时", "58432": "大", "58496": "地", "58562": "为", "58685": "子", "58412": "中", "58505": "你",
            "58492": "说",
            "58533": "生", "58410": "国", "58565": "年", "58696": "着", "58435": "就", "58707": "那", "58495": "和",
            "58400": "要",
            "58374": "她", "58568": "出", "58366": "也", "58399": "得", "58536": "里", "58676": "后", "58564": "自",
            "58591": "以",
            "58655": "会", "58594": "家", "58626": "可", "58424": "下", "58705": "而", "58681": "过", "58700": "天",
            "58445": "去",
            "58520": "能", "58668": "对", "58417": "小", "58459": "多", "58532": "然", "58625": "于", "58476": "心",
            "58581": "学",
            "58414": "么", "58689": "之", "58624": "都", "58622": "好", "58670": "看", "58440": "起", "58462": "发",
            "58523": "当",
            "58407": "没", "58693": "成", "58468": "只", "58397": "如", "58457": "事", "58456": "把", "58582": "还",
            "58623": "用",
            "58617": "第", "58508": "样", "58448": "道", "58699": "想", "58469": "作", "58549": "种", "58620": "开",
            "58660": "美",
            "58509": "总", "58642": "从", "58455": "无", "58378": "情", "58669": "已",
            "58433": "面", "58372": "最", "58635": "女", "58588": "但", "58347": "现", "58470": "前", "58651": "些",
            "58575": "所",
            "58627": "同", "58632": "日", "58525": "手", "58431": "又", "58713": "行", "58584": "意", "58550": "动",
            "58573": "方",
            "58563": "期", "58444": "它", "58515": "头", "58473": "经", "58667": "长", "58657": "儿", "58538": "回",
            "58616": "位",
            "58583": "分", "58358": "爱", "58365": "老", "58673": "因", "58612": "很", "58438": "给", "58524": "名",
            "58377": "法",
            "58425": "间", "58402": "斯", "58357": "知", "58682": "世", "58640": "什", "58659": "两", "58629": "次",
            "58506": "使",
            "58603": "身", "58577": "者", "58661": "被", "58559": "高", "58394": "己", "58547": "亲", "58586": "其",
            "58694": "进",
            "58645": "此", "58350": "话", "58368": "常", "58634": "与", "58465": "活", "58572": "正", "58574": "感",
            "58613": "见",
            "58576": "明", "58419": "问", "58530": "力", "58363": "理", "58472": "尔", "58545": "点", "58704": "文",
            "58371": "几",
            "58690": "定", "58537": "本", "58663": "公", "58557": "特", "58556": "做", "58464": "外", "58467": "孩",
            "58674": "相",
            "58458": "西", "58485": "果", "58413": "走", "58376": "将", "58352": "月", "58346": "十", "58441": "实",
            "58418": "向",
            "58529": "声", "58426": "车", "58482": "全", "58633": "信", "58527": "重", "58649": "三", "58644": "机",
            "58618": "工",
            "58353": "物", "58683": "气", "58387": "每", "58638": "并", "58708": "别", "58539": "真", "58678": "打",
            "58386": "太",
            "58463": "新", "58589": "比", "58656": "才", "58349": "便", "58654": "夫", "58607": "再", "58688": "书",
            "58639": "部",
            "58354": "水", "58502": "像", "58658": "眼", "58479": "等", "58344": "体", "58351": "却", "58452": "加",
            "58404": "电",
            "58373": "主", "58460": "界", "58403": "门", "58392": "利", "58610": "海", "58389": "受", "58605": "听",
            "58361": "表",
            "58709": "徳", "58401": "少", "58369": "克", "58534": "代", "58385": "员", "58672": "许", "58578": "稜",
            "58494": "先",
            "58416": "口", "58592": "由", "58593": "死", "58486": "安", "58436": "写", "58512": "性", "58566": "马",
            "58380": "光",
            "58611": "白", "58643": "或", "58580": "住", "58715": "难", "58388": "望", "58390": "教", "58552": "命",
            "58521": "花",
            "58679": "结", "58518": "乐", "58675": "色", "58585": "更", "58692": "拉", "58697": "东", "58423": "神",
            "58648": "记",
            "58513": "处", "58595": "让", "58489": "母", "58478": "父", "58517": "应", "58615": "直", "58528": "字",
            "58500": "场",
            "58370": "平", "58604": "报", "58531": "友", "58519": "关", "58356": "放", "58570": "至", "58498": "张",
            "58567": "认",
            "58569": "接", "58477": "告", "58540": "入", "58636": "笑", "58535": "内", "58551": "英", "58393": "军",
            "58714": "侯",
            "58481": "民", "58621": "岁", "58677": "往", "58415": "何", "58429": "度", "58609": "山", "58590": "觉",
            "58706": "路",
            "58695": "带", "58359": "万", "58406": "男", "58558": "边", "58362": "风", "58466": "解", "58602": "叫",
            "58493": "任",
            "58601": "金", "58348": "快", "58608": "原", "58450": "吃", "58702": "妈", "58398": "变", "58439": "通",
            "58541": "师",
            "58665": "立", "58542": "象", "58449": "数", "58630": "四", "58596": "失", "58637": "满", "58499": "战",
            "58434": "远",
            "58680": "格", "58597": "士", "58514": "音", "58420": "轻", "58510": "目", "58686": "条", "58383": "呢",
        }
        txt_content = ''
        for char in data:
            ret = str(ord(char))
            if font_mapping_dict.get(ret):
                t1 = font_mapping_dict.get(ret)
                txt_content += t1
            else:
                txt_content += char
        return txt_content
    
    def txt_format(data):
        txt_dict = {
            '58670': '0', '58413': '1', '58678': '2', '58371': '3', '58353': '4', '58480': '5', '58359': '6', '58449': '7',
            '58540': '8', '58692': '9', '58712': 'a', '58542': 'b', '58575': 'c', '58626': 'd', '58691': 'e', '58561': 'f',
            '58362': 'g', '58619': 'h', '58430': 'i', '58531': 'j', '58588': 'k', '58440': 'l', '58681': 'm', '58631': 'n',
            '58376': 'o', '58429': 'p', '58555': 'q', '58498': 'r', '58518': 's', '58453': 't', '58397': 'u', '58356': 'v',
            '58435': 'w', '58514': 'x', '58482': 'y', '58529': 'z', '58515': 'A', '58688': 'B', '58709': 'C', '58344': 'D',
            '58656': 'E', '58381': 'F', '58576': 'G', '58516': 'H', '58463': 'I', '58649': 'J', '58571': 'K', '58558': 'L',
            '58433': 'M', '58517': 'N', '58387': 'O', '58687': 'P', '58537': 'Q', '58541': 'R', '58458': 'S', '58390': 'T',
            '58466': 'U', '58386': 'V', '58697': 'W', '58519': 'X', '58511': 'Y', '58634': 'Z', '58611': '的', '58590': '一',
            '58398': '是', '58422': '了', '58657': '我', '58666': '不', '58562': '人', '58345': '在', '58510': '他', '58496': '有',
            '58654': '这', '58441': '个', '58493': '上', '58714': '们', '58618': '来', '58528': '到', '58620': '时', '58403': '大',
            '58461': '地', '58481': '为', '58700': '子', '58708': '中', '58503': '你', '58442': '说', '58639': '生', '58506': '国',
            '58663': '年', '58436': '着', '58563': '就', '58391': '那', '58357': '和', '58354': '要', '58695': '她', '58372': '出',
            '58696': '也', '58551': '得', '58445': '里', '58408': '后', '58599': '自', '58424': '以', '58394': '会', '58348': '家',
            '58426': '可', '58673': '下', '58417': '而', '58556': '过', '58603': '天', '58565': '去', '58604': '能', '58522': '对',
            '58632': '小', '58622': '多', '58350': '然', '58605': '于', '58617': '心', '58401': '学', '58637': '么', '58684': '之',
            '58382': '都', '58464': '好', '58487': '看', '58693': '起', '58608': '发', '58392': '当', '58474': '没', '58601': '成',
            '58355': '只', '58573': '如', '58499': '事', '58469': '把', '58361': '还', '58698': '用', '58489': '第', '58711': '样',
            '58457': '道', '58635': '想', '58492': '作', '58647': '种', '58623': '开', '58521': '美', '58609': '总', '58530': '从',
            '58665': '无', '58652': '情', '58676': '已', '58456': '面', '58581': '最', '58509': '女', '58488': '但', '58363': '现',
            '58685': '前', '58396': '些', '58523': '所', '58471': '同', '58485': '日', '58613': '手', '58533': '又', '58589': '行',
            '58527': '意', '58593': '动', '58699': '方', '58707': '期', '58414': '它', '58596': '头', '58570': '经', '58660': '长',
            '58364': '儿', '58526': '回', '58501': '位', '58638': '分', '58404': '爱', '58677': '老', '58535': '因', '58629': '很',
            '58577': '给', '58606': '名', '58497': '法', '58662': '间', '58479': '斯', '58532': '知', '58380': '世', '58385': '什',
            '58405': '两', '58644': '次', '58578': '使', '58505': '身', '58564': '者', '58412': '被', '58686': '高', '58624': '己',
            '58667': '亲', '58607': '其', '58616': '进', '58368': '此', '58427': '话', '58423': '常', '58633': '与', '58525': '活',
            '58543': '正', '58418': '感', '58597': '见', '58683': '明', '58507': '问', '58621': '力', '58703': '理', '58438': '尔',
            '58536': '点', '58384': '文', '58484': '几', '58539': '定', '58554': '本', '58421': '公', '58347': '特', '58569': '做',
            '58710': '外', '58574': '孩', '58375': '相', '58645': '西', '58592': '果', '58572': '走', '58388': '将', '58370': '月',
            '58399': '十', '58651': '实', '58546': '向', '58504': '声', '58419': '车', '58407': '全', '58672': '信', '58675': '重',
            '58538': '三', '58465': '机', '58374': '工', '58579': '物', '58402': '气', '58702': '每', '58553': '并', '58360': '别',
            '58389': '真', '58560': '打', '58690': '太', '58473': '新', '58512': '比', '58653': '才', '58704': '便', '58545': '夫',
            '58641': '再', '58475': '书', '58583': '部', '58472': '水', '58478': '像', '58664': '眼', '58586': '等', '58568': '体',
            '58674': '却', '58490': '加', '58476': '电', '58346': '主', '58630': '界', '58595': '门', '58502': '利', '58713': '海',
            '58587': '受', '58548': '听', '58351': '表', '58547': '徳', '58443': '少', '58460': '克', '58636': '代', '58585': '员',
            '58625': '许', '58694': '稜', '58428': '先', '58640': '口', '58628': '由', '58612': '死', '58446': '安', '58468': '写',
            '58410': '性', '58508': '马', '58594': '光', '58483': '白', '58544': '或', '58495': '住', '58450': '难', '58643': '望',
            '58486': '教', '58406': '命', '58447': '花', '58669': '结', '58415': '乐', '58444': '色', '58549': '更', '58494': '拉',
            '58409': '东', '58658': '神', '58557': '记', '58602': '处', '58559': '让', '58610': '母', '58513': '父', '58500': '应',
            '58378': '直', '58680': '字', '58352': '场', '58383': '平', '58454': '报', '58671': '友', '58668': '关', '58452': '放',
            '58627': '至', '58400': '张', '58455': '认', '58416': '接', '58552': '告', '58614': '入', '58582': '笑', '58534': '内',
            '58701': '英', '58349': '军', '58491': '侯', '58467': '民', '58365': '岁', '58598': '往', '58425': '何', '58462': '度',
            '58420': '山', '58661': '觉', '58615': '路', '58648': '带', '58470': '万', '58377': '男', '58520': '边', '58646': '风',
            '58600': '解', '58431': '叫', '58715': '任', '58524': '金', '58439': '快', '58566': '原', '58477': '吃', '58642': '妈',
            '58437': '变', '58411': '通', '58451': '师', '58395': '立', '58369': '象', '58706': '数', '58705': '四', '58379': '失',
            '58567': '满', '58373': '战', '58448': '远', '58659': '格', '58434': '士', '58679': '音', '58432': '轻', '58689': '目',
            '58591': '条', '58682': '呢'
        }
        txt_content = ''
        for char in data:
            ret = str(ord(char))
            if txt_dict.get(ret):
                t1 = txt_dict.get(ret)
                txt_content += t1
            else:
                txt_content += char
        return txt_content
    
    
    def get_chapter(book_id):
        url = f'https://fanqienovel.com/api/reader/directory/detail?bookId={book_id}'
        response = requests.get(url, headers=headers, timeout=30)
        ret = json.loads(response.text)
        return ret
    
    def get_novel_text(item_id):
        url = f'https://fanqienovel.com/reader/{item_id}?enter_from=page'
        response = requests.get(url, headers=headers, timeout=30)
        return response.text
    
    
    def get_bookId():
        url = "https://fanqienovel.com/api/author/library/book_list/v0/?page_count=18&page_index=0&gender=1&category_id=511&creation_status=0&word_count=-1&book_type=-1&sort=0"
        response = requests.get(url, headers=headers, timeout=30)
        ret = json.loads(response.text)
        for i in ret.get('data').get('book_list'):
            # 摘要
            abstract_fmt = i.get('abstract')
            abstract_txt_content = data_format(abstract_fmt)
            print('摘要:', abstract_txt_content)
            # 作者
            author_fmt = i.get('author')
            author_txt_content = data_format(author_fmt)
            print('作者:', author_txt_content)
            # 小说id
            book_id = i.get('book_id')
            print('小说id:', book_id)
            # 小说名字
            book_name_fmt = i.get('book_name')
            book_name_txt_content = data_format(book_name_fmt)
            print('小说名称:', book_name_txt_content)
            # 读取计数
            read_count_fmt = i.get('read_count')
            read_count_txt_content = data_format(read_count_fmt)
            print('读取计算:', read_count_txt_content)
            # 字数
            word_count_fmt = i.get('word_count')
            word_count_txt_content = data_format(word_count_fmt)
            print('字数:', word_count_txt_content)
            #  根据book_id 获取所有章节id
            ret = get_chapter(book_id)
            for x in ret.get('data').get('chapterListWithVolume')[0]:
                item_id = x.get('itemId')
                print('章节id:', item_id)
                title = x.get('title')
                print('章节标题:', title)
    
                # 获取小说内容
                response = get_novel_text(item_id)
                content = etree.HTML(response)
                content_list = content.xpath("//div[@class='muye-reader-content noselect']/div/p/text()")
                # 把列表合并成字符串
                txt = '\n'.join(content_list)
                # 小说章节文本
                txt_txt_content = txt_format(txt)
                print(txt_txt_content)
                print('===============分割线===================')
    
    
    if __name__ == '__main__':
        get_bookId()

    运行代码如下:

    image.png

    跟页面上对比下:

    image.png

    完成正确。

关键字

上一篇: aiohttp异步爬虫

下一篇: 没有了