news 2026/10/24 6:16:56

Python逆向实战:从抓包到PyQt5,手把手教你打造专属翻译客户端

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python逆向实战:从抓包到PyQt5,手把手教你打造专属翻译客户端

1. 为什么我要自己动手做一个翻译客户端

市面上的翻译工具其实不少,浏览器插件、在线网页、手机App,随手一搜一大把。但用久了总会遇到几个让人难受的点:网页版广告多、接口限流、复制粘贴来回切换窗口效率低;客户端软件要么收费,要么功能臃肿,要么干脆不支持我需要的语种组合。我平时看外文资料比较多,对翻译的响应速度和稳定性要求挺高,于是就动了念头——干脆自己写一个。

这个项目的核心思路很直接:找到某个在线翻译服务在浏览器里实际调用的Web接口,分析它的请求结构,搞清楚参数是怎么拼的、签名是怎么算的,然后用代码模拟这个请求,把翻译结果拿回来,封装成一个轻量的桌面客户端。整个过程涉及抓包分析、请求参数拆解、签名算法还原、接口调用封装、客户端界面搭建这几个环节,是一条完整的Web接口逆向链路。

说白了,这就是一次“从浏览器里偷师”的实践。你不需要有服务端的配合,也不需要官方开放API,只要浏览器能正常访问的功能,理论上都可以通过这种方式把它“搬”到自己的程序里。当然,这里要强调一点:本文讨论的技术仅用于个人学习和研究,实际使用中请遵守目标服务的使用条款,不要用于商业用途或大规模请求。

适合读这篇内容的人:有一定编程基础、对网络请求和HTTP协议有基本了解、想学习接口逆向分析思路的开发者。如果你完全没接触过抓包工具,也没关系,我会把每一步都拆开讲清楚。前端、后端、移动端的朋友都能从中找到有用的东西,因为接口逆向这套方法论是通用的。

我选择翻译服务作为练手对象,是因为它的请求结构相对清晰,返回数据格式规整,适合作为逆向入门的案例。而且做出来的东西确实能日常使用,有正反馈,不像有些练习项目做完就扔了。下面我把整个过程的思路、踩过的坑、关键细节都摊开来聊。

2. 整体方案设计与技术选型思路

2.1 为什么选Web接口而不是官方API

做翻译客户端,第一条路是走官方开放平台。注册账号、申请密钥、按量付费,流程正规,文档齐全。但问题也很明显:免费额度有限,超出后要充值;有些平台的接口调用有QPS限制,频繁查询会被限流;还有就是注册流程繁琐,对于只是想练手或者个人轻度使用的人来说,成本偏高。

第二条路就是直接分析Web端调用的接口。浏览器里能用的翻译功能,背后一定有一个HTTP请求在跑。把这个请求的结构搞清楚,用代码复现出来,就能拿到同样的结果。这条路的好处是:不需要注册、不需要密钥、请求结构相对固定。坏处是:接口可能随时变动,没有官方保障,稳定性依赖对方。但对于个人学习和轻量使用来说,这条路性价比最高。

我最终选了后者。核心考量是:这个项目的目的是学习接口逆向的完整方法论,而不是做一个商业级产品。走官方API虽然省事,但学不到东西。自己分析接口,从抓包到签名还原,每一步都是实打实的技术积累。

2.2 技术栈选择:Python做核心,PyQt做界面

语言层面我选了Python。原因很简单:处理HTTP请求、解析JSON、做字符串加密,Python的生态最顺手。requests库发请求,json库解析返回,hashlib做摘要,execjs或者纯Python复现JS加密逻辑,整套工具链成熟且轻量。

界面层面考虑过几个方案。Web技术栈(Electron之类)太重,打包出来几百兆,对于一个翻译小工具来说没必要。Tkinter太简陋,做出来的界面不好看。最后选了PyQt5,原因是:组件丰富、跨平台、打包相对方便,而且Python和Qt的结合很成熟,网上资料多,遇到问题好查。

整体架构分三层:网络请求层负责构造请求、处理签名、发送HTTP调用;业务逻辑层负责翻译结果的解析、缓存、历史记录管理;界面层负责输入输出、快捷键响应、托盘图标等交互。三层之间通过信号槽或者简单的函数调用通信,耦合度低,方便单独调试。

