如何破解字体反爬机制

        这几天爬取58租房信息的时候意外发现了它是一个字体反爬的网站,所谓的字体反爬就是网站将一些关键字替换为网站自己的字体,这样在网页上字体会正常显示,但是当爬取下来的时候,经过字体加密的字符都是乱码的,根本无法查看

如图所示:

可以看到,2390元/月在页面上是正常显示的,但是,当我们打开查看器查看的时候......

好端端的2390就变成了不知道什么字符.......

这就是网站使用了字体反爬机制,网站有自己的一套字体,只有在它的页面上才会正常显示,否则就是一串乱码,毫无价值。那么遇到这种问题该怎么解决,在经历几天的摸索之后终于将正确的信息抓取了下来。

首先,我们查看网页的源码,就是下面这样的

这是网页源码中一串base64的字符串,它就是网站的字体文件,很难想象一串base64的字符串就是它的字体文件。

我们将这一串base64的字符串复制下来,将它解码并保存成一个字体文件

import base64


font_face = "AAEAAAALAIAAAwAwR1NVQiCLJXoAAAE4AAAAVE9TLzL4XQjtAAABjAAAAFZjbWFwq8N/ZAAAAhAAAAIuZ2x5ZuWIN0cAAARYAAADdGhlYWQTcnjtAAAA4AAAADZoaGVhCtADIwAAALwAAAAkaG10eC7qAAAAAAHkAAAALGxvY2ED7gSyAAAEQAAAABhtYXhwARgANgAAARgAAAAgbmFtZTd6VP8AAAfMAAACanBvc3QFRAYqAAAKOAAAAEUAAQAABmb+ZgAABLEAAAAABGgAAQAAAAAAAAAAAAAAAAAAAAsAAQAAAAEAAOv6p7JfDzz1AAsIAAAAAADX9ZbuAAAAANf1lu4AAP/mBGgGLgAAAAgAAgAAAAAAAAABAAAACwAqAAMAAAAAAAIAAAAKAAoAAAD/AAAAAAAAAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEERAGQAAUAAAUTBZkAAAEeBRMFmQAAA9cAZAIQAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQJR2n6UGZv5mALgGZgGaAAAAAQAAAAAAAAAAAAAEsQAABLEAAASxAAAEsQAABLEAAASxAAAEsQAABLEAAASxAAAEsQA"
# 太长没复制完......

b = base64.b64decode(font_face)
with open('58.ttf','wb') as f:
    f.write(b)

这里我们将它保存成一个ttf的字体文件,然后使用fontCreator将这个字体文件打开(fontCreator需要自行下载,直接百度就能下载类,非常简单)

打开之后的效果

我们再来看看网页上的源码......

小伙伴们是不是会惊奇的发现,网页源码上被替换掉的数字,使用fontCreator都能找到与之对应的原本的数字,那么立刻就能想到的一个方法就是将这些网页中的编码与原本的值对应成一个字典,只要抓取到了字典中存在的值就将其替换成本来的值,但是(注意我这里使用了但是)......

同样的,这也是58的一个字体文件,但是解析出来编码与对应的数字与上一次解析的完全不一样,这倒不是因为fontCreator解析出错,而是因为58这个网站友好几套字体文件,它的每一页的数据使用的都是随机的字体文件,当你解析了第一页字体的对应关系,拿小本本美滋滋的将对应关系记下来,但是点击到第二页的时候,发现关系又完全对不上,是不是很气。而且又不可能将每一页的对应关系都用本子记录下来。这时候就需要另一个工具,python的第三方库fontTools,直接pip安装就行。

在进行解密之前,先将原先的字体文件保存成一个xml文件。

from fontTools.ttLib import TTFont
font = TTFont('58.ttf')
font.saveXML('test.xml')

打开这个xml看看到底是啥

看不懂......

这个就好像有点懂了,code的值不是网页上的数字被替换的字符串吗!!!

这里的name属性的值实际对应的就是网页上的数字,下面的代码可以帮助我们查看对应关系

from fontTools.ttLib import TTFont
font = TTFont('58.ttf')
# font.saveXML('test.xml')
print(font.keys())

a = font['cmap'].tables[2].ttFont.getGlyphOrder()
b = font['cmap'].tables[2].ttFont.getReverseGlyphMap()
c = font['cmap'].tables[2].ttFont.tables['cmap'].tables[1].cmap
print("ppp ::::: ",a)
print("ddd ::::: ",b)
print("ddd ::::: ",c)

输出的结果

这里最后一行输出的就是网页上显示的字符串与camp标签中的name值的对应关系。那么网页上显示的0x958f这种字符串究竟是什么意思呢,它其实是一个十六进制的数字,将这个十六进制的数字转换成十进制(int("0x985f",16)),得到的值就是最后一行输出的键,那么这个键对应的值就是第二行输出的键,第二行的值就是本来的数据,这样一来,在抓取每一页之前,先抓取到它这一页的字体文件,进行分析,得到对应关系,不就能获取到原始的数据了嘛。

ps:需要注意的是,使用fontCreator解析时发现数字对应关系前面有一个空值,所以实际解析到的对应的数字需要减1才能得到正确的值。

代码思路:1.在爬取每一页数据之前,先获取到源码中的base64的字符串,解码,保存成字体文件2.生成对应关系的字典3.抓取到页面的乱码文字,解码成16进制的数字3.将十六进制的数字转为十进制,判断字典中是否有这个键,如果有,则解析为原本的数字,如果没有,则说明这个文字没有进行机密处理,保存原来的文字即可。4.完成全部加密文字的替换得到正确的数据。

实际代码:

import re
import lxml.html
import base64
from fontTools.ttLib import TTFont
import requests
import random
import sqlite3
db = sqlite3.connect("58.db")
cursor = db.cursor()

