最近单位领导与我提起,说要做一个语音播报功能程序,意在定时提醒职工进行抄表工作。在下也是个刚毕业不久的小白,从头开始学习Python,对于这个程序虽说小,但也只是看起来而已,在细节方面还是查阅了很多的资料,毕竟新手上路,踩了不少坑。在此记录一下,以便于以后如果忘记了可以回来看看。
最初的设想呢,是使用Tkinter来做UI,pyttsx来做语音播报,因为没经费,值班室的电脑又是局域网不能连外网,只能调用电脑自带的声卡来播放,而不能考虑百度云等开放语音平台,所以决定用pyttsx库。又想到时间设置的问题,电脑上有别的软件在使用数据库,不允许私自动,在这个方面决定使用json文件做为时间配置文件。
话不多说,直接上代码。
以json为配置文件。形式为“第xx次抄表”:“xx:xx” 值为时间,不规定日期,因为每天都要,只固定每天抄表时间。
import json
def getData():
with open("config.json",encoding='utf-8-sig') as json_file:
config = json.load(json_file)
return config
导入json包后,通过load方法读取json文件,返回的config变量是一个dict类型的变量。在此注意,在windows系统下,要以utf-8-sig的解码形式打开文件,在其他系统不知道怎样。反正windows以记事本编辑任何文本文件保存后,都会变成BOM形式(就是其中有三个进制码,让操作系统识别这个用记事本编辑过)的文件,用其他文本编辑器倒不会出现这个问题。这个问题会直接导致你用记事本来修改完配置文件后程序无法识别其中的内容。这是第一个坑!
配置文件弄好了,接下来就是试试能否播放文本内容。
import pyttsx3
import datetime
def saudi():
curr_time = datetime.datetime.now()
engine = pyttsx3.init()
msg = '现在是,' + str(curr_time.year) + '年' + str(curr_time.month) + '月' + str(curr_time.day) + '日' + str(
curr_time.hour) + '点' + str(curr_time.minute) + '分'+str(curr_time.second)+',抄表时间到'
rate = engine.getProperty('rate')
engine.setProperty('rate', rate - 50)
volume = engine.getProperty('volume')
engine.setProperty('volume', volume + 0.25)
engine.say(msg)
engine.runAndWait()
这个是pyttsx的固定代码形式了。倒是挺顺利的,导入所需的包,init()方法初始化。这里是以获取当前系统时间,提醒抄表的固定播报形式,用getproperty来获取属性,setproperty来设置属性,其中rate为语速,volume为音量,都可在方法内调整大小。最后用say方法存入要说的字符串或文本,用runAndWait()方法来启动语音播报。可以说是最顺利的环节,基本无坑,形式固定。
既然上面说了,json文件拿出来的数据是dict类的,形式也要转换成时间格式,即要对json文件中的时间数据做格式化处理。同时要与当前时间做比对,如果时和分对得上,就进行播报。python中好像没有直接将dict转换为datatime的,所以只能自己写了。
import json
import datetime
import dateutil.parser
import decimal
from Data import getData
CONVERTERS = {
'datetime': dateutil.parser.parse,
'decimal': decimal.Decimal,
}
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime,)):
return {"val": obj.isoformat(), "_spec_type": "datetime"}
elif isinstance(obj, (decimal.Decimal,)):
return {"val": str(obj), "_spec_type": "decimal"}
else:
return super().default(obj)
def object_hook(obj):
_spec_type = obj.get('_spec_type')
if not _spec_type:
return obj
if _spec_type in CONVERTERS:
return CONVERTERS[_spec_type](obj['val'])
else:
raise Exception('Unknown {}'.format(_spec_type))
def getTime(i):
data = getData()
thing = json.dumps(data, cls=MyJSONEncoder)
a = json.loads(thing, object_hook=object_hook)
y = a[i]
time = datetime.datetime.strptime(y, "%H:%M")
return time.hour,time.minute
这个部分无非就是将json数组转为dict类型,我怎么越写越觉得我写得太麻烦了这个部分,反正不管了,懒得细分析了。最后返回的是datatime形式的一个时和分,用作对比。传入的i就是每个json的key。
这部分就是用一个线程,定时获取当前系统时间,同时遍历json数组,进行比对,如果找到时间,就进行播报。
import threading
import datetime
import pythoncom
from gtt import getTime
from Data import getData
from audio import saudi
def fun():
pythoncom.CoInitialize()
a = rec()
if a == 1:
saudi()
else:
print(a)
pythoncom.CoUninitialize()
timer = threading.Timer(60, fun)
timer.start()
def rec():
for i in getData():
h, m = getTime(i)
curr_time = datetime.datetime.now()+datetime.timedelta(seconds=-7)
if curr_time.hour == h and curr_time.minute == m and i in getData():
#print("时间到" + str(datetime.datetime.now())) 用做测试,看是否显示正常
return 1
#print(i+str(curr_time))
#print("------------------------"+"\n")
python线程的固定写法,用Timer方法来定义时间并开始线程,在此定一分钟检测一次。其实在我写好后播报时间设置间隔短的话(如一分钟),是非常正常的,但是间隔一长,有时候会报COM错误,有时候不会报,这让我百思不得其解。最后查阅资料后了解到如需创建单线程,需要用到pythoncom的CoInitialize方法来创建一个单线程。之后就稳定了,同时要记得以CoUninitialize方法结束。第二个坑!
所有的小部件都做好了,剩下就是一一对应做出UI来。具体思路是第一个界面,是一个启动按钮,按钮只可按一次,点击后显示json文件中的配置项,其中数据不可编辑,只可复制。另设更新按钮,以便更改数据时不需要退出,只需要更新就能显示新配置。
import os
import tkinter as tk
import tkinter.font as tf
from Data import getData
from Th import fun
#允许Ctrl+C复制
def ctrlEvent(event):
if (12 == event.state and event.keysym == 'c'):
return
else:
return "break"
window = tk.Tk()
window.title('定时抄表服务')
# window.geometry('500x300')
window.resizable(False, False) #Text不允许编辑
ft = tf.Font(size=20) #设置字体大小
#启动按钮的函数
def begin(x):
a = 1
b = len(getData())
msg = getData()
for i in getData():
# j =str(i)
# print(msg[j])
# t=tk.Text(window,width=10,height=1,font=ft)
d = tk.Text(window, width=5, height=1, font=ft)
#print(i)
d.bind("" , lambda a: ctrlEvent)
d.insert("insert", msg[i])
d["state"] = 'disabled'
l = tk.Label(window, text=i,width=10, height=1)
# t.grid(row=i,column=1)
if a <= b:
l.grid(row=1, column=a, padx=8, pady=4)
d.grid(row=2, column=a, padx=8, pady=4)
a += 1
if x==1:
d["state"] = 'normal'
d.delete(0.0, tk.END)
for j in getData():
d.insert("insert", msg[i])
d.update()
d["state"] = 'disabled'
else:
bt2 = tk.Button(window, text='更新', width=5, height=1, command=new)
bt2.grid(row=4, column=int(len(getData())/2)+2, padx=20, pady=20)
fun()
def new():
begin(1)
def out():
oo = os.getpid()
import subprocess
subprocess.Popen("cmd.exe /k taskkill /F /T /PID %i" % oo, shell=True)
# print(os.getpid())
def go():
begin(0)
def mixed_action(btn):
go()
btn["state"] = 'disabled'
bt = tk.Button(window, text='启动', width=8, height=1)
bt.config(command = lambda: mixed_action(bt))
bt.grid(row=4, column=int(len(getData())/2)-2,padx=20, pady=20)
window.protocol("WM_DELETE_WINDOW", out)
window.mainloop()
这个实现了,按下启动按钮,加载数据,同时往begin函数传递一个参数,begin函数中,若传入非1的数,就加载一个更新按钮,同时"启动"按钮变为不可使用。之后的更新按钮会向begin函数传递1,先令Text变为可编辑,之后一个个插入新的时间配置。注意到我最后使用了protocol方法,对窗口的退出按钮绑定了out函数。仔细看out函数会发现,这个函数是用来查询到主程序的PID,以命令行的形式来结束该PID对应的程序。这么做的原因就是Python天生的缺陷所在…无法结束线程!!就算你的主程序退出了,线程依然在运行。只能找到PID来结束程序,这是第三个坑…大坑!
原以为终于踏过无数个坑,可以好好的打包成exe完成任务啦~结果没想到!!最大的坑是在这T_T。
打包是用pyinstaller。要以-D打包。打包成功后,打开就一直闪退,查看错误记录(pyinstaller打包结束会有个warn-XX.txt),发现一堆乱七八糟的包没拿过来。我本身挺懒的,所以找了一天有没有什么打包方式能解决,不想一个个拿包进去,真的有十几个包,累。然而没有。。。只能乖乖一个个在lib里查然后放进去了。放完后终于正常运行
唉…太累了,小白自学就是这样,不过我喜欢python的地方是确实有很多包用,不用那么繁琐的写程序。记录一下吧,继续加油。