2.3 逆向分析的基本流程

在动手写代码之前,得先把目标接口摸清楚。我的分析流程是这样的:

  1. 打开浏览器开发者工具,切到Network面板,勾选Preserve log,过滤XHR/Fetch请求。
  2. 在翻译页面输入一段测试文本,触发翻译动作,观察新出现的网络请求。
  3. 定位到翻译接口,查看请求URL、请求方法、请求头、请求体。
  4. 分析请求参数,区分哪些是固定值、哪些是动态生成的、哪些是跟输入内容相关的。
  5. 重点排查签名字段,看是否有sign、token、salt之类的参数,如果有,去JS源码里找生成逻辑。
  6. 用代码复现请求,先不带签名发一次,看返回什么错误,再逐步补全参数,直到拿到正确结果。

这个流程看起来简单,但实际操作中会有很多细节需要处理。比如请求头里的Referer、Origin、User-Agent可能都有校验;比如签名参数可能是多个字段拼接后再做哈希;比如时间戳的精度可能是秒也可能是毫秒。这些都要在分析阶段确认清楚。

3. 抓包分析与接口结构拆解

3.1 定位翻译请求的关键技巧

打开开发者工具之后,第一件事是清空现有请求记录,然后输入一段有明显特征的测试文本,比如“这是一个测试句子abc123”。这样做的目的是方便在请求列表里快速定位——你可以在Filter框里直接搜“abc123”,如果请求体里包含这个字符串,就能立刻找到目标请求。

我实测下来,翻译类接口通常是POST请求,Content-Type一般是application/x-www-form-urlencoded或者application/json。请求URL里往往带有translate、trans、fanyi之类的关键词。找到候选请求后,点进去看详情,重点看三个地方:Payload(请求体)、Headers(请求头)、Response(返回值)。

有一个容易忽略的点:有些网站会把翻译请求放在iframe或者Web Worker里发,这种情况下在主的Network面板可能看不到,需要在开发者工具的设置里勾选“Show requests from iframes”或者切换到对应的上下文。我一开始就踩过这个坑,找了半天没找到请求,后来发现是在一个隐藏的iframe里发的。

3.2 请求参数的分类与含义

定位到请求之后,把Payload里的参数全部列出来,逐个分析。以我分析的这个翻译接口为例,请求体大概长这样:

i=hello world from=auto to=zh sign=xxxxx salt=1234567890 ts=1234567890123

我把它分成三类:

第一类是业务参数,比如i是待翻译的文本,from是源语言,to是目标语言。这些参数直接跟用户输入相关,含义明确。

第二类是控制参数,比如from=auto表示自动检测源语言。这类参数通常是枚举值,可以在JS代码里找到所有可选值。

第三类是安全参数,比如sign、salt、ts。这三个是重点。ts看起来是时间戳,salt是随机盐值,sign是签名。签名的存在意味着服务端会校验请求的合法性,如果签名不对,请求会被拒绝。

注意:不同平台的参数命名可能不同,有的叫token,有的叫nonce,有的叫signature。但核心逻辑是一样的——都是为了防止接口被随意调用。

3.3 签名参数的生成逻辑还原

找到签名参数之后,下一步是搞清楚它是怎么算出来的。方法很简单:在开发者工具的Sources面板里,全局搜索sign这个关键词,或者搜索salt、ts这些参数名。通常能定位到发起请求的那段JS代码。

我定位到的代码逻辑大致是这样的:

var salt = Date.now(); var sign = md5("appid" + i + salt + "secretKey");

翻译成人话就是:取当前时间戳作为salt,然后把一个固定的appid、待翻译文本、salt、以及一个固定的密钥拼接起来,做一次MD5哈希,结果就是sign。

这里有几个关键点需要确认:

  • 拼接顺序:是appid + i + salt + key还是i + appid + salt + key?顺序不同,结果完全不同。这个必须从JS代码里精确还原。
  • 哈希算法:是MD5、SHA1还是SHA256?代码里写的是md5,那就用MD5。
  • 密钥来源:那个secretKey是硬编码在JS里的,还是从某个接口动态获取的?如果是硬编码,直接抄过来就行;如果是动态获取,还得再分析一层。
  • 时间戳精度:Date.now()返回的是毫秒级时间戳。如果服务端校验时间戳的有效期(比如5分钟内),那客户端的时间必须准确。

