「懶惰的美德」我用 python 寫了個自動生成給文檔生成索引的腳本

我用 python 寫了一個自動生成索引的腳本

簡介:為了刷演算法題,建了一個 GitHub倉庫:PiperLiu / ACMOI_Journey,記錄自己的刷題軌跡,並總結一下方法、心得。想到一個需求:能不能在我每新增一條題目的筆記後,利用程式自動地將其歸類、創建索引?用 Python 實現一個入門級的小腳本,涉及到文件讀寫、命令行參數、數組操作應用等知識點,在此分享給朋友們。

需求實現

我有一個 Markdown 文檔,長成下面這個樣子:

# ACM/OI Journey
在此留下刷題痕迹與刷題心得。

不定期的方法論總結在這裡[./notes/README.md](./notes/README.md)。

學習資料:
OI Wiki: //oi-wiki.org/
力扣中國: //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 引擎渲染後的效果如下圖。 左邊是腳本處理過的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[:2and 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;36m//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 判斷是否有方括弧、括弧來區分類別欄位程式文件欄位。

這是不妥的,這樣,我就難以在題目里自由書寫。一個可行的改進,是使用強大的正則表達式進階屬性。

尚無精力討論,未來可能會進一步修改討論,歡迎持續關注我。

項目地址://github.com/PiperLiu/ACMOI_Journey

歡迎 star watch fork pr issue 五連。

祝各位變得更強。歡迎關注公眾號:Piper蛋窩,回復微信加我微信,邀請你進入高品質技術交流群 / 好文分享群。歡迎點贊、點擊在看將好文分享出去。