第五章 python的文件操作

5.1 计算机底层的文件操作原理

参考文档: 参考个人博客

1 文件系统

​ 磁盘用于永久性的存储数据,若要使用磁盘读写数据,则首先要对磁盘进行分区,分区之后还要对分区进行格式化,此后才能使用磁盘读写数据;格式化即创建文件系统的过程,文件系统本身相当于一个软件,它存放在磁盘的某个位置上的,而不是直接存放在某个分区上。

​ 无论文件系统是日志型文件系统还是非日志型系统,在分区格式化创建文件系统的过程中,分区都会划分为两个逻辑区域:inode区、block区;其中inode用于存放文件的元数据信息(文件的属性信息、文件持有的锁列表)以及一个指向block区的指针; block区域则用于存放文件的内容;

文件的查找

​ 文件系统通过文件名查找到inode,然后通过inode的指针查找到对应的block;从而实现获取文件内容的过程;

​ 文件名到inode的映射关系,存放在文件所在的目录的block信息中;有系统内核自引用的/根目录开始,根据文件的路径去逐级目录查找,最终获取到文件的inode信息;

总结

2 计算机底层对文件的处理

文件描述符

​ 在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。

​ 计算机底层是只能识别二进制码01,而不能识别其它字符,对于其它字符则是采用编码表完成转换成二进制之后才能识别;而文件名计算机底层同样是不能直接识别的,在计算机的底层其并不是采用编码表完成转换之后识别,而是采用一个特定的非符整数进行标识,此标识符就是文件描述符;

​ 文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。

​ 程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。
​ 文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际实现过程中内核是会做相应的处理的,一般最大打开文件数会是系统内存的10%(以KB来计算)(称之为系统级限制),查看系统级别的最大打开文件数可以使用sysctl -a | grep fs.file-max命令查看。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。在Web服务器中,通过更改系统默认值文件描述符的最大值来优化服务器是最常见的方式之一;

​ 每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。

数据结构 维护级别
文件描述符表 进程级
打开文件表 系统级
inode表 文件系统级

1)(进程级)描述符表 : 进程每次打开一个文件,系统就会在该进程的用户文件描述符表中分配一个相应的表项,表项的索引返回给该进程,用于标志该文件,找个索引就是常说的文件描述符。每个进程都有它独立的描述符表。
2) 打开文件表(又叫做系统级的描述符表) : 表格中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:

所有的进程共享这张表.每个文件表的表项组成包括有当前的文件位置,引用计数(当前)指向该表项的描述符项数,以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
3)v-node表 : 同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员.

进程级文件描述符表

​ 每个进程维护一个其独有的文件描述符表,进程每打开一个文件,系统就会在该进程的文件描述符表中创建一个条目;条目的索引值返回给进程,用于标识该文件,此索引值即为文件的描述符;

​ 文件的描述符,从0开始,其中数字0-2为系统保留的文件描述符值;posix标准规定了进程每次打开文件时必须使用当前进程最小可用的文件描述符号码;

表内容 说明
文件描述符操作标志 此类标志仅定义了一个,即close-on-exec标志
指向打开文件表条目的指针 此指针指向系统级文件打开表的一个条目,此条目存放了该文件的所有的信息;

系统级打开文件表

​ 系统为所有打开的文件维护一个系统级的文件描述符表,即打开文件表;打开文件表中记录了当前系统上所有进程打开的文件的信息,打开文件表的每一个条目称为一个文件句柄;系统级打开文件表是所有进程共享的;

​ 文件句柄:记录了进程打开的某个特定文件的全部信息;

文件句柄内容 说明
文件偏移量(offset) 记录文件打开时光标的位置,调用read() write()方法时会直接导致offset的值发生变化;
文件打开状态标记 即调用open()打开文件时的flags参数;
inode指针 此指针指向文件系统级别的一个条目,该条目记录文件的属性、锁以及指向block的指针;

​ 扩展说明:

​ 打开文件表的一个条目--文件句柄本身并没有记录完整的打开的文件的信息,而是结合inode指针指向的inode条目,从而实现了记录一个打开文件的如下信息:

  • 文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)

  • 打开文件时所使用的状态标识(即,open()的flags参数)

  • 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)

  • 与信号驱动相关的设置

  • inode指针

  • 文件元数据信息

  • 文件锁列表指针

