欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

程序员文章站 2022-04-19 20:33:44
字体反爬 字体反爬也就是自定义字体反爬,通过调用自定义的字体文件来渲染网页中的文字,而网页中的文字不再是文字,而是相应的字体编码,通过复制或者简单的采集是无法采集到编码后的文字内容的。 现在貌似不少网站都有采用这种反爬机制,我们通过猫眼的实际情况来解释一下。 下图的是猫眼网页上的显示: 检查元素看一 ......

 字体反爬

字体反爬也就是自定义字体反爬,通过调用自定义的字体文件来渲染网页中的文字,而网页中的文字不再是文字,而是相应的字体编码,通过复制或者简单的采集是无法采集到编码后的文字内容的。

现在貌似不少网站都有采用这种反爬机制,我们通过猫眼的实际情况来解释一下。

 

下图的是猫眼网页上的显示:

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

 

检查元素看一下

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

这是什么鬼,关键信息全是乱码。

熟悉 css 的同学会知道,css 中有一个 @font-face,它允许网页开发者为其网页指定在线字体。原本是用来消除对用户电脑字体的依赖,现在有了新作用——反爬。

汉字光常用字就有好几千,如果全部放到自定义的字体中,那么字体文件就会变得很大,必然影响网页的加载速度,因此一般网站会选取关键内容加以保护,如上图,知道了等于不知道。

这里的乱码是由于 unicode 编码导致的,查看源文件可以看到具体的编码信息。

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

搜索 stonefont,找到 @font-face 的定义:

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

 

这里的 .woff 文件就是字体文件,我们将其下载下来,利用 http://fontstore.baidu.com/static/editor/index.html 网页将其打开,显示如下:

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

网页源码中显示的  跟这里显示的是不是有点像?事实上确实如此,去掉开头的 &#x 和结尾的 ; 后,剩余的4个16进制显示的数字加上 uni 就是字体文件中的编码。所以 &#xea0b 对应的就是数字“9”。

知道了原理,我们来看下如何实现。

 

处理字体文件,我们需要用到 fonttools 库。

先将字体文件转换为 xml 文件看下:

from fonttools.ttlib import ttfont

font = ttfont('bb70be69aaed960fa6ec3549342b87d82084.woff')
font.savexml('bb70be69aaed960fa6ec3549342b87d82084.xml')

打开 xml 文件

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

开头显示的就是全部的编码,这里的 id 仅仅是编号而已,千万别当成是对应的真实值。实际上,整个字体文件中,没有任何地方是说明 ea0b 对应的真实值是啥的。

看到下面

Python:爬虫实例2:爬取猫眼电影——破解字体反爬

这里就是每个字对应的字体信息,计算机显示的时候,根本不需要知道这个字是啥,只需要知道哪个像素是黑的,哪个像素是白的就可以了。

 

猫眼的字体文件是动态加载的,每次刷新都会变,虽然字体中定义的只有 0-9 这9个数字,但是编码和顺序都是会变的。就是说,这个字体文件中“ea0b”代表“9”,在别的文件中就不是了。

但是,有一样是不变的,就是这个字的形状,也就是上图中定义的这些点。

 

我们先随便下载一个字体文件,命名为 base.woff,然后利用 fontstore 网站查看编码和实际值的对应关系,手工做成字典并保存下来。爬虫爬取的时候,下载字体文件,根据网页源码中的编码,在字体文件中找到“字形”,再循环跟 base.woff 文件中的“字形”做比较,“字形”一样那就说明是同一个字了。在 base.woff 中找到“字形”后,获取“字形”的编码,而之前我们已经手工做好了编码跟值的映射表,由此就可以得到我们实际想要的值了。

 

这里的前提是每个字体文件中所定义的“字形”都是一样的(猫眼目前是这样的,以后也许还会更改策略),如果更复杂一点,每个字体中的“字形”都加一点点的随机形变,那这个方法就没有用了,只能祭出杀手锏“ocr”了。

 

下面是完整的代码,抓取的是猫眼2018年电影的第一页,由于主要是演示破解字体反爬,所以没有抓取全部的数据。

代码中使用的 base.woff 文件跟上面截图显示的不是同一个,所以会看到编码跟值跟上面是对不上的。

import os
import time
import re
import requests
from fonttools.ttlib import ttfont
from fake_useragent import useragent
from bs4 import beautifulsoup

host = 'http://maoyan.com'


def main():
    url = 'http://maoyan.com/films?yearid=13&offset=0'
    get_moviescore(url)


os.makedirs('font', exist_ok=true)
regex_woff = re.compile("(?<=url\(').*\.woff(?='\))")
regex_text = re.compile('(?<=<span class="stonefont">).*?(?=</span>)')
regex_font = re.compile('(?<=&#x).{4}(?=;)')

basefont = ttfont('base.woff')
fontdict = {'unif30d': '0', 'unie6a2': '8', 'uniea94': '9', 'unie9b1': '2', 'unif620': '6',
            'uniea56': '3', 'unief24': '1', 'unif53e': '4', 'unif170': '5', 'uniee37': '7'}


def get_moviescore(url):
    # headers = {"user-agent": useragent(verify_ssl=false).random}
    headers = {'user-agent': 'mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) '
                             'chrome/68.0.3440.106 safari/537.36'}
    html = requests.get(url, headers=headers).text
    soup = beautifulsoup(html, 'lxml')
    ddlist = soup.find_all('dd')
    for dd in ddlist:
        a = dd.find('a')
        if a is not none:
            link = host + a['href']
            time.sleep(5)
            dhtml = requests.get(link, headers=headers).text
            msg = {}

            dsoup = beautifulsoup(dhtml, 'lxml')
            msg['name'] = dsoup.find(class_='name').text
            ell = dsoup.find_all('li', {'class': 'ellipsis'})
            msg['type'] = ell[0].text
            msg['country'] = ell[1].text.split('/')[0].strip()
            msg['length'] = ell[1].text.split('/')[1].strip()
            msg['release-time'] = ell[2].text[:10]

            # 下载字体文件
            woff = regex_woff.search(dhtml).group()
            wofflink = 'http:' + woff
            localname = 'font\\' + os.path.basename(wofflink)
            if not os.path.exists(localname):
                downloads(wofflink, localname)
            font = ttfont(localname)

            # 其中含有 unicode 字符,beautifulsoup 无法正常显示,只能用原始文本通过正则获取
            ms = regex_text.findall(dhtml)
            if len(ms) < 3:
                msg['score'] = '0'
                msg['score-num'] = '0'
                msg['box-office'] = '0'
            else:
                msg['score'] = get_fontnumber(font, ms[0])
                msg['score-num'] = get_fontnumber(font, ms[1])
                msg['box-office'] = get_fontnumber(font, ms[2]) + dsoup.find('span', class_='unit').text
            print(msg)


def get_fontnumber(newfont, text):
    ms = regex_font.findall(text)
    for m in ms:
        text = text.replace(f'&#x{m};', get_num(newfont, f'uni{m.upper()}'))
    return text


def get_num(newfont, name):
    uni = newfont['glyf'][name]
    for k, v in fontdict.items():
        if uni == basefont['glyf'][k]:
            return v


def downloads(url, localfn):
    with open(localfn, 'wb+') as sw:
        sw.write(requests.get(url).content)


if __name__ == '__main__':
    main()