文章目录
0.写在开头
申明/叠甲
该程序只用于个人学习,个人不会也请他人不要用于非法牟利。
需求
- 学习需要经常翻译某些单词。(整段话在网页翻译并不觉得麻烦,就不考虑这个功能)
- 翻译界面应置顶、不过多遮挡其他应用的界面。
分析
- 一开始想下载所有中英互译的单词用数据库(我也不确定?)实现,但太麻烦而且也不知道能不能成功,毕竟我只是想快速实现一个能让我方便一点的功能。
- 后来学习/参照/抄别人的爬虫程序,外加边学Python边设计的一点点UI实现了功能。
最终实现
1.爬虫学习
- 抄归抄,还是得学习一下别人是怎么实现的。
- 相关的爬虫教程有很多,这里仅记录相关的知识点。
1.1.Ajax
- Ajax的全称是Asynchronous JavaScript+XML,即异步的JavaScript和XML。
- Ajax是与服务器交换数据的技术,它在不重载整个页面的情况下,实现对部分网页的更新。
- 抓取网页在这里,我们要先判断该网页是不是使用Ajax请求
- 打开网页后右键-检查-Network
- 在网页输入文字进行翻译,选择Fetch/XHR,抓取异步加载的数据包。
- 在Headers中的Request Headers发现这行代码(如下),说明该网页是使用Ajax请求的。
X-Requested-With:XMLHttpRequest
- 也就是我们知道了网页是怎么加载数据的,方便后续我们对数据的查找。
1.2.POST请求
- 还是在Headers,可以看到Request Method:POST,说明网页是通过POST来提交翻译请求。
- python使用post请求的代码为——response=requests.post(url,data,headers)
- 只要知道url、data、headers这些信息,我们就能向服务器发送post请求获取翻译内容。
1.2.1.url
- 这里的url是指发往服务器接口的地址,并不是一开始我们打开网页的url
- 还是Headers,在General里找到Request URL,这就是发往服务器接口的地址。
Request URL:https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule
1.2.2.headers
- 这里的headers很明显就是对应网页的Headers,但不是所有信息都是必需的,我们记下不可缺少的几项即可。
Cookie: OUTFOX_SEARCH_USER_ID=-1091853714@10.169.0.83; OUTFOX_SEARCH_USER_ID_NCOO=1713181479.4418712; JSESSIONID=aaa_M7byIa4ulgC56O39x; SESSION_FROM_COOKIE=unknown; DICT_UGC=be3af0da19b5c5e6aa4e17bd8d90b28a|; JSESSIONID=abcs0WCkW9GQz9NWlBvmy; ___rl__test__cookies=1662626354497
Referer: https://fanyi.youdao.com/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.27
1.2.3.data
- data记录着各种数据,包括我们需要翻译的文本
- 点击Payload,From Data下所有数据都是需要填入的。
- 但是,并没有那么容易,有些数据是动态变化的,用来反爬,我们再输入内容进行翻译,比较出哪些是动态的。
#静态数据
from: AUTO
to: AUTO
smartresult: dict
client: fanyideskweb
bv: 01e27702dbb21a6d2b97645ec075ab88
doctype: json
version: 2.1
keyfrom: fanyi.web
action: FY_BY_REALTlME
#动态变化的数据
i: 你好(需要翻译的内容)
salt: 16626444326669
sign: 1775ab088613c2b68870b4fdb49baff3
lts: 1662644432666
1.3.JS文件
- 前面说到网页是使用Ajax请求加载数据的,所以到js文件查看动态数据是如何生成的。
- 点击Sources,Ctrl+Shift+f全局搜索salt(或sign/lts)
PS:快捷键没反应的可能是已经有搜索框,找找Search - 可以看见在fanyi.min.js的文件里出现过,打开文件,点击{}展开,Ctrl+f更精准的搜索salt
- 一番判断后,可以确定动态变化的数据是靠以下代码生成的(在8千多行)
var r = function(e) {
var t = n.md5(navigator.appVersion)
r = "" + (new Date).getTime()
i = r + parseInt(10 * Math.random(), 10);
return {
//推导得ts="" + (new Date).getTime()
ts: r,
bv: t,
//推导得salt="" + (new Date).getTime()+parseInt(10 * Math.random(),10)
salt: i,
//sign=md5加密后的("fanyideskweb" + e + salt + "Ygy_4c=r#e#4EX^NUGUc5")
//e通过在该处设置断点,网页输入文字进行翻译获得,e="输入内容",PS:断点用的有点奇奇怪怪的
sign: n.md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5")
}
};
……
var t=e.i,i=r(t);
……
//lts=ts
lts:i.ts,
- 这里稍微总结下——
//时间戳,从一个公认时间到现在的毫秒数
lts="" + (new Date).getTime()
//时间戳加一位随机数[0,10)
salt="" + (new Date).getTime()+parseInt(10 * Math.random(),10)
//e是需要翻译的文本,这些字符串合并后通过md5加密
sign=n.md5("fanyideskweb" + e + salt + "Ygy_4c=r#e#4EX^NUGUc5")
1.4.Python编写程序
- 所有数据已经准备好,用python代替js编写发送请求即可
- 代码来源于这里,在此基础上,进行了改动方便解释重要功能。
import requests
import time
import random
import hashlib
import re
from tkinter import END, Entry, Tk
class Youdao:
def __init__(self):
self.__i=''
self.__from='AUTO'
self.__to='AUTO'
self.__web_url='https://fanyi.youdao.com/'
self.__requests_url='https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
#标头
self.__headers={
"User-Agent":'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.27',
"Cookie":'OUTFOX_SEARCH_USER_ID=-1091853714@10.169.0.83; OUTFOX_SEARCH_USER_ID_NCOO=1713181479.4418712; JSESSIONID=aaa_M7byIa4ulgC56O39x; SESSION_FROM_COOKIE=unknown; DICT_UGC=be3af0da19b5c5e6aa4e17bd8d90b28a|; JSESSIONID=abcs0WCkW9GQz9NWlBvmy; ___rl__test__cookies=1662626354497',
"Referer":'https://fanyi.youdao.com/'
}
#请求出错等待时间
self.__sleep_second=3
def __get_sign_lastField(self):
'''获取生成sign所用的加密字符,如上述的"Ygy_4c=r#e#4EX^NUGUc5"'''
#向服务器发送请求,返回一个包含所有服务器资源的Response对象
web_url_response=requests.get(url=self.__web_url,headers=self.__headers)
'''
我们需要得到fanyi.min.js的链接,它在网页源代码中的代码为
<script type="text/javascript" src="https://shared.ydstatic.com/fanyi/newweb/v1.1.10/scripts/newweb/fanyi.min.js"></script>
采用正则表达式匹配
.表示匹配除换行以外的任何单字符,*表示零次或多次,后面搭配有?表示匹配尽可能少的字符。如a.*?b表示a开头遇到第一个b即停止。
()表示若匹配则将括号内的文本内容记录。
[]表示括号中一组字符有1个匹配即可,后面跟?表示匹配0个或1个,后面跟*表示匹配零个或多个字符。
a-zA-Z0-9表示a到z、A到Z、0到9的字符。
'''
fanyi_min_js=re.findall('<script.*?src="(http[s]?://[a-zA-Z0-9./]*fanyi.min.js)"></script>',web_url_response.text)
try:
#获得fanyi.min.js链接返回的网页代码
fanyi_min_js_response=requests.get(url=fanyi_min_js[0],headers=self.__headers)
except IndexError as ie:
print("IndexError:",ie)
#没有匹配到链接,等待时间结束重新运行函数
time.sleep(self.__sleep_second)
self.__get_sign_lastField()
#正则表达式搜索该段代码——sign: n.md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5"),记录最后一项字符串
#\将下一个字符记为一个特殊字符或原义字符,\s表示空格
res=re.search('"fanyideskweb"\s?\+\s?[a-zA-Z]\s?\+\s?[a-zA-Z]\s?\+\s?"(.*?)"',fanyi_min_js_response.text,re.S)
try:
#group(1)表示第一个括号(这里只有一个括号)匹配记录的内容
return res.group(1)
except AttributeError as ae:
print("AttributeError:",ae)
time.sleep(self.__sleep_second)
self.__get_sign_lastField()
def __translate(self):
'''翻译实例'''
#时间戳
timestamp=time.time()
#js使用的时间戳单位是毫秒,python的是秒,需换算。
lts=str(int(timestamp*1000))
salt=lts+str(random.randint(0,10))
sign='fanyideskweb'+self.__i+salt+self.__get_sign_lastField()
sign_md5=hashlib.md5(sign.encode(encoding='UTF-8')).hexdigest()
data={
'i':self.__i,
'from':self.__from,
'to':self.__to,
'smartresult':'dict',
'client':'fanyideskweb',
'salt':salt,
'sign':sign_md5,
'lts':lts,
'bv':'01e27702dbb21a6d2b97645ec075ab88',
'doctype': 'json',
'version': '2.1',
'keyfrom': 'fanyi.web',
'action': 'FY_BY_REALTlME'
}
response=requests.post(url=self.__requests_url,data=data,headers=self.__headers)
response.encoding='utf-8'
#反序列化成python的字典结构
result=response.json()
'''
result内容参照XHR包的Response,如下
{"errorCode":0,"translateResult":[[{"tgt":"hello","src":"你好"}]],"type":"zh-CHS2en"}
这是一个字典,字典后面加一个[]表示取相应的值,而下面的[0]表示取列表[]内的第一个元素
'''
self.__result=result['translateResult'][0][0]['tgt'] if result['errorCode']==0 else '翻译出错'
def translate(self,string:str) -> str:
'''翻译功能'''
self.__i=string
self.__translate()
return self.__result
2.简单UI设计
- 边学边写了一个简单的UI界面,搭配上述代码使用。
2.1.组件构建
def __init__(self) :
self.yd=Youdao()
#主窗口
self.window=Tk()
#窗口置顶
self.window.wm_attributes('-topmost',1)
#窗口大小
self.window.geometry('297x30')
#单行文本框Entry,self.window是父控件,font字体,bg背景颜色,fg字体颜色,exportselection在文本框选中将复制到粘贴板,这里=0关闭了功能,width宽度(单位是0的长度)
self.input_entry=Entry(self.window,font=('微软雅黑',14),bg='white',fg='black',exportselection=0,width=12)
#pack管理组件的布局,side="left"指定组件靠左放置,另外还提供expand参数可以将父组件的额外空间也填满
self.input_entry.pack(side="left")
self.output_entry=Entry(self.window,font=('微软雅黑',14),bg='white',fg='black',exportselection=0,width=14)
self.output_entry.pack(side="left")
2.2.绑定事件
- 在某些部件上将函数绑定(bind)到某些事件上,当触发事件将执行相应的函数。
- 这里给输入框分别进行了两个绑定,一个是回车事件,一个是获得焦点事件。
def __init__(self):
……
self.input_entry.bind("<Return>",self.commit)
self.input_entry.bind("<FocusIn>",self.Focus)
……
#输入框触发回车事件执行commit
def commit(self,event=None):
#输出框显示翻译内容
self.output_entry.insert(END,self.yd.translate(self.input_entry.get().replace('\n',' ')))
#输出框获得焦点,让输入框失去焦点
self.output_entry.focus_set()
#输入框获得焦点执行Focus
def Focus(self,event=None):
#清空输入框和输出框的内容
self.output_entry.delete(0,END)
self.output_entry.delete(0,END)
3.完整代码
- 由于编写问题,直接复制会出现tap和空格复用的问题(但我好像没用过空格,可能是格式不兼容?)
- 完整代码可查看这里,translate.py(推荐运行,无修改),translate_withAnnotation.py(带有注释,即上述代码,仅作理解用)
4.打包生成exe程序
- 在cmd窗口输入pip install pyinstaller安装pyinstaller
- 打包要在程序当前路径进行。打开带有程序的界面,点击上方路径空白处输入cmd。
- 输入pyinstaller -F -w 打包文件.py
- 生成的exe程序和系统自动生成文件即在当前路径。
5.最后
- 流程到这里就结束了,但关于Requests、Response、json、编码解码还感觉到有点迷糊,肝完软工作业再继续总结。