批量大文本筛选过滤工具开发记录

批量大文本筛选过滤工具开发记录

本周花了两三天的时间做了一个大文本数据筛选工具,主要是针对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")
当用户设置过滤条件完成后可点击过滤測试button直接对当前预览的文件进行过滤測试,測试的结果会直接显示在导入设置的预览表格原来的位置上,这样做而不是弹框显示的目的是为了便于用户直观的看到哪一行数据会被筛选出来。方便比較和验证过滤表达式的正确性。

统计输出设置界面

对用户来说输出的结果文件是csv还是txt没啥关系,所以为了简便我选择仅仅提供文本输出功能。

在该界面下用户能够设置要输出哪些字段,以及输出的编码和列分隔符。由于是在windows下嘛,所以默认还是设置成GBK靠谱点,免得用户直接把文件拉到excel里一看又是乱码,那感觉就不好了。

界面友好原则

至于界面友好,我感觉主要还是体如今处理进度的体现上。假设用户点击開始处理后半天看不到处理进度。非常可能就会认为工具是不是挂了还是咋的,说不定还会把工具给关闭停止了。

这就要求每一个文件要有实时的处理进度反馈。


当然。多线程是首选的方案,使用多线程处理数据。在主进程中更新界面。一開始我用的是python自带的线程Thread。但在測试过程中发现工具运行一段时间后就会莫名的直接崩溃掉,我设置了很多异常捕捉但啥也没捕捉到。那一个晚上熬到近2点还没解决,差点气死。
睡了一觉后思路清醒了些,首先要确定究竟是过滤代码潜在bug导致的崩溃还是由于界面处理的bug导致的。所以我将实际过滤代码所有独立出来并单独运行,发现代码运行全然正常,所以肯定是点击開始处理button后的界面处理时出现什么问题了。

一时半会后还是找不到Qt界面崩溃原因,仅仅能无奈的想算逑吧不找了,直接绕过去好了!既然不要界面单独运行没问题。那整个配置文件。独立出来的实际过滤运行模块运行时通过读取配置文件进行设置,而Qt界面仅仅负责将用户设置的參数同步到配置文件,然后开辟个单独的进程去运行实际过滤运行模块,并将进程的进度输出通过管道获取并更新界面好了。

这么一想却发现,嘿,还挺好嘛,数据处理和界面分离得更明显了,数据处理的稳定性和代码维护性都有所提高啊。
说干就干,将代码备份后(这个习惯太重要了,否则一旦改错了恢复回来就不easy了),首先是同意直接命令行运行实际运行模块,在这过程中遇到相对路径导入的问题。解决方法也简单,但由于代码量少被我绕过去了。同一时候还得注意使用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()

最终的进度更新界面例如以下:
统计输出进度反馈

如何维护优化?

这个工具是要给到生产、分析人员去用的。谁也不知道他们会怎么用,会处理什么数据,所以就把他们当做測试人员吧。后台偷偷记录下他们的操作和操作异常。

出于邮箱安全考虑。我是先将工具后台记录发送到指定的平台。平台再邮件通知我。当然,处理了哪些数据,数据量多大之类的数据我也会收集的,方便做绩效汇报。哈哈。

结束了

好吧,一本正经的写到这差点儿相同了吧。再装逼预计得遭雷劈了。写完后发现,看文章easy,写文章实在不easy呐。


后记
1、使用compile.compiledir(project_dir, legacy=False)来生产pyc文件。简单的防止查看改动源代码;
2、将运行文件后缀改为pyw,或者在代码中进行控制,參看这里

if __name__ == '__main__':
    import ctypes
    whnd = ctypes.windll.kernel32.GetConsoleWindow()
    if whnd != 0:
        ctypes.windll.user32.ShowWindow(whnd, 0)
        ctypes.windll.kernel32.CloseHandle(whnd)

    #运行带代码...

3、创建setup.py,使用py2exe生成exe文件,当然事先得通过pip安装py3版本号的py2exe。

创建的setup.py文件例如以下:

from distutils.core import setup    
import py2exe  
import sys

#this allows to run it with a simple double click.
sys.argv.append('py2exe')

# 创建目标目录,并将相应的文件拷贝到目录中
files = [
    ("images",["images/checked.png", "images/float_type.png", "images/int_type.png", "images/text_type.png", "images/running.png"]),
    ("core", ["core/__init__.py", "core/filter.ini"]),
    ("", ["filter.templ", "ui_main.py"])
    ]

py2exe_options = {
        "includes": ["sip"],
        "dll_excludes": ["MSVCP90.dll",],
        "compressed": 1,
        "optimize": 2,
        "ascii": 0,
        "bundle_files": 1,
        }

setup(
      name = 'btfilter',
      version = '1.0',
      windows = ['main.py',], 
      zipfile = None,
      options = {'py2exe': py2exe_options},
      data_files = files
      )

关于py2exe的设置參数,能够參考这里


4、使用Inno Setup Compiler打包成安装包

原文地址:https://www.cnblogs.com/yjbjingcha/p/7232099.html