UA = [
    "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
    "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
    "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1"
]

headers = {
    "User-Agent":random.choice(UA)
}

def resp(i):
    base_url = "https://tj.58.com/pinpaigongyu/pn/{}/"
    response = requests.get(base_url.format(i), headers=headers)
    print("正在下载:",response.url)
    return response

def get_base64_str(response):
    base_font = re.compile("base64,(.*?)\'")
    base64_str = re.search(base_font, response.text).group().split(',')[1].split('\'')[0]
    return base64_str

def make_font_file(base64_str):
    b = base64.b64decode(base64_str)
    with open("58.ttf","wb") as f:
        f.write(b)

def make_dict():
    font = TTFont('58.ttf')
    b = font['cmap'].tables[2].ttFont.getReverseGlyphMap()  # 编码对应的数字
    c = font['cmap'].tables[2].ttFont.tables['cmap'].tables[1].cmap  # 页面的十六进制数对应的编码
    return b, c


def parse_title(text):
    s = ""
    title_re = re.compile("\s")
    html = lxml.html.fromstring(text)
    title = html.xpath('//div[@class="des strongbox"]/h2/text()')[0]
    title = re.sub(title_re,'',title)
    for i in title:
        encode_str = str(i.encode("unicode-escape")).split(r'\\u')[-1].replace('\'','').replace(r'b(','').strip()
        num, code = make_dict()
        if len(encode_str) != 4:
            i = i
        elif int(encode_str,16) not in code:
            i = i
        else:
            i = str(num[code[int(encode_str,16)]] - 1)
        s += i
    return s

def parse_price(text):
    s = ""
    html = lxml.html.fromstring(text)
    price_code = html.xpath('//span[@class="strongbox"]/b/text()')[0]
    price_code = price_code.strip().replace('\r\n','').replace(' ','')
    price_encode_str = str(price_code.encode("unicode-escape")).split('\'')[1].split('-')
    if len(price_encode_str) > 1:
        s1 = ""
        s2 = ""
        encode_list1 = price_encode_str[0].split(r"\\u")[1:]
        encode_list2 = price_encode_str[1].split(r"\\u")[1:]
        for i in encode_list1:
            price = int(i,16)
            num, code = make_dict()
            s1 += str(num[code[price]] - 1)
        for i in encode_list2:
            price = int(i,16)
            num, code = make_dict()
            s2 += str(num[code[price]] - 1)
        s = s1 + '-' + s2

    else:
        str_list = price_encode_str[0].split(r'\\u')[1:]
        for i in str_list:
            price = int(i,16)
            num, code = make_dict()
            s += str(num[code[price]]-1)
    return s

def parse_room(text):
    s = ""
    html = lxml.html.fromstring(text)
    p_rooms = html.xpath('//p[@class="room"]/text()')[0]
    room_re = re.compile('[\s]')
    room_re1 = re.compile(r'[m²]')
    room_re2 = re.compile(r'/')
    rooms = re.sub(room_re,'',p_rooms)
    rooms = re.sub(room_re1,"平米",rooms)
    rooms = re.sub(room_re2,"至",rooms)
    for i in rooms:
        encode_str = str(i.encode("unicode-escape")).split(r'\\u')[-1].replace('\'', '').replace(r'b/','').strip()
        # print(encode_str)
        num, code = make_dict()
        if len(encode_str) != 4:
            i = i
        elif int(encode_str,16) not in code:
            i = i
        else:
            i = str(num[code[int(encode_str,16)]] - 1)
        s += i
    return s

def parse_dist(text):
    s = ""
    html = lxml.html.fromstring(text)
    p_dist_re = re.compile('\skm')
    try:
        p_dist = html.xpath('//p[@class="dist"]//text()')[1]
        p_dist = ''.join(p_dist).replace(' ','')
        p_dist = re.sub(p_dist_re,'千米',p_dist)
        for i in p_dist:
            encode_str = str(i.encode("unicode-escape")).split(r'\\u')[-1].replace('\'', '').replace(r'\\r','').replace(r'\\n','').replace(r'b.','').strip()
            num, code = make_dict()
            if len(encode_str) != 4:
                i = i
            elif int(encode_str, 16) not in code:
                i = i
            else:
                i = str(num[code[int(encode_str, 16)]] - 1)
            s += i
        dist = s
    except:
        dist = "暂无"
    return dist

def short_rent(text):
    html = lxml.html.fromstring(text)
    try:
        rent = html.xpath('//p[@class="room"]/b/text()')[0]
    except:
        rent = "不可短租"
    return rent

def parse_li(response):
    li_re = re.compile('<li logr([\s\S]*?)</li>')
    li_list = re.findall(li_re,response.text)
    return li_list

def parse_target(text):
    html = lxml.html.fromstring(text)
    try:
        target = html.xpath('//p[@class="spec"]/span/text()')
        target = ','.join(target)
    except:
        target = "暂无"
    return target

if __name__ == '__main__':
    for i in range(1,171):
        response = resp(i)
        base64_str = get_base64_str(response)
        make_font_file(base64_str)
        make_dict()
        li_list = parse_li(response)
        for i in li_list:
            title = parse_title(i)
            price = parse_price(i)
            room = parse_room(i)
            dist = parse_dist(i)
            rent = short_rent(i)
            target = parse_target(i)
            city = "天津"
            cursor.execute("insert into home(title, price, room, dist, rent,target, city) values (?,?,?,?,?,?,?)",[title,price,room,dist,rent,target,city])
            db.commit()
 

由于我的正则表达式功底有点差,所以这里的正则表达式都用的是比较low的..........

但是数据爬下来了,而且没有问题

一次愉快的破解字体反爬机制就到此结束了

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值