python3爬虫之有道翻译(上)


  平时偶尔会用到翻译工具,其中最常用的就是有道翻译了,web端的有道翻译,在早期是直接可以爬到接口来使用的,但自从有道翻译推出他的API服务的时候,就对这个接口做了反爬虫的机制,从而来推广他的付费接口服务。这个反爬虫机制在爬虫领域算是一个非常经典的技术手段,今天我们就来对它一探究竟吧。

一、莫听穿林打叶声,何妨吟啸且徐行。

首先我们使用chrome浏览器打开有道翻译的链接:http://fanyi.youdao.com
然后使用F12ctrl+shift+i唤起“开发者工具”,鼠标右键点击检查一样,也就是审查元素,选择Network网络监听窗口,如下图所示,之后页面中所有的请求都会在这里显示出来:

接着我们在左侧翻译的窗口输入我们需要翻译的文字,比如输入“你好”,然后浏览器就会向有道发起异步ajax请求,在下面就可以看到所有的请求:


我们点开第一条请求,在Headers可以看到,在进行翻译的时候,发送的请求就是图中Request URL后的URL:

接着我们滚动到最下面,可以看到有一个Form Data的地方,这是请求时所携带的参数,这些数据就是在翻译的时候浏览器给服务器发送的数据:


Tip:在我们请求多次后,发现每次翻译时有些参数固定不变,而有些参数会动态变化,这就是我们破解有道反爬虫机制的关键点,后面会讲到。

然后再点击Response,这里就是接口返回的结果:

最后我们选择Application应用窗口,点击左侧Storage-Cookies,选择有道翻译的域名,这里存储的是cookie信息,cookie校验也是反爬虫的常见手段,这也是我们后面需要注意的:

二、工欲善其事,必先利其器。

运行平台:Windows
Python版本:Python3.x
IDE:Eclipse + PyDev插件

到现在为止,我们得到了url和入参,我们就可以简单写一个爬虫,去调用有道翻译的接口了,我们选用python3来进行爬虫编写。
这里使用的是Python3自带的网络请求库urllib,也伪造一下包括cookie在内的请求头,相关代码如下:

import json
from urllib import parse
from urllib import request
    
# 等待用户输入需要翻译的单词
i = input('请输入需要翻译的句子:')
# 有道翻译的url链接
url = 'http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
# 创建Form_Data字典,存储请求体
Form_Data = {}
# 需要翻译的文字
Form_Data['i'] = i
# 下面这些都先按照我们之前抓包获取到的数据
Form_Data['from'] = 'AUTO'
Form_Data['to'] = 'AUTO'
Form_Data['smartresult'] = 'dict'
Form_Data['client'] = 'fanyideskweb'
Form_Data['salt'] = '15326858088180'
Form_Data['sign'] = '4805445cac590750301ad08319a79675'
Form_Data['ts'] = '1532685808818'
Form_Data['bv'] = '9deb57d53879cce82ff92bccf83a3e4c'
Form_Data['doctype'] = 'json'
Form_Data['version'] = '2.1'
Form_Data['keyfrom'] = 'fanyi.web'
Form_Data['action'] = 'FY_BY_REALTIME'
Form_Data['typoResult'] = 'false'
# 对数据进行字节流编码处理
data = parse.urlencode(Form_Data).encode('utf-8')
# 创建Request对象
req = request.Request(url=url, data=data, method='POST')
# 写入header信息
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
req.add_header('cookie', 'OUTFOX_SEARCH_USER_ID=-1626129620@10.168.8.63')
req.add_header('Referer', 'http://fanyi.youdao.com/')
# 传入创建好的Request对象
response = request.urlopen(req, timeout=5)
# 读取信息并进行字节流解码
html = response.read().decode('utf-8')
# 把返回来的json字符串解析成字典
translate_results = json.loads(html)
# 打印返回信息
print("返回的结果是:%s" % translate_results)

我们运行这个文件后,当我们输入的是“你好”的时候,我们可以得到“hello”的这个正确的翻译结果。而当我们输入其他需要翻译的字符串的时候,比如输入“我爱你”,那么就会得到一个错误代码{'errorCode': 50},这就是有道词典的反爬虫机制,接下来我们就来一步一步破解它。

三、横看成岭侧成峰,远近高低各不同。

我们刚才有讲到,在有道翻译web页面尝试进行了多次翻译,并且每次翻译后都去查看相应的网络请求入参,比较每次请求的Form Data的值,我们注意到,除i代表翻译的源字符串以外,saltsign以及ts这三个参数是动态变化的,其他的参数都是固定值。
这里我分别用“great”和“date”两个单词翻译时候Form Data的数据进行比较:

