Python调用Bing翻译网页版:免费API替代方案与实现详解
1. 项目概述:为什么我们需要一个免费的Bing翻译方案?
如果你正在用Python处理多语言文本,无论是分析海外社交媒体数据、本地化你的应用界面,还是处理一份多语言的文档,翻译API都是一个绕不开的需求。市面上成熟的方案不少,比如Google Cloud Translation、DeepL,功能强大但价格不菲,对于个人开发者、学生或者小项目来说,成本是个大问题。而微软Azure的认知服务翻译器,虽然也提供了免费额度,但需要绑定信用卡、创建Azure资源,流程相对复杂,而且免费额度用完后,一不小心就可能产生费用。
于是,很多人把目光投向了Bing翻译。作为微软旗下的产品,它本身提供了一个公开的网页版翻译界面。那么,有没有可能绕过官方的付费API,直接通过Python调用这个网页版背后的服务,实现一个“免费”的翻译功能呢?这正是我们今天要深入探讨的核心。我必须强调,这种方法本质上是模拟浏览器访问Bing翻译网页,解析其返回结果,并非使用官方、稳定、有服务保障的API。它适用于个人学习、小批量、非商业、对稳定性要求不高的场景。如果你需要高并发、高可靠性的生产级翻译服务,请务必考虑Azure Translator等官方付费方案。
2. 核心思路拆解:网页翻译是如何工作的?
在动手写代码之前,我们得先搞清楚Bing翻译网页是怎么完成一次翻译的。这能帮助我们找到模拟的切入点。
2.1 从用户操作到网络请求
当你在浏览器打开https://www.bing.com/translator,输入一段文字并点击翻译时,浏览器并不会刷新整个页面。现代网页普遍采用Ajax技术,浏览器会悄悄地向服务器发送一个HTTP请求,服务器处理后将翻译结果以数据(通常是JSON格式)的形式返回,浏览器再用JavaScript把结果显示在页面上。
我们的目标,就是用Python程序扮演这个“浏览器”的角色,自动完成“构造请求 -> 发送请求 -> 解析响应”这一系列动作。关键在于,我们需要知道:
- 请求发送到哪里?(URL)
- 请求里要带什么信息?(Headers, Parameters, Data)
- 服务器返回的数据长什么样?(如何从中提取翻译结果)
2.2 关键信息捕获:使用开发者工具
要回答上面的问题,我们必须请出前端开发者的“神器”——浏览器的开发者工具(按F12打开)。我们以Chrome浏览器为例,进行一次手动翻译并观察网络活动。
- 打开Bing翻译页面,并保持开发者工具的“网络(Network)”标签页开启,记得勾选“保留日志(Preserve log)”。
- 在翻译框输入“Hello world”,选择从“英语”到“中文(简体)”,点击翻译按钮。
- 在“网络”标签页中,你会看到一系列新出现的请求。我们需要寻找那个看起来像是提交翻译数据的请求。它通常是一个
POST请求,名称可能包含translate或ttranslatev3。 - 点击这个请求,查看其“标头(Headers)”和“负载(Payload)”或“预览(Preview)”。
通过分析,我们可以发现核心的请求信息。这里以我最近一次分析为例(请注意,Bing的接口细节可能会随时间变化,但原理相通):
- 请求URL:
https://www.bing.com/ttranslatev3 - 请求方法:
POST - 请求头(Headers): 其中
User-Agent(用于标识客户端)、Content-Type(告知服务器发送的数据格式)至关重要。通常还会有一个Referer头,标明请求来源。 - 请求参数(Query String Parameters): URL中可能附带一些参数,如
isVertical=1&IG=...&IID=...。其中IG和IID看起来像是某种会话或实例标识符,可能从页面初始加载的响应中获取。 - 请求体(Request Body): 这是最关键的部分,通常是
form-data或x-www-form-urlencoded格式。里面包含了我们输入的文本、源语言和目标语言代码。例如:text=Hello worldfromLang=entoLang=zh-Hans
服务器返回的响应体,通常是一个JSON数组,里面包含了翻译结果、检测到的语言等信息。我们的任务就是从这段JSON里把翻译文本提取出来。
注意:直接模拟这个
ttranslatev3接口有时会遇到障碍,比如需要处理动态生成的IG、IID参数,或者遇到验证机制。因此,更稳健的一种方法是模拟整个页面初始化流程,先获取必要的令牌(token)和会话ID。
3. 方案选型与工具准备
基于上述分析,我们有两种主要的实现路径。
3.1 方案一:直接请求翻译接口(简单但不稳定)
这种方法就是直接向观察到的翻译接口(如ttranslatev3)发送POST请求。它的优点是直接、代码简单。但缺点非常明显:极其脆弱。Bing可以随时更改这个接口的地址、参数格式或增加验证(如需要从首页获取动态密钥),导致你的脚本突然失效。它仅适用于临时、一次性的任务。
3.2 方案二:模拟完整会话(推荐,更稳定)
这种方法更接近真实浏览器的行为:
- 首先,用程序访问
https://www.bing.com/translator首页。 - 从返回的HTML页面中,解析出当前会话所需的关键令牌(Token)和ID。这些信息往往隐藏在页面内的某个
<script>标签或全局变量中。 - 然后,使用这些获取到的令牌,构造一个格式正确的请求,发送到翻译接口。
- 最后,解析接口返回的JSON数据。
这个方案虽然步骤稍多,但因为它模拟了正常的用户访问流程,所以稳定性更高,不易因接口微调而失效。我们将以这个方案作为重点进行实现。
3.3 工具库选择:为什么是requests和BeautifulSoup?
requests: Python中最著名、最易用的HTTP库。我们将用它来发送所有的GET和POST请求,模拟网络交互。它比Python内置的urllib更加人性化。BeautifulSoup4(bs4): 一个强大的HTML/XML解析库。当我们需要从Bing翻译首页的HTML代码中提取隐藏的令牌和ID时,BeautifulSoup能帮助我们轻松地定位和获取这些信息。
安装它们非常简单,只需在命令行中执行:
pip install requests beautifulsoup44. 分步实现:构建一个稳健的免费翻译函数
现在,让我们开始动手编写代码。我会将整个过程封装成一个易于使用的函数translate_text(text, to_lang='zh-Hans', from_lang='auto-detect')。
4.1 第一步:获取会话令牌与关键参数
这是整个流程中最关键的一步。我们需要先“拜访”一下Bing翻译的主页,拿到“入场券”。
import requests from bs4 import BeautifulSoup import re import json def get_bing_translation_token(): """ 访问Bing翻译首页,并从中提取翻译所需的关键令牌(IG, IID, key, token等)。 返回一个包含这些参数的字典。 """ session = requests.Session() # 设置一个合理的浏览器User-Agent,避免被简单的反爬机制屏蔽 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } # 1. 访问翻译首页 homepage_url = "https://www.bing.com/translator" try: response = session.get(homepage_url, headers=headers, timeout=10) response.raise_for_status() # 检查请求是否成功 except requests.exceptions.RequestException as e: print(f"访问首页失败: {e}") return None soup = BeautifulSoup(response.text, 'html.parser') # 2. 关键信息通常藏在<script>标签的JavaScript变量里 # 我们需要找到包含“IG”、“IID”、“params_AbusePreventionHelper”等信息的代码段 translation_params = {} # 方法A:尝试通过正则表达式查找IG和IID(常见于URL参数) # 在页面HTML中搜索类似 'IG:"...",IID:"...' 的字符串 script_tags = soup.find_all('script') for script in script_tags: if script.string: # 查找IG (可能是一个长字符串) ig_match = re.search(r'IG:"([^"]+)"', script.string) if ig_match: translation_params['IG'] = ig_match.group(1) # 查找IID (通常像 "translator.5023") iid_match = re.search(r'IID:"([^"]+)"', script.string) if iid_match: translation_params['IID'] = iid_match.group(1) # 查找更重要的:防滥用预防密钥和令牌 # 它可能是一个JSON对象,包含“key”和“token” params_match = re.search(r'params_AbusePreventionHelper\s*=\s*(\{.*?\});', script.string, re.DOTALL) if params_match: try: abuse_prevention_params = json.loads(params_match.group(1)) translation_params['key'] = abuse_prevention_params.get('key') translation_params['token'] = abuse_prevention_params.get('token') # 有时这个令牌也叫‘token’,有时是‘authorization’ except json.JSONDecodeError: pass # 方法B:如果方法A没找到,尝试另一种常见模式——从全局变量window 中获取 if not translation_params.get('key'): for script in script_tags: if script.string and 'var ' in script.string and 'params_' in script.string: lines = script.string.split('\n') for line in lines: if 'params_' in line and '=' in line: # 简化处理,寻找key和token的赋值 key_match = re.search(r'key\s*:\s*"([^"]+)"', line) token_match = re.search(r'token\s*:\s*"([^"]+)"', line) if key_match: translation_params['key'] = key_match.group(1) if token_match: translation_params['token'] = token_match.group(1) # 3. 确保我们至少找到了key和token,这是当前(经验上)最必须的参数 if not translation_params.get('key') or not translation_params.get('token'): print("警告:未能从页面中提取到关键的 key 或 token。接口可能已更新。") # 可以尝试回退到更简单的、不需要这些参数的旧接口格式,但成功率低 # 例如,有些资料显示旧的接口是 /ttranslatev3?isVertical=1... # 将session也返回,以便后续请求保持相同的会话上下文(如Cookies) return translation_params, session实操心得:Bing翻译前端的JavaScript代码结构可能会更新,导致提取令牌的正则表达式失效。如果上述代码无法找到
key和token,你需要手动打开开发者工具,在“网络”标签页中,仔细查看首页加载后第一个或第二个请求的响应内容(通常是某个.js文件或直接是HTML),然后搜索AbusePreventionHelper或IG、IID等关键词,根据最新的代码结构调整正则表达式。这是一个“猫鼠游戏”,也是此类非官方方法最大的维护成本。
4.2 第二步:构造并发送翻译请求
拿到“入场券”后,我们就可以正式请求翻译服务了。
def translate_text(text, to_lang='zh-Hans', from_lang='auto-detect', max_retry=2): """ 使用Bing翻译网页版接口翻译文本。 参数: text: 要翻译的文本(字符串)。 to_lang: 目标语言代码,例如 'zh-Hans'(简体中文), 'en'(英语), 'ja'(日语), 'fr'(法语)。 from_lang: 源语言代码,默认 'auto-detect' 为自动检测。 max_retry: 获取令牌失败时的重试次数。 返回: 翻译后的文本字符串,如果失败则返回None。 """ # 0. 输入校验 if not text or not isinstance(text, str): print("错误:输入文本无效。") return None # 1. 获取翻译令牌和会话 translation_params, session = None, None for i in range(max_retry): translation_params, session = get_bing_translation_token() if translation_params and session: break print(f"第{i+1}次获取令牌失败,重试...") if not translation_params or not session: print("错误:无法获取翻译所需的会话参数。") return None # 2. 准备请求URL和头部 # 注意:接口地址可能变化,目前是 /ttranslatev3 translate_url = "https://www.bing.com/ttranslatev3" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://www.bing.com/translator', 'Accept': 'application/json', } # 3. 准备请求体数据 # 根据抓包分析,必需的字段包括 text, fromLang, toLang # 此外,还需要包含从首页获取的 key 和 token form_data = { 'text': text, 'fromLang': from_lang, 'to': to_lang, } # 添加防滥用预防参数(如果获取到了) if translation_params.get('key'): form_data['key'] = translation_params['key'] if translation_params.get('token'): # 注意:有时参数名是 ‘token’,有时需要放在头部 ‘Authorization’。 # 根据当前分析,放在form-data里是可行的。 form_data['token'] = translation_params['token'] # 4. 可选:添加IG和IID作为查询参数(某些接口版本需要) # query_params = {} # if translation_params.get('IG'): # query_params['IG'] = translation_params['IG'] # if translation_params.get('IID'): # query_params['IID'] = translation_params['IID'] # 如果接口需要,可以将其拼接到URL后面,如 translate_url + '?' + urlencode(query_params) # 5. 发送POST请求 try: response = session.post( translate_url, headers=headers, data=form_data, # params=query_params, # 如果需要查询参数则启用 timeout=15 ) response.raise_for_status() # 6. 解析响应 result_json = response.json() # 响应结构通常是一个列表,第一个元素是一个字典,其中包含翻译结果 # 例如: [{"detectedLanguage": {"language": "en", "score": 1.0}, "translations": [{"text": "你好世界", "to": "zh-Hans"}]}] if isinstance(result_json, list) and len(result_json) > 0: first_item = result_json[0] if 'translations' in first_item and isinstance(first_item['translations'], list): translated_text = first_item['translations'][0].get('text') if translated_text: return translated_text # 如果上述解析失败,打印响应以便调试 print(f"解析响应失败,原始响应: {result_json}") return None except requests.exceptions.JSONDecodeError: print("错误:服务器返回的不是有效的JSON。响应文本:", response.text[:200]) return None except requests.exceptions.RequestException as e: print(f"翻译请求失败: {e}") return None4.3 第三步:测试与使用
现在,我们可以像调用普通函数一样使用这个翻译工具了。
if __name__ == "__main__": # 示例1:英译中 result = translate_text("Hello, world! This is a test of Bing Translator.", to_lang='zh-Hans') if result: print(f"翻译结果:{result}") # 示例2:中译英 result2 = translate_text("这是一个Python调用Bing翻译的示例程序。", to_lang='en') if result2: print(f"Translation: {result2}") # 示例3:自动检测语言并翻译成日语 result3 = translate_text("Bonjour le monde", to_lang='ja') if result3: print(f"翻訳結果:{result3}")5. 高级技巧与优化策略
基础的调用已经实现,但在实际使用中,你可能会遇到各种问题。下面分享一些提升脚本健壮性和可用性的经验。
5.1 处理长文本与分句
Bing翻译网页接口对单次请求的文本长度有限制(通常在5000字符左右)。直接提交很长的文本可能会失败。
解决方案:实现自动分句翻译
import re def translate_long_text(long_text, to_lang, from_lang='auto-detect', max_chunk_len=4000): """ 翻译长文本,通过按句子或长度分块处理。 这是一个简单实现,按标点符号和最大长度分块。 """ # 简单的分句逻辑:按句号、问号、感叹号、换行分割,并保留分隔符 sentence_endings = r'(?<=[。!?.!?])\s+' sentences = re.split(sentence_endings, long_text) chunks = [] current_chunk = "" for sentence in sentences: # 如果当前块加上新句子长度超限,且当前块不为空,则保存当前块并开始新块 if len(current_chunk) + len(sentence) > max_chunk_len and current_chunk: chunks.append(current_chunk) current_chunk = sentence else: current_chunk += sentence if current_chunk: chunks.append(current_chunk) # 翻译每个块 translated_chunks = [] for chunk in chunks: if chunk.strip(): # 忽略空块 translated = translate_text(chunk, to_lang, from_lang) if translated: translated_chunks.append(translated) else: # 如果某块翻译失败,可以记录日志或加入原文 translated_chunks.append(f"[翻译失败: {chunk[:50]}...]") # 建议在块之间加入短暂延时,避免请求过快 # time.sleep(0.5) return ' '.join(translated_chunks) # 或用换行符连接,根据原文格式决定5.2 设置请求间隔与错误重试
频繁、快速地发送请求会触发Bing服务器的反爬机制,导致返回429 Too Many Requests或403 Forbidden错误。
优化策略:
- 添加延时:在连续翻译请求之间,使用
time.sleep(random.uniform(1, 3))添加随机延时,模拟人类操作。 - 实现重试机制:对于网络错误或特定的服务器错误(如429、503),可以实现一个带指数退避的重试逻辑。
- 使用代理IP池:如果翻译需求量非常大,考虑使用多个代理IP轮询发送请求,但这会大大增加复杂度和成本。
import time import random def robust_translate(text, to_lang, from_lang='auto-detect', retries=3): """一个带有简单重试和延时的翻译包装函数""" for attempt in range(retries): try: result = translate_text(text, to_lang, from_lang) if result: return result else: # 如果返回None,可能是令牌获取失败,也触发重试 raise ValueError("Translation returned None") except (requests.exceptions.RequestException, ValueError) as e: if attempt < retries - 1: wait_time = (2 ** attempt) + random.random() # 指数退避加随机抖动 print(f"第{attempt+1}次尝试失败 ({e}),等待{wait_time:.2f}秒后重试...") time.sleep(wait_time) else: print(f"所有{retries}次尝试均失败。") raise return None5.3 语言代码对照表
你需要知道目标语言对应的代码。Bing翻译使用的语言代码通常是符合BCP 47标准的语言标签。
| 语言 | 代码 | 备注 |
|---|---|---|
| 简体中文 | zh-Hans | 最常用 |
| 繁体中文 | zh-Hant | |
| 英语 | en | |
| 日语 | ja | |
| 韩语 | ko | |
| 法语 | fr | |
| 西班牙语 | es | |
| 德语 | de | |
| 俄语 | ru | |
| 自动检测 | auto-detect | 源语言参数 |
你可以在Bing翻译网页上手动切换语言,然后观察网络请求中fromLang和to参数的变化,来确认最新的语言代码。
6. 常见问题与排查指南
在实际使用中,你几乎一定会遇到下面这些问题。这里提供一个快速排查的思路。
6.1 错误:KeyError或无法提取key/token
- 症状:
get_bing_translation_token函数返回的字典中key或token为空。 - 原因:Bing翻译前端代码已更新,正则表达式无法匹配到新的变量名或数据结构。
- 排查:
- 手动打开
https://www.bing.com/translator。 - 按F12打开开发者工具,切换到“网络(Network)”面板,刷新页面。
- 在筛选器里选择“文档(Doc)”或“全部(All)”,找到第一个请求(通常是
translator)。 - 在“响应(Response)”标签页里,搜索
AbusePreventionHelper或params_。 - 找到包含
key和token的JavaScript对象。观察它的结构和新变量名。 - 根据新结构,修改
get_bing_translation_token函数中的正则表达式或解析逻辑。
- 手动打开
6.2 错误:429 Too Many Requests
- 症状:请求翻译接口时,返回状态码429。
- 原因:请求频率过高,触发了服务器的速率限制。
- 解决:
- 立即降低请求频率:在每次翻译请求后增加
time.sleep(),例如time.sleep(random.uniform(2, 5))。 - 检查代码逻辑:是否在循环中无延迟地疯狂调用函数?
- 使用会话(Session):确保使用
requests.Session(),它有助于保持连接和Cookies,有时比每次新建连接行为更好。 - 如果问题持续,可能需要更换IP地址或等待一段时间再试。
- 立即降低请求频率:在每次翻译请求后增加
6.3 错误:403 Forbidden
- 症状:请求被拒绝,返回状态码403。
- 原因:
- 请求头不完整或异常:缺少必要的
Referer、User-Agent或Accept头。 - 令牌无效或过期:从首页获取的
key/token已经失效。每个令牌可能有很短的有效期或使用次数限制。 - IP或行为被标记:你的IP地址或请求模式被识别为爬虫。
- 请求头不完整或异常:缺少必要的
- 解决:
- 检查请求头:确保
headers字典包含了从浏览器抓包中看到的所有关键头信息,特别是Referer必须为https://www.bing.com/translator。 - 重新获取令牌:在每次翻译请求前都调用一次
get_bing_translation_token获取最新的令牌。虽然效率低,但更稳定。 - 模拟更真实的行为:在获取令牌和发送翻译请求之间增加随机延时,并使用更常见的
User-Agent。
- 检查请求头:确保
6.4 错误:返回结果解析失败
- 症状:
translate_text函数能收到响应,但无法从JSON中提取出翻译文本,返回None。 - 原因:翻译接口返回的JSON结构发生了变化。
- 排查:
- 在
translate_text函数的try块内,打印出result_json的完整内容。 - 分析这个JSON的结构,找到存放翻译文本的新路径。
- 根据新结构修改解析代码。例如,可能变成了
result_json[0][‘translations’][0][‘text’]或者有其他嵌套方式。
- 在
6.5 性能与可靠性权衡
- 痛点:稳定性和便利性不可兼得。越稳定的方案(如每次翻译前获取新令牌)速度越慢。
- 建议:
- 对于一次性或低频任务:采用“每次获取令牌”的模式,确保成功率。
- 对于需要翻译大量短句的任务:可以尝试“一次获取令牌,翻译多个句子”的模式,但在连续翻译10-20句后,主动重新获取一次令牌,以防过期。同时,务必在每句之间添加显著延时(如1-3秒)。
- 终极建议:如果翻译需求成为你项目的核心且频繁的功能,请认真考虑申请微软Azure认知服务翻译器的免费层。它每月有200万字符的免费额度,拥有稳定的API、官方SDK和支持,远比维护一个脆弱的网页爬虫脚本要省心、专业得多。
这个基于Python和requests库调用Bing翻译的方案,是一个很好的学习案例,它涉及了网络爬虫、HTTP协议、数据解析和错误处理等多个知识点。它能解决临时性、小规模的翻译需求,但请时刻记住它的“非官方”属性,做好接口变动的心理准备,并将关键功能与这个翻译模块解耦,以便未来替换为更稳定的服务。
