9

「懒惰的美德」我用 python 写了个自动生成给文档生成索引的脚本

 3 years ago
source link: http://www.cnblogs.com/piperliu/p/14088703.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我用 python 写了一个自动生成索引的脚本

简介:为了刷算法题,建了一个 GitHub仓库: PiperLiu / ACMOI_Journey ,记录自己的刷题轨迹,并总结一下方法、心得。想到一个需求:能不能在我每新增一条题目的笔记后,利用程序自动地将其归类、创建索引?用 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 引擎渲染后的效果如下图。 2MvyYvI.png!mobile

如上,我不但新增了三级标题 ### 匹配### 字符串 等,还为三级标题创建了目录索引链接。

最终程序实现如下图。 YJRBZ3J.gif!mobile

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()

最终的运行效果是,我在命令行执行该脚本,则文档自动规整。 YJRBZ3J.gif!mobile

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 五连。

祝各位变得更强。欢迎关注公众号: Piper蛋窝 ,回复 微信 加我微信,邀请你进入高质量技术交流群 / 好文分享群。欢迎 点赞 、点击 在看 将好文分享出去。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK