批量大文本筛选过滤工具开发记录
本周花了两三天的时间做了一个大文本数据筛选工具,主要是针对excel打开很慢或者无法打开的几百兆乃至几G的csv、txt文件,提供常规的数据筛选、统计和输出功能。这个大文本筛序需求对生产中的数据挑选和数据分析来说是比较常见的。本文就开发的过程简单记录如下:
- 使用什么开发语言?
- 怎样保证用户体验?
- 如何维护优化?
使用什么开发语言?
这问得有点像是废话。我很熟悉Python,它的开发速度足够的快,又足够灵活,特别是它强大的eval函数可以直接执行字符串代码,字符串代码中可以包含变量和函数,这也就意味着我可以在字符串中设定特定的变量来代替文件的每一行数据,然后执行对应的方法来判断这一行该不该输出,这对自定义筛选规则来说相当的适合。至于处理速度,凭经验python处理几百万行的数据也就几分钟事情,都在容忍范围内,因而python成了首选。
怎样保证用户体验?
这个工具的用户主要还是生产人员和分析人员,对他们来说,效率速度都是其次,简答好用、节省大部分时间就行。因而我将用户体验分解为操作简单、界面友好两部分。用户平时大部分是用excel来查看筛选数据的,因而最好是能提供类似的excel的数据查看界面和筛序手段。这就涉及到使用什么框架去开发界面的问题了。界面框架选择我还是秉着熟悉优先的原则,那理所当然是Qt,它的信号和槽机制用起来真叫一个爽。虽然之前用Qt都是在C++下的,不过Qt的Python版本-PYQT的接口都差不多,有不懂的直接看下文档就行。
操作简单原则
参考excel的数据导入功能,搞了一上午,设计的界面如下:
数据的编码格式一般是GBK、GB18030、UTF-8等几种,但好些用户很多时候是根本不知道也不关注数据的编码格式的(所以当他们打开一个csv看到一堆乱码的时候可能会说,怎么是乱码啊?),所以在导入数据时我使用了chardet模块来预测数据的编码格式,免去了用户的选择,代码如下:
with open(filepath, ‘rb‘) as rf:
#这里读取2kb内容是为了提高识别的准确度
charset = chardet.detect(rf.read(2048))[‘encoding‘]
if charset == ‘GB2312‘:charset = ‘GBK‘
对于大文本来说,用户不大可能去查看所有内容,他们一般来说知道数据的格式就足够了,所以我设定了每个文件只显示前100行数据。同时,为了便于用户查看同一目录下不同文件,我设置了一个预览文件列表选择框,选择改变时即时更换预览表格里面的内容。当用户改变文件编码格式、是否包含文件头以及列分隔符时,也会即时更新预览内容。在状态栏显示当前预览文件。
考虑到数据筛选一般是文本和数值筛选,所以在设置过滤条件时只提供了字符串操作和整型以及浮点型的操作。具体的过滤操作也是参考excel的,但考虑到用户可能会通过文本在过滤数据(例如提供一个关键字列表文件,要求将含有该文件中关键字的数据筛选出来),所以针对字符串我增加了“包含于(文件)”,“不包含于(文件)”两个操作供用户选择文件。考虑到有些过滤条件是不适于直接让用户选择的,例如筛选字段1前两位字符和字段2后3位字符的组合等于某值的数据或者对某些字段进行数值运算,所以我还是提供了直接的过滤表达式编辑功能,以便于了解python的用户设置更复杂的过滤条件,例如:((row["field1"][:2]+row["field2"][-3:])=="value")
。
当用户设置过滤条件完毕后可点击过滤测试按钮直接对当前预览的文件进行过滤测试,测试的结果会直接显示在导入设置的预览表格原来的位置上,这样做而不是弹框显示的目的是为了便于用户直观的看到哪一行数据会被筛选出来,方便比较和验证过滤表达式的正确性。
对用户来说输出的结果文件是csv还是txt没啥关系,所以为了简便我选择只提供文本输出功能。在该界面下用户可以设置要输出哪些字段,以及输出的编码和列分隔符。因为是在windows下嘛,所以默认还是设置成GBK靠谱点,免得用户直接把文件拉到excel里一看又是乱码,那感觉就不好了。
界面友好原则
至于界面友好,我感觉主要还是体现在处理进度的体现上,如果用户点击开始处理后半天看不到处理进度,很可能就会觉得工具是不是挂了还是咋的,说不定还会把工具给关闭停止了。这就要求每个文件要有实时的处理进度反馈。
当然,多线程是首选的方案,使用多线程处理数据,在主进程中更新界面。一开始我用的是python自带的线程Thread,但在测试过程中发现工具运行一段时间后就会莫名的直接崩溃掉,我设置了许多异常捕捉但啥也没捕捉到,那一个晚上熬到近2点还没解决,差点气死。
睡了一觉后思路清醒了些,首先要确定到底是过滤代码潜在bug导致的崩溃还是因为界面处理的bug导致的。所以我将实际过滤代码全部独立出来并单独执行,发现代码执行完全正常,所以肯定是点击开始处理按钮后的界面处理时出现什么问题了。一时半会后还是找不到Qt界面崩溃原因,只能无奈的想算逑吧不找了,直接绕过去好了!既然不要界面单独运行没问题,那整个配置文件,独立出来的实际过滤执行模块运行时通过读取配置文件进行设置,而Qt界面只负责将用户设置的参数同步到配置文件,然后开辟个单独的进程去运行实际过滤执行模块,并将进程的进度输出通过管道获取并更新界面好了。这么一想却发现,嘿,还挺好嘛,数据处理和界面分离得更明显了,数据处理的稳定性和代码维护性都有所提高啊。
说干就干,将代码备份后(这个习惯太重要了,否则一旦改错了恢复回来就不容易了),首先是允许直接命令行运行实际执行模块,在这过程中遇到相对路径导入的问题,解决方法也简单,但因为代码量少被我绕过去了。同时还得注意使用print方法打印进度时要设置flush=True避免缓冲,否则Qt主进程通过管道读取进度数据时没法获取实时的数据;然后在界面主进程里开启另一线程,在线程的run方法里使用subprocess.Popen([‘python‘, scriptfile], stdout=subprocess.PIPE)
开辟新进程执行实际过滤模块。开启另一线程时还是使用python的Thread,结果测试时终于捕捉到一个异常:
QObject::connect: Cannot queue arguments of type ‘QVector<int>‘
(Make sure ‘QVector<int>‘ is registered using qRegisterMetaType().)
知道了异常所在,解决问题就好办了,Google后很快就找到了解决方案,原来Qt的控件是只能在主线程里面的访问的!很是惭愧,以前用Qt还没用过多线程所以也不去注意,现在终于吃到苦头了。解决起来很是方便,我继承了QThread,在QThread的run方法里面发射进度更新信号,然后在界面主线程更新ui就行了,代码如下:
class WorkThread(QThread):
fileFinished = pyqtSignal(str)
completed = pyqtSignal()
def __init__(self, parent=None):
super(QThread, self).__init__(parent)
def run(self):
# 使用命令行启动真正的过滤程序,并使用管道进行通讯
scriptfile = os.path.join(os.path.dirname(__file__), ‘core/__init__.py‘)
popen = subprocess.Popen([‘python‘, scriptfile], stdout=subprocess.PIPE)
while True:
try:
next_line = popen.stdout.readline()
try:
next_line = next_line.decode(‘utf8‘)
except UnicodeDecodeError:
next_line = next_line.decode(‘gbk‘)
if next_line == ‘‘ and popen.poll() != None:
break
self.fileFinished.emit(next_line)
except Exception as ex:
print(str(ex))
popen.terminate()
self.completed.emit()
最终的进度更新界面如下:
如何维护优化?
这个工具是要给到生产、分析人员去用的,谁也不知道他们会怎么用,会处理什么数据,所以就把他们当做测试人员吧,后台偷偷记录下他们的操作和操作异常。出于邮箱安全考虑,我是先将工具后台记录发送到指定的平台,平台再邮件通知我。当然,处理了哪些数据,数据量多大之类的数据我也会收集的,方便做绩效汇报,哈哈。
结束了
好吧,一本正经的写到这差不多了吧,再装逼估计得遭雷劈了。写完后发现,看文章容易,写文章实在不容易呐。
版权声明:本文为博主原创文章,未经博主允许不得转载。