我遇到的一个坑是:JS里的字符串拼接用了+,但某些参数可能是数字类型,拼接时会自动转换。在Python里复现时,要确保类型转换一致,否则算出来的MD5会对不上。比如JS里123 + "abc"结果是"123abc",Python里如果直接拼接int和str会报错,得先str()转换。

3.4 请求头中的隐藏校验

除了请求体里的签名,请求头里也可能有校验字段。常见的有:

  • Referer:必须是目标网站的域名,否则可能被拒绝。
  • Origin:同上,用于跨域校验。
  • User-Agent:有些服务会检查UA,如果是脚本类的UA可能被拦截。
  • Cookie:可能包含会话标识,某些接口需要登录态才能调用。
  • X-Requested-With:有些接口要求必须是XMLHttpRequest。

我在复现请求时,先把浏览器里看到的请求头全部照抄到代码里,确保能跑通,然后再逐个删减,看哪些是必须的、哪些可以省略。这样既能保证功能可用,又能让代码尽量简洁。

实测下来,Referer和User-Agent通常是最关键的,Cookie看情况——如果接口不需要登录,一般可以不带。但有些服务会通过Cookie做频率限制,这种情况下带上Cookie反而可能被限流,需要根据实际情况调整。

4. 用Python复现请求并封装翻译功能

4.1 构造签名与请求体

搞清楚签名逻辑之后,用Python复现就是体力活了。核心代码如下:

import hashlib import time import requests APPID = "固定值" SECRET_KEY = "从JS里抄来的密钥" def make_sign(text, salt): raw = APPID + text + str(salt) + SECRET_KEY return hashlib.md5(raw.encode("utf-8")).hexdigest() def translate(text, from_lang="auto", to_lang="zh"): salt = int(time.time() * 1000) sign = make_sign(text, salt) data = { "i": text, "from": from_lang, "to": to_lang, "sign": sign, "salt": salt, "ts": salt, } headers = { "User-Agent": "Mozilla/5.0 ...", "Referer": "https://目标网站/", "Origin": "https://目标网站", } resp = requests.post(API_URL, data=data, headers=headers, timeout=10) return resp.json()

这段代码里有几个细节值得展开说。

时间戳的处理:JS里Date.now()返回毫秒级整数,Python里用int(time.time() * 1000)来模拟。注意不要用time.time()直接取整,那样是秒级的,跟JS对不上。

编码问题:MD5计算前要把字符串编码成bytes。JS内部是UTF-16,但做MD5时通常按UTF-8处理。Python里显式指定encode("utf-8"),避免中文文本出问题。

请求超时:一定要设timeout,否则网络异常时程序会卡死。我一般设10秒,翻译这种轻量请求足够了。

异常处理:实际使用中网络抖动、接口返回格式变化、签名失效都可能发生,要用try-except包起来,给用户友好的提示,而不是直接崩溃。

4.2 返回结果的解析与容错

接口返回的通常是JSON格式,结构大概是这样:

{ "code": 0, "data": { "translations": [{"translatedText": "你好世界"}] } }

解析的时候不能想当然,要做好容错。比如code不为0时表示出错,要把错误信息提取出来;比如data里可能没有translations字段,要用.get()方法安全访问;比如返回的可能是HTML转义过的文本,需要做反转义处理。

我踩过的一个坑是:某些翻译结果里包含HTML实体,比如&、<,直接显示会很难看。解决办法是用html.unescape()处理一下。还有一个坑是:长文本翻译时,接口可能对单次请求的字符数有限制,超长文本需要分段发送,然后把结果拼接起来。分段的时候要注意按句子边界切分,不要从单词中间截断。

4.3 缓存与频率控制

翻译接口通常有频率限制,短时间内大量请求可能被临时封禁。为了减少不必要的请求,我加了两层保护:

