抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

前阵子课题组的师弟问我怎么统计CDS序列的密码子频率,其实百度一下有很多在线分析网站可以把序列丢进去,直接出结果……

在做密码子偏好性分析的领域,有一个老牌的经典软件codonw,不仅可以分析每条序列的密码子使用频率,还可以计算近10种密码子偏好性分析常用的指标,以及做关联分析(对应性分析)图。

这个工具在windows系统中要在命令行下运行,linux系统中可以通过conda安装。以及我在github上看到有人重构了原代码,用c语言写了底层方法并且绑定了python,我在用这个项目编译的时候失败了,感兴趣的话可以访问这个codonw-slim项目smsaladi/codonw-slim

当然,这篇博客的重点不是codonw软件,前面说的需求是统计CDS序列的密码子频率,使用在线工具或者现有的工具当然快——前人栽树后人乘凉,这无可厚非。我比较感兴趣的是做这些工具的逻辑,以及如何自己手搓一个工具,让一个没有任何编程经验的人可以快速用上。

python强大之处在于有着丰富和强大的第三方库,编程语言很容易懂,很适合我这种编程小白练手。想要做工具,首先就要设计图形用户界面(Graphical User Interface,GUI),tkinter是python自带的最经典的GUI编程库,官方后续又对tkinter库做了优化也就是Ttk库(基于tkinter库开发的),也有很多人写了一系列针对tkinter的美观拓展插件。万变不离其宗,掌握一个工具的用法,其他的工具思路也都是类似的。

这里不讲tkinter如何使用,网上的教程太多了,CSDN上可以翻到各种组件的详细用法,还有经典的packgridplace三种布局,基本概念知道了依葫芦画瓢就行。

这里我先把自己写的密码子频率统计工具的代码放出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from collections import OrderedDict
from tkinter import *
from tkinter import filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
import time
import re
#from Bio import SeqIO

file_name = ''
codon_table = {
'Ala':['GCG','GCA','GCT','GCC'],
'Cys':['TGT','TGC'],
'Asp':['GAT','GAC'],
'Glu':['GAG' ,'GAA'],
'Phe':['TTT' ,'TTC'],
'Gly':['GGG','GGA','GGT','GGC'],
'His':['CAT','CAC'],
'Ile':['ATA','ATT','ATC'],
'Lys':['AAG','AAA'],
'Leu':['TTG','TTA','CTG','CTA','CTT','CTC'],
'Met':['ATG'],
'Asn':['AAT','AAC'],
'Pro':['CCG','CCA','CCT','CCC'],
'Gln':['CAG','CAA'],
'Arg':['AGG','AGA','CGG','CGA','CGA','CGT','CGC'],
'Ser':['AGT','AGC','TCG','TCA','TCT','TCC'],
'Thr':['ACG','ACA','ACT','ACC'],
'Val':['GTG','GTA','GTT','GTC'],
'Trp':['TGG'],
'Tyr':['TAT','TAC'],
'*':['TGA','TAG','TAA']
}

# 读入fasta序列(代替SeqIO.parse)
def fasta_generator(file_path):
with open(file_path, 'r') as file:
first_line = file.readline().strip()
if not first_line.startswith('>'): # 给一个fasta格式判断
raise ValueError('Invalid file format')
sequence_id = first_line[1:] # 储存当前序列名称
sequence = '' # 储存当前序列内容
for line in file:
line = line.strip()
if line.startswith('>'): # >开头为序列名称行
if sequence_id: # 如果已存在序列名称,返回上一个序列名称和内容
yield {'id': sequence_id, 'seq': sequence}
sequence_id = line[1:]
sequence = ''
else: # 否则,行为序列内容
sequence += line
if sequence_id: # 处理完最后一个序列,返回最后一个序列名称和内容
yield {'id': sequence_id, 'seq': sequence}

class MY_GUI():

# 初始化函数,定义实例的属性
def __init__(self,init_window_name):
self.init_window_name = init_window_name
self.image_file = None # PhotoImage没有引用会自动销毁,这里需要显示引用

