Python爬取58同城租房数据,破解字体加密[Python基础]

python

本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理。

以下文章来源于CSDN,作者:TRHX • 鲍勃

刚接触Python的新手、小白,可以复制下面的链接去免费观看Python的基础入门教学视频

https://v.douyu.com/author/y6AZ4jn9jwKW

 

【1】加密字体攻克思路

F12 打开调试模板,通过页面分析,可以观察到,网站里面凡是涉及到有数字的地方,都是显示为乱码,这种情况就是字体加密了,那么是通过什么手段实现字体加密的呢?

CSS 中有一个 @font-face 规则,它允许为网页指定在线字体,也就是说可以引入自定义字体,这个规则本意是用来消除对电脑字体的依赖,现在不少网站也利用这个规则来实现反爬

右侧可以看到网站用的字体,其他的都是常见的微软雅黑,宋体等,但是有一个特殊的:fangchan-secret ,不难看出这应该就是58同城的自定义字体了

 

我们通过控制台看到的乱码事实上是由于 unicode 编码导致,查看网页源代码,我们才能看到他真正的编码信息

 

要攻克加密字体,那么我们肯定要分析他的字体文件了,先想办法得到他的加密字体文件,同样查看源代码,在源代码中搜索 fangchan-secret 的字体信息

 

选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 .woff 的字体文件,这个过程可以通过以下代码实现:

import requests

import base64

headers = {

"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"

}

url = "https://wh.58.com/chuzu/"

response = requests.get(url=url, headers=headers)

# 匹配 base64 编码的加密字体字符串

base64_string = response.text.split("base64,")[1].split(""")[0].strip()

# 将 base64 编码的字体字符串解码成二进制编码

bin_data = base64.decodebytes(base64_string.encode())

# 保存为字体文件

with open("58font.woff", "wb") as f:

f.write(bin_data)

得到字体文件后,我们可以通过 FontCreator 这个软件来看看字体对应的编码是什么:

 

观察我们在网页源代码中看到的编码:类似于 龤、龒

对比字体文件对应的编码:类似于 uni9FA4、nui9F92

可以看到除了前面三个字符不一样以外,后面的字符都是一样的,只不过英文大小写有所差异

现在我们可能会想到,直接把编码替换成对应的数字不就OK了?然而并没有这么简单

尝试刷新一下网页,可以观察到 base64 编码的加密字体字符串会改变,也就是说编码和数字并不是一一对应的,再次获取几个字体文件,通过对比就可以看出来

 

可以看到,虽然每次数字对应的编码都不一样,但是编码总是这10个,是不变的,那么编码与数字之间肯定存在某种对应关系,,我们可以将字体文件转换为 xml 文件来观察其中的对应关系,改进原来的代码即可实现转换功能:

import requests

import base64

from fontTools.ttLib import TTFont

headers = {

"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"

}

url = "https://wh.58.com/chuzu/"

response = requests.get(url=url, headers=headers)

# 匹配 base64 编码的加密字体字符串

base64_string = response.text.split("base64,")[1].split(""")[0].strip()

# 将 base64 编码的字体字符串解码成二进制编码

bin_data = base64.decodebytes(base64_string.encode())

# 保存为字体文件

with open("58font.woff", "wb") as f:

f.write(bin_data)

# 获取字体文件,将其转换为xml文件

font = TTFont("58font.woff")

font.saveXML("58font.xml")

打开 58font.xml 文件并分析,在<cmap>标签内可以看到熟悉的类似于 0x9476、0x958f 的编码,其后四位字符恰好是网页字体的加密编码,可以看到每一个编码后面都对应了一个 glyph 开头的编码

将其与 58font.woff 文件对比,可以看到 code 为 0x958f 这个编码对应的是数字 3,对应的 name 编码是 glyph00004

 

我们再次获取一个字体文件作为对比分析

 

依然是 0x958f 这个编码,两次对应的 name 分别是 glyph00004 和 glyph00007,两次对应的数字分别是 3 和 6,那么结论就来了,每次发送请求,code 对应的 name 会随机发生变化,而 name 对应的数字不会发生变化,glyph00001 对应数字 0、glyph00002 对应数字 1,以此类推

