从 Markdown 文本中批量制作Anki卡片的Python脚本
标签: anki, markdown ;
Anki的卡片本质上是一个html页面,Anki虽然提供了富文本编辑,但是在批量制作卡片进并没有用处。于是我动手写了这个脚本。
首先,本程序只支持两种类型的卡片:基础(Basic),填空题(Cloze)。我觉得这两种类型的卡片已经足以应付大多数需求。
格式约定:
卡片之间用独占一行的至少3个短横---
分隔
“基础”卡片有两个字段:正面和背面,用3个连续的回车分隔。
“填空题”只有一个字段,需要挖空的填空部分用大括号括起来就行了。比如“这是一个{填空题}”,在导入后会变成“这是一个{{c1::填空题}”。
除此之外,几乎所有的Markdown语法都支持。
另外,程序会在运行时从 style.css
导入CSS,我只在style.css中美化了一下表格,如有其它需求可自行添加。
用法:
mkcards.py [-d 记忆库] cards.md
源码:
# 从文本文件中批量制作 Anki 记忆卡片
# 本程序需要在 Anki 中安装 AnkiConnect 插件,该插件会在本地启动一个Web API服务
# 按 Ctrl + Shift + A 组合键,在对话框中点“获取插件”, 在“代码”处输入:2055492159,点“确定”
# 即可安装 AnkiConnect 插件
# 本程序支持两种卡片类型:基础(Basic), 填空题(Cloze)
# 基础卡片支持两个字段,对应正面和背面,字段间用至少两个连续的换行进行分隔
# 填空题只支持一个字段,在文本中使用大括号插入填空
# 不同的卡片之间用顶格的三个短横分隔,类型于 Markdown 语法
import json
import requests
import re
import argparse
import mistune
#from markupsafe import Markup
mark = mistune.create_markdown(escape=True, plugins=[
'strikethrough',
'table',
'url',
'task_lists',
'mark',
'insert',
'superscript',
'subscript'
])
DECK = ''
CLOZE_MOD = None
BASIC_MOD = None
def read_file(f):
with open(f, 'r') as f:
return f.read()
def apply_css(model):
data = {
'action': 'updateModelStyling',
'version': 6,
'params': {
'model': {
'name': model,
'css': read_file('style.css')
}
}
}
return post(data)
MODELS = {}
def post(data):
res = requests.post('http://localhost:8765', data=json.dumps(data)).json()
if res['error']:
print(res['error'])
return res
def create_card(deck_name, model_name, fields):
field_names = MODELS[model_name]
fields_dic = dict(zip(field_names, fields))
# print(fields_dic)
data = {
'action': 'addNote',
'version': 6,
'params': {
'note': {
'deckName': deck_name,
'modelName': model_name,
'fields': fields_dic,
'options': {
'allowDuplicate': False
},
'tags': []
}
}
}
res = post(data)
if res['error']:
print(model_name, fields_dic)
def parse_file(path):
with open(path, 'r') as file:
return [s.strip() for s in re.split('^-{3,}', file.read(), flags=re.M)]
def add_card(text):
rcloze = r'\{([^}]+)\}'
m = re.findall(rcloze, text)
if m:
cloze_card = mark(re.sub(r'\{([^}]+)\}', r'{{c1::\1}}', text))
fields = [cloze_card, '']
create_card(DECK, CLOZE_MOD, fields)
else:
basic_card = re.split(r'(?:\r?\n){3,}|(?:\r){3,}', text)
if len(basic_card) != 2:
print(f'字段解析失败:{text}')
else:
fields = [mark(basic_card[0]), mark(basic_card[1])]
create_card(DECK, BASIC_MOD, fields)
def envinit():
global CLOZE_MOD
global BASIC_MOD
data = {
'action': 'modelNames',
'version': 5
}
res = post(data)
if not res['error']:
models = res['result']
for model in models:
fields = model_fields(model)
MODELS[model] = fields
if 'Cloze' in models:
CLOZE_MOD = 'Cloze'
elif '填空题' in models:
CLOZE_MOD = '填空题'
else:
print('示找到填空题卡片类型')
exit()
if 'Basic' in models:
BASIC_MOD = 'Basic'
elif '基础' in models:
BASIC_MOD = '基础'
else:
print('未找到基础卡片类型')
exit()
for model in (CLOZE_MOD, BASIC_MOD):
apply_css(model)
else:
print(res['error'])
def list_decks():
data = {
'action': 'deckNames',
'version': 5
}
res = post(data)
if not res['error']:
return res['result']
def model_fields(model):
data = {
'action': 'modelFieldNames',
'version': 5,
'params': {
'modelName': model
}
}
return post(data)['result']
if __name__ == '__main__':
envinit()
parser = argparse.ArgumentParser(description='批量制作Anki记忆卡')
parser.add_argument('file', type=str, help='指定文件')
parser.add_argument('-d', '--deck', type=str, help='指定要添加的记忆库')
args = parser.parse_args()
decks = list_decks()
if args.deck:
if not args.deck in decks:
print(f'记忆库不存在: {args.deck}')
exit()
DECK = args.deck
else:
# if '默认' in decks or 'Default' in decks:
# DECK = '默认' if '默认' in decks else 'Default'
print('必须指定要添加的记忆库')
exit()
for card in parse_file(args.file):
add_card(card)