文件系统级别inode表

​ 系统维护了一个文件系统级别的inode表,此表记录了文件系统上每个文件的属性信息、文件锁信息以及指向存放文件内容的block区的指针;文件系统级inode表式所有的进程共享的;

表条目的内容 说明
文件的元数据信息 保存文件的属性信息: 如大小、时间戳、类型、权限等属性信息;
文件的锁列表 保存文件的锁信息,避免多个进程同时写文件造成文件损坏的机制;
block指针 保存有存放文件内容的内存地址;

三张表之间的关系

3.文件偏移量
类型 说明
功能 文件偏移量用于在进行文件操作时,定位光标的位置;
定义 文件偏移量指的是从指定的位置向前或向后移动的字节数;文件偏移量标识了当前文件正在处理的位置,即光标所在的位置
类型 分类依据:按照指定的位置的不同进行划分;
type1: 从文件开头向后移动若干字节数;起始地址为0;
type2: 从文件末尾向前移动若干字节数;起始地址为-1;
type3: 相对偏移量,相对某个指定的位置向后移动若干字节数;

文件偏移量的值

​ 标识方法:(offset,whence)

​ 说明:

​ offset: 表示从文件的whence位置开始偏移的位置大小,其单位:字节

​ whence:标识文件偏移的起始位置,其有三种: 文件开头、当前位置、文件结尾三种;

offset的值
offset的值:有正值和负值两种,其主要用于标识光标相对于whence位置上的前后移动的字节数;
正值标识向后移动的字节数,而负值则是标识向前移动的字节数;

偏移的类型whence

whence offset类型 offset值类型 文件打开模式
0 相对开始位置 只能为正值 t b模式均可,默认类型
1 相对上次光标所在位置 正负值均可 只能为b模式
2 相对文件结束位置 正负值均可 只能为b模式

扩展说明
case1:文件偏移量是一个数字,该数字有正负两种情况,该数字表示光标从某个位置向前或向后移动多少个字节;文件偏移量为正值 标识向后移动的字节数,而文件偏移量为负值则标识从指定位置向前移动的字节数;

​ case2:文件的读写操作都会直接造成文件偏移量值的变化,即光标的移动;
​ 读写一个字符或行等,光标都会向后移动若干个字节,即文件偏移量就会相应的变化;

5.2 打开文件

1.文件处理的步骤

​ step1: 通过open()打开文件,获取到一个文件句柄;
​ step2: 通过该文件句柄操作文件;
​ step3: 关闭文件;
扩展说明

​ case1: open()打开一个文件,并未将文件的内容加载到内存中,而是仅仅获取到一个访问文件内容的对象--文件句柄

​ case2: 文件一旦打开进行操作之后,在内存中就会产生数据,这些数据并未写入到磁盘上,在完成文件操作之后一定要关闭文 件让内存中的文件的内容同步到磁盘上,同时若不关闭文件,则会导致内存中的内容一直消耗内存资源;

2.打开文件

语法

​ open('/path/to/file','mode',encoding='xx')

​ with open(xxx) as var , open(xxx) as var:

参数说明:

​ 文件路径: 此路径可以为相对路径(相对于当前python程序文件所在路径),也可以为相对于文件系统的绝对路径;

​ mode: 后续详解;

​ encoding: 指定python解释器打开文件时采用的编码方式;
​ 此编码方式即解码文件内容的编码方式,同时也是程序代码写文件时的编码方式;

