2020软件工程第二次作业(个人编程)

2020软件工程第二次作业(个人编程)

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 180 120
Estimate 估计这个任务需要多少时间 60 40
Development 开发 500 1200
Analysis 需求分析 (包括学习新技术) 300 500
Design Spec 生成设计文档 60 58
Design Review 设计复审 60 70
Coding Standard 代码规范 (为目前的开发制定合适的规范) 120 110
Design 具体设计 240 150
Coding 具体编码 500 998
Code Review 代码复审 500 400
Test 测试(自我测试,修改代码,提交修改) 500 400
Reporting 报告 200 140
Test Report 测试报告 200 100
Size Measurement 计算工作量 100 80
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 100 60
合计 3520 4426

一、思考历程

最初很困难,看完题目,除了知道要统计分析用户行为数据,其他的都是天书:

  1. 首先是GH Achieve是什么,怎么下载里面的数据;
  2. github actions又是什么;
  3. 最费解的是格式要求里的内容,究竟怎么用;
  4. 对于数据范围10Gb我没有很明确的概念,它所限制的是什么;
  5. github早前已用过,但fork与pull request依然令我不解;
  6. 要编写一个自己的代码规范,至少任务是明确的;
  7. 代码如何向github提交并编译;pep表格是什么;
  8. 单元测试覆盖率优化...

由于没有python基础,期间课多,在查资料、理解代码、写注释、直至弄懂参考代码的解题流程后,已经是第三天。运行代码也是问题,问了同学,费老大劲才明白是命令行。但这也成了切入点,由命令行的启发转去研究格式要求,初始化和查询是分开的、路径是文件夹是由参考代码写法决定的、-i -e等命令是由argparser命令行解析器来解析的...上述问题,虽然现在看起来颇为稚嫩,但在几天前,我明白一个写法,就好像要去发现一座新大陆。第五天我才开始考虑如何优化代码,使之能够支持大文件读入。

很容易想到,大文件读入可采取逐行读入并处理的方式进行优化。便修改了读入模块,但由于参考数据较小,优化没有明显效果。由于如何在GH Archive下载数据那时仍困扰着我,我就开始看大量的大数据处理方法的资料,意图重构代码逻辑,期间了解到处理数据用的第三方库,ijson、pandas、simplejson等,但没有获得有针对性的信息。

期间也纠结于是否换用其他语言,Java并不熟练,C++在命令行参数的处理以及字符串处理上似乎较繁琐,较之python的简洁,实在难以选择。参考代码已给出,只做优化似乎不需要花费太多时间学习python语法。因此最终选择如此。

即使是在GH Archive下载数据这样简单的操作,我也反复看了许多遍。偶然发现GH Achieve里的数据文件只需要通过url下载(之前因此浪费了许多时间)

二、设计实现过程

实现过程并不难:

1)初始化:将json文本(原文本是多个字典)读入 --> 提取关键信息 --> 整理后输出(此时的输出是单个字典)

2)查询:将整理后的文件读入 --> 按字典查询即可。

  • 程序运行流程图:

三、迭代过程描述

​ 在最初仅进行逐行读入的优化基础上测试,效果并不明显... 当数据量达到10 GB,时间很难看到尽头。

​ 接着考虑代码结构上修改,发现在将读入的 json 文件转换得到的字典处理过程中,原代码存在冗余的操作,于 是将其简化,每读入一行即进行一次关键信息筛选并统计。如此一来,在10 GB的测试数据下,能达到3分钟的 初始化速度,几乎不占用内存。

  • V 1.0:
    def _init(self, jsonAddress):
        
        self.__4Events4PerP = {}
        self.__4Events4PerR = {}
        self.__4Events4PerPPerR = {}
        for root, dic, files in os.walk(jsonAddress):
            for f in files:
                if f[-5:] == '.json':                    
                    with open(jsonAddress + '\\' + f, 'r', encoding = 'UTF-8') as f:  
                        while True:
                            i = f.readline()  # 读入优化
                            if not i:
                                break
                            line = json.loads(i)
                            rType = line['type']
                            if rType == 'PushEvent' or rType == 'IssueCommentEvent' or \
                                rType == 'IssueEvent' or rType == 'PullRequestEvent':
                                self._eventNumAdd(line)

        ...
	 def _eventNumAdd(self, dic: dict):  # 结构优化
        rId = dic['actor']['login']
        rRepo = dic['repo']['name']
        rType = dic['type']

        if not self.__4Events4PerP.get(rId, 0):  # *
            self.__4Events4PerP.update({rId: {}})
            self.__4Events4PerPPerR.update({rId: {}})
        self.__4Events4PerP[rId][rType] \
            = self.__4Events4PerP[rId].get(rType, 0) + 1

        if not self.__4Events4PerR.get(rRepo, 0):
            self.__4Events4PerR.update({rRepo: {}})
        self.__4Events4PerR[rRepo][rType] \
            = self.__4Events4PerR[rRepo].get(rType, 0) + 1
            
        if not self.__4Events4PerPPerR[rId].get(rRepo, 0):
            self.__4Events4PerPPerR[rId].update({rRepo: {}})
        self.__4Events4PerPPerR[rId][rRepo][rType] \
            = self.__4Events4PerPPerR[rId][rRepo].get(rType, 0) + 1       