# 定义一个类方法,设置窗口
def set_init_window(self):
self.init_window_name.title("密码子统计工具_v1.0")
self.init_window_name.geometry('1048x680+400+150') # 1068x681窗口大小,+横坐标 +纵坐标 定义窗口弹出时的默认展示位置
self.init_window_name.attributes("-alpha",1) # 虚化,值越小虚化程度越高
self.init_window_name.iconbitmap('pic/bitbug_favicon.ico') # 可以换自己的ico图标
self.init_window_name.resizable(0,0) # 禁止改变窗口大小
# 画布(可以不要,或者换自己的gif图片)
canvas = Canvas(self.init_window_name, width=1024, height=680, bg=None)
self.image_file = PhotoImage(file="pic/bg.gif")
self.resized_image = self.image_file.zoom(2, 2)
canvas.create_image(520, 35, anchor='n', image=self.resized_image)
canvas.grid(row=0,rowspan = 20,column=0,columnspan=23)
# frame(暂时不用了)
#self.frame = Frame(self.init_window_name, borderwidth=1, height=2, relief='groove',bd=1)
#self.frame.grid(row=21,column=0, columnspan=24)
# 标签
self.init_data_label = Label(self.init_window_name, text="导入序列")
self.init_data_label.grid(row=0, column=0)
self.result_data_label = Label(self.init_window_name, text="Number(Frequency)")
self.result_data_label.grid(row=0, column=12)
self.result_out_label = Label(self.init_window_name, text="Codon Usage results")
self.result_out_label.grid(row=6,column=13)
self.log_label = Label(self.init_window_name, text="运行日志")
self.log_label.grid(row=11, column=0)
# 文本框
self.init_data_Text = ScrolledText(self.init_window_name, width=60, height=35) # 原始数据录入框
self.init_data_Text.grid(row=1, column=0, rowspan=10, columnspan=10, padx=20,pady=5)
self.init_data_Text.bind('<KeyPress>', lambda f: 'break') # 绑定事件禁止键入
self.log_data_Text = ScrolledText(self.init_window_name, width=60, height=9,) # 日志框
self.log_data_Text.grid(row=12, column=0, rowspan=5, columnspan=10,padx=20,pady=5)
self.log_data_Text.bind('<KeyPress>', lambda f: 'break')
self.result_data_Text = ScrolledText(self.init_window_name, width=60, height=20, wrap='none') # 频数频率展示
self.result_data_Text.grid(row=1, column=12, rowspan=5, columnspan=10, padx=10,pady=5)
self.result_data_Text.bind('<KeyPress>', lambda f: 'break')
self.result_fre_Text = ScrolledText(self.init_window_name, width=40, height=20) # 结果展示
self.result_fre_Text.grid(row=7, column=12, rowspan=10, columnspan=10,padx=10,pady=5)
self.result_fre_Text.bind('<KeyPress>', lambda f: 'break')
# 按钮
self.load_file_button = Button(self.init_window_name, text = "导入fasta文件",borderwidth=2,relief=RAISED, command=self.select_file) # 绑定内部命令
self.load_file_button.grid(row=2, column=11)
self.codon_count_button = Button(self.init_window_name, text="开始统计",borderwidth=2,relief=RAISED,command=self.codon_count)
self.codon_count_button.grid(row=3, column=11)
self.export_count_button = Button(self.init_window_name, text="导出表格",borderwidth=2,relief=RAISED,command=self.export_count)
self.export_count_button.grid(row=4, column=11)
self.export_fre_button = Button(self.init_window_name, text="导出结果", borderwidth=2,relief=RAISED,command=self.export_fre)
self.export_fre_button.grid(row=5,column=11)
self.clean_button = Button(self.init_window_name, text='清空窗口', borderwidth=2,relief=RAISED,command=self.window_clean)
self.clean_button.grid(row=6,column=11)
# 滚轮与绑定(ScrolledText只带有垂直滚动条)
self.scrollbar_x = Scrollbar(self.init_window_name, orient=HORIZONTAL) # 创建滚动条部件
self.result_data_Text.config(xscrollcommand=self.scrollbar_x.set) # 文本框-控制-滚动条
self.scrollbar_x.config(command=self.result_data_Text.xview) # 滚动条-控制-文本框
self.scrollbar_x.grid(row=5, column=12, rowspan=1,columnspan=10, sticky="ESW",padx=10) # 设置滚动条位置

