返回列表 发帖

[原创代码] Python实现的命令行全文翻译工具

本帖最后由 CrLf 于 2017-5-11 02:23 编辑

因为一直没找到合适的命令行工具,所以自己实现了一个,很适合用来翻译外国命令行工具的自带帮助
(是的,就是用在 Batch-CN 第三方库的搜集整理上)

一年多前写的,用的是百度翻译api,实现了双语对照、全文翻译、参数控制等功能
但因为这个api有限制每月翻译字符数,共用难免会超,所以一直都未公开

现在把源码分享出来,基于 python3,另外需要大家自行注册api,修改【】符号所在位置的 appid 和 secretKey 即可使用
如需使用剪贴板相关功能,还需要 pyperclip 模块支持,请运行 pip3 install pyperclip 自行安装,不安装该模块也不影响其他功能的使用

百度翻译api接入申请地址:http://api.fanyi.baidu.com/api/trans/product/index
#/usr/bin/env python
# -*- encoding:utf-8 -*-
#by CrLf
import getopt, sys
import json
import re
import http.client
import hashlib
import urllib
import urllib.request
import urllib.parse
import random
import os
from io import StringIO
try:
  import pyperclip
except ImportError:
  pyperclip = {}
zhPattern = re.compile(u'[\u4e00-\u9fa5]+')
appid     = '20151113000005349'      #【测试用的 appid】
secretKey = 'osubCEzlGjzvw8qdQc41'   #【测试用的 secretKey】
jsonPath = jsonPath_base = (os.path.realpath(sys.argv[0]))+'.json'
#配置文件路径默认为此脚本/程序所在目录
#有 .json 时,默认使用该文件中的配置
if not ( appid and secretKey ) :
  print("你需要在脚本中设置 appid 和 secretKey", file=sys.stderr)
  print("API接口申请: http://api.fanyi.baidu.com/api/trans/product/index", file=sys.stderr)
  sys.exit(2)