经过多次变换语种等尝试,观察键值的变化,我们可以对请求携带的参数有如下猜测:

  • i:需要进行翻译的字符串
  • from:源语言的语种
  • to:翻译后的语种
  • smartresult:智能结果,固定值
  • client:客户端,固定值
  • salt:加密用到的盐,待定
  • sign:签名字符串,待定
  • ts:毫秒时间戳
  • bv:未知的md5值,固定值
  • doctype:文档类型,固定值
  • version:版本,固定值
  • keyfrom:键来源,固定值
  • action:操作动作,固定值
  • typoResult:是否打印错误,固定值

那么这三个动态参数的值是怎么产生的呢?这里我们可以分析一下,这三个值在每次请求的时候都不一样,只有两种情况:
第一是每次翻译的时候,浏览器会从有道服务器获取这三个值。这样可以达到每次翻译的时候值不同的需求。
第二是在本地,用js代码按照一定的规则生成的。
那么我们首先来看第一个情况,我们可以看到在每次发送翻译请求的时候,并没有一个请求是专门用来获取这几个值的:


所以就可以排除了第一种情况。就只剩下一种可能,那就是在本地自己生成的,如果是在本地自己生成的,那么规则是什么呢?

四、欲穷千里目,更上一层楼。

这里我们点击Elements审查元素,查看网页源代码,查找所有的js文件,找到一个fanyi.min.js

同样,我们在Sources-Page中也可以看到该js文件:

打开该js文件,可以看到该js是被压缩过的,我们复制出来使用在线格式化工具对齐进行格式化,然后把格式化后的代码,复制下来,用编辑器打开,然后搜索salt,可以找到相关的代码片段:

这里我们就可以发现动态值的生成原理了:

  • i:需要进行翻译的字符串的前5000字
  • salt:当前毫秒时间戳与10以内随机数字字符串的拼接
  • sign:"fanyideskweb"+i+salt+"p09@Bn{h02_BIEe]$P^nG"的md5值
  • ts:当前毫秒时间戳

在得到saltsign以及ts的生成原理后,我们就可以开始进一步改写Python代码,来对接有道的接口了:

import json
import random
import time
import hashlib
from urllib import parse
from urllib import request
    
# 等待用户输入需要翻译的单词
i = input('请输入需要翻译的句子:')
# 有道翻译的url链接
url = 'http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
# 构造有道的加密参数
client = "fanyideskweb"
ts = int(time.time() * 1000)
salt = str(ts + random.randint(1, 10))
flowerStr = "p09@Bn{h02_BIEe]$P^nG"
sign = hashlib.md5((client + i + salt + flowerStr).encode('utf-8')).hexdigest()
bv = '9deb57d53879cce82ff92bccf83a3e4c'
# 创建Form_Data字典,存储请求体
Form_Data = {}
# 需要翻译的文字
Form_Data['i'] = i
# 下面这些都先按照我们之前抓包获取到的数据
Form_Data['from'] = 'AUTO'
Form_Data['to'] = 'AUTO'
Form_Data['smartresult'] = 'dict'
Form_Data['client'] = client
Form_Data['salt'] = salt
Form_Data['sign'] = sign
Form_Data['ts'] = ts
Form_Data['bv'] = bv
Form_Data['doctype'] = 'json'
Form_Data['version'] = '2.1'
Form_Data['keyfrom'] = 'fanyi.web'
Form_Data['action'] = 'FY_BY_REALTIME'
Form_Data['typoResult'] = 'false'
# 对数据进行字节流编码处理
data = parse.urlencode(Form_Data).encode('utf-8')
# 创建Request对象
req = request.Request(url=url, data=data, method='POST')
# 写入header信息
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
req.add_header('cookie', 'OUTFOX_SEARCH_USER_ID=-1626129620@10.168.8.63')
req.add_header('Referer', 'http://fanyi.youdao.com/')
# 传入创建好的Request对象
response = request.urlopen(req, timeout=5)
# 读取信息并进行字节流解码
html = response.read().decode('utf-8')
# 把返回来的json字符串解析成字典
translate_results = json.loads(html)
# 打印翻译结果
translate_result = translate_results["translateResult"][0][0]['tgt']
print("翻译的结果是:%s" % translate_result)

运行,输入内容,翻译结果,一气呵成,大功告成。


五、纸上得来终觉浅,绝知此事要躬行。

像有道翻译这样,通过用js在本地生成随机字符串的反爬虫机制,是爬虫经常会遇到的一个问题。授人以鱼不如授人以渔,还是但愿大家能多多实践与练习,在编写爬虫的过程中也能学到一些反爬虫的知识为我所用。希望通过以上的讲解,能为大家提供一种思路,以后再碰到这种问题的时候知道该如何解决,这样本篇文章的目的也就达到了。

写到这里的时候突然又有了新的想法,那就将此篇作为上篇,期待下一篇吧~


特别感谢@Jack-Cui @南窗客斯黄