那么以 glyph 开头的编码是如何对应相应的数字的呢?在 xml 文件里面,每个编码都有一个 TTGlyph 的标签,标签里面是一行一行的类似于 x,y 坐标的东西,这个其实就是用来绘制字体的,用 matplotlib 根据坐标画个图,就可以看到是一个数字

 

此时,我们就知道了编码与数字的对应关系,下一步,我们可以查找 xml 文件里,编码对应的 name 的值,也就是以 glyph 开头的编码,然后返回其对应的数字,再替换掉网页源代码里的编码,就能成功获取到我们需要的信息了!

总结一下攻克加密字体的大致思路:

  • 分析网页,找到对应的加密字体文件
  • 如果引用的加密字体是一个 base64 编码的字符串,则需要转换成二进制并保存到 woff 字体文件中
  • 将字体文件转换成 xml 文件
  • 用 FontCreator 软件观察字体文件,结合 xml 文件,分析其编码与真实字体的关系
  • 搞清楚编码与字体的关系后,想办法将编码替换成正常字体

【2】思维导图

 

【3】加密字体处理模块

【3.1】获取字体文件并转换为xml文件

def get_font(page_url, page_num):

response = requests.get(url=page_url, headers=headers)

# 匹配 base64 编码的加密字体字符串

base64_string = response.text.split("base64,")[1].split(""")[0].strip()

# print(base64_string)

# 将 base64 编码的字体字符串解码成二进制编码

bin_data = base64.decodebytes(base64_string.encode())

# 保存为字体文件

with open("58font.woff", "wb") as f:

f.write(bin_data)

print("" + str(page_num) + "次访问网页,字体文件保存成功!")

# 获取字体文件,将其转换为xml文件

font = TTFont("58font.woff")

font.saveXML("58font.xml")

print("已成功将字体文件转换为xml文件!")

return response.text

由主函数传入要发送请求的 url,利用字符串的 split() 方法,匹配 base64 编码的加密字体字符串,利用 base64 模块的 base64.decodebytes() 方法,将 base64 编码的字体字符串解码成二进制编码并保存为字体文件,利用 FontTools 库,将字体文件转换为 xml 文件

【3.2】将加密字体编码与真实字体进行匹配

def find_font():

# 以glyph开头的编码对应的数字

glyph_list = {

"glyph00001": "0",

"glyph00002": "1",

"glyph00003": "2",

"glyph00004": "3",

"glyph00005": "4",

"glyph00006": "5",

"glyph00007": "6",

"glyph00008": "7",

"glyph00009": "8",

"glyph00010": "9"

}

# 十个加密字体编码

unicode_list = ["0x9476", "0x958f", "0x993c", "0x9a4b", "0x9e3a", "0x9ea3", "0x9f64", "0x9f92", "0x9fa4", "0x9fa5"]

num_list = []

# 利用xpath语法匹配xml文件内容

font_data = etree.parse("./58font.xml")

for unicode in unicode_list:

# 依次循环查找xml文件里code对应的name

result = font_data.xpath("//cmap//map[@code="{}"]/@name".format(unicode))[0]

# print(result)

# 循环字典的key,如果code对应的name与字典的key相同,则得到key对应的value

for key in glyph_list.keys():

if key == result:

num_list.append(glyph_list[key])

print("已成功找到编码所对应的数字!")

# print(num_list)

# 返回value列表

return num_list

由前面的分析,我们知道 name 的值(即以 glyph 开头的编码)对应的数字是固定的,glyph00001 对应数字 0、glyph00002 对应数字 1,以此类推,所以可以将其构造成为一个字典 glyph_list

同样将十个 code(即类似于 0x9476 的加密字体编码)构造成一个列表

循环查找这十个 code 在 xml 文件里对应的 name 的值,然后将 name 的值与字典文件的 key 值进行对比,如果两者值相同,则获取这个 key 的 value 值,最终得到的列表 num_list,里面的元素就是 unicode_list 列表里面每个加密字体的真实值

【3.3】替换掉网页中所有的加密字体编码

def replace_font(num, page_response):

# 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5

result = page_response.replace("", num[0]).replace("", num[1]).replace("", num[2]).replace("", num[3]).replace("", num[4]).replace("", num[5]).replace("", num[6]).replace("", num[7]).replace("", num[8]).replace("", num[9])

print("已成功将所有加密字体替换!")

return result

传入由上一步 find_font() 函数得到的真实字体的列表,利用 replace() 方法,依次将十个加密字体编码替换掉

【4】租房信息提取模块

def parse_pages(pages):

num = 0

soup = BeautifulSoup(pages, "lxml")

# 查找到包含所有租房的li标签

all_house = soup.find_all("li", class_="house-cell")

for house in all_house:

# 标题

title = house.find("a", class_="spanbox").text.strip()

# print(title)

# 价格

price = house.find("div", class_="money").text.strip()

# print(price)

# 户型和面积

layout = house.find("p", class_="room").text.replace("", "")

# print(layout)

# 楼盘和地址

address = house.find("p", class_="infor").text.replace("", "").replace("

", "")

# print(address)

# 如果存在经纪人

if house.find("div", class_="jjr"):

agent = house.find("div", class_="jjr").text.replace("", "").replace("

", "")

# 如果存在品牌公寓

elif house.find("p", class_="gongyu"):

agent = house.find("p", class_="gongyu").text.replace("", "").replace("

", "")

# 如果存在个人房源

else:

agent = house.find("p", class_="geren").text.replace("", "").replace("

", "")

# print(agent)

data = [title, price, layout, address, agent]

save_to_mysql(data)

num += 1

print("" + str(num) + "条数据爬取完毕,暂停3秒!")

time.sleep(3)

利用 BeautifulSoup 解析库很容易提取到相关信息,这里要注意的是,租房信息来源分为三种:经纪人、品牌公寓和个人房源,这三个的元素节点也不一样,因此匹配的时候要注意

 

【5】MySQL数据储存模块

【5.1】创建MySQL数据库的表

def create_mysql_table():

db = pymysql.connect(host="localhost", user="root", password="000000", port=3306, db="58tc_spiders")

cursor = db.cursor()

sql = "CREATE TABLE IF NOT EXISTS 58tc_data (title VARCHAR(255) NOT NULL, price VARCHAR(255) NOT NULL, layout VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, agent VARCHAR(255) NOT NULL)"

cursor.execute(sql)

db.close()

首先指定数据库为 58tc_spiders,需要事先使用 MySQL 语句创建,也可以通过 MySQL Workbench 手动创建

然后使用 SQL 语句创建 一个表:58tc_data,表中包含 title、price、layout、address、agent 五个字段,类型都为 varchar

此创建表的操作也可以事先手动创建,手动创建后就不需要此函数了

【5.2】将数据储存到MySQL数据库

def save_to_mysql(data):

db = pymysql.connect(host="localhost", user="root", password="000000", port=3306, db="58tc_spiders")

cursor = db.cursor()

sql = "INSERT INTO 58tc_data(title, price, layout, address, agent) values(%s, %s, %s, %s, %s)"

try:

cursor.execute(sql, (data[0], data[1], data[2], data[3], data[4]))

db.commit()

except:

db.rollback()

db.close()

commit() 方法的作用是实现数据插入,是真正将语句提交到数据库执行的方法,使用 try except 语句实现异常处理,如果执行失败,则调用 rollback() 方法执行数据回滚,保证原数据不被破坏

【6】完整代码

import requests

import time

import random

import base64

import pymysql

from lxml import etree

from bs4 import BeautifulSoup

from fontTools.ttLib import TTFont

headers = {

"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"

}

# 获取字体文件并转换为xml文件

def get_font(page_url, page_num):

response = requests.get(url=page_url, headers=headers)

# 匹配 base64 编码的加密字体字符串

base64_string = response.text.split("base64,")[1].split(""")[0].strip()

