1. 字体反爬-汽车之家
1.1 字体反爬现象
字体反爬通常出现在论坛、小说网站等,因为这些网站的文本内容通常具有较高的商业价值。首先给出相关网站的链接:汽车之家的示例链接
从上图中看,某些文字在开发者工具的element中并没有显示为正常的字体,比如“了”和“一”都显示成了某种特殊字符。上述现象表明网站对论坛中帖子的文本内容做了相关的反爬措施,也就是我们所说的字体反爬。
1.2 代码思路
这里先尝试爬取相关的文本内容:
import requests
url = 'https://club.autohome.com.cn/bbs/thread/02b5d5c1fdb18614/93403806-1.html#pvareaid=6830286'
req_header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
}
res_obj = requests.get(url, headers=req_header)
print(res_obj.text)
运行结果:
图中显示,我们可以获取到帖子的文本内容,接下来的工作就是筛选所需的内容。笔者这里采用xpath,当然也可以使用其他方法(正则、bs4)看个人喜好,对于该案例,笔者推荐使用xpath。
回顾帖子文本的标签结构:
获取相应div标签下的文本内容:
# 获取网页源代码
origin_text = res_obj.text
# 创建xpath对象
html_obj = etree.HTML(origin_text)
# 获取对应标签下的所有文本,结果返回一个列表
cont_list = html_obj.xpath('//div[@class="tz-paragraph"]/text()')
print(cont_list)
运行结果:
从上图看,目前获取的文本并不完整,因为span标签里面的文本(虽然看起来是乱码)没有获取到,修改xpath语句以获取完整文本:
cont_list = html_obj.xpath('//div[@class="tz-paragraph"]//text()')
运行结果:
现在可以获取到完整的代码了,但是乱码显示的内容是’\uee0a’等看似服从某种规律的字符串。实际上,这是网站为了字体反爬而设计的某种特殊字体,让某些文字在网页源代码中显示时变成看不懂的特殊字符串。
如何解决上述特殊字体:
Windows系统也存储了许多字体:
查找网站采用的特殊字体:在element里面搜索“myfont”(“myfont”来源于span标签)
这里复制上图中ttf的url,然后粘贴到浏览器进行访问,就可以下载网站的特殊字体文件:
推荐使用FontCreator软件,可以打开该字体文件:
在代码中加载该字体文件:
# pip install fontTools
from fontTools.ttLib import TTFont
# 加载字体文件,文件已保存到.py同级目录下
ttf_obj = TTFont('ChcCQ1sUz2iANCDMAABj6LtIHts14..ttf')
# 打印特殊字体
print(ttf_obj.getGlyphOrder())
运行结果:
上图中,列表里面保存的就是网站使用的特殊字体加载后的字符串,这些字符串与FontCreator软件打开字体文件后显示的汉字一一对应,比如’uniED78’表示汉字“低”。
列表中第一个字符串’.notdef’表示空白,其余字符串中的’uni’表示Unicode,后四位表示具体内容。此处,需要将上述列表进行一定的处理,将’uni’替换为’\u’,与网页源代码中获取到的特殊字符串相对应。另外,特殊字符串对应的汉字,在代码中只能手写出来。
# 处理uni字符串列表
new_uni_list = []
for uni in uni_list[1:]:
new_uni = r"'\u" + uni[3:] + "'"
new_uni_list.append(new_uni)
print(new_uni_list)
运行结果:
此时的结果并不符合我们的预期,还需进行处理,方法如下:
new_uni = eval(new_uni)
Windows系统自动加上一层斜杠,需采用eval()方法处理后得到想要的结果。
运行结果:
最后一步就是替换文本中的特殊字符串为正常汉字:
# 拼接字符串
contents = ''.join(cont_list)
# 准备好正确的汉字列表
word_list = ['', '', '', '']
for i in range(len(new_uni_list)):
# 将特殊字符串替换为正常的汉字
contents.replace(new_uni_list[i], word_list[i])
print(contents)
1.3 完整代码
import requests
from lxml import etree
from fontTools.ttLib import TTFont
class TiffSpider(object):
def __init__(self, target_url, target_tag, tiff_file, word_list):
self.req_header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, '
'like Gecko) Chrome/87.0.4280.141 Safari/537.36 '
}
self.tiff_file = tiff_file
self.target_url = target_url
self.target_tag = target_tag
self.word_list = word_list
# 获取网页源代码
def GetHtmlText(self):
res_obj = requests.get(self.target_url, headers=self.req_header)
return res_obj.text
# 获取原始文本
def GetTagInfo(self, html_text):
# 创建xpath对象
html_obj = etree.HTML(html_text)
# 获取对应标签下的所有文本,结果返回一个列表
cont_list = html_obj.xpath(self.target_tag)
# print(cont_list)
contents = ''.join(cont_list)
return contents
# 替换文本中的特殊字符
def GetResultText(self, contents):
# 加载字体文件
ttf_obj = TTFont(self.tiff_file)
uni_list = ttf_obj.getGlyphOrder()
# 处理uni字符串列表
new_uni_list = []
for uni in uni_list[1:]:
new_uni = r"'\u" + uni[3:] + "'"
new_uni = eval(new_uni)
new_uni_list.append(new_uni)
# print(new_uni_list)
for i in range(len(new_uni_list)):
# 将特殊字符串替换为正常的汉字
contents = contents.replace(new_uni_list[i], self.word_list[i])
return contents
# 将结果写入文件
def WriteResult(self, contents):
write_contents = ""
for index, cont in enumerate(contents):
write_contents += cont
if index > 0 and index % 80 == 0:
write_contents += '\n'
with open('result.txt', 'w', encoding='utf-8') as fobj:
fobj.write(write_contents)
def Run(self):
html_text = self.GetHtmlText()
origin_contents = self.GetTagInfo(html_text)
result_contents = self.GetResultText(origin_contents)
self.WriteResult(result_contents)
if __name__ == '__main__':
url = 'https://club.autohome.com.cn/bbs/thread/02b5d5c1fdb18614/93403806-1.html#pvareaid=6830286'
tag = '//div[@class="tz-paragraph"]//text()'
file = 'ChcCQ1sUz2iANCDMAABj6LtIHts14..ttf'
word = ['低', '右', '大', '十', '得', '七', '一', '着', '是',
'远', '少', '上', '矮', '三', '呢', '二', '很', '四',
'和', '多', '八', '近', '了', '短', '长', '更', '不',
'下', '小', '左', '九', '地', '高', '的', '六', '坏', '五', '好']
tiffspider = TiffSpider(target_url=url, target_tag=tag, tiff_file=file, word_list=word)
tiffspider.Run()
运行结果:
2. css反爬-自如案例
2.1 css反爬现象
演示链接:自如的示例链接
自如网站对租房的价格进行了一些css反爬措施:
span标签里显示了一张png格式图片的url,在浏览器里面直接访问,可以看到如下的图片,该图片也称为css雪碧图:
百度CSS雪碧:
从字面意思理解,雪碧图通过偏移一定像素来定位所要的图片部分。
以上图为例,偏移-192.6px显示雪碧图中的数字1,偏移-171.2px显示雪碧图中的数字0。
如何寻找偏移量与数字的具体对应关系:
雪碧图中最简单的数字是6,因为它的偏移是0,然后寻找数字9,它的偏移量就是两个数字间的间隔。
笔者总结的雪碧图中数字与偏移量的对应关系:
num_dict = {
'6': '-0px',
'9': '-21.4px',
'4': '-42.8px',
'8': '-64.2px',
'5': '-85.6px',
'2': '-107px',
'7': '-128.4px',
'3': '-149.8px',
'0': '-171.2px',
'1': '-192.6px'
}
2.2 代码思路
这里先尝试爬取网页的文本内容:
import requests
url = 'http://sh.ziroom.com/z/'
req_headers = {
'Referer': 'http://sh.ziroom.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
}
res_obj = requests.get(url, headers=req_headers)
html_text = res_obj.text
print(html_text)
运行结果:
尝试获取房屋的名字和价格文本内容(关键标签截图如下):
# 创建xpath对象
html_obj = etree.HTML(html_text)
# 获取div[@class="info-box"]标签列表
info_box_list = html_obj.xpath('//div[@class="info-box"]')
# 获取房子的名字,以第一个为例
house_name = info_box_list[0].xpath('h5/a/text()')
print(house_name)
# 获取房子的价格信息,以第一个为例
price_texts = info_box_list[0].xpath('div[@class="price "]/span/@style')
print(price_texts)
运行结果:
提取价格信息中数字的偏移量,并转换成最终的价格:
price_num_list = []
for price_text in price_texts:
result = re.search(r'.+(background-position: )(\-.+px)', price_text)
print(result.group(2))
price = num_dict[result.group(2)]
price_num_list.append(price)
real_price = ''.join(price_num_list)
print(house_name, real_price)
运行结果:
-171.2px
-85.6px
-192.6px
-107px
-85.6px
['整租·中环明珠2室1厅-南'] 05125
这里的价格与实际页面上的并不相符,是因为网站的雪碧图在不断变化,导致原有的num_dict无法使用,不过这里笔者主要掌握方法,不要太在意这些。
2.3 完整代码
以下代码中,笔者自己把雪碧图中数字与偏移量的对应关系写在csv文件,如果嫌麻烦也可以写在代码里。
class SpiderZiroom(object):
def __init__(self, css_pict_file):
self.ziroom_url = 'http://sh.ziroom.com/z/'
self.req_headers = {
'Referer': 'http://sh.ziroom.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
}
# 传递雪碧图对应的数字文件路径
self.css_pict_file = css_pict_file
self.num_dict = {}
self.ReadCssPictNum()
# 初始化偏移量与数字的对应关系
def ReadCssPictNum(self):
with open(self.css_pict_file, 'r') as fobj:
conts = csv.DictReader(fobj)
for cont in conts:
offset = cont['offset']
num = cont['num']
self.num_dict[offset] = num
# 获取网页源代码
def GetHtmlText(self):
res_obj = requests.get(self.ziroom_url, headers=self.req_headers)
html_text = res_obj.text
return html_text
# 获取对应的文本信息
def GetHouseInfo(self, html_text):
html_obj = etree.HTML(html_text)
# 获取div[@class="info-box"]标签列表
info_box_list = html_obj.xpath('//div[@class="info-box"]')
house_name_list = []
house_priceinfo_list = []
for info in info_box_list:
# 获取房子名字
house_name = info.xpath('h5/a/text()')[0]
house_name_list.append(house_name)
# 获取房子的价格信息
price_texts = info.xpath('div[@class="price "]/span/@style')
house_priceinfo_list.append(price_texts)
return zip(house_name_list, house_priceinfo_list)
# 打包房子的名字和价格到字典
def GetHouseNameAndPrice(self, house_info_zip):
result_list = []
for house_name, price_texts in house_info_zip:
price_num_list = []
house_info_dict = {}
for price_text in price_texts:
result = re.search(r'.+(background-position: )(\-.+px)', price_text)
# print(result.group(2))
price = self.num_dict[result.group(2)]
price_num_list.append(price)
real_price = ''.join(price_num_list)
# 将结果保存到字典中
house_info_dict['house_name'] = house_name
house_info_dict['house_price'] = real_price
result_list.append(house_info_dict)
return result_list
# 将结果保存到本地csv文件
def WriteResult(self, result_list):
titles = ['house_name', 'house_price']
with open('ziroom_test.csv', 'w', encoding='utf-8', newline='') as fobj:
# 创建writer对象, 第二个参数传递表头,对应数据的key
writer_obj = csv.DictWriter(fobj, titles)
# 写入表头
writer_obj.writeheader()
# 写入内容
writer_obj.writerows(result_list)
def run(self):
html_text = self.GetHtmlText()
house_info_zip = self.GetHouseInfo(html_text)
result_list = self.GetHouseNameAndPrice(house_info_zip)
self.WriteResult(result_list)
if __name__ == '__main__':
ziroom = SpiderZiroom('csspictfile.csv')
ziroom.run()
运行结果:
house_name,house_price
整租·中环明珠2室1厅-南,10390
合租·南宜花苑4居室-南卧,3860
合租·宏明雅舍3居室-南卧,4890
整租·静安康鑫家园2室1厅-南,13560
合租·金铭福邸4居室-南卧,2030
整租·怡景大厦2室1厅-南,9990
整租·凯旋花苑2室1厅-西,13390
整租·公安住宅小区2室1厅-南,10790
合租·锦华花园3居室-南卧,3090
合租·龙湖北城天街2居室-北卧,2990
合租·紫叶花园东园4居室-南卧,3030
整租·黄山锦庭2室1厅-东南,8990
限于篇幅,以上结果只是其中的一部分。