Python 的文件处理——从几个实际的例子出发
前言
有些时候我们需要对某个文件中的内容进行重复的处理。例如,我们有一个名为interface3.ind
的文件,里面的内容大体如下:
这个文件约有 4000 行。我们需要从中提取出所有形如\a_b_c:n
的字符串。例如,从第 3967 行:\item font \verb*&\vcoffin&\_\verb*&set:Nnw&},
提取出\vcoffin_set:Nnw
。
又或者,我们想从名为colorscraping.html
的文件里得到各种颜色的名字、拼音和 RGB 值:
并且把每个颜色都转换成\definecolor{颜色名称}{RGB}{R值,G值,B值}
这种形式。
我们肯定不愿意花上半天时间来做这么无聊的事情,而且难免会出错和遗漏。利用 python,花 10 分钟,写上不到 30 行的代码,便能迅速、准确地解决这两个问题。
python 简介
使用编程语言处理文件,本身就不是那么基础的东西,因此本文并不打算讲解基础的语法知识。这要求读者有一定的编程经验,不过,由于 python 语言较为接近自然语言,读者从这些代码中便能大致理解它的意思。
为了方便有兴趣的读者,这里仍然简要介绍一下 python 语言。
- for 循环结构:
for i in range(0,5):
print(i)
range(0,5)
生成 0-4 的数字序列,i
取遍这个数列中的每个数字,print(i)
将其分别输出。结果如下:
- if 判断结构:
a = 5
if a > 4:
print(str(a) + " is greater than 4")
else:
print(str(a) + " is less than 4")
输出结果为:5 is greater than 4
,这几行代码应该是很容易理解的。
- 数组和字典
list1 = [1, 2, 3, 4]
dict1 = {'ke': 'va', 'ke2': '2'}
print(list1[0], '-1: ' + str(list1[-1]))
print(dict1['ke'], dict1['ke2'])
其中第一行声明了一个列表,列表元素为1,2,3,4
,第二行声明了一个字典,字典有两个键:'ke'
和'ke2'
,键与值一一对应。第三行通过使用数组元素的索引将其输出,其中str()
函数将一个数字转为一个字符串,+
为合并字符串,'-1: ' + str(list1[-1])
即为'-1: 4'
。第四行通过键访问字典元素。输出结果如下:
- 函数
def foo():
print('Hello, World!')
foo()
前两行定义了一个简单的foo()
函数,第四行调用它,输出Hello, World!
。
第一个问题的处理
我们首先大致看一下interface3.ind
这个文件。不难发现,我们需要提取的东西都被&
所包裹,并且,要提取的字符的所在行都包含\verb*
和_
。
这启发我们这样想:如果 line 包含 '\verb*' 且 line 包含 '_' 那么 ...
,用 python 写出来就是:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0:
do something
我们需要对每一行都进行这样的操作,这可以使用 for 循环:
for line in lines:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0:
do something
其中,line.find(r'\verb*')
的意思是:在line
中查找r'\verb*'
这个字符串,如果有则返回其索引,否则返回-1
。其中,字符串前的r
表示原始字符串,不对字符进行转义。
下面就可以打开interface3.ind
文件对它进行处理了:
import codecs # codecs模块可以更加方便地处理文件的编码问题
f = codecs.open('interface3.ind', 'r', 'utf-8')
lines = f.readlines()
f.close()
for line in lines:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0:
do something
第 2-4 行为:指定utf-8
编码读取文件,读取文件的所有行存储到lines
列表中,关闭文件。
下面我们来编写do something
部分。
对于满足条件的一行,如:\item font \verb*&\vcoffin&\_\verb*&set:Nnw&},
,不难发现我们需要的部分都被&
包裹,因此我们只要把这一行按&
分割开,再去掉首尾和多余的'\verb*', '_'
,再将它合并就行了。
line = r'\item font \verb*&\vcoffin&\_\verb*&set:Nnw&},'
line = line.split('&') # 按&分割
line = line[1:-1] # 去除第一项和最后一项
line = ''.join(line) # 将列表合并为字符串
# 将'\verb*'和'_'替换为空
line = line.replace('\\verb*', '').replace('\\', '')
print(line)
输出结果为vcoffin_set:Nnw
,我们发现它与预期少了一个\
,只要在前面加上即可:
line = '\\' + line
print(line)
# 输出:\vcoffin_set:Nnw
将它们写为一行:
line = '\\' + ''.join(line.split('&')[1:-1]).replace('\\verb*', '').replace('\\','')
放到do something
的位置即可:
import codecs # codecs模块可以更加方便地处理文件的编码问题
f = codecs.open('interface3.ind', 'r', 'utf-8')
lines = f.readlines()
f.close()
for line in lines:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0:
line = '\\' + ''.join(line.split('&')[1:-1]).replace('\\verb*', '').replace('\\','')
print(line)
输出的一部分如下:
发现还有些许不足:\.bool_gset:N
本不应出现,只要在判断语句中再加上一个line.find(r'.')<0
即可:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0 and line.find(r'.') 0:
line = '\\' + ''.join(line.split('&')[1:-1]).replace('\\verb*', '').replace('\\','')
看看结果:
大功告成!我们再将输出的结果写入replace.cwl
文件中:
import codecs
f = codecs.open('interface3.ind', 'r', 'utf-8')
lines = f.readlines()
f.close()
with open('replace.cwl', 'w', 'utf-8') as f:
for line in lines:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0 and line.find(r'.') 0:
line = '\\' + ''.join(line.split('&')[1:-1]).replace('\\verb*', '').replace('\\','')
f.write(line + '\n')
这样就完成了!整个不到 10 行代码!不到 5 分钟就完成了手动需要做半天的工作!
更进一步,我们将得到的每个项目都写成形如:
"vcoffin_set:Nnw": {
"prefix": "vcoffin_set:Nnw",
"body": "\\vcoffin_set:Nnw",
"discription": " "
}
的形式,这是一个 python 字典。
import codecs, json
f = codecs.open('interface3.ind', 'r', 'utf-8')
lines = f.readlines()
f.close()
json = {}
for line in lines:
if line.find(r'\verb*') >= 0 and line.find(r'_') >= 0 and line.find(r'.') 0:
name = ''.join(line.split('&')[1:-1]).replace('\\verb*', '').replace('\\','')
json[name] = {
"prefix": name,
"body": "\\" + name,
"discription": " "
}
json = json.dumps(json)
with codecs.open('latex3.json', 'w', 'utf-8') as f:
f.write(json)
这样我们就把提取出来的字符按照一定的规则写入到了cwl
和json
文件中。
(注:正如标题所讲,这个问题源于我想把 LeTeX3 标准的宏命令写入 TeXStudio 的 cwl 文件和 VS Code 的 snippets 文件,LaTeX3 标准的宏命令有近 2000 个,写入的 snippets 文件有 8000 行,要是全都用手输入,不知道要多久才能弄完(ˉ▽ˉ;)...。可以看到,使用编程脚本可以很方便迅速地完成简单重复的劳动。)
第二个问题
这个问题需要处理html
文件,我们使用lxml
库来处理,这个库不是标准库,需要额外安装。
可以看到以上html
文件的结构为:
...
<body>
<div>
...
<nav>
...
<a>
<span>颜色名span>
<span>颜色pinyinspan>
<span>16进制RGB值span>
a>
...
nav>
...
div>
body>
...
直接使用lxml
库解析html
文件:
from lxml import etree # lxml==4.2.5 高版本无法导入etree
# 使用etree.parse解析html文件
html = etree.parse('colorscraping.html', etree.HTMLParser())
# 使用xpath访问html节点,并用text()方法获取其文本
result = html.xpath('//nav//a/span/text()')
result
的结果形如:['暗玉紫', 'anyuzi', '5c2223', '牡丹粉红', 'mudanfenhong', 'eee2a4', ...]
,三个一组,分别是颜色名称、颜色拼音和 16 进制的 RGB 值。
由于我们需要获得形如:\definecolor{颜色名称}{RGB}{R值,G值,B值}
的格式,首先需要将 16 进制的字符串转为 10 进制的。
def hex2rgb(hex_str):
r = int(hex_str[0:2], 16)
g = int(hex_str[2:4], 16)
b = int(hex_str[4:6], 16)
return str(r) + ',' + str(g) + ',' + str(b)
int(str, n)
函数将str
按n
进制转为数值,str()
函数和+
我们已经很熟悉了。
接下来只要将result
每三个合并起来就可以了。
with open('color.tex', 'w') as f:
for i in range(0, int(len(result)/3)):
color_def = '\\definecolor{' \
+ result[3*i] + '}{RBG}{' \
+ hex2rgb(result[3*i + 2]) + '}'
f.write(color_def + '\n')
这样就完成了。完整的代码如下:
from lxml import etree
html = etree.parse('colorscraping.html', etree.HTMLParser())
result = html.xpath('//nav//a/span/text()')
def hex2rgb(hex_str):
r = int(hex_str[0:2], 16)
g = int(hex_str[2:4], 16)
b = int(hex_str[4:6], 16)
return str(r) + ',' + str(g) + ',' + str(b)
with open('color.tex', 'w', encoding='utf-8') as f:
for i in range(0, int(len(result)/3)):
color_def = '\\definecolor{' \
+ result[3*i] + '}{RBG}{' \
+ hex2rgb(result[3*i + 2]) +'}'
f.write(color_def + '\n')
输出文件的部分内容如下:
可以看到,有效代码不到 10 行。
(注:这个问题源于我在 http://zhongguose.com/ 这个网站中看到许多好看的颜色,而 LaTeX 的宏包中命名的颜色太少,便想着把这些颜色全都定义下来,大概有 500 多种颜色,手动弄也是非常麻烦的。)
总的来说,这些代码的复用性较低,大都是临时处理一下文本,但是花上 10 分钟,却能省下不少事。
(这篇文章太水了 QAQ)