# print(base64_string)

# 将 base64 编码的字体字符串解码成二进制编码

bin_data = base64.decodebytes(base64_string.encode())

# 保存为字体文件

with open("58font.woff", "wb") as f:

f.write(bin_data)

print("" + str(page_num) + "次访问网页,字体文件保存成功!")

# 获取字体文件,将其转换为xml文件

font = TTFont("58font.woff")

font.saveXML("58font.xml")

print("已成功将字体文件转换为xml文件!")

return response.text

# 将加密字体编码与真实字体进行匹配

def find_font():

# 以glyph开头的编码对应的数字

glyph_list = {

"glyph00001": "0",

"glyph00002": "1",

"glyph00003": "2",

"glyph00004": "3",

"glyph00005": "4",

"glyph00006": "5",

"glyph00007": "6",

"glyph00008": "7",

"glyph00009": "8",

"glyph00010": "9"

}

# 十个加密字体编码

unicode_list = ["0x9476", "0x958f", "0x993c", "0x9a4b", "0x9e3a", "0x9ea3", "0x9f64", "0x9f92", "0x9fa4", "0x9fa5"]

num_list = []

# 利用xpath语法匹配xml文件内容

font_data = etree.parse("./58font.xml")

for unicode in unicode_list:

# 依次循环查找xml文件里code对应的name

