python反反爬虫系列一(文本混淆)

python反反爬虫系列一(文本混淆)

声明:仅供技术交流,请勿用于非法用途,如有其它非法用途造成损失,和本博客无关

1,图片伪装反爬虫

图片伪装:即你在浏览器上看到的文字或者数字,其实是一张图片,那么在网页源代码里面是找不到你想要的文字的,这种混淆方式并不会影响用户阅读,但是可以让爬虫程序无法获得“所见”的文字内容。这就是图片伪装反爬虫。

那么攻破的思路是:找不到文字,那么就拿图片呗,识别图片里面的文字或者数字即可。

网上很多人用的是光学字符识别技术(PyTesseract 库)来识别图中的文字,但光学字符识别技术也有一定的缺陷,在面对扭曲文字、生僻字和有复杂干扰信息的图片时,它就无法发挥作用了。而且要安装的东西还挺多(主要是不想装)

所以我使用的是百度的文字识别API,通用文字识别日调用量就有50000次,而且我发现识别率也是很高的。

下面以广西人才网为例子


第一步、分析页面



可以看到联系电话是一张图片,但是正常第一次看到页面都不觉得它是一张图片吧。查看网页源代码看到了图片的下载链接,那么只要拿到这张图片然后把它识别出来就行了。


第二步、编写代码
from parsel import Selector
import time
import requests
import base64
import urllib
import os

# 调用百度API获取联系电话号码
def ocr_get_phone(ak,sk,img_path):
    host = f'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={ak}&client_secret={sk}'
    response = requests.get(host)
    access_token=response.json()['access_token'] #获取access_token
    api_url='https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic' + '?access_token=' + access_token #通用文字识别
    headers={'Content-Type':'application/x-www-form-urlencoded'}
    f=open(img_path,'rb')
    img=base64.b64encode(f.read())
    f.close()
    data={'image':img}
    response=requests.post(api_url,data=data,headers=headers)
    result=response.json()['words_result'][0]['words']
    os.remove(img_path)
    return result
#获取信息
def get_data(ak,sk):
    url='https://www.gxrc.com/jobDetail/aac3654c1149499b950a1d70fb13e285'
    headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'}
    r=requests.get(url,headers=headers)
    response=Selector(r.text)
    p_list=response.xpath('//*[text()="联系方式"]/following-sibling::div[1]/p')
    info=['联系人','联系电话','电子邮箱','联系地址']
    massage=[]
    for p in p_list:
        temp=p.xpath('./label/text()').get()
        if temp is not None:
            massage.append(temp)
        else:
            img_url=urllib.parse.urljoin(url,p.xpath('.//img/@src').get())
            img_path=str(time.time())+'.jpg'
            urllib.request.urlretrieve(img_url,img_path)
            phone=ocr_get_phone(ak,sk,img_path)
            massage.append(phone)
    return dict(zip(info,massage))

if __name__ == '__main__':
    ak='XXX' #百度API创建应用即可拿到
    sk='XXX' #百度API创建应用即可拿到
    data=get_data(ak,sk)
    for key,value in data.items():
        print(key+':'+value,end='\n')

可以看到输出如下:

联系人:黄小姐
联系电话:0771-3925354
电子邮箱:hr@nnleray.com
联系地址:南宁市高新区新苑路17号华成都市广场华城大厦A座1505-1510

通过比对,发现完全正确!

2,css偏移反爬虫

css偏移,即通过修改css样式,打乱文字的排版使得网页源代码中的信息与在浏览器上看到的信息不一致,从而达到反爬虫的效果。

下面以去哪儿网为例子


第一步、分析页面



通过分析、对比,发现了其隐藏的规律:显示在网页上的数字只有<i>标签,然后其通过下面的<b>标签来更改<i>标签上的数字;第一个<b>标签的style属性已经说明了其宽度即图中的style="width:48px;left:-48px",平均一个数字的宽度<i>标签也已说明即图中的style="width: 16px;",而下面的<b>标签上的数字是通过其设定的style属性来错位更改<i>标签上的数字。

听起来好像有那么一点绕,不过没关系,下面通过一张图来补充说明一下


第二步、编写代码

ps:爬这个还是有点难度的其实,因为它不只这一个css偏移,还有一些其他的,比如说:

  1. 如果你直接用requests发请求获取源代码,返回的却不是页面的信息,而是有一大部分的js代码;
  2. 然后呢,用selenium来打开网页会发现打开之后找不到航班信息,这个其实是检测到selenium
  3. 存在心跳机制