3 文件打开读写模式
读写模式 文件存在性 默认偏移量
r 文件必须存在,否则报错 offset=0 只读模式,只能读不能写
调用读方法会引起offset变化
w 文件不存在:创建新文件
文件存在: 创建新文件
覆盖源文件
offset=0|-1 只写模式,只能写不能读
调用写方法会引起offset变化;
通过offset操作可以指定写的位置
若写的位置存在内容,则覆盖写入
a 文件不存在:创建新文件
文件存在:不做任何操作
offset=-1 只追加写模式,只能追加写不能读
offset=-1永远不变,无法通过offset操作指定写的位置
无论如何操作offset,写之前offset会自动设置为-1
r+ 同r模式 同r模式 可读可写模式;
可以操作offset实现在指定的位置读写
若光标所在的位置有内容,则是覆盖写入;
w+ 同w模式 同w模式 可读可写模式;
可以操作offset实现在指定的位置读写
若光标所在的位置有内容,则是覆盖写入
a+ 同a模式 同a模式 可读可写模式
读:可以操作offset实现指定位置读
写:offset=-1永远不变,只能追加写入;
只要调用写方法,在写之前系统底层会首先设置offset=-1然后写
4 文件打开编码模式
模式 应用方向 处理对象 读&写
t 处理文本文件 str 读:读取到的磁盘上的二进制采用open()指定的编码方式解码后返回
写:将要写入的字符串采用open(),指定的方式进行编码后写入到磁盘
b 爬虫应用
未知编码文件
(图片、音频、视频)
二进制(bytes) 读:直接返回磁盘上获取的二进制bytes;
写:将要写入的字符串采用str.encode()之后写入二进制bytes;

5.3 文件操作方法

操作方法总结---20个

操作类别 方法 统计
上下文管理协议双下方法 [__enter__,__exit__] 2
读方法 [read,readline,readlines] 3
写方法 [write,writelines] 3
关闭文件 [close] 1
文件偏移量 [seek,tell] 2
判定类方法 [readable,writable,closed,seekable,isatty] 5
其它 [mode,name,fileno,flush,truncate] 5
总结 20

读方法--3个
read(self,n=-1)
功能: 带参数n则读取光标当前所在位置后的n个字符/字节的内容;不带参数则读取光标所在位置后的全部内容;

​ 返回值:文件的内容字符串;

​ 参数n:值为数字,单位为字符/字节;
​ 文本打开模式:n的单位为字符;
​ 二进制打开模式:n的单位为字节;

​ 光标移动: 带参数n,则光标移动到当前位置后n个字符后;不带参数光标移动到文件尾部;

​ readline(self,n=-1):
​ 功能:带参数n则是读取光标所在行的光标后的n个字符,若n大于一行的长度仍然只会返回一行的内容;
​ 不带参数n则是读取光标所在位置到光标所在行行尾的内容;

​ 返回值:指定的行内容字符串;

​ 参数n:值为数字,单位为字符/字节;
​ 文本打开模式:n的单位为字符;
​ 二进制打开模式:n的单位为字节;

​ 光标移动: 不带参数,光标移动到下一行的行首;带参数光标移动到当前行当前位置后n个字符;

​ readlines(self,n=-1)
​ 功能:不带参数,读取文件的所有内容,将文件的每一行作为列表的一个元素;
​ 带参数n,则是读取光标所在位置后的n个字符,若第n个字符在第n行,则输出这n行的内容,并将这n行内容,每行的内容 作为一个列表元素进行输出;

​ 参数n:值为数字,单位为字符/字节;
​ 文本打开模式:n的单位为字符;
​ 二进制打开模式:n的单位为字节;
​ 返回值:列表,列表中的元素即为文件的一行的内容,行尾会带上换行符号;
​ 光标移动:光标以行为单位进行移动,本次只要输出了某行,光标就移动到下一行;

扩展说明
read()会导致当前文件的文件偏移量发生变化,即导致光标的位置发生变化;read()了多少个字符光标就会向后移动多少个字符,在文件没有关闭的情况下,继续read(),读到的内容会是上次read()之后产生光标移动了的新位置后的内容;同样的readline() readlines()也会导致文件偏移量变化,从而引起光标位置的变化;所有的读操作都会产生文件偏移量,而文件偏移量就会导致光标位置的变化;

写方法--2个

方法 return 功能
write(self,str) number 向文件中写入指定的str,若要换行则采用\n进行换行;
扩展说明:
文本模式:写入的为字符串,返回值为写入的字符的个数;
二进制模式:写入的为bytes,返回值为写入的字节的个数;
writelines(self,iterable) None 调用for循环将iterable中每个元素写入到文件中;注意若每个iterable的每个元素中不包含有\n换行符则各个元素是不会换行写入的,而是依次写入;