​ 解决了内存问题后,接着进一步考虑速度优化,我想到了多线程与多进程。但由于此前还没有关于线程进程的概念,查找资料花了很多时间,在写的过程中又bug频出:

  • V 2.0:下面是多进程分行的读入优化:
    def __init__(self, jsonAddress: str = None, isInit: int = 0):

        self.list = []
        self.count = 0
        self.num = 32

        if isInit == 1:
            self._init(jsonAddress)
            return  # 优化
        if jsonAddress is None and not os.path.exists("1.json") and not os.path.exists("2.json") and not os.path.exists("3.path"):
            raise RuntimeError("error: init failed")
        x = open('1.json', 'r', encoding='utf-8').read()
        self.__4Events4PerP = json.loads(x)
        x = open('2.json', 'r', encoding='utf-8').read()
        self.__4Events4PerR = json.loads(x)
        x = open('3.json', 'r', encoding='utf-8').read()
        self.__4Events4PerPPerR = json.loads(x)

    def _handle(self, line):
        rType = line['type']
        # 控制仅读入4种关注事件
        if rType == 'PushEvent' or rType == 'IssueCommentEvent' or \
                rType == 'IssueEvent' or rType == 'PullRequestEvent':
            self._eventNumAdd(line)

    def _init(self, jsonAddress):

        self.__4Events4PerP = {}
        self.__4Events4PerR = {}
        self.__4Events4PerPPerR = {}

        pool = multiprocessing.Pool(self.num)

        for root, dic, files in os.walk(jsonAddress):
            for f in files:
                if f[-5:] == '.json':
                    n = 0
                    with open(jsonAddress + '\\' + f, 'r', encoding = 'UTF-8') as f:  # 优化
                        while True:
                            i = f.readline()
                            if not i:
                                break
                            line = json.loads(i)
                            if self.count == self.num:
                                for i in range(n - self.num, n):
                                    pool.apply_async(self._handle, (self.list[i], ))
                                self.count = 0
                            self.list.append(line)
                            n += 1
                            self.count += 1

​ 但事实上发现这种方式并不能解决问题,多进程情况下,根本跑不出结果。

  • V 3.0 :下面是多线程分文件的读入优化
 	def _handle(self, jsonAddress, f):
        with open(jsonAddress + '\\' + f, 'r', encoding='UTF-8') as af:  # 优化
            while True:
                i = af.readline()
                if not i:
                    break
                line = json.loads(i)
                rType = line['type']
                # 控制仅读入4种关注事件
                if rType == 'PushEvent' or rType == 'IssueCommentEvent' or \
                        rType == 'IssueEvent' or rType == 'PullRequestEvent':
                    self._eventNumAdd(line)

    def _init(self, jsonAddress):

        self.__4Events4PerP = {}
        self.__4Events4PerR = {}
        self.__4Events4PerPPerR = {}


        for root, dic, files in os.walk(jsonAddress):
            for f in files:
                if f[-5:] == '.json':
                    t = threading.Thread(target= self._handle, args=(jsonAddress, f))
                    t.start()
                    self.threads.append(t)

        if not self.threads:
            with open('./1.json', 'w', encoding='utf-8') as f:
                json.dump(self.__4Events4PerP, f)  # 写入
            with open('./2.json', 'w', encoding='utf-8') as f:
                json.dump(self.__4Events4PerR, f)
            with open('./3.json', 'w', encoding='utf-8') as f:
                json.dump(self.__4Events4PerPPerR, f)

​ 在多线程情况下对分文件开线程测试,速度反而变慢。也试了多线程的分行以及多进程的分文件,均告败。

​ 查询资料分析原因:多线程读取同一文件时,如果并发执行,会导致读取出的数据有遗漏或重复。要保证不出 错,需要对线程进行同步,可以使用队列或加锁的方式实现。而且似乎python的多线程但这样做并不能提高效 率,我始终没能实现。

​ 由于对python也是初次接触,各种语法问题也耗费我很多时间。我在线程的控制上也还存在问题,写法上bug 也存在,时间限制我没能解决之。

​ 因此最终的版本仍然来自于V 1.0

四、测试(基于 0.99 GB数据):

  • 1)单元测试:

  • 2)单元测试覆盖率:

    初始化覆盖率:

    查询覆盖率:

  • 3)性能测试:

    主要函数时间占比

五、代码规范链接

https://github.com/rebuilder945/2020-personal-python/blob/master/codestyle.md

六、总结

时间与回报总是相宜的,在这次作业中,关于数据处理以及性能优化我有了更多的认识,也看到了自己的不足,接下去会继续学习,注意在这方面的提升。

posted @ 2020-09-16 21:15  吴何  阅读(301)  评论(2编辑  收藏  举报