每天一个Python小技巧(12).md

2022/6/13 Python小技巧

本文以真实场景为例,讲述aiofiles找出项目中关键字相关的使用体验,也警醒自己,不要盲目使用异步。

# 打招呼

大家好,我是大家熟悉的技巧君,故事来源于生活,今天我要说的是关于文件读取的内容,还有其中的一些踩坑二三事。

# 故事

前几天,博主接到了老板的一个任务,是因为我们这边做了一些数据库连接的改造,让我们检查一下咱们组开发的项目里面有没有对应的关键字

比如给一个文件目录,让我去里面批量找每个文件的文本内容,最后查询出有没有类似的关键字,有的话就捞出来让开发进行修改。

# 思考方案

其实我最开始想的比较简单,就是直接拿到文本数据,然后判断keyword是否在文本中就行。

但事情没有想象的简单,首先我们要匹配的内容可能做了数据库分片,比如可能需要匹配sharding00~64这种文本,所以我们需要支持正则,然后我们需要找出对应的行数,也就是说in是行不通了。

# 那我们就转换方案,for line in f, 然后去line里面匹配关键字,这样就有行号了。

接着我们来看我的实际解决方案:

# 遍历文件夹,获取所有文件

我们知道os.walk是可以拿到文件夹下面所有文件信息的,所以我们编写了这样的方法:

def get_files(dir_path):
    file_list = []
    for root, dirs, files in os.walk(dir_path):
        for f in files:
            if root.endswith(".git") or f.endswith(ignore_file):
                continue
            filepath = os.path.join(root, f)
            file_list.append(filepath)
    return file_list
1
2
3
4
5
6
7
8
9

由于公司项目都是git维护的,我们不需要检测.git里面的文件,这样可以加快获取文件的速度。最后我们返回了文件列表,里面的内容是文件的具体路径。

至于f.endswith(ignored_file),是因为项目中存在很多图片文件,我们也不需要检测,因为它不是文本。

# 尝试同步写法

def read_file_sync(files):
    keyword = "xxx"
    flag = re.compile(keyword)
    for file in files:
        with open(file, mode='r', encoding='utf-8') as f:
            current = 0
            for line in f:
                current += 1
                data = re.findall(flag, line)
                if len(data) > 0:
                    print(f"有文件存在敏感信息, 请检查: \nfile: {file}, keyword: {keyword} line: {current}")
1
2
3
4
5
6
7
8
9
10
11

一气呵成,写下同步读取文件的代码,这样就可以拿到对应的line和文件了。

# 踩坑1

项目文件中有非utf-8编码的文件,导致直接打开文件失败:

红框部分即非utf-8内容

所以我们需要打开文件的时候,不但加上try except处理,还要加上一个参数:errors="ignore",这样就可以忽略对应的错误。

# 异步上瘾

因为最近异步库使用的比较多,所以我根据了解,选定了aiofiles去替代同步的open

其实这个库没有什么难度,封装得几乎与open的api一致,所以要上手的话会非常快。所以我也尝了下新鲜:

import aiofiles
async def read_single_async(filepath, keyword):
    flag = re.compile(r)
    try:
        async with aiofiles.open(filepath, mode='r', encoding='utf-8', errors="ignore") as f:
            data = await f.readlines()
            for idx, line in enumerate(data, 1):
                data = re.findall(flag, line)
                if len(data) > 0:
                    print(f"有文件存在敏感信息, 请检查: \nfile: {filepath}, keyword: {keyword} line: {idx}")
    except Exception as e:
        print(f"{filepath}解析内容失败, error: {e}")
1
2
3
4
5
6
7
8
9
10
11
12

可以看到,除了await和async的添加以外,open换成了aiofiles.open,其他几乎没有差异。

真实的代码远远不只这些,我是抽离出了最核心的部分,提供给需要制作类似关键字检查工具的小伙伴。

# 踩坑2

给想要继续使用aiofiles的人一些建议:

  1. 虽然官网给出了async for异步迭代器的功能,但是我实测速度非常慢,比同步还慢。千万不要使用!!!

  2. 同步和异步速度差距不大,建议想使用aiofiles的比对以后再说。

写这个例子是想说明,异步的aiofiles在读文件方面,没有想象中那么给力。

我个人觉得是这样:

能用到异步的地方(节省时间)一般是在await f.readlines(),但是我观察了,文件不大的时候是看不出来差距的,几乎一样的速度。 而我们同步的时候

# 踩坑3

在我极力想要验证aiofiles和open差距的时候,我把文件扩大到了200MB左右,发现差距不大。再继续扩大的话,报了内存超出的错误。

但同步就表现的很好,虽然我一个文件夹里面4个600MB的文件,耗时也才20多秒可以读完所有。

异步的aiofiles可能因为async for并不完善,导致一直卡在那边。而readlines一次性读出文件又会引起内存超出的错误,所以结论就是,慎用aiofiles,除非你测试的效果比预期更好。

# 测试截图

  • 大文件测试

周报目录文件

同步耗时

等了很久都没有等到结束

  • 去掉100MB+的文件,改用readlines

文件最多是20多M,这样readlines不会memory error

同步1.5s 非常优秀

异步1.6s

当然1次测试是具有一定的偶然性,但是差距并不如aiohttp那样夸张,也可能因为readlines相对比较快,比起网络io的话。如果aiofiles的async for真的有用的话,我想aiofiles理论上还是会快一些的。