Python 的文件处理——从几个实际的例子出发

前言

有些时候我们需要对某个文件中的内容进行重复的处理。例如,我们有一个名为interface3.ind的文件,里面的内容大体如下:

python replace 最后出现的_字符串

这个文件约有 4000 行。我们需要从中提取出所有形如\a_b_c:n的字符串。例如,从第 3967 行:\item font \verb*&\vcoffin&\_\verb*&set:Nnw&},提取出\vcoffin_set:Nnw

又或者,我们想从名为colorscraping.html的文件里得到各种颜色的名字、拼音和 RGB 值:

python replace 最后出现的_json_02

并且把每个颜色都转换成\definecolor{颜色名称}{RGB}{R值,G值,B值}这种形式。

我们肯定不愿意花上半天时间来做这么无聊的事情,而且难免会出错和遗漏。利用 python,花 10 分钟,写上不到 30 行的代码,便能迅速、准确地解决这两个问题。

python 简介

使用编程语言处理文件,本身就不是那么基础的东西,因此本文并不打算讲解基础的语法知识。这要求读者有一定的编程经验,不过,由于 python 语言较为接近自然语言,读者从这些代码中便能大致理解它的意思。

为了方便有兴趣的读者,这里仍然简要介绍一下 python 语言。

  1. for 循环结构:
for i in range(0,5):
    print(i)

range(0,5)生成 0-4 的数字序列,i取遍这个数列中的每个数字,print(i)将其分别输出。结果如下:

python replace 最后出现的_html_03

  1. 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,这几行代码应该是很容易理解的。

  1. 数组和字典
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'。第四行通过键访问字典元素。输出结果如下:

python replace 最后出现的_字符串_04

  1. 函数
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)

输出的一部分如下:

python replace 最后出现的_字符串_05

发现还有些许不足:\.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('\\','')

看看结果:

python replace 最后出现的_字符串_06

大功告成!我们再将输出的结果写入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)

这样我们就把提取出来的字符按照一定的规则写入到了cwljson文件中。

(注:正如标题所讲,这个问题源于我想把 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)函数将strn进制转为数值,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')

输出文件的部分内容如下:

python replace 最后出现的_json_07

可以看到,有效代码不到 10 行。

(注:这个问题源于我在 http://zhongguose.com/ 这个网站中看到许多好看的颜色,而 LaTeX 的宏包中命名的颜色太少,便想着把这些颜色全都定义下来,大概有 500 多种颜色,手动弄也是非常麻烦的。)

总的来说,这些代码的复用性较低,大都是临时处理一下文本,但是花上 10 分钟,却能省下不少事。

(这篇文章太水了 QAQ)