第一层是本地缓存。用字典或者SQLite存一份“原文+源语言+目标语言 → 译文”的映射。每次翻译前先查缓存,命中就直接返回,不再发请求。缓存可以设一个过期时间,比如24小时,避免长期占用存储。

第二层是请求间隔控制。在两次请求之间加一个最小间隔,比如200毫秒。用time.sleep()实现,简单粗暴但有效。如果用户连续输入,可以做一个防抖处理——等用户停止输入500毫秒后再触发翻译,而不是每敲一个字符就发一次请求。

提示:缓存键的构造要注意,from和to语言不同,同一个原文的译文是不同的,所以缓存键必须包含语言对信息。

4.4 封装成可复用的翻译类

把上面的逻辑封装成一个类,方便界面层调用:

class Translator: def __init__(self): self.cache = {} self.last_request_time = 0 self.min_interval = 0.2 def translate(self, text, from_lang="auto", to_lang="zh"): cache_key = f"{from_lang}:{to_lang}:{text}" if cache_key in self.cache: return self.cache[cache_key] # 频率控制 elapsed = time.time() - self.last_request_time if elapsed < self.min_interval: time.sleep(self.min_interval - elapsed) # 发请求 result = self._do_request(text, from_lang, to_lang) self.last_request_time = time.time() self.cache[cache_key] = result return result

这样界面层只需要调用translator.translate(text),不用关心签名、缓存、频率控制这些细节。职责分离,代码也好维护。

5. 客户端界面搭建与交互优化

5.1 界面布局设计

PyQt5做界面,核心是布局管理。我的设计很简单:上面一个多行输入框,下面一个多行输出框,中间放一排按钮(翻译、清空、复制、交换语言),底部一个状态栏显示当前状态。

用QVBoxLayout做垂直布局,把输入框、按钮行、输出框依次放进去。按钮行用QHBoxLayout做水平布局。输入框和输出框都用QTextEdit,支持多行文本和富文本显示。

窗口大小设一个合理的默认值,比如600x500,同时允许用户自由调整。设置setMinimumSize防止窗口被缩得太小导致控件重叠。

5.2 快捷键与托盘功能

翻译工具的使用场景往往是“随手查一下”,所以快捷键很重要。我设置了两个:

  • Ctrl+Enter:触发翻译。
  • Ctrl+Shift+T:全局快捷键,在任何窗口下按下都能唤起翻译窗口并聚焦到输入框。

全局快捷键在PyQt里需要用QHotkey或者调用系统API实现,稍微麻烦一点,但体验提升很大。托盘图标也是同理,最小化到托盘而不是任务栏,需要的时候点一下托盘图标就能唤出窗口。

5.3 异步请求避免界面卡顿

这是新手最容易犯的错误:在界面线程里直接发HTTP请求。网络好的时候可能感觉不到,一旦网络慢或者接口响应慢,整个界面就会卡死,按钮点不动,窗口拖不动,用户体验极差。

正确的做法是把翻译请求放到单独的线程里执行,完成后通过信号槽通知界面更新。PyQt5里可以用QThread配合自定义信号:

class TranslateWorker(QThread): finished = pyqtSignal(str) def __init__(self, translator, text): super().__init__() self.translator = translator self.text = text def run(self): result = self.translator.translate(self.text) self.finished.emit(result)

界面层创建worker,连接finished信号到更新输出框的槽函数,然后start()。这样界面始终保持响应,用户可以看到“翻译中...”的提示,而不是整个窗口假死。

5.4 打包与分发

开发完成后,用PyInstaller打包成单个可执行文件。命令大概是这样:

pyinstaller --onefile --windowed --icon=app.ico main.py

--onefile把所有依赖打成一个文件,方便分发;--windowed去掉控制台窗口;--icon设置程序图标。打包出来的文件体积大概几十兆,对于个人工具来说可以接受。

需要注意的是,如果代码里用了execjs调用外部JS引擎,打包时要确保相关依赖也被包含进去。纯Python实现的签名逻辑就没有这个问题,这也是我推荐用纯Python复现签名的原因之一。

6. 常见问题与排查技巧实录

6.1 签名校验失败的排查思路

