我用 python 写了一个自动生成索引的脚本
需求实现
我有一个 Markdown 文档,长成下面这个样子:
# ACM/OI Journey
在此留下刷题痕迹与刷题心得。
不定期的方法论总结在这里[./notes/README.md](./notes/README.md)。
学习资料:
- OI Wiki: https://oi-wiki.org/
- 力扣中国: https://leetcode-cn.com/
## 归档
## 日期归档
注意到,两个二级标题## 归档
与## 日期归档
下空空如也。
我的需求是,我刷完一道题,就将其记录在## 日期归档
下,格式为: - uu 日期 题目名称与概括 类别A 类别B 类别C... [程序文件1] [程序文件2] [程序文件3]...
假设我今天刷了 2 道题,那么我就将其记录在我的## 日期归档
下面,如下所示。
## 日期归档
- uu 2020.11.26 盛最多水的容器『因为两个边共同决定了上限,因此将较短边向内移动,抛弃搜索次优解』 双指针法 搜索 [py](./vsc_leetcode/11.盛最多水的容器.py) [cpp](./vsc_leetcode/11.盛最多水的容器.cpp)
- uu 2020.11.27 整数转罗马数字『生活中从大的位数开始描述数字,因此从大的数与字符开始匹配』 匹配 字符串 [cpp](./vsc_leetcode/12.整数转罗马数字.cpp)
而我的## 归档
下面还什么都没有,我希望我的脚本可以自动帮我在## 归档
下创建三级目录:双指针法
、搜索
、匹配
、字符串
,并且将对应的题目放到下面去。
最终的效果是:
## 归档
- [匹配](#匹配)
- [字符串](#字符串)
- [双指针法](#双指针法)
- [搜索](#搜索)
### 匹配
- 整数转罗马数字『生活中从大的位数开始描述数字,因此从大的数与字符开始匹配』 [cpp](./vsc_leetcode/12.整数转罗马数字.cpp) 2020.11.27
### 字符串
- 整数转罗马数字『生活中从大的位数开始描述数字,因此从大的数与字符开始匹配』 [cpp](./vsc_leetcode/12.整数转罗马数字.cpp) 2020.11.27
### 双指针法
- 盛最多水的容器『因为两个边共同决定了上限,因此将较短边向内移动,抛弃搜索次优解』 [py](./vsc_leetcode/11.盛最多水的容器.py) [cpp](./vsc_leetcode/11.盛最多水的容器.cpp) 2020.11.26
### 搜索
- 盛最多水的容器『因为两个边共同决定了上限,因此将较短边向内移动,抛弃搜索次优解』 [py](./vsc_leetcode/11.盛最多水的容器.py) [cpp](./vsc_leetcode/11.盛最多水的容器.cpp) 2020.11.26
## 日期归档
- 2020.11.26 盛最多水的容器『因为两个边共同决定了上限,因此将较短边向内移动,抛弃搜索次优解』 双指针法 搜索 [py](./vsc_leetcode/11.盛最多水的容器.py) [cpp](./vsc_leetcode/11.盛最多水的容器.cpp)
- 2020.11.27 整数转罗马数字『生活中从大的位数开始描述数字,因此从大的数与字符开始匹配』 匹配 字符串 [cpp](./vsc_leetcode/12.整数转罗马数字.cpp)
经过 Markdown 引擎渲染后的效果如下图。
如上,我不但新增了三级标题### 匹配
、### 字符串
等,还为三级标题创建了目录索引链接。
最终程序实现如下图。
Python 与脚本文件
这样就要派上我们的 Python 出场了。我觉得这才是 Python 的老本行:脚本文件。记得Python猫曾经有篇文章,讲过为什么 Python 中的注释符号是 #
而不是 //
。
原因很可能是:Python的老本行,就是写这一个个易用的脚本文件的,与shell
类似。
想想 Python 的特点:解释型语言、动态型语言、在命令行里可以一条一条地输入、os.system()
可以直接调用命令...所以,拿 Python 来执行一个个小任务(脚本文件)再合适不过了。
整体逻辑
逻辑是:
- 先把文件读到内存中,以列表
list
的形式保存 - 列表
list
内,每一元素对应一句话 - 遍历列表,遇到元素
## 归档
则其之后的元素按照不同条件取出、分析 - 直到遇到元素
## 日期归档
,则把其之后的元素按条件取出、分析
细节在代码里(代码文件refresh.py
),我使用汉语标明了。
""" """
import os.path as osp
import re
def refreah():
"""
我要处理的文件是 README.md
那么我获取其绝对路径
注意这里处理的文件和代码文件处于同一目录下
"""
dirname = osp.dirname(__file__)
filepath = osp.join(dirname, "README.md")
"""
打开这个文件,其变量名是 f
"""
with open(filepath, 'r+', encoding='utf-8') as f:
"""
将文件的内容读到内存 f.read()
"""
content = f.read()
"""
以“换行符”/“回车”进行字符串分割
这样,row_list 每个元素就是一行文字了
"""
row_list = content.split('\n')
"""
下面开始把不同的目录对应的条目取出
"""
# found the un-packed row
un_packed_rows = []
dict_cata = {}
dict_row_flag = False
date_row_flag = False
dict_row_num = 0
date_row_num = 0
cur_cata = None
for idx, row in enumerate(row_list):
"""
如果到了 ## 归档 下面
"""
if dict_row_flag:
if "### " in row[:4]:
cur_cata = row[4:]
"""
data_cata 是我们的类别字典,最终效果为
data_cata = {
"匹配": [匹配的第1题, 匹配的第2题, ...],
"字符串": [字符串的第1题, 字符串的第2题, ...],
...
}
"""
dict_cata.setdefault(cur_cata, [])
elif "- " in row[:2] and not re.match('\[.*\]\(.*\)', row[2:]):
"""
这里用了一个正则
因为索引格式为
- [索引名称](#索引名称)
而题目格式为
- 题目 程序 日期
因此如果仅凭是否以「- 」开头,则难以区分二者
因此加了一个是否正则匹配 [*](*) 的判断
"""
dict_cata[cur_cata] = [row] + dict_cata[cur_cata]
else:
"""
判断是否到了 ## 归档 下面
"""
if row == "## 归档":
dict_row_flag = True
dict_row_num = idx + 1
"""
如果到了 ## 日期归档 下面
"""
if date_row_flag:
"""
- uu 是我自己设的格式
如果题目有 uu ,那么这条就是我要用脚本加到归档里的题目
"""
if '- uu ' in row[:5]:
un_packed_rows = [row] + un_packed_rows
row_list[idx] = "- " + row[5:]
else:
"""
判断是否到了 ## 日期归档 下面
"""
if row == "## 日期归档":
date_row_flag = True
dict_row_flag = False
date_row_num = idx + 1
# pack those rows to "## 日期归档"
"""
下面是把新题目(uu)加到 data_cata 字典中
"""
for row in un_packed_rows:
row = row.split(' ')
file_num = 0
file_name = ""
for ele in row:
if re.match('\[.*\]\(.*\)', ele):
file_num += 1
file_name += (ele + ' ')
catas = row[4:-file_num]
for c in catas:
dict_cata.setdefault(c, [])
row_ = '- ' + row[3] + ' ' + file_name + row[2]
dict_cata[c].append(row_)
# del file "## 归档"
"""
下面是清空 ## 归档 的内容
根据 dict_cata 书写新的全部内容
"""
row_list_a = row_list[:dict_row_num]
row_list_c = row_list[date_row_num-2:]
## row_list_b
row_list_b = []
for key in dict_cata:
row_list_b.append("\n### " + key)
for row in dict_cata[key]:
row_list_b.append(row)
row_list_b[0] = row_list_b[0][1:]
row_list = row_list_a + row_list_b + row_list_c
"""
把新处理好的文本,逐行写到文件中
(文件先清空,原文本被覆盖)
"""
with open(filepath, 'w', encoding='utf-8') as f:
for row in row_list:
f.write(row + '\n')
"""
提示用户,处理好了
"""
print("\033[1;34mREADME.md refresh done\033[0m")
print("\033[1;36mhttps://github.com/PiperLiu/ACMOI_Journey\033[0m")
print("star"
+ "\033[1;36m the above repo \033[0m"
+ "and practise together!")
def cata_index():
"""
这是我用于生成索引的函数
索引就是:
## 归档
- [匹配](#匹配)
- [字符串](#字符串)
- [双指针法](#双指针法)
- [搜索](#搜索)
思路很简单,还是取各个三级标题
然后规整到 ## 归档 下面
"""
dirname = osp.dirname(__file__)
filepath = osp.join(dirname, "README.md")
with open(filepath, 'r+', encoding='utf-8') as f:
content = f.read()
row_list = content.split('\n')
cata_list = []
dict_row_flag = False
dict_row_num = 0
cata_row_num = 0
for idx, row in enumerate(row_list):
if dict_row_flag:
if cata_row_num == 0:
cata_row_num = idx
if "### " in row[:4]:
cata = row[4:]
cata = "- [" + cata + "]" + "(#" + cata + ")"
cata_list.append(cata)
elif row == "## 归档":
dict_row_flag = True
dict_row_num = idx + 1
elif row == "## 日期归档":
cata_list.append("\n")
break
# add idx
row_list_a = row_list[:dict_row_num]
row_list_c = row_list[cata_row_num:]
row_list = row_list_a + cata_list + row_list_c
with open(filepath, 'w', encoding='utf-8') as f:
for row in row_list:
f.write(row + '\n')
refresh()
cata_index()
最终的运行效果是,我在命令行执行该脚本,则文档自动规整。
argparse应用
注意到上面我输入了一个参数 -r
,这个是为了让 refresh.py
这个文件有更多功能,并且在不同参数时做不同的事。参数仿佛不同的「按钮」。
我将各个功能封装在不同函数中,将应用解耦,即不同功能间不互相依赖,防止出现逻辑错误。
此外,我新建了一个函数,用于获取参数。
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument(
'--refresh', '-r',
action='store_true',
help='refreah README.md'
)
args = parser.parse_known_args()[0]
return args
这样,我们就可以获取到 -r
这个参数,在主进程里,我们判断用户是否使用 r
这个功能,使用的话,则调用相应函数。
def main(args=get_args()):
if args.refresh:
refreah()
cata_index()
if __name__ == "__main__":
main()
注意事项:encoding
此外,因为是中文,因此编码规则值得注意。
比如,在文件开头加入 #-*- coding:UTF-8 -*-
;在 open 文件时,加入 encoding='uft-8'
参数。
值得改进的点:更好的正则
如果你读我的代码,你会发现读取、判断行的逻辑上有些“粗暴”。
仅仅通过判断 - []
等是否是行的前四个字符是不妥的,并且我在判断 - uu 日期 题目名称与概括 类别A 类别B 类别C... [程序文件1] [程序文件2] [程序文件3]...
时,也仅仅是通过 if else
判断是否有方括号、括号来区分类别字段
与程序文件
字段。
这是不妥的,这样,我就难以在题目里自由书写。一个可行的改进,是使用强大的正则表达式进阶属性。
尚无精力讨论,未来可能会进一步修改讨论,欢迎持续关注我。
项目地址:https://github.com/PiperLiu/ACMOI_Journey
欢迎 star watch fork pr issue 五连。