fzu2020软工作业2

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/SE2020
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/SE2020/homework/11167
这个作业的目标 大数据处理,json解析,文件io
学号 031802504

PSP 表格

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

解题思路

  • 先看作业描述,下载示例数据
  • 根据作业要求fork项目代码并clone到本地
  • 阅读github actions相关文档,了解评测步骤
  • 认真看一遍参考程序,理解操作
  • 学习github的commit规范
  • 参照已有代码规范编写本项目的codestyle.md
  • 编写程序
  • 编写单元测试
  • 搜索学习cProfile和Coverage
  • 优化程序
  • 写完博客并提交作业

设计实现/迭代过程

优化的参考程序

参考程序主要有两点影响了运行速度,一个是一次读出整个文件,另一个是对字典的递归解嵌套。
初版据此优化了变量命名、函数逻辑、文件读取等,得到了较快的速度(25M 0.9s)。

进一步优化

通过性能测试发现耗时严重的主要是json的解析
因此读原始文件时逐行使用正则表达式匹配得到所需的信息三元组
计数完成后再通过pickle库写入结果文件

  • pandas
    一开始试图用pandas读取json,然而查了资料发现偏离了它原本的用途,改用正则表达式

  • 正则变慢
    EVENTS = ("PushEvent", "IssueCommentEvent", "IssuesEvent", "PullRequestEvent", )
    r'.*?((?:%s)Event).*?actor.*?"login":"(\\S+?)".*?repo.*?"name":"(\\S+?)"' % '|'.join(EVENTS)
    这个正则匹配速度非常慢


运行速度提升近一倍

协程

        async with aiofiles.open(filename, 'r', encoding='utf-8') as f:
            async for line in f:
                res = pattern.search(line)
            ...

考虑到文件io仍是性能瓶颈,于是想并发读取文件并处理数据,而众所周知python由于GIL的存在多线程表现并不好,于是想借助asyncio模块进行异步读取。看了python官方文档及博客等很多资料都没有找到相关用法,最多只有网络io。最后在Stack Overflow上看到有人提及aifiles库,配合asyncio试了一下发现速度反而慢了几倍,大概是由于它是多线程并非真正的协程,而且异步的readline造成了线程锁的不断切换,或许改成一次性全部读出并配合json.load效果会更好?

多进程

        pool = Pool(processes=cpu_count())
        for cur_dir, sub_dir, filenames in os.walk(dir_path):
            for name in filenames:
                pool.apply_async(self.parse_events, args=(f'{cur_dir}/{name}', u_e, r_e, ur_e))
            ...

既然协程行不通,那么多进程总可以了吧。
用到的是python的标准库multiprocessing ,利用其中的Pool根据cpu数构建一个进程池,将对文件的读取处理抽象成一个函数,递归查找目录下json文件并将文件名作为参数通过线程池调用处理函数。
马上就遇到了一个棘手的问题,各进程之间无法直接交换数据。查看了文档后发现了管道Pipe通信,但send与recv的时机很难把握,尝试后发现了奇怪的结果,大概确实只能用于两个进程。

后来尝试了管理器Manager,将那三个事件字典换成manager.dict并作为参数传入,预期是各个能够共同修改它,然而一直有问题。后来发现是manager的字典不能检测到深层的修改,于是用了个三中间变量,最后这个特殊的dict还得转换成dict保存才总算跑通,比协程又更加慢了,144M数据花了17秒,大部分时间都在切换进程,剩下的时间又有一大部分花在了进程通信,或许采用官方不推荐的内存变量共享效果会好一些。

多线程

结果最后还是回到了多线程,毕竟理论上即便有锁,对于io应该也是有提升的。
利用concurrent.futures.ThreadPoolExecutor构建了线程池,与多进程时一样将文件名传给各个线程处理,此时不需要考虑变量问题,代码得以简化很多。
将近600M数据的测试默认的40大小线程池需要花费3.5s初始化,简直是重大突破,

但如果线程池大小为1则只需要3s,问题还是出在了线程的频繁切换,大概让线程一次读出整个文件会好些,但岂不是又有可能内存不足...主要还是没有真正的数据测试很难决定。

关键函数流程图(analyse)

代码说明

  1. Run.analyse
    def analyse(self):
        args = self.parser.parse_args()
        event, user, repo = args.event, args.user, args.repo

        if args.init:
            self.data.init(args.init)
            return 'init done'
        self.data.load()

        if not event:
            raise RuntimeError('error: the following arguments are required: -e/--event')
        if not user and not repo:
            raise RuntimeError('error: the following arguments are required: -u/--user or -r/--repo')

        if user and repo:
            res = self.data.user_repo_events.get(user, {}).get(repo, {}).get(event, 0)
        elif user:
            res = self.data.user_events.get(user, {}).get(event, 0)
        else:
            res = self.data.repo_events.get(repo, {}).get(event, 0)
        return res

根据参数决定初始化或者取数据

  1. Data.init
    def init(self, dir_path: str):
        pool = ThreadPoolExecutor()
        for cur_dir, sub_dir, filenames in os.walk(dir_path):
            filenames = filter(lambda r: r.endswith('.json'), filenames)
            for name in filenames:
                pool.submit(self.__count_events, f'{cur_dir}/{name}')
        pool.shutdown()

        with open('0.pkl', 'wb') as f:
            pickle.dump(self.user_events, f)
        ...

创建线程池,递归目录,筛选数据文件,线程池调用计数函数完成数据处理,最后将处理好的三个字典序列化写入文件

  1. Data.__count_events
    def __count_events(self, filename: str):
        with open(filename, 'r', encoding='utf-8') as f:
            for line in f:
                res = pattern.search(line)
                if res is None or res[1] not in EVENTS:
                    continue

                event, user, repo = res.groups()
                self.user_events.setdefault(user, {})
                self.user_repo_events.setdefault(user, {})
                self.repo_events.setdefault(repo, {})
                self.user_repo_events[user].setdefault(repo, {})

                self.user_events[user][event] = self.user_events[user].get(event, 0)+1
                self.repo_events[repo][event] = self.repo_events[repo].get(event, 0)+1
                self.user_repo_events[user][repo][event] = self.user_repo_events[user][repo].get(event, 0)+1

打开json文件中逐行使用正则表达式匹配信息,抽取所需三元组(event, user, repo),并存入字典

单元测试

截图

描述

  • 对Data类测试初始化(数据组成为4份2020-01-01-15.json + 1份2015-01-01-15.json),借助path.exists断言文件成功生成
  • 三种情形下事件的数量,通过与参考程序运行结果对比来验证正确性
  • 此处因为同文件有四份,因此事件数也对应*4,结果正确

单元测试覆盖率


因为此处没有测试需要命令行参数的Run类,因此覆盖率仅有67%

性能优化和性能测试

多线程

注释掉线程池(单进程单线程)

总结

  • 以上测试均为初始化(--init)测试,实际的运行(-e/-u/-r)约0.1s
  • 数据为4份2020-01-01-15.json + 1份2015-01-01-15.json,共589M
  • 就目前来看线程越多效率越低,差距明显(约0.5s),令人失望

代码规范链接

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

总结

  • python的文件读取各种花样(协程/多进程/多线程)终究还是比不上淳朴的readline
  • json解析很花时间,但使用正则表达式也不是越长越好
  • 对于任务应该不管好坏先做出来再慢慢优化,不然最后可能会来不及
posted @ 2020-09-16 21:19  NoNoe  阅读(121)  评论(1编辑  收藏  举报