如何快速解析和整理Python代码中成千上万的数据文件,很多新手都不是很清楚。为了帮助大家解决这个问题,下面小编就为大家详细讲解一下。需要的人可以从中学习,希望你能有所收获。
在这个世界上,人们每天都在用Python做不同的工作。文件操作是每个人都需要解决的最常见任务之一。使用Python,您可以轻松地为他人生成漂亮的报告,并且只需几行代码就可以快速分析和整理成千上万的数据文件。
在编写与文件相关的代码时,我们通常会注意这些事情:我的代码是不是足够快?我的代码有没有事半功倍的完成任务?在本文中,我将与您分享一些与之相关的编程建议。我会向大家推荐一个被低估的Python标准库模块,演示读取大文件的最佳方式,最后分享一下我对函数设计的想法。
现在,让我们进入第一个“模块化安利”时代。
由于注意:不同操作系统的文件系统差异较大,本文的主要编写环境是Mac OS/Linux系统,部分代码可能不适合Windows系统。
00-1010如果您需要用Python进行文件处理,那么
和操作系统
os.path兄弟必须是两个你无法避免的模块。在这两个模块中,有许多与文件路径处理、文件读写和文件状态查看相关的工具功能。
让我用一个例子来展示它们的使用场景。有一个目录有很多数据文件,但是它们的后缀名称并不统一,两者都有。txt,又来了。csv .我们需要把它放进去
结尾的文件。txt全部修改为。csv后缀。
我们可以这样写一个函数:
学习过程中不知道的可以给我补充什么?
学蟒交流扣曲,784758214
群内有不错的学习视频教程、开发工具和电子书。
跟大家分享一下目前python企业的人才需求,如何从零基础学习python,学什么。
1.`importos `
2.`importos.path `
5.`defunify_ext_with_os_path(路径): `
6.'''统一目录。txt文件名后缀为。CSV `。
7.`'''`
8 . ` for filenaminos . listdir(路径): `
9.`basename,ext=os.path.splitext(文件名)`
10.`ifext=='。txt': '
11.`abs_filepath=os.path.join(路径,文件名)`
12.` OS.rename (ABS _ filepath,OS.path.join (path,f' {basename})。CSV ')`让我们看看上面的代码共享了哪些与文件处理相关的功能:
Os.listdir(路径):列出路径目录中的所有文件(包括文件夹)。
Os.path.splitext(文件名):在文件名中拆分基本名称和后缀。
Os.path.join(路径,文件名):组合操作的文件名是绝对路径。
Os.rename(.):重命名文件。
虽然以上功能可以满足要求,但是说实话,即使写了很多年Python代码,我还是觉得:这些函数不光很难记,而且最终的成品代码也不怎么讨人喜欢。
建议一:使用 pathlib 模块
为了让文件处理更容易,Python在3.4版本中引入了一个新的标准库模块:pathlib。基于面向对象的设计,它封装了许多与文件操作相关的功能。如果用它重写上面的代码,结果会大不相同。
使用p
athlib 模块后的代码:
1. `from pathlib import Path` 3. `def unify_ext_with_pathlib(path):` 4. `for fpath in Path(path).glob('*.txt'):` 5. `fpath.rename(fpath.with_suffix('.csv'))`
和旧代码相比,新函数只需要两行代码就完成了工作。而这两行代码主要做了这么几件事:
-
首先使用 Path(path) 将字符串路径转换为
Path
对象 -
调用 .glob(‘*.txt’) 对路径下所有内容进行模式匹配并以生成器方式返回,结果仍然是
Path
对象,所以我们可以接着做后面的操作 -
使用 .with_suffix(‘.csv’) 直接获取使用新后缀名的文件全路径
-
调用 .rename(target) 完成重命名
相比os
和os.path
,引入pathlib
模块后的代码明显更精简,也更有整体统一感。所有文件相关的操作都是一站式完成。
其他用法
除此之外,pathlib 模块还提供了很多有趣的用法。比如使用/
运算符来组合文件路径:
1. `# ???? 旧朋友:使用 os.path 模块` 2. `>>> import os.path` 3. `>>> os.path.join('/tmp', 'foo.txt')` 4. `'/tmp/foo.txt'` 6. `# ✨ 新潮流:使用 / 运算符` 7. `>>> from pathlib import Path` 8. `>>> Path('/tmp') / 'foo.txt'` 9. `PosixPath('/tmp/foo.txt')`
或者使用.read_text()
来快速读取文件内容:
1. `# 标准做法,使用 with open(...) 打开文件` 2. `>>> with open('foo.txt') as file:` 3. `... print(file.read())` 4. `...` 5. `foo` 7. `# 使用 pathlib 可以让这件事情变得更简单` 8. `>>> from pathlib import Path` 9. `>>> print(Path('foo.txt').read_text())` 10. `foo`
除了我在文章里介绍的这些,pathlib 模块还提供了非常多有用的方法,强烈建议去 官方文档 详细了解一下。
如果上面这些都不足以让你动心,那么我再多给你一个使用 pathlib 的理由:PEP-519 里定义了一个专门用于“文件路径”的新对象协议,这意味着从该 PEP 生效后的 Python 3.6 版本起,pathlib 里的 Path 对象,可以和以前绝大多数只接受字符串路径的标准库函数兼容使用:
1. `>>> p = Path('/tmp')` 2. `# 可以直接对 Path 类型对象 p 进行 join` 3. `>>> os.path.join(p, 'foo.txt')` 4. `'/tmp/foo.txt'`
所以,无需犹豫,赶紧把 pathlib 模块用起来吧。
Hint: 如果你使用的是更早的 Python 版本,可以尝试安装 pathlib2 模块 。
建议二:掌握如何流式读取大文件
几乎所有人都知道,在 Python 里读取文件有一种“标准做法”:首先使用withopen(fine_name)
上下文管理器的方式获得一个文件对象,然后使用for
循环迭代它,逐行获取文件里的内容。
下面是一个使用这种“标准做法”的简单示例函数:
1. `def count_nine(fname):` 2. `"""计算文件里包含多少个数字 '9'` 3. `"""` 4. `count = 0` 5. `with open(fname) as file:` 6. `for line in file:` 7. `count += line.count('9')` 8. `return count`
假如我们有一个文件small_file.txt
,那么使用这个函数可以轻松计算出 9 的数量。
1. `# small_file.txt` 2. `feiowe9322nasd9233rl` 3. `aoeijfiowejf8322kaf9a` 5. `# OUTPUT: 3` 6. `print(count_nine('small_file.txt'))`
为什么这种文件读取方式会成为标准?这是因为它有两个好处:
-
with
上下文管理器会自动关闭打开的文件描述符 -
在迭代文件对象时,内容是一行一行返回的,不会占用太多内存
标准做法的缺点
但这套标准做法并非没有缺点。如果被读取的文件里,根本就没有任何换行符,那么上面的第二个好处就不成立了。当代码执行到forlineinfile
时,line 将会变成一个非常巨大的字符串对象,消耗掉非常可观的内存。
让我们来做个试验:有一个
5GB 大的文件big_file.txt
,它里面装满了和small_file.txt
一样的随机字符串。只不过它存储内容的方式稍有不同,所有的文本都被放在了同一行里:
1. `# FILE: big_file.txt` 2. `df2if283rkwefh... <剩余 5GB 大小> ...`
如果我们继续使用前面的count_nine
函数去统计这个大文件里9
的个数。那么在我的笔记本上,这个过程会足足花掉
65 秒,并在执行过程中吃掉机器
2GB 内存 [注1]。
使用 read 方法分块读取
为了解决这个问题,我们需要暂时把这个“标准做法”放到一边,使用更底层的file.read()
方法。与直接循环迭代文件对象不同,每次调用file.read(chunk_size)
会直接返回从当前位置往后读取chunk_size
大小的文件内容,不必等待任何换行符出现。
所以,如果使用file.read()
方法,我们的函数可以改写成这样:
1. `def count_nine_v2(fname):` 2. `"""计算文件里包含多少个数字 '9',每次读取 8kb` 3. `"""` 4. `count = 0` 5. `block_size = 1024 * 8` 6. `with open(fname) as fp:` 7. `while True:` 8. `chunk = fp.read(block_size)` 9. `# 当文件没有更多内容时,read 调用将会返回空字符串 ''` 10. `if not chunk:` 11. `break` 12. `count += chunk.count('9')` 13. `return count`
在新函数中,我们使用了一个while
循环来读取文件内容,每次最多读取 8kb 大小,这样可以避免之前需要拼接一个巨大字符串的过程,把内存占用降低非常多。
利用生成器解耦代码
假如我们在讨论的不是 Python,而是其他编程语言。那么可以说上面的代码已经很好了。但是如果你认真分析一下count_nine_v2
函数,你会发现在循环体内部,存在着两个独立的逻辑:数据生成(read 调用与 chunk 判断) 与
数据消费。而这两个独立逻辑被耦合在了一起。
为了提升复用能力,我们可以定义一个新的chunked_file_reader
生成器函数,由它来负责所有与“数据生成”相关的逻辑。这样count_nine_v3
里面的主循环就只需要负责计数即可。
1. `def chunked_file_reader(fp, block_size=1024 * 8):` 2. `"""生成器函数:分块读取文件内容` 3. `"""` 4. `while True:` 5. `chunk = fp.read(block_size)` 6. `# 当文件没有更多内容时,read 调用将会返回空字符串 ''` 7. `if not chunk:` 8. `break` 9. `yield chunk` 12. `def count_nine_v3(fname):` 13. `count = 0` 14. `with open(fname) as fp:` 15. `for chunk in chunked_file_reader(fp):` 16. `count += chunk.count('9')` 17. `return count`
进行到这一步,代码似乎已经没有优化的空间了,但其实不然。iter(iterable) 是一个用来构造迭代器的内建函数,但它还有一个更少人知道的用法。当我们使用iter(callable,sentinel)
的方式调用它时,会返回一个特殊的对象,迭代它将不断产生可调用对象 callable 的调用结果,直到结果为 setinel 时,迭代终止。
1. `def chunked_file_reader(file, block_size=1024 * 8):` 2. `"""生成器函数:分块读取文件内容,使用 iter 函数` 3. `"""` 4. `# 首先使用 partial(fp.read, block_size) 构造一个新的无需参数的函数` 5. `# 循环将不断返回 fp.read(block_size) 调用结果,直到其为 '' 时终止` 6. `for chunk in iter(partial(file.read, block_size), ''):` 7. `yield chunk`
最终,只需要两行代码,我们就完成了一个可复用的分块文件读取函数。那么,这个函数在性能方面的表现如何呢?
和一开始的
2GB 内存/耗时 65 秒 相比,使用生成器的版本只需要
7MB 内存 / 12 秒 就能完成计算。效率提升了接近 4 倍,内存占用更是不到原来的 1%。
建议三:设计接受文件对象的函数
统计完文件里的 “9” 之后,让我们换一个需求。现在,我想要统计每个文件里出现了多少个英文元音字母(aeiou)。只要对之前的代码稍作调整,很快就可以写出新函数count_vowels
。
在学习过程中有什么不懂得可以加我的 python学习交流扣扣qun,784758214 群里有不错的学习视频教程、开发工具与电子书籍。 与你分享python企业当下人才需求及怎么从零基础学习好python,和学习什么内容 1. `def count_vowels(filename):` 2. `"""统计某个文件中,包含元音字母(aeiou)的数量` 3. `"""` 4. `VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}` 5. `count = 0` 6. `with open(filename, 'r') as fp:` 7. `for line in fp:` 8. `for char in line:` 9. `if char.lower() in VOWELS_LETTERS:` 10. `count += 1` 11. `return count` 14. `# OUTPUT: 16` 15. `print(count_vowels('small_file.txt'))`
和之前“统计 9”的函数相比,新函数变得稍微复杂了一些。为了保证程序的正确性,我需要为它写一些单元测试。但当我准备写测试时,却发现这件事情非常麻烦,主要问题点如下:
-
函数接收文件路径作为参数,所以我们需要传递一个实际存在的文件
-
为了准备测试用例,我要么提供几个样板文件,要么写一些临时文件
-
而文件是否能被正常打开、读取,也成了我们需要测试的边界情况
如果,你发现你的函数难以编写单元测试,那通常意味着你应该改进它的设计。上面的函数应该如何改进呢?答案是:让函数依赖“文件对象”而不是文件路径。
修改后的函数代码如下:
1. `def count_vowels_v2(fp):` 2. `"""统计某个文件中,包含元音字母(aeiou)的数量` 3. `"""` 4. `VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}` 5. `count = 0` 6. `for line in fp:` 7. `for char in line:` 8. `if char.lower() in VOWELS_LETTERS:` 9. `count += 1` 10. `return count` 13. `# 修改函数后,打开文件的职责被移交给了上层函数调用者` 14. `with open('small_file.txt') as fp:` 15. `print(count_vowels_v2(fp))`
这个改动带来的主要变化,在于它提升了函数的适用面。因为 Python 是“鸭子类型”的,虽然函数需要接受文件对象,但其实我们可以把任何实现了文件协议的 “类文件对象(file-like object)” 传入count_vowels_v2
函数中。
而 Python 中有着非常多“类文件对象”。比如 io 模块内的 StringIO 对象就是其中之一。它是一种基于内存的特殊对象,拥有和文件对象几乎一致的接口设计。
利用 StringIO,我们可以非常方便的为函数编写单元测试。
1. `# 注意:以下测试函数需要使用 pytest 执行` 2. `import pytest` 3. `from io import StringIO` 6. `@pytest.mark.parametrize(` 7. `"content,vowels_count", [` 8. `# 使用 pytest 提供的参数化测试工具,定义测试参数列表` 9. `# (文件内容, 期待结果)` 10. `('', 0),` 11. `('Hello World!', 3),` 12. `('HELLO WORLD!', 3),` 13. `('你好,世界', 0),` 14. `]` 15. `)` 16. `def test_count_vowels_v2(content, vowels_count):` 17. `# 利用 StringIO 构造类文件对象 "file"` 18. `file = StringIO(content)` 19. `assert count_vowels_v2(file) == vowels_count`
使用 pytest 运行测试可以发现,函数可以通过所有的用例:
1. `❯ pytest vowels_counter.py` 2. `====== test session starts ======` 3. `collected 4 items` 5. `vowels_counter.py ... [100%]` 7. `====== 4 passed in 0.06 seconds ======`
而让编写单元测试变得更简单,并非修改函数依赖后的唯一好处。除了 StringIO 外,subprocess 模块调用系统命令时用来存储标准输出的 PIPE 对象,也是一种“类文件对象”。这意味着我们可以直接把某个命令的输出传递给count_vowels_v2
函数来计算元音字母数:
1. `import subprocess` 3. `# 统计 /tmp 下面所有一级子文件名(目录名)有多少元音字母` 4. `p = subprocess.Popen(['ls', '/tmp'], stdout=subprocess.PIPE, encoding='utf-8')` 6. `# p.stdout 是一个流式类文件对象,可以直接传入函数` 7. `# OUTPUT: 42` 8. `print(count_vowels_v2(p.stdout))`
正如之前所说,将函数参数修改为“文件对象”,最大的好处是提高了函数的
适用面 和
可组合性。通过依赖更为抽象的“类文件对象”而非文件路径,给函数的使用方式开启了更多可能,StringIO、PIPE 以及任何其他满足协议的对象都可以成为函数的客户。
不过,这样的改造并非毫无缺点,它也会给调用方带来一些不便。假如调用方就是想要使用文件路径,那么就必须得自行处理文件的打开操作。
如何编写兼容二者的函数
有没有办法即拥有“接受文件对象”的灵活性,又能让传递文件路径的调用方更方便?答案是:有,而且标准库中就有这样的例子。
打开标准库里的xml.etree.ElementTree
模块,翻开里面的ElementTree.parse
方法。你会发现这个方法即可以使用文件对象调用,也接受字符串的文件路径。而它实现这一点的手法也非常简单易懂:
1. `def parse(self, source, parser=None):` 2. `"""*source* is a file name or file object, *parser* is an optional parser` 3. `"""` 4. `close_source = False` 5. `# 通过判断 source 是否有 "read" 属性来判定它是不是“类文件对象”` 6. `# 如果不是,那么调用 open 函数打开它并负担起在函数末尾关闭它的责任` 7. `if not hasattr(source, "read"):` 8. `source = open(source, "rb")` 9. `close_source = True`
使用这种基于“鸭子类型”的灵活检测方式,count_vowels_v2
函数也同样可以被改造得更方便,我在这里就不再重复啦。
如果你依然在编程的世界里迷茫,可以加入我们的Python学习扣qun:784758214,看看前辈们是如何学习的!交流经验!自己是一名高级python开发工程师,从基础的python脚本到web开发、爬虫、django、数据挖掘等,零基础到项目实战的资料都有整理。送给每一位python的小伙伴!分享一些学习的方法和需要注意的小细节,点击加入我们的
python学习者聚集地
文件操作我们在日常工作中经常需要接触的领域,使用更方便的模块、利用生成器节约内存以及编写适用面更广的函数,可以让我们编写出更高效的代码。
让我们最后再总结一下吧:
-
使用 pathlib 模块可以简化文件和目录相关的操作,并让代码更直观
-
PEP-519 定义了表示“文件路径”的标准协议,Path 对象实现了这个协议
-
通过定义生成器函数来分块读取大文件可以节约内存
-
使用
iter(callable,sentinel)
可以在一些特定场景简化代码 -
难以编写测试的代码,通常也是需要改进的代码
-
让函数依赖“类文件对象”可以提升函数的适用面和可组合性
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注行业资讯频道,感谢您对的支持。
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/133893.html