[Python爬虫] 「暴力」破解猫眼电影票房数据的反爬虫机制
12月28日,人民日报发文批评豆瓣、猫眼上对《长城》、《摆渡人》、《铁道飞虎》等电影的差评伤害了中国电影产业。
第二天(12月29日),人民日报再次发文,说中国电影要有容得下一星的肚量。
我对国产电影已经无话可说,所以咱们还是来聊一聊有关数据分析的话题。
01. 常见反爬虫机制
01.01 通过Headers反爬虫
Headers就像寄快递填的那个单子,这些信息与正文无关,却关系着通信能否成功。以下图为例,当我访问自己的知乎主页时,消息头就包括:
消息头 | 简介 |
---|---|
请求网址 | 我们与之互动通信的网络地址 |
请求方法 | GET指从这个网址获取内容,而输入用户名密码登录网站则是POST方法 |
远程地址 | 115.159.241.95是知乎的服务器IP,443是SSL加密通信的端口号 |
状态码 | 200表示OK,另一个很有名的状态码是404 Not Found |
当然消息头远不止这些内容,还包括你的操作系统型号、浏览器型号、语言、cookie等。多年以前还有人专门做个网页,搞得好像算命似的来“猜”你的电脑有什么信息,其实都是浏览器悄悄地出卖了你。
如果直接调用Python的urllib等扩展包,也可以发起网络请求,但是默认的Headers信息也会诚实地显示出来。目标网站受到带有Headers信息的请求,就可以知道请求的发起方是人是鬼还是爬虫。
这是最基础的一种反爬虫机制,破解也很简单,只要在发起请求时伪造Headers,装的像个人就行了。
01.02 基于用户行为反爬虫
有时候网络状况不好,点击一个按钮没有反应,我们就会连续点击很多次,然后网站弹出个对话框说“您在短时间内执行了太多相同操作”,并暂时封禁了这一行为。
人类的手工操作上限是很低的,高桥名人每秒也不过17次,专业电竞选手APM也只有几百,而爬虫程序则可以高出几个数量级,这会给服务器带来很大负担。
除了操作频率以外,真正的人类和程序还有很多行为模式上的差别,很多网站都会采取机器学习算法来鉴别请求的发起方是否为正常人。
破解手段也是很多样的,比如建立“IP池”,把大量的请求分散到不同的IP地址来源上,这样看起来好像是很多用户在短时间内自然操作的。
01.03 其他手段
爬虫与反爬虫之间的消长已有几十年历史,发展出的技术理念和手段纷繁多样,如通过AJAX、JS脚本等方式动态产生网页元素。应对这些反爬虫技术,我的习惯是使用自动化测试框架Selenium,驱动浏览器内核,完全模拟用户行为,也就是“不是爬虫的爬虫”,所谓手中无虫、心中有虫也。
在实际应用中,不论爬虫还是反爬虫,都是多种方式结合起来的。
02. 猫眼的反爬虫机制
打开浏览器控制台可以发现,票房数据其实是加密过的生僻unicode编码,而且每次访问获得的unicode是随机生成的。也就是说,明文攻击只对单次访问有效。
而前端的阅读是正常的,这是因为猫眼使用了来自美团的特殊字体,把密文编码对应的字符,通过样式表渲染成为数字,这其实就是一个解密的过程。
如果手动把数据元素的
class="cs"
属性去掉,那么在系统默认字体中,前端出来的也是生僻字符。
破解这种机制,大致上有两个方向:
- 密码学破译: 采集足量的明文-密文对照样本,挖掘映射关系。
- 模式识别: 绕过加密系统,从图像中“读”出数字。
密文的unicode编码位数不多,破解的难度不大。但是问题在于,就像上面提到过的,反爬虫技术也是综合了多种方式,要高频、大量地采集数据,很有可能触发其他防御手段。
所以这里我采用了第二个方向。
03. 网页自动截图
(这一部分涉及较多本地文件处理,因各人操作系统差别较大,暂时不将代码在线互动化)
03.01 扩展包
这一步用到的扩展包主要有两个:
- selenium: 自动化测试框架
- PIL: Python Image Library,即Python图片处理库
from selenium import webdriver from PIL import Image
03.02 获取数字图像
driver = webdriver.Firefox() # 创建webdriver对象 url = "http://piaofang.maoyan.com" # 定义目标url driver.get(url) # 打开目标页面 # 获取当前电影名称列表 movie_names = [driver.find_element_by_xpath(".//*[@id='ticket_tbody']/ul[{}]/li[1]/b".format(i)).text for i in range(1,24)] # 获取实时票房列表 current_piaofang = [driver.find_element_by_xpath(".//*[@id='ticket_tbody']/ul[{}]/li[2]/b/i".format(i)).text for i in range(1,24)]
接下来是如何自动截图并存储,注意FireFox内核的截图API只能截取当前可视页面,而猫眼票房超过一屏,就必须加一个向下滚动操作。
从整个页面的图像中,再根据实时票房数据的位置和尺寸,单独把数字截取出来。为了区分这两个步骤,前一步叫“截图”,后一步叫“抠图”。
# 定义截图函数 def snap_shot(url, image_path, scroll_top=90): # 打开页面,窗口最大化 driver.get(url) driver.maximize_window() # 调用JS脚本滚动页面 scroll_js = "var q=document.documentElement.scrollTop={}".format(scroll_top) driver.execute_script(scroll_js) # 截图存储 driver.save_screenshot(image_path) # 定义抠图函数 def crop_image(image_path, pattern_xpath, crop_path, scroll_top=90): # 获取页面元素及其位置、尺寸 element = driver.find_element_by_xpath(pattern_xpath) location = element.location size = element.size # 计算抠取区域的绝对坐标 left = location['x'] top = location['y'] - scroll_top right = location['x'] + size['width'] bottom = location['y'] + size['height'] - scroll_top # 打开图片,抠取相应区域并存储 im = Image.open(image_path) im = im.crop((left, top, right, bottom)) im.save(crop_path) # 获取当前时间戳 now = datetime.datetime.now() now_sign = str(now.day)+str(now.hour)+str(now.minute)+str(now.second) # 启动截图函数,获取当前页面 snap_shot_path_1 = "snap_shot/maoyan_{0}_{1}.png".format('1', now_sign) snap_shot_path_2 = "snap_shot/maoyan_{0}_{1}.png".format('2', now_sign) snap_shot(url, snap_shot_path_1, scroll_top=90) snap_shot(url, snap_shot_path_2, scroll_top=720) # 启动抠图函数 for i in range(1,12): pattern = ".//*[@id='ticket_tbody']/ul[{}]/li[2]/b/i".format(i) crop_path = "snap_shot/crop/current_piaofang_{}.png".format(movie_names[i-1]) crop_image(snap_shot_path_1, pattern, crop_path, scroll_top=90) for i in range(13,24): pattern = ".//*[@id='ticket_tbody']/ul[{}]/li[2]/b/i".format(i) crop_path = "snap_shot/crop/current_piaofang_{}.png".format(movie_names[i-1]) crop_image(snap_shot_path_2, pattern, crop_path, scroll_top=720)
03.03 建立训练集
如此我们已经获得了实时票房数据的图像,但是这些数据少则三位(有效数字),多则六位,赶上大片的话七八位都是有可能的。对于程序来说要认识这么多数字,需要很长的训练过程和极大的训练集,那是不理智、不合适的。
其实我们地球人认识数字也不是这样认的,而是先认识0~9这十个阿拉伯数字,再结合数位的知识,组成一个多位数字。
在这一案例中,加密字体是按位加密的,但是整个数字的结构没有变,有效数字和小数点位置都是可以通过爬虫直接获取的信息。这就相当于,程序已经学会了关于“数位”的知识,那么只需要再让程序学会0~9十个数字就行了。
要建立这样的训练集,还需要进一步处理上面得到的图片,就是单独把每一位数字切出来。
# 获取实时票房数据的有效数字长度列表 curpf_lenths = [len(current_piaofang[i-1]) for i in range(1,24)] # 定义切图函数 def single_digit(index=1): lenth = curpf_lenths[index-1] name = movie_names[index-1] im = Image.open("snap_shot/crop/current_piaofang_{}.png".format(name)) # 转换为灰度图像 im = im.convert('L') # 切分整数部分 for j in range(lenth-3): locals()['digit_'+ str(j)] = im.crop((0+j*6, 0, 6+j*6, 12)) # 切分小数部分 for j in range(lenth-3, lenth-1): locals()['digit_'+ str(j)] = im.crop((j*6+4.8, 0, 6+j*6+4.8, 12)) # 对每部电影,按位存储图片 for k in range(0, lenth-2): locals()['digit_'+ str(k)].save("snap_shot/train/digit_{0}_{1}_{2}.png".format(k, name, now_sign)) # 启动切图函数 for i in range(1,24): single_digit(i)
将得到的数字图像分类整理(这一步真的只能人工完成了),因为猫眼的刷新频率不高,所以没有积累太多的训练样本,每个数字只有2、30个。但因为是印刷体,远比手写体容易识别,所以准确率还勉强可以接受。
04. 模式识别
04.01 扩展包
- os: 操作系统API
- PIL: 图像处理
- numpy: 基础数值计算
- pandas: 结构化数据处理
- scikit-learn: 机器学习
因为本案例识别难度不高,所以没有使用专门的图像处理模块如keras, mxnet等。
04.02 图像矢量化
这里的“图像矢量化”跟“矢量图”不是一个概念,后者是平面设计中常见的图片格式。“矢量化”是一个特征提取的过程,对于scikit-learn,每一个训练样本(一张图)都是一个高维矢量。
之前获得的训练集是由6×12的灰度图片构成的,灰度的值域是0~255,每个像素点的灰度值都构成一个特征维度,这样每个样本就有18432维的特征,这就太高了。如果不转换为灰度图像,而是保留色彩,那维度又会高出几个数量级。
所以对图片需要进行特征压缩,比如数字识别可以说是白纸黑字,那么不需要知道具体的灰度值,只要确定一个像素点是背景还是字体就可以了,也就是二值化,非0即1。
# 二值化函数 def binary_image(im): threshold = 200 # 阈值设为200 table = [] for i in range(256): if i < threshold: table.append(0) else: table.append(1) out = im.point(table,'1') return out # 矢量化函数 def buildvector(im): v = [] for i in im.getdata(): v.append(i) return v
经过二值化与矢量化,每张图片就变成了1行、72列的矢量,每一列都是0或1,代表纸或字。
04.03 机器学习
最后的过程我没有弄得太复杂,直接调用了Scikit-learn里的支持向量分类器,基本上保留了默认设置,交叉验证得分在
0.85
左右。
实际体验是比85%准确率要好的,因为票房数据中0,1,2的出现频率更高,训练样本多、识别率就更高。
End.
转载请注明来自36大数据(36dsj.com): 36大数据 » [Python爬虫] 「暴力」破解猫眼电影票房数据的反爬虫机制