option = {}
globalVar = {'num':0}
httpClient = None
stdinfo = sys.stderr
def main():
  if os.path.isfile(jsonPath) :
    getConfig(jsonPath)
  try:
    opts,args = getopt.getopt(sys.argv[1:], "hnf:t:DTc", ["help", "from=", "to=", "dict", "text", "num", "debug","web=","clip"])
   
  except Exception as e:
    print("参数错误", file=sys.stderr)
    sys.exit(2)
  for opt,arg in opts:
    if opt in ("-h", "--help"):
      usage()
      sys.exit(0)
    elif opt in ("-c","--clip"):
      if not pyperclip :
        print('-c 选项需 pyperclip 模块支持', file=sys.stderr)
        sys.exit(2)
      
      option['clip'] = True
      args = [pyperclip.paste()]
    elif opt in ("-n","--num"):
      option['num'] = True
    elif opt in ("-f", "--form"):
      option['from'] = arg
      if 'to' in option and arg.lower() != 'zh' : option['to']='zh'
    elif opt in ("-t", "--to"):
      option['to'] = arg
      if 'from' in option and arg.lower() != 'zh' : option['from']='zh'
    elif opt in ("-D","--dict"):
      option['dict'] = True
    elif opt in ("-T","--text"):
      option['text'] = True
    elif opt in ("--debug"):
      option['debug'] = True
    elif opt in ("--web"):
      if arg == 'baidu':
        os.system("start http://fanyi.baidu.com/")
      elif arg == 'google':
        os.system("start https://translate.google.cn/")
      elif arg == 'youdao':
        os.system("start http://fanyi.youdao.com/")
      elif arg == 'iciba':
        os.system("start http://fy.iciba.com/")
      elif arg == 'haici':
        os.system("start http://fanyi.dict.cn/")
      else:
        print("--web  baidu | google | youdao | iciba | haici")
        sys.exit(0)
    else:
      print("%s  ==> %s" %(opt, arg))
  if 'debug' in option:
    print("Opts:\r\n%s" % opts, file=stdinfo)
    print("Args:\r\n%s" % args, file=stdinfo)
    print("Option:\r\n%s" % option, file=stdinfo)
  fromLang = option['from'] if 'from' in option else 'auto'
  toLang = option['to'] if 'to' in option else 'zh'
  
  
  try:
    if len(args)==0 :
      if sys.stdin.isatty():
        usage()
      else:
        q = re.sub(r'\r?\n','\r\n',sys.stdin.read())
        choiceMode(q, fromLang, toLang)
      sys.exit(0)
    if len(args)==1 :
      choiceMode(args[0], fromLang, toLang)
      sys.exit(0)
      
    if 'dict' in option:
      for arg in args:
         
        if fromLang == 'auto' and zhPattern.search(arg) :
          toLang = 'en'
         
        print('[Dict Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
        
        words = re.split(r'\s+',arg)
        for word in words:
          word = word.strip()
          trans_dict(word, fromLang, toLang)
          print('\r\n');
      sys.exit(0)
    else:
      q = '\r\n'.join(args)
      
      if fromLang == 'auto' and zhPattern.search(q) :
        toLang = 'en'
        
      print('[Text Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
      trans_text(q, fromLang, toLang)
  except Exception as e:
    print(e, file=sys.stderr)
    sys.exit(1)
def trans_dict(word, fromLang, toLang):
  resp=urllib.request.urlopen(compose_request(word))
  if(resp.status==200):
    handle_result(resp.read().decode('utf-8'))
  else:
    print('Rquest Error: \r\n HTTP Status: %d \r\n Reason: %s \r\n', resp.status, resp.reason)
def trans_text(q, fromLang, toLang):
  salt = random.randint(32768, 65536)
  
  sign = appid+q+str(salt)+secretKey
  m1 = hashlib.md5()
  m1.update(bytes(sign, encoding = "utf8") )
  sign = m1.hexdigest()
  
  myurl = '/api/trans/vip/translate?appid='+appid+'&q='+urllib.parse.quote(q)+'&from='+fromLang+'&to='+toLang+'&salt='+str(salt)+'&sign='+sign
  
  try:
    httpClient = http.client.HTTPConnection('api.fanyi.baidu.com')
    httpClient.request('GET', myurl)
   
    #response是HTTPResponse对象
    response = httpClient.getresponse()
    ret = response.read()
    retstr = str(ret,'utf-8')
    retobj= json.loads(retstr)
   
    if 'num' in option:
      str_template='[%05d] -----------------------------------------\r\n%s'
    else:
      str_template='%0.s-------------------------------------------------\r\n%s'
    if 'error_code' in retobj and retobj['error_code'] != '52000' :
      print("  错误码: " + retobj['error_code'], file=sys.stderr)
      print("  说明:   " + retobj['error_msg'],  file=sys.stderr)
      sys.exit(retobj['error_code'])
    else :
      for trans_result in retobj['trans_result']:
        globalVar['num']=globalVar['num']+1
        print(str_template % (globalVar['num'], trans_result['src']), file=stdinfo)
        print(trans_result['dst'])
  except Exception as e:
    print(e, file=sys.stderr)
  finally:
    if httpClient:
      httpClient.close()
def getConfig(configPath):
  config_default = {}
  config = config_default
  
  if os.path.isfile(configPath) :
    try :
      fp = open(configPath,"r")
      config = json.load(fp)
      fp.close()
    except Exception as e:
      config = config_default
    global appid, secretKey
   
  if 'appid' in config and 'secretKey' in config and config['appid'] and config['secretKey'] :
    appid     = config['appid']
    secretKey = config['secretKey']
   
def usage():
  argshelp = '''
  -f lang  --from lang
    指定翻译前的语言为 lang,可选项详见百度翻译api文档
   
  -t lang  --to lang
    指定翻译后的语言为 lang,可选项详见百度翻译api文档
   
  -T  --text
    以文本形式翻译
   
  -D  --dict
    以字典形式翻译
   
  -n  --num
    以数字形式翻译
   
  -c  --clip
    从剪贴板获取要翻译的内容
   
  --web name
    用浏览器打开翻译页面(不支持长文本),name 为所用的翻译引擎
    可选项为 baidu/google/youdao/iciba/haici
  
  --debug
    调试模式
   
  -h  --help
    显示简单的帮助信息'''
      
  print("Usage:%s [-f lang] [-t lang] [--web name] [-c] [-n|-T|-D] args...." % os.path.basename(sys.argv[0]))
  print(argshelp)
  
  if os.path.isfile(jsonPath) :
    print("\r\n  配置文件: \"%s\"" % jsonPath)
    print("  该文件用于设置要使用的 appid 和 secretKey")
    print("  接口申请地址:\"http://api.fanyi.baidu.com/api/trans/product/index\"")
  else :
    print("\r\n  当前无配置文件,默认使用测试 ID")
    print("  接口申请地址:\"http://api.fanyi.baidu.com/api/trans/product/index\"")
    print("\r\n  如需自定义 appid 和 secretKey,请以 JSON 格式保存到配置文件:")
    print("    \"%s\"" % jsonPath)
    print("\r\n  配置文件的格式为:")
    print("    {'appid': '你的 APPID', 'secretKey': '你的秘钥'}")
class Result:
  def __init__(self, src, dest, meanings=None):
    self.src=src
    self.dest=dest
    self.meanings=meanings
  def parse_from_json(json_data):
    trans_data=json_data['trans_result']['data'][0]
    try:
      means=None
      if 'simple_means' in json_data['dict_result']:
        dict_data=json_data['dict_result']['simple_means']['symbols'][0]['parts']
        means=list()
        for item in dict_data:
          tmp=item['means']
          if isinstance(tmp[0],dict):
            for t_item in tmp:
              means.append(t_item['word_mean'])
          else:
            means.append(tmp)
    except KeyError:
      means=None
    return Result(trans_data['src'],trans_data['dst'],means)
  def show(self,file=sys.stdout):
    if 'num' in option:
      str_template='<<<translate\r\n %05d:\t  %s ---> %s\r\n\r\n<<<meaning'
    else:
      str_template='<<<translate\r\n %0.s%s ---> %s\r\n\r\n<<<meaning'
    globalVar['num']=globalVar['num']+1
     
    print(str_template % (globalVar['num'], self.src, self.dest), file=stdinfo)
    [print("  %s" % meaning) for meaning in self.meanings]
   
def handle_result(content):
  json_data=json.loads(content)
  Result.parse_from_json(json_data).show()
def compose_request(word):
  TRANS_URL='http://fanyi.baidu.com/v2transapi'
  ORIGIN_HOST='fanyi.baidu.com'
  r"""
  compose urllib.request.Request object accordingly
   
  """
  body=StringIO()
  body.write('from=zh&to=en' if zhPattern.search(word) else 'from=en&to=zh')
  body.write('&')
  body.write(urllib.parse.urlencode({'query': word }, encoding='utf-8'))
  body.write('&transtype=trans&simple_means_flag=3')
  body=body.getvalue()
  headers={'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With':'XMLHttpRequest'
    }
  return urllib.request.Request(TRANS_URL, body.encode(encoding='utf-8'), headers=headers, origin_req_host=ORIGIN_HOST, method='POST')
def choiceMode(q, fromLang, toLang):
  if fromLang == 'auto' and zhPattern.search(q) :
    toLang = 'en'
   
  word = re.sub(r'\r|\r\n','',q).strip()
  if re.search(r'\s',word):
      
    print('[Text Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
    trans_text(q, fromLang, toLang)
  else:
    print('[Dict Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
    trans_dict(word, fromLang, toLang)
main()COPY
3

评分人数

为啥我想转.srt 字幕文本就不成功呢,因为里面有多行?空白行?

TOP

其他的用法我乱摸索中

TOP

回复 6# happy886rr


    我要翻译成 法语怎么写?
python "C:\Program Files\Python 3.5\Python实现的命令行全文翻译工具.py" "%~f1" "叼你" "%~f2" "-t fra" COPY
python "C:\Program Files\Python 3.5\Python实现的命令行全文翻译工具.py" "%~f1" "叼你" -t fraCOPY
还有 你命令行里突然用
fanyi ................COPY
这个
 fanyi COPY
是怎么实现的?

TOP

本帖最后由 CrLf 于 2017-5-6 12:13 编辑

回复 13# codegay


用 dlls 查看外部依赖,发现打包后的 fanyi 依赖 msvcr100.dll,这个是 PyInstaller 的问题
内置的 python35.dll 依赖 VCRUNTIME140.dll 和 api-ms-win-crt-conio-l1-1-0.dll 这类 crt 库文件,但是生成 exe 的时候都一并打包了

dlls 工具用于 Batch-CN 依赖关系检测与自动打包:http://www.bathome.net/viewthread.php?tid=38592
1

评分人数

TOP

回复 12# CrLf


    安装python之前好像是需要VC++组件的,因为官方的cpython windows 版是用的vc++ 编译的。
那么打包后也是需要VC++组件吧?

搜索了一下py2exe是有这种情况。
去学去写去用才有进步。安装python3代码存为xx.py 双击运行或右键用IDLE打开按F5运行

TOP

回复 10# happy886rr


    用 upx 压缩 python35.dll 之后,打包的 fanyi.exe 体积为 1.27MB,够了

TOP

回复 10# happy886rr


    有什么修改可以发出来哈,我本来想做外部配置文件的,但是想想又删了
    毕竟 appid 比较私密,如果不能编译到exe里,将配置文件外置的意义也不太大
    还是发一个作备份吧:

    多了一个 --config 参数

TOP

回复 8# CrLf
不错,最后这个版本几乎完美,就是py体积大,启动慢。百度提供的测试KEY也能用。

TOP

回复 8# CrLf


    不错哦。
去学去写去用才有进步。安装python3代码存为xx.py 双击运行或右键用IDLE打开按F5运行

TOP

回复 7# codegay


    3.78MB

TOP

PyInstaller py2exe之类打包出来,体积默认是20多M的吧?
去学去写去用才有进步。安装python3代码存为xx.py 双击运行或右键用IDLE打开按F5运行

TOP

翻译的合情合理。
python "%~f1" "In line with the conviction that I will do whatever it takes to serve my country even at the cost of my own life,regardless of fortune or misfortune to myself."
[Text Mode] auto -> zh
-------------------------------------------------
In line with the conviction that I will do whatever it takes to serve my country
even at the cost of my own life,regardless of fortune or misfortune to myself.
我坚信我将不惜一切代价为国家服务,不惜牺牲自己的生命,无论命运或厄运。COPY

TOP

回复 4# 523066680


    修改了一下,改个bug,完善帮助信息,再加上从剪贴板获取来源的选项

TOP

C:\>fanyi.py 叼你
[Dict Mode] en -> zh

<<<translate
叼你 ---> f**k you

<<<meaning
'NoneType' object is not iterable


不错不错
[url=][/url]

TOP

返回列表