我用 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 引擎渲染後的效果如下圖。
如上,我不但新增了三級標題### 匹配
、### 字串
等,還為三級標題建立了目錄索引連結。
最終程式實現如下圖。
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 五連。