扩展说明
wirte()会导致当前文件的文件偏移量发生变化,即导致光标的位置发生变化;write()了多少个字符光标就会向后移动多少个字符;同理wirtelines()也会导致文件偏移量变化,从而引起光标位置的变化;所有的写操作都会产生文件偏移量,而文件偏移量就会导致光标位置的变化;

关闭文件--1个

方法 return 功能
close(self) None 关闭文件,将内存中的文件内容写入到磁盘;
根据open()中closefd参数的设置选择是否回收文件描述符;

文件偏移量操作--2个

方法 return 功能
tell(self) number 返回文件的文件偏移量的值;
seek(self,offset,whence) offset seek()函数用于设置文件偏移量的类型和文件偏移量的值;

seek(self,offset,whence)
功能: seek()函数用于设置文件偏移量的类型和文件偏移量的值;
参数:
offset: 数字,有正值和负值;用于标识光标相对于whence参数设定位置前后移动的字节数;
正值标识向后移动的字节数,而负值则是向前移动的字节数;
whence: 定义文件偏移量的类型;可选参数;
0: 相对于文件的起始位置的偏移值,默认值;此种情况下,offset值只能为正值;
1: 相对于光标上次的位置的偏移值,offset即可以向前移动也可以向后移动,此时offset即可以为正值也可以为负值;
2: 相对于文件尾部的偏移量值,此种情况下光标只能向前移动,故此offset为正值负值均可;
重点说明
若要采用非默认的即并非相对于文件开头的文件偏移量,则文件的打开模式必须要设置为b模式-即字节模式,否则会报错;

判定类方法

方法 return 功能
readable(self) 布尔值 判定文件是否可读
writeable(self) 布尔值 判定文件是否可写
seekable(self) 布尔值 判定文件是否可以设置文件偏移量
closed 布尔值 判定文件是否已经关闭
isatty(self) 布尔值 判定文件是否为一个tty文件(终端文件)

其他方法

方法 return 功能
mode 字符串 输出open()打开文件时,设置的模式;
name 字符串 输出打开的文件名
fileno(self) 数字 返回文件的描述符
flush(self) None 不关闭文件的情况下,将内存中的文件内容强制写入到磁盘的文件上;
truncate(self,size) 数字 不论光标在哪里,截取文件开头到size个字节的内容;返回截取的字节数;

5.4 文件操作应用补充

1 读大文件
案列:
	线上生产系统内存16G,而若有个文件有40G需要查看起内容,如何实现??
分析:
	若直接将此文件打开,而此文件过大会直接干跨内存,故此必须另寻其他方法

解决方法一

​ 原理:

​ 一点一点读文件的内容,直接完成整个文件的读操作;

​ 代码:

​ with open('file','r',encoding='utf-8') as f:

​ while 1:

​ print(f.read(100))

​ 此方法的缺陷:

​ 采用此种方法根本就不能判定文件何时读取完成,即无法设置循环的终止条件,一旦整个文件读取完成,死循环还在被执行,此时读取到的文件内容为空,但是print()默认会输出\n,直接导致程序一直输出换行符号;

解决方法二

​ 原理:

​ 采用for遍历文件,每次得到文件的一行内容,直到完成整个文件的读操作;文件行遍历完成循环终止;

​ 代码:

​ with open('file','r',encoding='utf-8') as f:

​ for item in f:

​ print(item)

2 文件内容的修改

底层原理分析:

​ 文件的写操作,是覆盖写入的;针对覆盖写入的特点是不存在修改文件的内容的;故此文件操作无法进行修改操作;

​ 系统的底层文件是不存在修改操作的,而只存在读写操作;所谓的文件的修改操作是模拟出来的;---人和软件都不可能直接去修改磁盘上的二进制代码;
​ 文件修改操作的模拟方法是:加载文件到内存中,虽然文件无法在磁盘上进行修改,但是可以在内存中听过读写方法完成修改;修改的方法是将该文件中的内容按需read()出来,然后写入到一个新文件中;此后采用新的文件去覆盖磁盘上的该文件,用以达到修改的目的;
​ 在linux平台,打开一个文件就会产生一个.xx.swap的文件,而windows平台,打开一个文件会产生一个~$xxx的文件,这些文件就是内存中加载后的源文件修改的文件;修改完成之后,即用这个文件去覆盖磁盘上的该文件;

