Board logo

标题: [原创代码] Python实现的命令行全文翻译工具 [打印本页]

作者: CrLf    时间: 2017-5-5 13:02     标题: 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
  1. #/usr/bin/env python
  2. # -*- encoding:utf-8 -*-
  3. #by CrLf
  4. import getopt, sys
  5. import json
  6. import re
  7. import http.client
  8. import hashlib
  9. import urllib
  10. import urllib.request
  11. import urllib.parse
  12. import random
  13. import os
  14. from io import StringIO
  15. try:
  16.   import pyperclip
  17. except ImportError:
  18.   pyperclip = {}
  19. zhPattern = re.compile(u'[\u4e00-\u9fa5]+')
  20. appid     = '20151113000005349'      #【测试用的 appid】
  21. secretKey = 'osubCEzlGjzvw8qdQc41'   #【测试用的 secretKey】
  22. jsonPath = jsonPath_base = (os.path.realpath(sys.argv[0]))+'.json'
  23. #配置文件路径默认为此脚本/程序所在目录
  24. #有 .json 时,默认使用该文件中的配置
  25. if not ( appid and secretKey ) :
  26.   print("你需要在脚本中设置 appid 和 secretKey", file=sys.stderr)
  27.   print("API接口申请: http://api.fanyi.baidu.com/api/trans/product/index", file=sys.stderr)
  28.   sys.exit(2)
  29. option = {}
  30. globalVar = {'num':0}
  31. httpClient = None
  32. stdinfo = sys.stderr
  33. def main():
  34.   if os.path.isfile(jsonPath) :
  35.     getConfig(jsonPath)
  36.   try:
  37.     opts,args = getopt.getopt(sys.argv[1:], "hnf:t:DTc", ["help", "from=", "to=", "dict", "text", "num", "debug","web=","clip"])
  38.    
  39.   except Exception as e:
  40.     print("参数错误", file=sys.stderr)
  41.     sys.exit(2)
  42.   for opt,arg in opts:
  43.     if opt in ("-h", "--help"):
  44.       usage()
  45.       sys.exit(0)
  46.     elif opt in ("-c","--clip"):
  47.       if not pyperclip :
  48.         print('-c 选项需 pyperclip 模块支持', file=sys.stderr)
  49.         sys.exit(2)
  50.       
  51.       option['clip'] = True
  52.       args = [pyperclip.paste()]
  53.     elif opt in ("-n","--num"):
  54.       option['num'] = True
  55.     elif opt in ("-f", "--form"):
  56.       option['from'] = arg
  57.       if 'to' in option and arg.lower() != 'zh' : option['to']='zh'
  58.     elif opt in ("-t", "--to"):
  59.       option['to'] = arg
  60.       if 'from' in option and arg.lower() != 'zh' : option['from']='zh'
  61.     elif opt in ("-D","--dict"):
  62.       option['dict'] = True
  63.     elif opt in ("-T","--text"):
  64.       option['text'] = True
  65.     elif opt in ("--debug"):
  66.       option['debug'] = True
  67.     elif opt in ("--web"):
  68.       if arg == 'baidu':
  69.         os.system("start http://fanyi.baidu.com/")
  70.       elif arg == 'google':
  71.         os.system("start https://translate.google.cn/")
  72.       elif arg == 'youdao':
  73.         os.system("start http://fanyi.youdao.com/")
  74.       elif arg == 'iciba':
  75.         os.system("start http://fy.iciba.com/")
  76.       elif arg == 'haici':
  77.         os.system("start http://fanyi.dict.cn/")
  78.       else:
  79.         print("--web  baidu | google | youdao | iciba | haici")
  80.         sys.exit(0)
  81.     else:
  82.       print("%s  ==> %s" %(opt, arg))
  83.   if 'debug' in option:
  84.     print("Opts:\r\n%s" % opts, file=stdinfo)
  85.     print("Args:\r\n%s" % args, file=stdinfo)
  86.     print("Option:\r\n%s" % option, file=stdinfo)
  87.   fromLang = option['from'] if 'from' in option else 'auto'
  88.   toLang = option['to'] if 'to' in option else 'zh'
  89.   
  90.   
  91.   try:
  92.     if len(args)==0 :
  93.       if sys.stdin.isatty():
  94.         usage()
  95.       else:
  96.         q = re.sub(r'\r?\n','\r\n',sys.stdin.read())
  97.         choiceMode(q, fromLang, toLang)
  98.       sys.exit(0)
  99.     if len(args)==1 :
  100.       choiceMode(args[0], fromLang, toLang)
  101.       sys.exit(0)
  102.       
  103.     if 'dict' in option:
  104.       for arg in args:
  105.          
  106.         if fromLang == 'auto' and zhPattern.search(arg) :
  107.           toLang = 'en'
  108.          
  109.         print('[Dict Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
  110.         
  111.         words = re.split(r'\s+',arg)
  112.         for word in words:
  113.           word = word.strip()
  114.           trans_dict(word, fromLang, toLang)
  115.           print('\r\n');
  116.       sys.exit(0)
  117.     else:
  118.       q = '\r\n'.join(args)
  119.       
  120.       if fromLang == 'auto' and zhPattern.search(q) :
  121.         toLang = 'en'
  122.         
  123.       print('[Text Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
  124.       trans_text(q, fromLang, toLang)
  125.   except Exception as e:
  126.     print(e, file=sys.stderr)
  127.     sys.exit(1)
  128. def trans_dict(word, fromLang, toLang):
  129.   resp=urllib.request.urlopen(compose_request(word))
  130.   if(resp.status==200):
  131.     handle_result(resp.read().decode('utf-8'))
  132.   else:
  133.     print('Rquest Error: \r\n HTTP Status: %d \r\n Reason: %s \r\n', resp.status, resp.reason)
  134. def trans_text(q, fromLang, toLang):
  135.   salt = random.randint(32768, 65536)
  136.   
  137.   sign = appid+q+str(salt)+secretKey
  138.   m1 = hashlib.md5()
  139.   m1.update(bytes(sign, encoding = "utf8") )
  140.   sign = m1.hexdigest()
  141.   
  142.   myurl = '/api/trans/vip/translate?appid='+appid+'&q='+urllib.parse.quote(q)+'&from='+fromLang+'&to='+toLang+'&salt='+str(salt)+'&sign='+sign
  143.   
  144.   try:
  145.     httpClient = http.client.HTTPConnection('api.fanyi.baidu.com')
  146.     httpClient.request('GET', myurl)
  147.    
  148.     #response是HTTPResponse对象
  149.     response = httpClient.getresponse()
  150.     ret = response.read()
  151.     retstr = str(ret,'utf-8')
  152.     retobj= json.loads(retstr)
  153.    
  154.     if 'num' in option:
  155.       str_template='[%05d] -----------------------------------------\r\n%s'
  156.     else:
  157.       str_template='%0.s-------------------------------------------------\r\n%s'
  158.     if 'error_code' in retobj and retobj['error_code'] != '52000' :
  159.       print("  错误码: " + retobj['error_code'], file=sys.stderr)
  160.       print("  说明:   " + retobj['error_msg'],  file=sys.stderr)
  161.       sys.exit(retobj['error_code'])
  162.     else :
  163.       for trans_result in retobj['trans_result']:
  164.         globalVar['num']=globalVar['num']+1
  165.         print(str_template % (globalVar['num'], trans_result['src']), file=stdinfo)
  166.         print(trans_result['dst'])
  167.   except Exception as e:
  168.     print(e, file=sys.stderr)
  169.   finally:
  170.     if httpClient:
  171.       httpClient.close()
  172. def getConfig(configPath):
  173.   config_default = {}
  174.   config = config_default
  175.   
  176.   if os.path.isfile(configPath) :
  177.     try :
  178.       fp = open(configPath,"r")
  179.       config = json.load(fp)
  180.       fp.close()
  181.     except Exception as e:
  182.       config = config_default
  183.     global appid, secretKey
  184.    
  185.   if 'appid' in config and 'secretKey' in config and config['appid'] and config['secretKey'] :
  186.     appid     = config['appid']
  187.     secretKey = config['secretKey']
  188.    
  189. def usage():
  190.   argshelp = '''
  191.   -f lang  --from lang
  192.     指定翻译前的语言为 lang,可选项详见百度翻译api文档
  193.    
  194.   -t lang  --to lang
  195.     指定翻译后的语言为 lang,可选项详见百度翻译api文档
  196.    
  197.   -T  --text
  198.     以文本形式翻译
  199.    
  200.   -D  --dict
  201.     以字典形式翻译
  202.    
  203.   -n  --num
  204.     以数字形式翻译
  205.    
  206.   -c  --clip
  207.     从剪贴板获取要翻译的内容
  208.    
  209.   --web name
  210.     用浏览器打开翻译页面(不支持长文本),name 为所用的翻译引擎
  211.     可选项为 baidu/google/youdao/iciba/haici
  212.   
  213.   --debug
  214.     调试模式
  215.    
  216.   -h  --help
  217.     显示简单的帮助信息'''
  218.       
  219.   print("Usage:%s [-f lang] [-t lang] [--web name] [-c] [-n|-T|-D] args...." % os.path.basename(sys.argv[0]))
  220.   print(argshelp)
  221.   
  222.   if os.path.isfile(jsonPath) :
  223.     print("\r\n  配置文件: \"%s\"" % jsonPath)
  224.     print("  该文件用于设置要使用的 appid 和 secretKey")
  225.     print("  接口申请地址:\"http://api.fanyi.baidu.com/api/trans/product/index\"")
  226.   else :
  227.     print("\r\n  当前无配置文件,默认使用测试 ID")
  228.     print("  接口申请地址:\"http://api.fanyi.baidu.com/api/trans/product/index\"")
  229.     print("\r\n  如需自定义 appid 和 secretKey,请以 JSON 格式保存到配置文件:")
  230.     print("    \"%s\"" % jsonPath)
  231.     print("\r\n  配置文件的格式为:")
  232.     print("    {'appid': '你的 APPID', 'secretKey': '你的秘钥'}")
  233. class Result:
  234.   def __init__(self, src, dest, meanings=None):
  235.     self.src=src
  236.     self.dest=dest
  237.     self.meanings=meanings
  238.   def parse_from_json(json_data):
  239.     trans_data=json_data['trans_result']['data'][0]
  240.     try:
  241.       means=None
  242.       if 'simple_means' in json_data['dict_result']:
  243.         dict_data=json_data['dict_result']['simple_means']['symbols'][0]['parts']
  244.         means=list()
  245.         for item in dict_data:
  246.           tmp=item['means']
  247.           if isinstance(tmp[0],dict):
  248.             for t_item in tmp:
  249.               means.append(t_item['word_mean'])
  250.           else:
  251.             means.append(tmp)
  252.     except KeyError:
  253.       means=None
  254.     return Result(trans_data['src'],trans_data['dst'],means)
  255.   def show(self,file=sys.stdout):
  256.     if 'num' in option:
  257.       str_template='<<<translate\r\n %05d:\t  %s ---> %s\r\n\r\n<<<meaning'
  258.     else:
  259.       str_template='<<<translate\r\n %0.s%s ---> %s\r\n\r\n<<<meaning'
  260.     globalVar['num']=globalVar['num']+1
  261.      
  262.     print(str_template % (globalVar['num'], self.src, self.dest), file=stdinfo)
  263.     [print("  %s" % meaning) for meaning in self.meanings]
  264.    
  265. def handle_result(content):
  266.   json_data=json.loads(content)
  267.   Result.parse_from_json(json_data).show()
  268. def compose_request(word):
  269.   TRANS_URL='http://fanyi.baidu.com/v2transapi'
  270.   ORIGIN_HOST='fanyi.baidu.com'
  271.   r"""
  272.   compose urllib.request.Request object accordingly
  273.    
  274.   """
  275.   body=StringIO()
  276.   body.write('from=zh&to=en' if zhPattern.search(word) else 'from=en&to=zh')
  277.   body.write('&')
  278.   body.write(urllib.parse.urlencode({'query': word }, encoding='utf-8'))
  279.   body.write('&transtype=trans&simple_means_flag=3')
  280.   body=body.getvalue()
  281.   headers={'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',
  282.     'X-Requested-With':'XMLHttpRequest'
  283.     }
  284.   return urllib.request.Request(TRANS_URL, body.encode(encoding='utf-8'), headers=headers, origin_req_host=ORIGIN_HOST, method='POST')
  285. def choiceMode(q, fromLang, toLang):
  286.   if fromLang == 'auto' and zhPattern.search(q) :
  287.     toLang = 'en'
  288.    
  289.   word = re.sub(r'\r|\r\n','',q).strip()
  290.   if re.search(r'\s',word):
  291.       
  292.     print('[Text Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
  293.     trans_text(q, fromLang, toLang)
  294.   else:
  295.     print('[Dict Mode] %s -> %s\r\n' % (fromLang, toLang), file=stdinfo)
  296.     trans_dict(word, fromLang, toLang)
  297. main()
复制代码

作者: CrLf    时间: 2017-5-5 13:10

本帖最后由 CrLf 于 2017-5-5 16:03 编辑

主要用法:
  1. fanyi 测试文本
  2. ::参数用法
  3. fanyi -c
  4. ::剪贴板用法,从剪贴板获取要翻译的内容
  5. echo 测试文本 | fanyi
  6. ::管道用法
  7. fanyi <测试文件.txt
  8. ::重定向用法
复制代码
如果你只要翻译后的文字,那屏蔽句柄 2 就行了:
  1. fanyi <测试文件.txt 2>nul
  2. ::重定向用法,并屏蔽原文和其他辅助信息
复制代码
还有些可选参数,含义如下:
Usage:fanyi.py [-f lang] [-t lang] [--web name] [-c] [-n|-T|-D] args....

  -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
    显示简单的帮助信息

作者: CrLf    时间: 2017-5-5 13:11

为了使用方便,我私底下是给打包成 exe 了,好像是用了 PyInstaller
作者: 523066680    时间: 2017-5-5 14:28

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

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

<<<meaning
'NoneType' object is not iterable


不错不错
作者: CrLf    时间: 2017-5-5 16:05

回复 4# 523066680


    修改了一下,改个bug,完善帮助信息,再加上从剪贴板获取来源的选项
作者: happy886rr    时间: 2017-5-5 17:55

翻译的合情合理。
  1. 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."
  2. [Text Mode] auto -> zh
  3. -------------------------------------------------
  4. In line with the conviction that I will do whatever it takes to serve my country
  5. even at the cost of my own life,regardless of fortune or misfortune to myself.
  6. 我坚信我将不惜一切代价为国家服务,不惜牺牲自己的生命,无论命运或厄运。
复制代码

作者: codegay    时间: 2017-5-5 19:52

PyInstaller py2exe之类打包出来,体积默认是20多M的吧?
作者: CrLf    时间: 2017-5-5 20:34

回复 7# codegay


    3.78MB
作者: codegay    时间: 2017-5-5 21:11

回复 8# CrLf


    不错哦。
作者: happy886rr    时间: 2017-5-5 22:08

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

作者: CrLf    时间: 2017-5-5 23:07

回复 10# happy886rr


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

    多了一个 --config 参数
作者: CrLf    时间: 2017-5-5 23:33

回复 10# happy886rr


    用 upx 压缩 python35.dll 之后,打包的 fanyi.exe 体积为 1.27MB,够了
作者: codegay    时间: 2017-5-6 02:58

回复 12# CrLf


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

搜索了一下py2exe是有这种情况。
作者: CrLf    时间: 2017-5-6 12:06

本帖最后由 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
作者: pendave    时间: 2017-6-4 22:07

回复 6# happy886rr


    我要翻译成 法语怎么写?
  1. python "C:\Program Files\Python 3.5\Python实现的命令行全文翻译工具.py" "%~f1" "叼你" "%~f2" "-t fra"
复制代码
  1. python "C:\Program Files\Python 3.5\Python实现的命令行全文翻译工具.py" "%~f1" "叼你" -t fra
复制代码
还有 你命令行里突然用
  1. fanyi ................
复制代码
这个
  1. fanyi
复制代码
是怎么实现的?
作者: pendave    时间: 2017-6-4 23:03

其他的用法我乱摸索中
作者: pendave    时间: 2017-6-4 23:05

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




欢迎光临 批处理之家 (http://bbs.bathome.net/) Powered by Discuz! 7.2