文件工具
Python中的内建函数open 是脚本用来在计算机底层系统下访问文件的主要工具。open 函数被调用时将返回一个新的与外部文件相连的文件对象。这个文件对象有一些方法,可以与文件双向传输数据,并且能执行多种文件的相关操作。open 函数还为底层文件系统提供了可移植接口。
回忆一下,可以对打开的对象运行dir(obj) 来查看其属性、方法等;可以运行help(obj) 获取通用帮助信息,还可以运行help(obj.method) 获取method 特定方法的帮助信息。
Python 3.X中的文件对象类型
Python 3.X中的str 字符串总是代表Unicode文本(ASCII或更广的字符集),而bytes 和bytearray 字符串代表原始二进制数据。
- 文本文件含有Unicode文本。在你的脚本中,文本文件内容始终是一个**
str 字符串——字符构成的序列**(准确地说是Unicode“代码点”)。在本章文本文件默认执行换行符自动转换,而且自动将 Unicode 编码应用于文件内容:它们在文件进行双向传输时依照一个给定或默认的编码名称,对原始二进制字节进行编码和解码。编码对 ASCII 文本来说很简单,但在其他情况下可能比较复杂。 - 二进制文件包含原始的8位字节。在你的脚本中,二进制文本文件的文件内容始终是一个字节字符串, 通常是一个字节对象——小整数构成的序列,支持大多数 str 操作并尽量显示为 ASCII 字符。二进制文件与其他文件进行双向传输的时候不进行换行符转换和 Unicode 编码。
在实践中,文本文件用于所有真正的文本相关数据,而二进制文件则用于存储内容。
从程序员的角度看,这两种文件类型是在通过传入open 的模式字符串参数来区分的:参数中填上“b”(比如rb 、wb 等),将意味着文件包含二级制数据。在对编码新文件内容时,对文本使用普通字符串(如’spam’或bytes.decode ),而对二进制使用字节字符串(如b’spam’或str.encode )。
由于文本模式要求文件内容能够按照某个Unicode编码方案的内容进行解码,所以你必须在二进制模式下将不可解码的内容读取为字节字符串(或者在try 语句中捕获Unicode异常,然后整个跳过该文件)。这些文件可能包括真正的二进制文件以及使用非默认的未知编码的文本文件。
使用内建文件对象
输出文件
>>> file = open('data.txt', 'w')
>>> file.write('1')
1
>>> file.write('2')
1
>>> file.writelines(['3', '4'])
>>> file.writelines(['5', '6'])
>>> file.close()
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat data.txt
123456
open('file', 'w').write('Good night\n')
open('file', 'r').read()
- 因为以上两个表达式生成了一个临时文件对象,并且马上被使用,而且没有保存对对象的引用,所以在数据传输后,文件对象马上就被回收并在进程中自动关闭。在这种代码中通常没有必要去显示调用
close 方法
确保文件关闭:异常处理器和上下文管理器
当程序抛出异常,文件还未手动关闭时,首先要确认是否必须关闭——文件回收时会自动关闭,关闭最终会执行,即便发生异常。
如果必须关闭,那么又两种基本解决方法。
my_file = open(filename, 'w')
try:
...process my_file...
finally:
my_file.close()
with ... 语句。这个语句依赖于文件对象的上下文管理器:无论发生什么异常行为,代码在进入和离开语句时都能自动运行。with 语句也可指定多个(嵌套)上下文管理器。
with open(filename, 'w') as my_file:
...process my_file, auto-closed on statement exit...
with open(file_name_1, 'w') as file_1, open(file_name_2, 'w') as file_2:
...statements...
with open(file_name_1, 'w') as file_1:
with open(file_name_2, 'w') as file_2:
...statements...
with 仅应用于支持上下文管理协议的对象,而try...finally 允许任意异常情况下的任意退出操作。
输入文件
>>> file = open('data.txt')
>>>
>>> file.read()
'123456'
>>>
>>> file.seek(0)
0
>>> file.read(1)
'1'
>>> file.read(3)
'234'
>>> file.read(-1)
'56'
>>>
>>> file.seek(0)
0
>>> file.readline()
'123456'
>>>
>>> file.seek(0)
0
>>> file.readlines()
['123456']
file.seek(N) :调用seek 只是让我们下次的传输操作移动到偏移值为N的新位置,文件中所有的读取和写入操作都发生于当前位置。file.read() :返回一个字符串,它包含返回在文件中的所有字符。file.read(N) :返回一个字符串,它包含文件中接下来的N个字符(或字节)。file.readline() :读取下一个\n之前的内容并返回一个行字符串。file.readlines() :读取整个文件并返回一个行字符串列表。
使用文件迭代器读取行
演示需要,修改了data.txt:
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat data.txt
1
2
3
4
5
6
无需使用readlines ,迭代器会自动读取行:
>>> for line_str in open('data.txt'):
... print(line_str, end='')
...
1
2
3
4
5
6
- 你可以在这个循环本身把文件作为临时对象打开,后者将在循环结束后在垃圾回收时自动关闭。
你可以手动使用文件迭代器:它只是一个__next__ 方法(由内建函数next 运行),与每次调用readline 方法类似,只是readline 方法在文件末尾(EOF)返回一个空字符串,而迭代器则抛出一个异常来结束迭代。
>>> file = open('data.txt')
>>>
>>> file.readline()
'1\n'
>>> file.readline()
'2\n'
>>> file.readline()
'3\n'
>>> file.readline()
'4\n'
>>> file.readline()
'5\n'
>>> file.readline()
'6\n'
>>> file.readline()
''
>>>
>>> file.seek(0)
0
>>> file.__next__()
'1\n'
>>> file.__next__()
'2\n'
>>> file.__next__()
'3\n'
>>> file.__next__()
'4\n'
>>> file.__next__()
'5\n'
>>> file.__next__()
'6\n'
>>> file.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
>>> file.seek(0)
0
>>> next(file)
'1\n'
>>> next(file)
'2\n'
>>> next(file)
'3\n'
>>> next(file)
'4\n'
>>> next(file)
'5\n'
>>> next(file)
'6\n'
>>> next(file)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
有趣的是,在所有迭代场景下,迭代器都自动得以调用,包括但不仅限于以下几种情况:
>>> list(open('data.txt'))
['1\n', '2\n', '3\n', '4\n', '5\n', '6\n']
>>> lines_list = [line_str.rstrip() for line_str in open('data.txt')]
>>> lines_list
['1', '2', '3', '4', '5', '6']
>>> lines_list = [int(line_str) ** 2 for line_str in open('data.txt')]
>>> lines_list
[1, 4, 9, 16, 25, 36]
>>> list(map(int, open('data.txt')))
[1, 2, 3, 4, 5, 6]
>>> line = '3\n'
>>> line in open('data.txt')
True
<<<<<<< HEAD
二进制和文本文件
>>> file = open('data.txt', 'wb')
>>> file = open('data.txt', 'wb')
- 打开二进制文件时,传入的
open 模式参数要加上b。 read 、write 方法用来读取文件内容。在这里,readline 和readlines 方法仍可使用,但是对于真正的二进制数据时,这下操作是没有意义的(换行符字节即使存在,也毫无意义)。
在所有情况下,文件与程序间传输的数据,即使是二进制数据,在脚本中还是表示为Python字符串。然而如果使用二进制模式打开文件,文件内容则显示为字节字符串。
>>> open('data.txt', 'r').read()
'1\n2\n3\n4\n5\n6\n'
>>> open('data.txt', 'rb').read()
b'1\n2\n3\n4\n5\n6\n'
>>>
>>> for line_bstr in open('data.txt', 'rb'):
... print(line_bstr)
...
b'1\n'
b'2\n'
b'3\n'
b'4\n'
b'5\n'
b'6\n'
- 产生这种结果的原因是Python 3.X把文本模式文件当做Unicode来处理,并自动的在输入时对文件内容进行解码,在输出时又进行编码。二进制模式文件则允许我们把文件字符串作为原始字节字符串来访问,而不进行转换,因此它们忠实地反映了该文件的存储内容。但要谨记,你必须在二进制模式下打开真正的二进制数据,因为它作为 Unicode 文本时是不可解码的。
你也必须为二进制输出文件提供字节字符串。
>>> open('data.bin', 'wb').write(b'spam\n')
5
>>> open('data.bin', 'rb').read()
b'spam\n'
请注意该文件每行仅以“\n”结尾,在Windows下以“\r\n”结尾。严格说来,二进制模式不仅使Unicode转码无法进行,还阻止了文本模式下默认换行符的自动转换。
文本文件的Unicode编码
下面这个字符串包含一个Unicode字符的字符串,其二进制值在ASCII编码标准的7位范围之外。
>>> open('data.bin', 'wb').write(b'spam\n')
5
>>> open('data.bin', 'rb').read()
b'spam\n'
>>> data_str = 'sp\xe4m'
>>> data_str
'sp?m'
>>> 0xe4, bin(0xe4), chr(0xe4)
(228, '0b11100100', '?')
也可以手动编码:
>>> data_str.encode('latin1')
b'sp\xe4m'
>>> data_str.encode('utf8')
b'sp\xc3\xa4m'
>>> data_str.encode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\xe4' in position 2: ordinal not in range(128)
>>> data_str.encode('utf16')
b'\xff\xfes\x00p\x00\xe4\x00m\x00'
>>> data_str.encode('cp500')
b'\xa2\x97C\x94'
不过,如果我们在二进制模式下打开文件,是不会进行编码转换的。
>>> open('data.txt', 'w', encoding='latin1').write(data_str)
4
>>> open('data.txt', 'r', encoding='latin1').read()
'sp?m'
>>> open('data.txt', 'rb').read()
b'sp\xe4m'
>>> open('data.txt', 'w', encoding='utf8').write(data_str)
4
>>> open('data.txt', 'r', encoding='utf8').read()
'sp?m'
>>> open('data.txt', 'r', encoding='latin1').read()
'sp?¤m'
>>> open('data.txt', 'rb').read()
b'sp\xc3\xa4m'
这一次,虽然原始文件内容有所不同,但文本模式的自动解码使得字符串在脚本读取返回之前变得相同。请注意,尝试写入不可编码的数据或阅读不可编码的数据都将引起错误。
除非使用了二进制模式,否则以下代码可以在编码类型已知时重新创建原始字符串,在编码类型未知时操作失败。
>>> open('data.txt', 'w', encoding='cp500').write('I LOVE U\n')
9
>>> open('data.txt', 'r', encoding='cp500').read()
'I LOVE U\n'
>>> open('data.txt', 'r').read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/alone/anaconda3/lib/python3.7/codecs.py", line 322, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc9 in position 0: invalid continuation byte
>>> open('data.txt', 'rb').read()
b'\xc9@\xd3\xd6\xe5\xc5@\xe4%'
文本文件的换行符转换
如果Python脚本在Windows下运行,在默认情况下,文件对象会自动将DOS “\r\n”序列映射为单独的“\n”。
- 对于文本模式下打开的文件,输入“\r\n”会转换成“\n”。
- 对于文本模式下打开的文件,输出时“\n”会转换成“\r\n”。
- 对于二进制模式下打开的文件,输入输出都不会发生转换。
在类Unix平台上之所以不会发生任何转换,是因为文件里使用的是“\n”。这些规则中,有两条重要结果必须要牢记:
- 首先,不论以何种形式存储于顶层平台的外部文件中,文本模式文件换行符几乎总在Python中显示为“\n”。通过在输入时映射为“\n”,在输出时映射为“\n”,Python得以隐藏特定平台间的差异。
- 其次,在处理二进制文件中,二进制打开模式有效地关闭了换行转换。
最后,需要牢记的是:文本文件的内容一般都应当以“\n”来代表结尾,而二进制数据应当总是在二进制模式下打开,以便阻止换行符转换和Unicode编码。
用struct 模块解析打包的二进制数据
struct 模块提供用于打包和解压二进制的调用。它能够用你想用的任何一种字节序来组合和分解(字节序决定了二进制数字的最高有效位是居左还是居右)。
>>> import struct
>>>
>>> data_b = struct.pack('>i4shf', 2, b'spam', 3, 1.234)
>>> data_b
b'\x00\x00\x00\x02spam\x00\x03?\x9d\xf3\xb6'
>>> open('data.bin', 'wb').write(data_b)
14
- 这里
pack 调用的格式字符串是一个高位优先(> ),它包含一个整数(i )、一个四字符的字节字符串(4s )、一个半整数(h ),以及一个浮点数(f )。
也可以解析数据:
>>> values_tuple = struct.unpack('>i4shf', open('data.bin', 'rb').read())
>>> values_tuple
(2, b'spam', 3, 1.2339999675750732)
我们可以对字符串和按位运算进行深层探索:
>>> bin(values_tuple[0] | 0b1)
'0b11'
>>> values_tuple[1], list(values_tuple[1]), values_tuple[1][0]
(b'spam', [115, 112, 97, 109], 115)
随机访问文件
文件的open 模式字符串添加+ 号后可读取又可写入。这种模式通常与文件对象的seek 方法联合使用,以便支持随机读取或写入访问。这种灵活的文件处理模式允许我们执行从一处读取字节,再在另一处写入等操作。当脚本把这一性能与二进制文件模式结合在一起时,便可在一个文件内获取和更新任意字节。
Python的seek 方法也接受可选的第二参数,这个参数有3种值:0 表示绝对文件位置(默认值);1 表示基于当前所在的相对位置;2 表示寻求基于文件结尾的相对位置。
为了证明上述结论,让我们在wb+ 的模式下创建一个文件,但如果文件已经存在,此模式会清空文档内容(所有w 模式都会)。
>>> records_list = [bytes([i_str] * 8) for i_str in b'spam']
>>> records_list
[b'ssssssss', b'pppppppp', b'aaaaaaaa', b'mmmmmmmm']
>>>
>>> file = open('random.bin', 'wb+')
>>> file.writelines(records_list)
>>> file.seek(0)
0
>>> file.read()
b'ssssssssppppppppaaaaaaaammmmmmmm'
现在让我们在rb+ 模式下重新打开文件,这个模式也允许读取和写入,但是初始化时并不会清空文件。
>>> file = open('random.bin', 'rb+')
>>> file.read()
b'ssssssssppppppppaaaaaaaammmmmmmm'
>>> file.seek(0)
0
>>> file.write(b'X' * 8)
8
>>> file.seek(0)
0
>>> file.read(8)
b'XXXXXXXX'
>>> file.write(b'Y' * 8)
8
>>> file.seek(0)
0
>>> file.read()
b'XXXXXXXXYYYYYYYYaaaaaaaammmmmmmm'
os 模块中的底层文件工具
os 模块包含一个文件处理函数的附加集合。下面列出了部分os 文件的相关调用:
os.open(path, flags, mode) :打开文件并返回其描述符。os.read(descriptor, N) :最多读取N个字节并返回字节字符串。os.write(descriptor, string) :把文件字符串string中的字节写入文件。os.lseek(descriptor, position, how) :在文件中移至position
严格地说,os 调用通过文件的描述符来处理文件,描述符是整数代码或“句柄”,代表着操作系统中的文件。基于描述符的文件以原始字节的形式来处理,而且没有我们之前所学的文本的换行符和Unicode转换的概念。事实上,除了缓冲等额外性能,基于描述符的文件一般都能对应上二进制模式文件对象,而且我们也可以类似地读取和写入bytes字符串而非str字符串。然而,与带有open 内建函数的内建文件对象相比,os 中基于文件描述符的文件工具更底层且更复杂,所以,除非有非常特殊的文件处理需求,否则一般使用open 函数。
使用os.open 文件
>>> import sys
>>>
>>> for stream_file in (sys.stdin, sys.stdout, sys.stderr):
... print(stream_file.fileno())
...
0
1
2
>>>
>>>
>>> import os
>>> sys.stdout.write('Hello stdio world!\n')
Hello stdio world!
19
>>> os.write(1, b'Hello descriptor world!\n')
Hello descriptor world!
24
fileno 文件对象方法返回的整数描述符是与某个内建文件对象相关联的。例如,标准流文件对象拥有描述符0、1、2;调用os.write 函数,通过描述符将数据发送至stdout ,与调用sys.stdout.write 方法的效果是一样的。
我们可以通过内建函数open 、os 模块·中的工具或者二者结合起来使用处理给定的外部文件。
>>> file = open('spam.txt', 'w')
>>> file.write('Hello stdio world!\n')
19
>>> file.flush()
>>> file.fileno()
3
>>>
>>>
>>> os.write(3, b'Hello descriptor world!\n')
24
>>> file.close()
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat spam.txt
Hello stdio world!
Hello descriptor world!
需要注意两点:
os.write 写入的是字节字符串(bytes)。- 必须在
os.write 之后使用file.close() 。
os.open 的模式标识符
为什么Python会提供额外的文件工具呢?简而言之,它们为文件处理提供更多底层控制。内建的open 函数受限于所使用的的底层文件系统,os 模块让脚本进行更精细地控制。
>>> fdfile = os.open('spam.txt', os.O_RDWR)
>>> os.read(fdfile, 20)
b'Hello stdio world!\nH'
>>> os.lseek(fdfile, 0, 0)
0
>>> os.read(fdfile, 100)
b'Hello stdio world!\nHello descriptor world!\n'
>>> os.lseek(fdfile, 0, 0)
0
>>> os.write(fdfile, b'HELLO')
5
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat spam.txt
HELLO stdio world!
Hello descriptor world!
- 在某些系统上,第一行代码需改成
fdfile = os.open('spam.txt', (os.O_RDWR | os.O_BINARY)) ,这样通过对os 导出的两个模式标识符进行二进制的“或”操作,从而以读-写和二进制模式打开一个基于描述符的文件。
>>> spam_file_rbplus = open('spam.txt', 'rb+')
>>> spam_file_rbplus.read()
b'HELLO stdio world!\nHello descriptor world!\n'
>>> spam_file_rbplus.seek(0)
0
>>> spam_file_rbplus.write(b'Hello')
5
>>> spam_file_rbplus.flush()
>>> spam_file_rbplus.close()
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat spam.txt
Hello stdio world!
Hello descriptor world!
但在某些系统上,os.open 标识符可指定更高级的参数,因此有些标识符是不可移植的。
把描述符封装进文件对象
我们可以利用os.fdopen 调用把文件描述符封装进文件对象。
>>> import os
>>>
>>> fd_int = os.open('spam.txt', os.O_RDWR)
>>> fdfile = os.fdopen(fd_int, 'rb')
>>> fdfile.read()
b'Hello stdio world!\nHello descriptor world!\n'
文件描述符封装的文件对象:在文本模式下,读取和写入时将执行我们之前学过的Unicode编码和换行符转换,并且处理的是str字符串,而非bytes字符串:
>>> fd_int = os.open('spam.txt', os.O_RDWR)
>>> fdfile = os.fdopen(fd_int, 'r')
>>> fdfile.read()
'Hello stdio world!\nHello descriptor world!\n'
>>> type(fdfile.read())
<class 'str'>
open 和os.fdopen 都可接受额外的控制参数。
>>> fd_int = os.open('spam.txt', os.O_RDWR)
>>>
>>> file = open(fd_int, 'r', encoding='latin1', closefd=False)
>>> file.read()
'Hello stdio world!\nHello descriptor world!\n'
>>> fdfile = os.fdopen(fd_int, 'r', encoding='latin1', closefd=False)
>>> fdfile.read()
'Hello stdio world!\nHello descriptor world!\n'
>>> fdfile.close()
其他的os 模块文件工具
>>> os.chmod('spam.txt', 0o777)
>>> os.rename(fn_1, fn_2)
>>> os.remove(fn)
>>> open('spam.txt', 'w').write('Hello stat world\n')
17
>>> info_tuple = os.stat('spam.txt')
>>> info_tuple
os.stat_result(st_mode=33279, st_ino=256914, st_dev=2049, st_nlink=1, st_uid=1000, st_gid=1000, st_size=17, st_atime=1627653023, st_mtime=1627655368, st_ctime=1627655368)
>>> info_tuple.st_mode, info_tuple.st_size
(33279, 17)
>>> import stat
>>> info_tuple[stat.ST_MODE], info_tuple[stat.ST_SIZE]
(33279, 17)
>>> stat.S_ISDIR(info_tuple.st_mode), stat.S_ISREG(info_tuple.st_mode)
(False, True)
文件扫描器
现在让我们做一个工具,它能演示目前为止我们已经学习过的内容。
下面的模块定义了通用文件扫描例行程序:
示例:scan_file.py
def file_scanner(fn_str, function):
"通用文件扫描例行函数"
file = open(fn_str, 'r')
while True:
line_str = file.readline()
if not line_str:
break
function(line_str)
file.close()
下面是一个进行简单逐行转换的客户端脚本:
示例:commands.py
from sys import argv
from scan_file import file_scanner
class UnknownCommand(Exception):
"一个“未知命令”异常的类"
pass
def process_file(line_str):
"一个逐行将“*name”和“+name”转换为“Ms.name”和“Mr.name”的函数"
if line_str[0] == '+':
print('Mr.' + line_str[1:-1])
elif line_str[0] == '*':
print('Ms.' + line_str[1:-1])
else:
raise UnknownCommand(line_str)
if len(argv) == 2:
fn_str = argv[1]
else:
fn_str = 'data.txt'
file_scanner(fn_str, process_file)
测试:
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ cat hillbillies.txt
*Granny
+Jethro
*Elly May
+"Uncle Jed"
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ ./commands.py hillbillies.txt
Ms.Granny
Mr.Jethro
Ms.Elly May
Mr."Uncle Jed"
示例:file_scan.py替代方案
def file_scanner(fn_str, function):
for line_str in open(fn_str, 'r'):
function(line_str)
def file_scanner(fn_str, function):
list(map(function, open(fn_str, 'r')))
def file_scanner(fn_str, function):
[function(line_str) for line_str in open(fn_str, 'r')]
def file_scanner(fn_str, function):
list(function(line_str) for line_str in open(fn_str, 'r'))
示例:commands.py替代方案
commands_dict = {'+': 'Mr.', '*': 'Ms.'}
def process_file(line_str):
try:
print(commands_dict[line_str[0]] + line_str[1:-1])
except KeyError:
raise UnknownCommand(line_str)
文件过滤器
import sys
def filter_file(fn_str, function):
"显示指定文件"
input_file = open(fn_str, 'r')
output_file = open(fn_str + '.out', 'w')
for line_str in input_file:
output_file.write(function(line_str))
input_file.close()
output_file.close()
def filter_stream(function):
"利用标准输入/输出流允许在命令行中重定向"
while True:
line_str = sys.stdin.readline()
if not line_str:
break
print(function(line_str), end='')
if __name__ == '__main__':
filter_stream(lambda line_str: line_str)
运行结果:
beacherhou@alone-Vostro-14-5401:/media/beacherhou/Coding/code_obsidian_知识库/Python编程_Markdown笔记/pp4e/system$ ./filters.py < hillbillies.txt
*Granny
+Jethro
*Elly May
+"Uncle Jed"
>>> from filters import filter_file
>>> filter_file('hillbillies.txt', str.upper)
>>> print(open('hillbillies.txt.out').read())
*GRANNY
+JETHRO
*ELLY MAY
+"UNCLE JED"
:)学完博客后,是不是有所启发呢?如果对此还有疑问,欢迎在评论区留言哦。 如果还想了解更多的信息,欢迎大佬们关注我哦,也可以查看我的个人博客网站BeacherHou
|