针对小文件:

with open('2.txt','r+',encoding='utf-8') as f:
    data=f.read()
    data=data.replace('WX-F4-6','DSB')
    f.seek(0)
    f.write(data)

针对大文件:

import os
with open('2.txt','r+',encoding='utf-8') as f1 , open('3.txt','w+',encoding='utf-8') as f2:
    for line in f1:
        f2.write(line.replace('XXX','XSXBX'))
os.remove('2.txt')
os.rename('3.txt','2.txt')
3 文件内容输出的换行问题
>>>f= open('1.txt','r+',encoding='utf-8')
>>>print(f.read())
11111
22222
33333
44444
55555
>>>f.seek(0)
>>>for line in f:print(line);
11111

22222

33333

44444

55555

>>>

解释器对换行符\n的处理:

​ 解释器一旦遇到换行符号\n,则理解将光标的位置移动到下一行的行首然后输出\n字符之后的字符;故此上述案例中print(f.read())的输出如上;

​ 而采用for进行遍历时,for每次遍历取值获取的都是文件的每一行,其中行内容会包含所有的字符(可打印字符和不可打印字符),故此采用print(line)时,打印的是:print('xxxx\n'),先输出行内可打印字符,在行结束时遇到\n则将光标移动到下一行的行首;此时print()要输出的内容结束,但是print()默认有end='\n',即每次要打印的内容输出完成时会输出一个\n,解释器再次进行换行;故此采用for遍历输出文件的内容时,行与行之间会产生一个空行; 如不需要此空行,将print()的函数的end参数设置一下即可;

4 文件的大小计算

​ 底层原理:

​ 计算机是电气设备,其仅仅只能识别二进制01,计算机对数据的处理全部是识别为字符串进行处理的;其中字符串分为两种unicode和bytes两种类型的字符串;存在于内存中的是Unicode字符串,内存中采用Unicode的原因是Unicode编码方式将所有的字符全部采用定长的4byte进行编码便于进行计算,内存中不采用其utf-8的原因是utf-8是不定长的编码方式,两种不同的字符类型其编码的二进制长度不同,若进行位运算会引起一些计算的问题;而存储在磁盘和网络传输的数据全部为bytes字符串;

​ 文件的内容是由字符串构成的,根据不同编码方式,每个不同的字符采用对应的编码所占用的字节数,然后统计出该文件中所有的字符串占用的字节数,进行单位换算之后,即计算出了文件的大小;

5 下载工具的断点续传功能的实现

案列:

​ 假如,采用迅雷下载一个文件,该文件有5G,而下载到4.9G后,网络中断;断点续传即是网络恢复时,再下载该文件不再是从头开始下载而是从4.9的位置继续下载该文件;

底层原理:

​ 下载端:通过文件文件偏移量定位到已经下载的文件的最后,获取其文件偏移量;然后将文件偏移量的值告诉server段;

​ server段:同样打开该文件,通过seek()操作,将文件偏移量设置到客户端相同的位置,然后读取该文件偏移量后的内容,传输给客户端;通过此种文件偏移量的操作,即实现了断点续传的功能;

6 案例
1.写一个查看文件后几行的工具,即写一个类似于linux的tail工具;

import sys
n = int(sys.argv[1])
with open('a.txt', 'rb') as f:
    off = -5
    while True:
        f.seek(off, 2)
        data = f.readlines()
        if len(data) > n:
            for i in data[1:]:
                print(i.decode('utf-8'), end='')
        else:
            off *= 2

2.写一个文件复制工具,类似于linux的cp工具;

import sys
if len(sys.argv) != 3:
    print('usage: cp source_file target_file')
    sys.exit()
source_file, target_file = sys.argv[1], sys.argv[2]
with open(source_file, 'rb') as read_f, open(target_file, 'wb') as write_f:
    for line in read_f:
        write_f.write(line)
posted @ 2019-12-24 14:47  大兵0815  阅读(139)  评论(0)    收藏  举报