result = font_data.xpath("//cmap//map[@code="{}"]/@name".format(unicode))[0]

# print(result)

# 循环字典的key,如果code对应的name与字典的key相同,则得到key对应的value

for key in glyph_list.keys():

if key == result:

num_list.append(glyph_list[key])

print("已成功找到编码所对应的数字!")

# print(num_list)

# 返回value列表

return num_list

# 替换掉网页中所有的加密字体编码

def replace_font(num, page_response):

# 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5

result = page_response.replace("", num[0]).replace("", num[1]).replace("", num[2]).replace("", num[3]).replace("", num[4]).replace("", num[5]).replace("", num[6]).replace("", num[7]).replace("", num[8]).replace("", num[9])

print("已成功将所有加密字体替换!")

return result

# 提取租房信息

def parse_pages(pages):

num = 0

soup = BeautifulSoup(pages, "lxml")

# 查找到包含所有租房的li标签

all_house = soup.find_all("li", class_="house-cell")

for house in all_house:

# 标题

title = house.find("a", class_="spanbox").text.strip()

# print(title)

# 价格

price = house.find("div", class_="money").text.strip()

# print(price)

# 户型和面积

layout = house.find("p", class_="room").text.replace("", "")

# print(layout)

# 楼盘和地址

address = house.find("p", class_="infor").text.replace("", "").replace("

", "")

# print(address)

# 如果存在经纪人

if house.find("div", class_="jjr"):

agent = house.find("div", class_="jjr").text.replace("", "").replace("

", "")

# 如果存在品牌公寓

elif house.find("p", class_="gongyu"):

agent = house.find("p", class_="gongyu").text.replace("", "").replace("

", "")

# 如果存在个人房源

else:

agent = house.find("p", class_="geren").text.replace("", "").replace("

", "")

# print(agent)

data = [title, price, layout, address, agent]

save_to_mysql(data)

num += 1

print("" + str(num) + "条数据爬取完毕,暂停3秒!")

time.sleep(3)

# 创建MySQL数据库的表:58tc_data

def create_mysql_table():

db = pymysql.connect(host="localhost", user="root", password="000000", port=3306, db="58tc_spiders")

cursor = db.cursor()

sql = "CREATE TABLE IF NOT EXISTS 58tc_data (title VARCHAR(255) NOT NULL, price VARCHAR(255) NOT NULL, layout VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, agent VARCHAR(255) NOT NULL)"

cursor.execute(sql)

db.close()

# 将数据储存到MySQL数据库

def save_to_mysql(data):

db = pymysql.connect(host="localhost", user="root", password="000000", port=3306, db="

以上是 Python爬取58同城租房数据,破解字体加密[Python基础] 的全部内容, 来源链接: utcz.com/z/529896.html

回到顶部