Anki 笔记读取器
Anki 笔记数据库 Anki 的笔记数据都存储在 collection.anki2 文件中,包含了用户的所有笔记、卡片、标签等信息 文件路径 1 C:\Users\Administrator\AppData\Roaming\Anki2\账户1\collection.anki2 可以使用 SQLite 或 Python 的 sqlite3 模块连接并查询该数据库,查看具体的表和字段。 本地笔记检索 使用 sqlite3 链接数据库 打印 collection.anki2 数据库中的所有表格 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import sqlite3 # 连接到 Anki 数据库(此处注意替换为自己的文件路径) anki_db_path = r'C:\Users\Administrator\AppData\Roaming\Anki2\本地账户\collection.anki2' conn = sqlite3.connect(anki_db_path) # 查看 notes 表的结构 cursor = conn.cursor() cursor.execute("PRAGMA table_info(notes);") columns = cursor.fetchall() # 打印字段名 for column in columns: print(column) # 关闭连接 conn.close() 输出结果为 1 2 3 4 5 6 7 8 9 10 11 (0, 'id', 'INTEGER', 0, None, 1) (1, 'guid', 'TEXT', 1, None, 0) (2, 'mid', 'INTEGER', 1, None, 0) (3, 'mod', 'INTEGER', 1, None, 0) (4, 'usn', 'INTEGER', 1, None, 0) (5, 'tags', 'TEXT', 1, None, 0) (6, 'flds', 'TEXT', 1, None, 0) (7, 'sfld', 'INTEGER', 1, None, 0) (8, 'csum', 'INTEGER', 1, None, 0) (9, 'flags', 'INTEGER', 1, None, 0) (10, 'data', 'TEXT', 1, None, 0) 说明可以正常连接数据库,准备工作已做好 制作一个 Python 小程序读取数据库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import sys import sqlite3 import pandas as pd import re from PyQt6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QPushButton, QLineEdit, QTextEdit, QMessageBox ) class AnkiNoteReader(QWidget): def __init__(self): super().__init__() self.initUI() self.notes_df = None def initUI(self): self.setWindowTitle('Anki 笔记读取器') layout = QVBoxLayout() self.load_button = QPushButton('加载笔记', self) self.load_button.clicked.connect(self.load_notes) layout.addWidget(self.load_button) self.search_box = QLineEdit(self) self.search_box.setPlaceholderText('搜索关键词') layout.addWidget(self.search_box) self.search_button = QPushButton('搜索', self) self.search_button.clicked.connect(self.search_notes) layout.addWidget(self.search_button) self.text_area = QTextEdit(self) layout.addWidget(self.text_area) self.setLayout(layout) def load_notes(self): try: # 连接到 Anki 数据库 anki_db_path = r'C:\Users\Administrator\AppData\Roaming\Anki2\本地账户\collection.anki2' conn = sqlite3.connect(anki_db_path) query = 'SELECT id, flds FROM notes' self.notes_df = pd.read_sql_query(query, conn) conn.close() self.display_notes(self.notes_df) except Exception as e: QMessageBox.critical(self, "错误", str(e)) def display_notes(self, notes): self.text_area.clear() for index, row in notes.iterrows(): # 清除不可见字符,保留中文和其他可见字符 fields = re.sub(r'[^\u4e00-\u9fa5\x20-\x7E]', ' ', row['flds']) # 保留中文和可见字符 self.text_area.append(f"Note ID: {row['id']}\nFields: {fields.strip()}\n") def search_notes(self): if self.notes_df is not None: search_term = self.search_box.text() if search_term: filtered_notes = self.notes_df[self.notes_df['flds'].str.contains(search_term, na=False)] self.display_notes(filtered_notes) else: self.display_notes(self.notes_df) else: QMessageBox.warning(self, "警告", "请先加载笔记!") if __name__ == '__main__': app = QApplication(sys.argv) ex = AnkiNoteReader() ex.resize(600, 400) ex.show() sys.exit(app.exec()) 加载笔记以及搜索笔记如下图 远程笔记检索 功能实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from flask import Flask, request, jsonify import sqlite3 import pandas as pd app = Flask(__name__) # 数据库文件路径 DATABASE_PATH = r'C:\Users\Administrator\AppData\Roaming\Anki2\本地账户\collection.anki2' def query_database(query): """执行 SQL 查询并返回结果为 DataFrame""" conn = sqlite3.connect(DATABASE_PATH) df = pd.read_sql_query(query, conn) conn.close() return df @app.route('/search', methods=['GET']) def search_notes(): """根据查询词搜索笔记""" search_term = request.args.get('term', '') query = f"SELECT id, flds FROM notes WHERE flds LIKE '%{search_term}%'" try: results = query_database(query) notes = results.to_dict(orient='records') return jsonify(notes) except Exception as e: return jsonify({'error': str(e)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) # 可访问所有 IP 运行后提示: 1 2 3 4 5 6 * Serving Flask app 'app' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on all addresses (0.0.0.0) * Running on http://127.0.0.1:5000 * Running on http://192.168.1.94:5000 这时,我们可以在浏览器输入 1 http://192.168.1.94:5000/search?term= 要搜索的内容 完善输出 增加搜索按钮 优化输出前端效果 表格形式输出 添加图片显示(需添加图片数据库地址) 添加导出 CSV 功能 项目结构为: 1 2 3 4 5 6 7 /project /static styles.css javascript.js /templates index.html app.py 主程序 以下代码中,需要将 Anki 数据库路径及图片媒体路径替换为自己电脑对应的路径 app.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 from flask import Flask, request, send_from_directory, render_template, send_file import sqlite3 import pandas as pd import os import re import csv app = Flask(__name__) # 数据库文件路径(需要替换成自己Anki数据库及图片路径) DATABASE_PATH = r'C:\Users\Administrator\Desktop\collection.anki2' MEDIA_PATH = r'C:\Users\Administrator\AppData\Roaming\Anki2\账户1\collection.media' # 允许访问媒体文件 @app.route('/media/<path:filename>') def media(filename): full_path = os.path.join(MEDIA_PATH, filename) print(f"Attempting to access: {full_path}") return send_from_directory(MEDIA_PATH, filename) def query_database(query, params): """执行 SQL 查询并返回结果为 DataFrame""" conn = sqlite3.connect(DATABASE_PATH) df = pd.read_sql_query(query, conn, params=params) conn.close() return df @app.route('/', methods=['GET', 'POST']) def index(): results = [] message = "" if request.method == 'POST': search_term = request.form.get('term', '') print(f"Searching for: {search_term}") # query = "SELECT id, flds FROM notes WHERE flds LIKE ?" # 更新查询,获取 flags 字段 # query = """ # SELECT id, flds, flags # FROM notes # WHERE flds LIKE ? # """ query = """ SELECT id, flds FROM notes WHERE flds LIKE ? """ try: results_df = query_database(query, (f'%{search_term}%',)) print(f"Query results: {results_df}") if results_df.empty: message = "未找到相关结果。" else: for _, row in results_df.iterrows(): fields = row['flds'].split('\x1f') print(f"Fields content: {fields}") for i, field in enumerate(fields): matches = re.findall(r'src="([^"]+\.(?:jpg|png|gif))"', field) for match in matches: img_tag = f' class="image" src="/media/{match}" alt="Image" ' field = re.sub(r'src="[^"]*"', img_tag, field, count=1) fields[i] = field.strip() # results.append({'id': row['id'], 'flds': fields}) # 添加 flags 数据 # results.append({'id': row['id'], 'flds': fields, 'flags': row['flags']}) results.append({'id': row['id'], 'flds': fields}) except Exception as e: message = f"查询错误: {str(e)}" print(message) # 使用 render_template 加载 HTML 模板 return render_template('index.html', results=results, message=message) @app.route('/export', methods=['GET']) def export_notes(): search_term = request.args.get('term', '') print(f"导出请求,搜索词: {search_term}") # 确认请求是否到达 # query = """ # SELECT id, flds, flags # FROM notes # WHERE flds LIKE ? # """ query = """ SELECT id, flds FROM notes WHERE flds LIKE ? """ try: results_df = query_database(query, (f'%{search_term}%',)) temp_file = 'exported_notes.csv' # 使用 utf-8-sig 编码 results_df.to_csv(temp_file, index=False, encoding='utf-8-sig') return send_file(temp_file, as_attachment=True) except Exception as e: print(f"导出错误: {str(e)}") return "导出失败", 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) HTML index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Anki 笔记查询</title> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}"> <script src="static/javascript.js"></script> <!-- 引入 JavaScript 文件 --> </head> <body> <h1>Anki 笔记查询</h1> <form method="post"> <input type="text" name="term" placeholder="输入搜索关键词" required> <input type="text" id="searchInput" placeholder="搜索内容并导出内容"> <button type="submit">搜索</button> <button onclick="exportNotes()">导出笔记</button> </form> <h2>查询结果</h2> <div class="message">{{ message }}</div> <table> <tr> <th>Note ID</th> <th>Fields</th> <!-- <th>flags</th>--> </tr> {% for note in results %} <tr> <td>{{ note.id }}</td> <td>{{ note.flds | join('<br>') | safe }}</td> <!-- 展示旗帜数据 --> <!-- <td>{{ note.flags }}</td>--> </tr> {% endfor %} </table> </body> </html> javascript javascript.js 1 2 3 4 5 6 function exportNotes() { const searchTerm = document.getElementById('searchInput').value; // 获取搜索词 const url = `/export?term=${encodeURIComponent(searchTerm)}`; // 构建请求 URL window.location.href = url; // 直接跳转到 URL 下载文件 } CSS styles.css 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 /* styles.css */ body { font-family: Arial, sans-serif; margin: 20px; background-color: #f9f9f9; /* 背景颜色 */ } table { width: 100%; border-collapse: collapse; /* 确保边框合并 */ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加阴影 */ } th, td { border: 1px solid #ddd; /* 添加边框 */ padding: 12px; /* 增加内边距 */ text-align: left; } th { background-color: #4CAF50; /* 表头背景色 */ color: white; /* 表头字体颜色 */ } tr:nth-child(even) { background-color: #f2f2f2; /* 偶数行背景色 */ } tr:hover { background-color: #e0f7fa; /* 鼠标悬停行的背景色 */ } .message { color: red; } .image { max-width: 600px; /* 调整图片尺寸 */ max-height: 600px; border-radius: 5px; /* 圆角 */ }