限于本人当前的知识能力范畴,我选择了selenium来爬,具体破解请看代码及注释

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from parsel import Selector
import time
from pandas import DataFrame
import requests

def get_df(start_city,arrive_city):
    options=webdriver.ChromeOptions()
    options.add_experimental_option("excludeSwitches", ["enable-automation"]) #消除正在受自动化测试的警告
    options.add_experimental_option('useAutomationExtension', False) #消除正在受自动化测试的警告
    script = '''
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined
    })
    '''
    driver=webdriver.Chrome(options=options)
    driver.maximize_window()
    # 执行script语句破解selenium的反爬虫
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": script}) #将windows.navigator.webdriver设置为undefined
    url='https://flight.qunar.com/site/oneway_list.htm?'
    params={
        'searchDepartureAirport': start_city,
        'searchArrivalAirport': arrive_city,
        'searchDepartureTime': time.strftime('%Y-%m-%d',time.gmtime(time.time())), #获取当前日期时间
        'searchArrivalTime': time.strftime('%Y-%m-%d',time.gmtime(time.time()+(86400*5))) #加多五天
    }
    r=requests.get(url,params=params)
    driver.get(r.url) #为了拿到加了参数的url链接
    time.sleep(2)
    driver.refresh() #刷新是确保页面正常
    time.sleep(2)
    names=[] # 航班名称
    citys=[] # 城市-城市
    dep_times=[] # 起飞时间
    arr_times=[] # 到达时间
    prices=[] # 价格
    while True:
        response=Selector(driver.page_source)
        div_list=response.xpath('//div[@class="b-airfly"]')
        for div in div_list:
            name=div.xpath('.//div[@class="air"]/span/text()').get()
            dep_time=div.xpath('.//div[@class="sep-lf"]/h2/text()').get()
            arr_time=div.xpath('.//div[@class="sep-rt"]/h2/text()').get()
            rels=div.xpath('.//em[@class="rel"]')
            # 下面是处理价格的css反爬,逻辑跟上面图片说明的差不多
            for rel in rels:
                nums=rel.xpath('.//text()').getall()
                total=int(re.findall('left:-(\d+)px',rel.xpath('./b[1]/@style').get())[0])
                average=int(re.findall('width: (\d+)px',rel.xpath('.//i[1]/@style').get())[0])
                result=nums[:total//average]
                pxs=[int(re.findall('left:-(\d+)px',i)[0])//average for i in rel.xpath('.//b/@style').getall()[1:]]
                for key,value in dict(zip(pxs,nums[total//average:])).items():
                    result[-key]=value
            price=''.join(result)
            names.append(name)
            dep_times.append(dep_time)
            arr_times.append(arr_time)
            prices.append(price)
            citys.append(f'{start_city}-{arrive_city}')
        next_page=driver.find_elements_by_xpath('//a[text()="下一页"]')
        if next_page != []:
            ActionChains(driver).send_keys(Keys.END).perform()
            next_page[0].click()
            time.sleep(2)
        else:
            break
#     driver.quit()
    # 将爬取的数据放在Dataframe中,方便后续保存
    columns=['航空公司','地点','起飞时间','着陆时间','价格']
    df=DataFrame([names,citys,dep_times,arr_times,prices]).T
    df.columns=columns
    return df

if __name__ == '__main__':
    df=get_df(start_city='北京',arrive_city='上海')

输出df如下:

对比如下:

可以发现,完全正确!

3,自定义字体反爬虫

自定义字体反爬虫,即目标站点自己定义的一中字体,通常以woffsvgttfeot格式的文件嵌套在网页端上,通过特定的编码与字体一一映射。用户不需下载该自定义字体,字体就能在页面上显示出来,这种混淆方式也不会影响用户阅读,只是在网页源代码中出现乱码的情况,进而达到反爬虫的效果。

下面以大众点评为例子


第一步、分析页面


不单单是商店的基本信息的字体这样,包括菜名、用户评论等等,都是这样的情况。单独复制一个字符运行看看:

其实,这就是一个特殊的字体编码,浏览器根据这个编码从自定义字体中找到与之匹配的真正的字体,然后渲染在页面上的。

所以我们的目标是找到这个自定义字体的文件,找出字体的映射关系,然后就可以解析出网页源代码中的特殊字体。

这中字体通常是在一个css的文件当中,打开浏览器的检查,在Network下的CSS中可以找到这个css请求,没有的话,刷新页面就加载出来了

可以看到num、address、shopdesc这样的关键字,这不就是对应特殊字符的class属性吗?ok,找到自定义字体,复制链接在浏览器中打开,就能直接下载字体,直接下载后面的woff文件即可。

那么,下载下来怎么打开这个文件呢,这里推荐FontLab VI,百度一下就能找到资源,30天试用期,不过好像有破解版的。

安装之后,打开下载的字体文件,可以看到:

可以看到,这些字体上面对应着一个编码,其实这正是网页上的特殊字符,刚刚运行的那个\ue765就是unie765所对应的字体,即数字8

拿到字体之后,接下来就是找到与真正字体一一对应的特殊编码了,那么怎么用python来操作呢,这里用到了一个第三方库fontTools,直接pip install fontTools即可,它可以读取并操作woff文件。但是对这个库不怎么熟悉,这里只用到了它的转xml的函数,最后用标准库xml来操作即可。

在此之前,需要先把文件里面的所有字按顺序记录下来,并保存在一个列表中。可是,一看有603个字,这得敲到猴年马月呀。所以,我这里使用一种比较简单的做法:也是用百度的文字识别API,这次要用高精度版点击跳转,亲测识别率99%,准确率99%

首先你的FontLab VI要设置一下,将编码、和多余的边框给去掉,不然会干扰到识别


第二步、编写代码

手动截图保存,共截取6张图片,用以下代码去识别字体:

import requests
import base64

def ocr_get_fonts(ak,sk,img_paths):
    fonts=['',' '] # 前两个空的字体先定义好
    host = f'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={ak}&client_secret={sk}'
    response = requests.get(host)
    access_token=response.json()['access_token']
    api_url='https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic' + '?access_token=' + access_token
    headers={'Content-Type':'application/x-www-form-urlencoded'}
    for j,img_path in enumerate(img_paths):
        f=open(img_path,'rb')
        img=base64.b64encode(f.read())
        f.close()
        data={'image':img}
        response=requests.post(api_url,data=data,headers=headers)
        result=response.json()
        words=''
        for eachone in result['words_result']:
            words+=eachone['words']
        for i in words:
            fonts.append(i)
        print(f'第{j+1}张识别了{len(words)}个字') #为了查看哪一张出现了识别错误,然后对照修正
    return fonts

if __name__ == '__main__':
	ak='XXX' #百度API创建应用即可拿到
    sk='XXX' #百度API创建应用即可拿到
	img_paths=list(map(lambda x:'./fonts_jpg/'+x,os.listdir('./fonts_jpg')))#我这里截的图放在了./fonts_jpg目录下
	fonts=ocr_get_fonts(ak,sk,img_paths) #拿到所有识别的字体

运行输出如下:

第1张识别了109个字
第2张识别了112个字
第3张识别了112个字
第4张识别了112个字
第5张识别了112个字
第6张识别了43个字

明显看出第一张图识别少了一个字,然后通过比对,发现缺了个“一”字,然后对得到的字体再做处理,如下:

for i,num in enumerate(fonts):
    if num == '容': # 意思是找到“容”字,然后在其前面加上个“一”字
        index1=i
fonts.insert(index1,'一')


那么,现在拿到了所有字体,接下来就是找到对应字体的笔画轮廓图,因为我发现:

  1. 字体的编码不同woff文件是不一样的,不能与字体相对应
  2. 字体的轮廓图不同文件也是一样的,因此拿这个来当键值就行

那么,怎么拿到字体的轮廓图呢,那就用到xml了,具体处理如下:

try:
	import xml.etree.cElementTree as et #速度更快
except:
	import xml.etree.ElementTree as et

root = et.parse('num.xml')
names=root.findall('./GlyphOrder/GlyphID') #按顺序拿到所有编码
xyons=[] # 存储轮廓数据即x、y、on的值
for name in names:
    bihua=[]
    temp=name.attrib['name']
    pts=root.findall(f'./glyf/TTGlyph[@name="{temp}"]/contour/pt')
    for pt in pts:
        bihua.append(pt.attrib)
    xyons.append(bihua)

拿到字体和字体的轮廓数据之后呢,要保存起来,方便下次直接使用,不用重新截图识别字体。

def save_font(fonts,xyons):
    data=dict(zip(fonts,xyons)) # 注意这里,字体作为key,轮廓作为value,因为轮廓是列表不能当键值
    json_str = json.dumps(data, indent=4,ensure_ascii=False)
    with open('fonts.json', 'w', encoding='utf-8') as f:
        f.write(json_str)

那么,接下来就是重头戏爬取数据了,但是呢,可以发现的是有些字需要去比对编码获取真正的字体,而有些又不用。所以我的思路是:
在一个xpath语句下,拿到:

  • 此语句下的所有class属性值
  • class属性值对应的text值
  • 此语句下的所有的text值

然后有class值节点的text值去对应class值的文件中找到其对应的text值的轮廓数据,再比对我们保存的字体数据,找到真正的字体返回来,最后拼接所有text值,得到一句完整的和浏览器上看到的话。具体请看代码及代码注释。

try:
    import xml.etree.cElementTree as et #速度更快
except:
    import xml.etree.ElementTree as et
import requests
from parsel import Selector
from fontTools.ttLib import TTFont
import urllib
import os
import re

class Perfect():
    '''
    这个类的主要功能是先加载字体文件,
    通过传入进来的xpath语句进行解析,
    返回得到的真正字体的字符串列表。
    即:所有处理对应轮廓数据与字体的全部逻辑,调用其xpath函数即可
    '''
    
    def __init__(self):
        self.fonts,self.xyons=self.load_font
    
    def check_down_font(self,path='./fonts_file/'):
        if not os.path.exists(path):
            os.makedirs(path)

        url='http://www.dianping.com/shop/112223644'
        headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'}
        r=requests.get(url,headers=headers)
        response=Selector(r.text)
        fonts_link='http:'+response.xpath('//link[contains(@href,"svgtextcss")]/@href').get()
        r=requests.get(fonts_link,headers=headers)
        now_filenames=[i + '.woff' for i in re.findall('@font-face{font-family: "PingFangSC-Regular-(\w+)"',r.text)]
        now_font_urls=['https:' + i for i in re.findall('format\("embedded-opentype"\),url\("(.*?)"\)',r.text)]
        if not os.path.exists('check.json'):
            json_str = json.dumps(dict(zip(now_filenames,now_font_urls)), indent=4,ensure_ascii=False)
            with open('check.json', 'w', encoding='utf-8') as f:
                f.write(json_str)
        with open('check.json','r',encoding='utf-8') as f:
            check_json=json.load(f)
        filenames=list(check_json.keys())
        font_urls=list(check_json.values())

        if (filenames==now_filenames) and (font_urls==now_font_urls):
            print('字体没有被修改,不需要重新下载!')
            if not os.path.exists(path + 'num.xml'):
                num = TTFont(path + 'num.woff')
                num.saveXML(path + 'num.xml')
            if not os.path.exists(path + 'shopdesc.xml'):
                shopdesc = TTFont(path + 'shopdesc.woff')
                shopdesc.saveXML(path + 'shopdesc.xml')
            if not os.path.exists(path + 'review.xml'):
                review = TTFont(path + 'review.woff')
                review.saveXML(path + 'review.xml')
            if not os.path.exists(path + 'address.xml'):
                address = TTFont(path + 'address.woff')
                address.saveXML(path + 'address.xml')
            if not os.path.exists(path + 'dishname.xml'):
                dishname = TTFont(path + 'dishname.woff')
                dishname.saveXML(path + 'dishname.xml')
            if not os.path.exists(path + 'hours.xml'):
                hours = TTFont(path + 'hours.woff')
                hours.saveXML(path + 'hours.xml')
        else:
            print('字体已变更,正在下载最新字体!')
            json_str = json.dumps(dict(zip(now_filenames,now_font_urls)), indent=4,ensure_ascii=False)
            with open('check.json', 'w', encoding='utf-8') as f:
                f.write(json_str)
            for filename,font_url in zip(now_filenames,now_font_urls):
                urllib.request.urlretrieve(font_url,path+filename)
                time.sleep(2)
            num = TTFont(path + 'num.woff')
            num.saveXML(path + 'num.xml')
            shopdesc = TTFont(path + 'shopdesc.woff')
            shopdesc.saveXML(path + 'shopdesc.xml')
            review = TTFont(path + 'review.woff')
            review.saveXML(path + 'review.xml')
            address = TTFont(path + 'address.woff')
            address.saveXML(path + 'address.xml')
            dishname = TTFont(path + 'dishname.woff')
            dishname.saveXML(path + 'dishname.xml')
            hours = TTFont(path + 'hours.woff')
            hours.saveXML(path + 'hours.xml')
    
    @property
    def load_font(self):
        with open('fonts.json','r',encoding='utf-8') as f:
            data=json.load(f)
        xyons=list(data.values())
        fonts=list(data.keys())
        return fonts,xyons
    
    def get_word(self,i,class_name,path='./fonts_file/'):
        uni_text=ascii(i).replace('\\u','uni').replace("'",'').replace("'",'') #将字符编码转为字符串
        # 根据传进来的class属性值打开对应xml文件
        root = et.parse(path + f'{class_name}.xml')
        bihua=[]
        # 根据特殊编码找到对应字体轮廓数据
        pts=root.findall(f'./glyf/TTGlyph[@name="{uni_text}"]/contour/pt')
        for pt in pts:
            bihua.append(pt.attrib)
        # 再根据得到的轮廓数据比对找到真正的字
        for j,true_text in zip(self.xyons,self.fonts):
            if j == bihua:
                break
        return true_text

    def get_data(self,class_names,uni_texts,total_texts):
        result=[]
        k=0 # 充当当前xpath语句下的所有的text值的游标
        i=0 # 充当当前xpath语句下的所有的有class属性值的text值的游标
        while True:
            if uni_texts[i] == total_texts[k]:
                result.append(self.get_word(uni_texts[i],class_names[i]))
                if i<len(uni_texts)-1:
                    i+=1
                if len(result) != len(total_texts):
                    k+=1
                else:
                    break
            else:
                result.append(total_texts[k])
                if len(result) == len(total_texts):
                    break
                else:
                    k+=1
        return ''.join(result)
    
    def process_total_texts(self,total_texts):
        output=[]
        for i in total_texts:
            temp=i.strip()
            # 若含有空格字符则去掉
            if '\xa0' in temp:
                temp=temp.replace('\xa0','')
                if '\xa0' in temp:
                    temp=temp.replace('\xa0','')
            if temp != '':
                output.append(temp)
        return output
    
    def xpath(self,ress):
        # 传进来的ress是一个elements列表
        output=[]
        for res in ress:
            class_names=res.xpath('.//*[(@class="num") or (@class="shopdesc") or (@class="review") or (@class="address") or (@class="dishname") or (@class="hours")]/@class').getall()
            uni_texts=res.xpath('.//*[(@class="num") or (@class="shopdesc") or (@class="review") or (@class="address") or (@class="dishname") or (@class="hours")]/text()').getall()
            total_texts=res.xpath('.//text()').getall()
            total_texts=self.process_total_texts(total_texts)
            data=self.get_data(class_names,uni_texts,total_texts)
            output.append(data)
        return output

if __name__ == '__main__':
	url='http://www.dianping.com/shop/112223644'
	headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'}
	r=requests.get(url,headers=headers)
	response=Selector(r.text)
	perfect=Perfect()
	perfect.check_down_font()
	shop_name=response.xpath('//h1/text()').get()
	shop_info=perfect.xpath(response.xpath('//div[@class="brief-info"]'))[0]
	shop_address=perfect.xpath(response.xpath('//span[@id="address"]'))[0]
	shop_phone=perfect.xpath(response.xpath('//p[@class="expand-info tel"]'))[0]
	shop_open=perfect.xpath(response.xpath('//p[@class="info info-indent"]'))[0]
	user_names=response.xpath('//a[@class="name"]/text()').getall()
	user_comments=perfect.xpath(response.xpath('//p[@class="desc"]'))
	comments=dict(zip(user_names,user_comments))
	item={'商店名称':shop_name,'商店信息':shop_info,'商店地址':shop_address,'商店电话':shop_phone,'营业时间':shop_open,'用户评论':comments}
	item

输出如下:

通过比对浏览器上的信息,完全一致!


参考链接
https://www.ituring.com.cn/book/tupubarticle/28992


写在最后

因为这本《python3反爬虫原理与绕过实战》书,让我知道了以前没有遇到过的反爬虫策略,以及反反爬虫策略。然而发现自己好像在爬虫这方面其实还有很多的不足的,很多知识点是需要进一步学习与攻克的,因为爬虫涉及到前端、后端等等很多发面的知识,故这条路还很长呀,要慢慢来才行哦。前路漫漫,不过未来可期!加油吧!

后面有时间我会继续出反爬虫系列的,敬请期待~

  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值