# 导入序列功能函数
def select_file(self):
global file_name
file_name = filedialog.askopenfilename(title="选择文件")
if file_name != '':
try:
records = fasta_generator(file_name)
number = 0
for i in records:
name = i['id']
seq = i['seq']
if len(seq)>48:
seq = seq[:45] + '...' + seq[-3:]
self.init_data_Text.insert(END,f"Name:{name}\nSeq:'{seq}'\n")
number += 1
self.write_log_to_Text(f"INFO:fasta文件导入成功!共加载{number}条序列。")
except:
messagebox.showerror("ERROR", "载入失败,请检查文件是否为fasta格式!")
self.write_log_to_Text("ERROR:载入失败,请检查文件格式。")

# 分析统计功能函数
def codon_count(self):
if file_name != '':
records = fasta_generator(file_name)
CodonsDict = OrderedDict([('GCG', 0), ('GCA', 0), ('GCT', 0), ('GCC', 0), ('TGT', 0), ('TGC', 0), ('GAT', 0), ('GAC', 0), ('GAG', 0), ('GAA', 0), ('TTT', 0), ('TTC', 0), ('GGG', 0), ('GGA', 0), ('GGT', 0), ('GGC', 0), ('CAT', 0), ('CAC', 0), ('ATA', 0), ('ATT', 0), ('ATC', 0), ('AAG', 0), ('AAA', 0), ('TTG', 0), ('TTA', 0), ('CTG', 0), ('CTA', 0), ('CTT', 0), ('CTC', 0), ('ATG', 0), ('AAT', 0), ('AAC', 0), ('CCG', 0), ('CCA', 0), ('CCT', 0), ('CCC', 0), ('CAG', 0), ('CAA', 0), ('AGG', 0), ('AGA', 0), ('CGG', 0), ('CGA', 0), ('CGT', 0), ('CGC', 0), ('AGT', 0), ('AGC', 0), ('TCG', 0), ('TCA', 0), ('TCT', 0), ('TCC', 0), ('ACG', 0), ('ACA', 0), ('ACT', 0), ('ACC', 0), ('GTG', 0), ('GTA', 0), ('GTT', 0), ('GTC', 0), ('TGG', 0), ('TAT', 0), ('TAC', 0), ('TGA', 0), ('TAG', 0), ('TAA', 0)])
# 输出表头
self.result_data_Text.insert(END, f"{'Name':<15}")
for key in CodonsDict:
if key != 'TAA':
self.result_data_Text.insert(END, f'{key:<12}')
else:
self.result_data_Text.insert(END,f'{key:<12}\n')
for i in records:
# 每条序列判断ATG开头,是否有屏蔽序列,长度是否为3的倍数
if i['seq'].startswith('ATG') and 'N' not in i['seq'] and len(i['seq']) % 3 ==0:
for j in range(0, len(str(i['seq'])), 3):
codon = str(i['seq'])[j:j+3]
if codon in CodonsDict.keys():
CodonsDict[codon] +=1
else:
self.write_log_to_Text("WARNING:序列%s存在未识别的密码子,跳过。" % (i['id']))
break
total = sum([CodonsDict[key] for key in CodonsDict.keys()])
name = i['id']
self.result_data_Text.insert(END, f'{name:<15}') # 这里的f-string格式化输出f'{i['id']:<15}'会报错,所以用了个变量name代替
self.result_fre_Text.insert(END,'Results for %d residue sequence "%s":\n\nAA\tCodon\tNumber\tFrequency\n\n' % (total, i['id']))
for key in CodonsDict.keys():
# 计算每个密码子使用频率(标准密码子表)
frequency = "%.2f" % (CodonsDict[key]*3000/total)
content = str(CodonsDict[key]) + '(%s)' % (frequency)
self.result_data_Text.insert(END,f'{content:<12}')
if key in codon_table['Ala']:
self.result_fre_Text.insert(END,'Ala\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Cys']:
self.result_fre_Text.insert(END,'Cys\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Asp']:
self.result_fre_Text.insert(END,'Asp\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Glu']:
self.result_fre_Text.insert(END,'Glu\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Phe']:
self.result_fre_Text.insert(END,'Phe\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Gly']:
self.result_fre_Text.insert(END,'Gly\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['His']:
self.result_fre_Text.insert(END,'His\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Ile']:
self.result_fre_Text.insert(END,'Ile\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Lys']:
self.result_fre_Text.insert(END,'Lys\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Leu']:
self.result_fre_Text.insert(END,'Leu\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Met']:
self.result_fre_Text.insert(END,'Met\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Asn']:
self.result_fre_Text.insert(END,'Asn\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Pro']:
self.result_fre_Text.insert(END,'Pro\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Gln']:
self.result_fre_Text.insert(END,'Gln\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Arg']:
self.result_fre_Text.insert(END,'Arg\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Ser']:
self.result_fre_Text.insert(END,'Ser\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Thr']:
self.result_fre_Text.insert(END,'Thr\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Val']:
self.result_fre_Text.insert(END,'Val\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Trp']:
self.result_fre_Text.insert(END,'Trp\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['Tyr']:
self.result_fre_Text.insert(END,'Tyr\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
elif key in codon_table['*']:
self.result_fre_Text.insert(END,'*\t%s\t%d\t%s\n' % (key, CodonsDict[key], frequency))
self.result_fre_Text.insert(END,'\n----------------------------------------\n' )
self.result_data_Text.insert(END,'\n')
else:
self.write_log_to_Text("WARNING:序列%s非ATG开头/存在屏蔽序列/非3的倍数,该序列将不会出现在统计结果中。" % (i['id']))
self.write_log_to_Text("INFO:统计结束!")
else:
self.write_log_to_Text("ERROR:请先载入fasta文件!")

# 导出频数频率统计表
def export_count(self):
content = self.result_data_Text.get('1.0', END)
if 'Name' not in content:
self.write_log_to_Text("ERROR:请先点击“载入fasta文件”并点击“开始统计”按钮!")
else:
try:
count_file = filedialog.asksaveasfilename()
count_file = count_file + '.csv'
with open(count_file, 'w') as count:
content = re.sub(r"[^\S\r\n]+",',',content) # 正则匹配换行符之外的所有空格
count.write(content)
self.write_log_to_Text("INFO:密码子频数频率统计表保存成功!文件路径:%s" % (count_file))
except:
self.write_log_to_Text("ERROR:ERROR:导出失败,请检查是否有同名文件未关闭!")

# 导出结果文件
def export_fre(self):
content = self.result_fre_Text.get('1.0', END)
if 'Results' not in content:
self.write_log_to_Text("ERROR:请先点击“载入fasta文件”并点击“开始统计”按钮!")
else:
try:
result_file = filedialog.asksaveasfilename()
result_file = result_file + '.txt'
with open(result_file, 'w') as out:
out.write(content)
self.write_log_to_Text("INFO:结果文件保存成功!保存路径:%s" % (result_file))
except:
self.write_log_to_Text("ERROR:导出失败,请检查是否有同名文件未关闭!")

# 清空所有文本框
def window_clean(self):
self.init_data_Text.delete(1.0, END)
self.log_data_Text.delete(1.0, END)
self.result_data_Text.delete(1.0, END)
self.result_fre_Text.delete(1.0, END)

# 日志打印
def write_log_to_Text(self,logmsg):
current_time = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
logmsg_in = str(current_time) +" " + str(logmsg) + "\n"
self.log_data_Text.insert(END, logmsg_in)

def gui_start():
init_window = Tk()
GUI = MY_GUI(init_window)
GUI.set_init_window()
init_window.mainloop()

gui_start()

第一次用tkinter这个工具,代码看起来也有点乱……就当作自己的尝试 = =,各个部件写了注释方便以后自己参考。

运行时可以生成如下gui界面:

界面比较简单,点击“导入fasta文件”,左边的文本框可以看到序列大致信息,点击“开始统计”后右边两个文本框可以输出两种格式的结果,后面两个按钮分别自定义导出两种结果,“清空窗口”就是清除文本框的所有数据。

整体逻辑是识别导入的序列是否是ATG开头,是否含有屏蔽序列,长度是否能被3整除,以上条件都满足才会计入右边的统计结果,否则跳过该序列,跳过的信息会在运行日志框中显示。如果用的不是标准密码子表,可以自行更改codon_table变量。

除开代码中间一大堆判断密码子编码哪个氨基酸(我是真的没想到还能用什么方法判断21种不同的编码氨基酸),其他地方都尽量写地简洁了一些。原先打算直接用BioSeqIO.parse的方法载入序列,确实很方便,但是不利于程序的最后打包……Bio库的一个依赖库是numpy,用过python的小伙伴都知道numpy也是一个重量级科学计算库,但是用pyinstaller将程序打包成可执行文件的时候,numpy有一个重量级的openblas依赖,可以看看有多“重量级”:

一个依赖占了37Mb的空间……况且我用Bio库只是为了导入和识别fasta序列,所以这里搓了个fasta_generator()方法函数来代替SeqIO.parse,调用方法函数后都是生成一个生成器对象,逐行读取fasta文件,id储存序列名称,seq储存序列内容。这样就不用调用Bio库,可以看到这里调用的都是python的内置库,可以有效减少打包后的体量。

说到打包,为什么要打包?因为写的python程序要运行起来还有个前提,就是别人的电脑里也装了python,并且要在命令行中运行python解释器,这不是我的最终目的,因此接下来是把py文件“转“成能在windows系统中运行的可执行文件,这里用典型的python打包库pyinstaller。因为pyinstaller不是自带的python库,所以需要单独安装。

1
2
3
4
# 在windows命令行的对应文件路径下(我是windows环境下开发的)
pip install pyinstaller

pyinstaller -w -D -n Codon -i bitbug_favicon.ico codon.py

pyinstaller参数:

  • -w 指定程序运行时不显示命令行窗口(美观起见,发给别人用就加这个参数)
  • -D 产生一个目录作为可执行程序(如果想都打包进一个可执行文件的话,用参数-F,运行起来会很慢!)
  • -n 指定项目名称
  • -i 指定可执行文件图标,ico格式
  • –hidden-impor 如果有隐藏导入项(非标准库)的时候需要手动加入

pyinstaller安装后可以直接使用,如果提示该命令不是系统命令的话,需要把对应的exe文件路径写到环境变量中。

打包后会在当前目录生成spec后缀的打包配置文件,名为build的中间文件,还有dist——最终生成的可执行文件所在目录。我们打包的可执行程序就在dist中。

这里打包以后还要注意,py脚本中导入的图片不会被打包,找到exe文件,根据脚本中写的导入图片的相对路径,创建文件夹pic并且把图片一起丢进去。还可以顺便把自己的测试文件也创个test文件夹丢进去。

最终整个文件夹打包后的大小为25.6Mb……怎么说呢,python写的代码转成exe文件体量就是会很大,相比之下原来的一个依赖就要37Mb,已经好很多了……

既然都做到这一步了,顺便就把整个文件夹再打包打包做个windows安装包(虽然也可以直接打包成压缩包,但是少了点仪式感),麻雀虽小五脏俱全,体验下流程~

制作windows安装包要用到两个软件:

  • NSIS——开源的Windows系统安装程序制作工具,相当于一门脚本语言,描述安装程序的行为和逻辑
  • HM NIS Edit——NSIS编辑器,以向导的方式自动生成NSIS脚本,再通过NSIS编译成windows安装包

这两个软件就像咱学生信用到的R和Rstudio,没啥好说的,在后者创建一个脚本向导,只要点点选择按钮就可以定制自己程序的安装和卸载过程以及逻辑了,不需要专门去学这个脚本语言,非常方便。

下载和使用过程CSDN上有详细的每一步流程,放个链接,不重复造轮子。Windows下使用Pyinstaller做成客户端安装包_

瞧这安装界面,是不是有那味儿了~

软件安装包顺便在这里也备份一个,可以直接在windows系统中安装使用,以后有空再做界面的美化和其他新功能。点击这里获取安装包

2023/09/04 更新

我真的是脑子抽了用了二十几个判断……既然写了标准密码子表,只要判断是不是在condon_table中并且输出key值就可以了,后面输出格式都是一样的……顺便优化了一下代码,有序字典也不用了。

果然代码还是要多练,越看越不对劲,以后尽量避免“面向结果编程”的错误 = =

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
from tkinter import *
from tkinter import filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
import time
import re

file_name = ''
codon_table = {
'Ala':['GCG','GCA','GCT','GCC'],
'Cys':['TGT','TGC'],
'Asp':['GAT','GAC'],
'Glu':['GAG' ,'GAA'],
'Phe':['TTT' ,'TTC'],
'Gly':['GGG','GGA','GGT','GGC'],
'His':['CAT','CAC'],
'Ile':['ATA','ATT','ATC'],
'Lys':['AAG','AAA'],
'Leu':['TTG','TTA','CTG','CTA','CTT','CTC'],
'Met':['ATG'],
'Asn':['AAT','AAC'],
'Pro':['CCG','CCA','CCT','CCC'],
'Gln':['CAG','CAA'],
'Arg':['AGG','AGA','CGG','CGA','CGA','CGT','CGC'],
'Ser':['AGT','AGC','TCG','TCA','TCT','TCC'],
'Thr':['ACG','ACA','ACT','ACC'],
'Val':['GTG','GTA','GTT','GTC'],
'Trp':['TGG'],
'Tyr':['TAT','TAC'],
'*':['TGA','TAG','TAA']
}

# 读入fasta序列(代替SeqIO.parse)
def fasta_generator(file_path):
with open(file_path, 'r') as file:
first_line = file.readline().strip()
if not first_line.startswith('>'): # 给一个fasta格式判断
raise ValueError('Invalid file format')
sequence_id = first_line[1:] # 储存当前序列名称
sequence = '' # 储存当前序列内容
for line in file:
line = line.strip()
if line.startswith('>'): # >开头为序列名称行
if sequence_id: # 如果已存在序列名称,返回上一个序列名称和内容
yield {'id': sequence_id, 'seq': sequence}
sequence_id = line[1:]
sequence = ''
else: # 否则,行为序列内容
sequence += line
if sequence_id: # 处理完最后一个序列,返回最后一个序列名称和内容
yield {'id': sequence_id, 'seq': sequence}

class MY_GUI():

# 初始化函数,定义实例的属性
def __init__(self,init_window_name):
self.init_window_name = init_window_name
self.image_file = None # PhotoImage没有引用会自动销毁,这里需要显示引用

# 定义一个类方法,设置窗口
def set_init_window(self):
self.init_window_name.title("密码子统计工具_v1.0")
self.init_window_name.geometry('1048x680+400+150') # 1068x681窗口大小,+横坐标 +纵坐标 定义窗口弹出时的默认展示位置
self.init_window_name.attributes("-alpha",1) # 虚化,值越小虚化程度越高
self.init_window_name.iconbitmap('d:/zhuomian/python/myscript/codon/codon源码/bitbug_favicon.ico')
self.init_window_name.resizable(0,0) # 禁止改变窗口大小
canvas = Canvas(self.init_window_name, width=1024, height=680, bg=None)
self.image_file = PhotoImage(file="d:/zhuomian/python/myscript/codon/codon源码/bg.gif")
self.resized_image = self.image_file.zoom(2, 2)
canvas.create_image(520, 35, anchor='n', image=self.resized_image)
canvas.grid(row=0,rowspan = 20,column=0,columnspan=23)
# 标签
self.init_data_label = Label(self.init_window_name, text="导入序列")
self.init_data_label.grid(row=0, column=0)
self.result_data_label = Label(self.init_window_name, text="Number(Frequency)")
self.result_data_label.grid(row=0, column=12)
self.result_out_label = Label(self.init_window_name, text="Codon Usage results")
self.result_out_label.grid(row=6,column=13)
self.log_label = Label(self.init_window_name, text="运行日志")
self.log_label.grid(row=11, column=0)
# 文本框
self.init_data_Text = ScrolledText(self.init_window_name, width=60, height=35) # 原始数据录入框
self.init_data_Text.grid(row=1, column=0, rowspan=10, columnspan=10, padx=20,pady=5)
self.init_data_Text.bind('<KeyPress>', lambda f: 'break') # 绑定事件禁止键入
self.log_data_Text = ScrolledText(self.init_window_name, width=60, height=9,) # 日志框
self.log_data_Text.grid(row=12, column=0, rowspan=5, columnspan=10,padx=20,pady=5)
self.log_data_Text.bind('<KeyPress>', lambda f: 'break')
self.result_data_Text = ScrolledText(self.init_window_name, width=60, height=20, wrap='none') # 频数频率展示
self.result_data_Text.grid(row=1, column=12, rowspan=5, columnspan=10, padx=10,pady=5)
self.result_data_Text.bind('<KeyPress>', lambda f: 'break')
self.result_fre_Text = ScrolledText(self.init_window_name, width=40, height=20) # 结果展示
self.result_fre_Text.grid(row=7, column=12, rowspan=10, columnspan=10,padx=10,pady=5)
self.result_fre_Text.bind('<KeyPress>', lambda f: 'break')
# 按钮
self.load_file_button = Button(self.init_window_name, text = "导入fasta文件",borderwidth=2,relief=RAISED, command=self.select_file) # 绑定内部命令
self.load_file_button.grid(row=2, column=11)
self.codon_count_button = Button(self.init_window_name, text="开始统计",borderwidth=2,relief=RAISED,command=self.codon_count)
self.codon_count_button.grid(row=3, column=11)
self.export_count_button = Button(self.init_window_name, text="导出表格",borderwidth=2,relief=RAISED,command=self.export_count)
self.export_count_button.grid(row=4, column=11)
self.export_fre_button = Button(self.init_window_name, text="导出结果", borderwidth=2,relief=RAISED,command=self.export_fre)
self.export_fre_button.grid(row=5,column=11)
self.clean_button = Button(self.init_window_name, text='清空窗口', borderwidth=2,relief=RAISED,command=self.window_clean)
self.clean_button.grid(row=6,column=11)
# 滚轮与绑定(ScrolledText只带有垂直滚动条)
self.scrollbar_x = Scrollbar(self.init_window_name, orient=HORIZONTAL) # 创建滚动条部件
self.result_data_Text.config(xscrollcommand=self.scrollbar_x.set) # 文本框-控制-滚动条
self.scrollbar_x.config(command=self.result_data_Text.xview) # 滚动条-控制-文本框
self.scrollbar_x.grid(row=5, column=12, rowspan=1,columnspan=10, sticky="ESW",padx=10) # 设置滚动条位置

# 导入序列功能函数
def select_file(self):
global file_name
file_name = filedialog.askopenfilename(title="选择文件")
if file_name != '':
try:
records = fasta_generator(file_name)
number = 0 # 计数,导入多少序列
for i in records:
name = i['id']
seq = i['seq']
if len(seq)>48:
seq = seq[:45] + '...' + seq[-3:]
self.init_data_Text.insert(END,f"Name:{name}\nSeq:'{seq}'\n")
number += 1
self.write_log_to_Text(f"INFO:fasta文件导入成功!共加载{number}条序列。")
except:
messagebox.showerror("ERROR", "载入失败,请检查文件是否为fasta格式!")
self.write_log_to_Text("ERROR:载入失败,请检查文件格式。")

# 分析统计功能函数
def codon_count(self):
if file_name != '':
records = fasta_generator(file_name)
CodonsDict = {codon: 0 for codon_list in codon_table.values() for codon in codon_list} # 新的字典,统计密码子数量用
# 输出表头
self.result_data_Text.insert(END, f"{'Name':<15}")
for key in CodonsDict:
self.result_data_Text.insert(END, f'{key:<12}')
self.result_data_Text.insert(END,'\n')
for i in records:
# 每条序列判断ATG开头,是否有屏蔽序列,长度是否为3的倍数
if i['seq'].startswith('ATG') and 'N' not in i['seq'] and len(i['seq']) % 3 ==0:
for j in range(0, len(str(i['seq'])), 3):
codon = str(i['seq'])[j:j+3]
if codon in CodonsDict.keys():
CodonsDict[codon] +=1
else:
self.write_log_to_Text("WARNING:序列%s存在未识别的密码子,跳过。" % (i['id']))
break
total = sum([CodonsDict[key] for key in CodonsDict.keys()])
name = i['id']
self.result_data_Text.insert(END, f'{name:<15}') # 这里的f-string格式化输出f'{i['id']:<15}'会报错,所以用了个变量name代替
self.result_fre_Text.insert(END,'Results for %d residue sequence "%s":\n\nAA\tCodon\tNumber\tFrequency\n\n' % (total, name))
# 计算频率
for key, value in CodonsDict.items():
frequency = '%.2f' % (value * 3000 / total)
content = '%d(%s)' % (value, frequency)
self.result_data_Text.insert(END,f'{content:<12}')
# 结果文本框内的输出
for key_, value_ in codon_table.items():
if key in value_:
AA = key_
self.result_fre_Text.insert(END, '%s\t%s\t%d\t%s\n' % (AA, key, value, frequency))
self.result_fre_Text.insert(END,'\n----------------------------------------\n' )
self.result_data_Text.insert(END,'\n')
else:
self.write_log_to_Text("WARNING:序列%s非ATG开头/存在屏蔽序列/非3的倍数,该序列将不会出现在统计结果中。" % (i['id']))
self.write_log_to_Text("INFO:统计结束!")
else:
self.write_log_to_Text("ERROR:请先载入fasta文件!")

# 导出频数频率统计表
def export_count(self):
content = self.result_data_Text.get('1.0', END)
if 'Name' not in content:
self.write_log_to_Text("ERROR:请先点击“载入fasta文件”并点击“开始统计”按钮!")
else:
try:
count_file = filedialog.asksaveasfilename()
count_file = count_file + '.csv'
with open(count_file, 'w') as count:
content = re.sub(r"[^\S\r\n]+",',',content) # 正则匹配换行符之外的所有空格,处理成csv格式的输出
count.write(content)
self.write_log_to_Text("INFO:密码子频数频率统计表保存成功!文件路径:%s" % (count_file))
except:
self.write_log_to_Text("ERROR:ERROR:导出失败,请检查是否有同名文件未关闭!")

# 导出结果文件
def export_fre(self):
content = self.result_fre_Text.get('1.0', END)
if 'Results' not in content:
self.write_log_to_Text("ERROR:请先点击“载入fasta文件”并点击“开始统计”按钮!")
else:
try:
result_file = filedialog.asksaveasfilename()
result_file = result_file + '.txt'
with open(result_file, 'w') as out:
out.write(content)
self.write_log_to_Text("INFO:结果文件保存成功!保存路径:%s" % (result_file))
except:
self.write_log_to_Text("ERROR:导出失败,请检查是否有同名文件未关闭!")

# 清空所有文本框
def window_clean(self):
self.init_data_Text.delete(1.0, END)
self.log_data_Text.delete(1.0, END)
self.result_data_Text.delete(1.0, END)
self.result_fre_Text.delete(1.0, END)

# 日志打印
def write_log_to_Text(self,logmsg):
current_time = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
logmsg_in = str(current_time) +" " + str(logmsg) + "\n"
self.log_data_Text.insert(END, logmsg_in)

def gui_start():
init_window = Tk()
GUI = MY_GUI(init_window)
GUI.set_init_window()
init_window.mainloop()

gui_start()

欢迎小伙伴们留言评论~