签名对不上是最常见的问题。排查步骤:

  1. 确认拼接顺序:回到JS代码,逐字符核对拼接顺序,包括分隔符(有没有&、|之类的连接符)。
  2. 确认哈希算法:是MD5、SHA1还是SHA256?输出是十六进制还是Base64?
  3. 确认编码:中文字符串在拼接前是否做了URL编码?是否做了UTF-8编码?
  4. 确认时间戳:服务端校验的时间戳是秒级还是毫秒级?客户端时间是否准确?
  5. 打印中间结果:在JS里console.log出拼接后的原始字符串和最终签名,在Python里也打印出来,逐字符对比。

我遇到过一次签名对不上的情况,排查了半天发现是JS里用了encodeURIComponent对文本做了编码,而我在Python里没做。加上urllib.parse.quote()之后问题解决。

6.2 接口返回异常的状态码处理

接口可能返回各种异常状态:

状态码/错误码含义处理方式
200 + code=0成功正常解析译文
200 + code!=0业务错误提取错误信息提示用户
429请求过于频繁增加请求间隔,稍后重试
403签名无效或权限不足检查签名逻辑,确认请求头完整
500服务端错误提示用户稍后重试
超时网络问题重试一次,仍失败则提示检查网络

对于429,除了增加间隔,还可以考虑在本地做更严格的频率限制,比如每分钟最多请求N次。对于403,优先检查签名和请求头,而不是怀疑网络问题。

6.3 接口变动后的快速适配

Web接口随时可能变动,这是走这条路必须接受的风险。为了降低维护成本,我把所有跟接口相关的配置抽到一个单独的配置文件里:

API_URL = "https://..." APPID = "..." SECRET_KEY = "..." SIGN_TEMPLATE = "{appid}{text}{salt}{key}"

这样接口变动时,只需要改配置,不用动核心代码。如果签名逻辑变了,那就得重新分析JS,更新make_sign函数。为了快速定位问题,我在代码里加了详细的日志,记录每次请求的参数和返回,出问题时一看日志就知道是哪一步不对。

6.4 实操心得与避坑清单

最后分享几条我踩过坑之后总结的经验:

  • 不要硬编码时间戳精度:先用秒级试,不行再换毫秒级,或者直接看JS代码里用的是Date.now()还是Math.floor(Date.now()/1000)。
  • 请求头能抄就抄:分析阶段把浏览器里的请求头全部复制过来,跑通之后再逐个删减,不要一开始就自己精简。
  • 文本编码要统一:从输入框拿到的是Unicode字符串,发请求前统一编码成UTF-8,解析返回时也按UTF-8处理。
  • 缓存要设上限:字典缓存如果不清理,长时间运行会占用大量内存。可以设一个最大条目数,超出后按LRU策略淘汰。
  • 异常要兜底:任何网络请求都可能失败,任何JSON解析都可能出错,用try-except包住,给用户一个能看懂的提示,而不是一堆traceback。
  • 测试要覆盖边界:空字符串、超长文本、特殊字符、emoji、多语言混合,这些都要测一遍,确保不会崩溃。

这个项目从分析到完成大概花了我两个周末的时间,其中大部分时间花在签名分析和异常处理上。真正写代码的时间反而不多。如果你也想动手做一个类似的工具,我的建议是:先花时间把接口分析透彻,把签名逻辑用最笨的方法(打印中间结果逐字符对比)确认无误,然后再写代码。分析阶段多花一小时,编码阶段能省三小时。

后续如果想扩展,可以考虑加入多引擎切换(同时接入几个翻译源,取最优结果)、划词翻译(监听鼠标选中事件)、OCR翻译(截图识别文字再翻译)等功能。不过那是另一个话题了,先把核心链路跑通再说。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 6:16:28

PCA9422与PIC18F4685的I²C电源管理方案设计与调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 6:14:41

Mapbox GL JS 多点拖拽实战:创建、事件绑定与坐标同步全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 6:14:39

TensorFlow GPU环境搭建:Anaconda、CUDA到Keras实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 6:14:38

Win32 C++消息循环、窗口与GDI线程实战源码解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 6:13:47

Chrome XP最终版:面向工业嵌入式设备的极简浏览器重构指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 6:12:27

GPU二分查找实战:pylibcudf.search加速亿级数据区间匹配

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华