ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python+SQLite打造个人乐高收藏管理系统

2026/9/3 11:14:14 拓冰建站 浏览量
Python+SQLite打造个人乐高收藏管理系统 石家庄跑了一趟一口气带回 70 余套乐高听起来确实是件快乐的事。不过真正让人头疼的往往不是搬回家那一路而是搬回来之后这 70 套分别是什么套装摆在哪个箱子哪些已经拆封哪些零件还齐全如果只是为了拍一条 Vlog 把它们铺满整张桌子那视频结束后的“整理环节”才是工作量飙升的开始。我见过不少收藏朋友兴奋点集中在“买”和“开箱”上却忽视了“管理”。而收藏一旦超过 50 套记忆就不再可靠。你可能会重复买同一套可能为了找某一个小零件翻遍所有盒子也可能在半年后根本想不起自己有一套适合送人的全新套装。这些问题本质不是收纳习惯问题而是信息管理问题。与其靠拍脑袋不如给每套乐高建一个“数据库档案”用脚本批量录入、查询、统计和导出让整理过程和 Vlog 一样有迹可循。本文就以“石家庄收获 70 余套乐高”的整理场景为例从零搭建一个私人乐高库存管理系统。技术方案不复杂Python 3 SQLite 做基础数据管理CSV 做批量导入Rebrickable 公开接口做套装信息补全最后用一个 CLI 工具完成查询和导出。整套代码跑完后你会拥有一个可离线查询、可重复导入、可导出的个人乐高“仓库系统”。如果你收藏积木、手办、图书也可以直接套用这套思路。1. 70 余套乐高回家之后真正的麻烦才开始我先把结论放在前面收藏规模越大管理成本增长得越快。这里的“成本”不单指钱还包括三类容易被忽略的开销。第一类是“选择成本”。70 多套摆在眼前时你很容易陷入选择困难今晚拼哪套哪套适合用 Vlog 记录完整拼装过程如果不知道每套的主题、颗粒数和场景定位选一套可能要花掉半小时而不是两分钟。第二类是“查找成本”。套装外盒都差不多大小堆起来之后很难分清里面是谁。更麻烦的是你只有拼到一半发现缺件才想起要去对应盒子里找备用件。没有位置记录你会在客厅柜子、书房抽屉和储物间箱子之间来回拉锯。第三类是“重复成本”。看到某个历史套装价格合适你觉得“自己没有”于是又买了一套。回家一查拼搭记录才发现三年前已经入过同款。这种情况在收藏圈并不少见。避免它的唯一可靠方法就是在入手前先查库存表而不是先查购物车。也就是说70 套左右的库存已经处于“靠脑子和 Excel 都容易失控”的临界区。Excel 当然能记但手工维护的问题很快会出现编号格式不统一、字段越加越乱、想按状态统计要写一堆筛选数据很容易变成一滩死水。相比之下SQLite 单文件数据库更轻量配合脚本可以做到批量操作、条件查询和自动补全更适合拿来管理私人收藏。当然这套方案也有明确边界它管理的是“套装级别”的信息不是每一个零件的精确位置。如果将来要处理零件级 MOC 拼搭还需要更细的 BOM 管理那是另一个话题。本文先解决最实用的 70 套库存管理问题。2. 管理收藏不等于记账先想清楚要记什么设计数据库之前首先要克制住“什么字段都想要”的冲动。字段过多会让录入变成负担录入一旦变得麻烦系统很快就会被丢弃。我建议第一版只保留真正影响使用体验的字段。2.1 套装编号每套乐高的“主键”套装编号是最关键的字段。对乐高来说官方套装通常有一个类似“42115-1”的编号其中连字符后面的“-1”代表第几版设计。这个编号就像数据库里的主键作用是唯一标识一套乐高。这里容易犯一个错误以为套装编号只是纯数字。实际上如果不记录“-1”这个后缀某些产品可能出现多个版本。更稳妥的做法是完整记录官方套装编号并在数据库层设置唯一约束从根源上避免重复导入。2.2 状态、位置、来源记录里有价值的字段除了名称和编号真正有价值的是状态和位置。状态我建议使用一套固定枚举例如待拆封、拼搭中、拼好展示、拆件备用、已转出。用固定枚举而不是自由输入才能后续做统计。如果允许“放到一半”“拼好了”“散了”这类即兴输入统计时就会因为文本不统一而失控。位置字段也很实用。它不是让你精确到每个抽屉而是记录“客厅展示柜”“书房顶柜”“卧室床边柜”这一层即可。日常找套装能定位到柜子就已经省下大量时间。来源字段则记录了购买渠道和地点。对于“石家庄购入”“实体店”“朋友转让”这类信息很多人初看觉得没必要等到想复盘一年花了多少钱、从哪些渠道买入时就会庆幸当初记录了来源。2.3 数据字典第一版字段设计第一版表结构可以参考下表字段含义示例是否必填set_code官方套装编号DEMO-001是set_name套装名称演示套装是theme所属系列/主题科技系列否release_year发售年份2023否pieces颗粒数1200否status当前状态待拆封是place存放位置客卧顶层柜否source购买来源石家庄实体店否note备注盒况轻微压角否created_at入库时间自动生成-updated_at更新时间自动生成-这个数据字典不算复杂却足够支撑大部分日常操作按状态统计、按位置查询、按来源复盘、按名称模糊搜索。记住第一版系统最重要的目标不是功能丰富而是让用户愿意持续录入和维护。3. 环境准备一个干净、可复现的 Python 项目为了让整理过程可复现我建议使用独立虚拟环境而不是直接往系统 Python 里塞依赖。下面操作在 Windows、macOS、Linux 上思路一致命令差异我会标注。3.1 需要准备什么本机需要安装 Python 3。当前主流 Python 3.8 以上版本都可以运行本文示例。检查方式python --version如果提示找不到命令Windows 用户可以尝试py --version版本请以你本机实际安装为准本文不依赖特殊新语法Python 3 即可。3.2 项目目录规划在合适位置创建目录后建议按下面的结构组织文件lego-warehouse/ ├── schema.sql # 建表 SQL ├── init_db.py # 初始化数据库 ├── import_sets.py # 批量导入 CSV ├── query_sets.py # 查询、统计、导出 ├── find_sets_api.py # 调用 Rebrickable 接口 ├── sample_sets.csv # 示例数据 └── lego_warehouse.db # SQLite 数据库文件会自动生成把代码、数据、数据库放在一个目录内方便以后整体备份或上传到自己的私有仓库。3.3 安装依赖本文只需要三个第三方库requests、tabulate以及生成 Excel 时可选用的 pandas 和 openpyxl。为了减少变量先把基础依赖装上cd lego-warehouse python -m venv venvWindows 环境激活虚拟环境venv\Scripts\activatemacOS / Linux 环境source venv/bin/activate激活后安装依赖pip install requests tabulate如果后续需要生成 xlsx 文件再补充安装pip install pandas openpyxl这里统一用 requests 发 HTTP 请求。Python 标准库虽然有 urllib但 requests 在处理 JSON 和错误码时更直观适合新手快速落地。4. 初始化数据库先建好 70 套库存的“容器”数据表是一套系统的容器。建表时把唯一约束、默认值和索引设计好后面导入和查询都会轻松很多。4.1 建表 SQL创建schema.sql-- file: schema.sql PRAGMA foreign_keys ON; CREATE TABLE IF NOT EXISTS lego_sets ( id INTEGER PRIMARY KEY AUTOINCREMENT, set_code TEXT NOT NULL UNIQUE, set_name TEXT NOT NULL, theme TEXT NOT NULL DEFAULT 未分类, release_year INTEGER, pieces INTEGER, status TEXT NOT NULL DEFAULT 待拆封, place TEXT, source TEXT, note TEXT, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), updated_at TEXT NOT NULL DEFAULT (datetime(now, localtime)) ); CREATE INDEX IF NOT EXISTS idx_lego_sets_status ON lego_sets(status); CREATE INDEX IF NOT EXISTS idx_lego_sets_theme ON lego_sets(theme);这里的关键设计有三个set_code TEXT NOT NULL UNIQUE确保同一套装不会插入两遍这是防止重复购买的第一道防线。status TEXT NOT NULL DEFAULT 待拆封批量导入时即使忘了填状态也会先落到最合理的默认值。created_at和updated_at自动记录入库与更新时间方便日后复盘。索引不是越多越好。第一版只需要给status和theme加索引因为这两个字段未来会高频出现在统计条件里。4.2 用 Python 执行建库脚本你可以直接在命令行用 sqlite3 执行 schema但为了统一我更推荐用 Python 脚本完成初始化。创建init_db.py# file: init_db.py import sqlite3 import pathlib BASE_DIR pathlib.Path(__file__).resolve().parent DB_PATH BASE_DIR / lego_warehouse.db SCHEMA_PATH BASE_DIR / schema.sql def init_db(): conn sqlite3.connect(DB_PATH) try: sql SCHEMA_PATH.read_text(encodingutf-8) conn.executescript(sql) print(f数据库初始化成功{DB_PATH}) finally: conn.close() if __name__ __main__: init_db()执行python init_db.py看到“数据库初始化成功”后目录下会出现lego_warehouse.db。这个单文件就是后续所有数据的存放位置。5. 批量录入把“石家庄收获”变成可查询的记录70 余套乐高如果一套一套手工录入不仅效率低还容易出错。更合理的方式是先列一张 CSV 清单再写一个可重复运行的导入脚本。CSV 的好处是普通表格软件可以直接编辑团队成员或家人也能帮忙填写。5.1 先准备一份 CSV 清单创建sample_sets.csv这里只放 3 条演示数据。实际使用时请把“流水账”替换成你真正的套装记录set_code,set_name,theme,release_year,pieces,status,place,source,note DEMO-001,演示套装A,示例主题,2023,500,待拆封,客厅展示柜,石家庄,本地示例数据 DEMO-002,演示套装B,示例主题,2022,800,拼搭中,书房桌面,石家庄,本地示例数据 DEMO-003,演示套装C,示例主题,2021,300,拼好展示,卧室置物架,石家庄,本地示例数据建议在 Excel 中填写纯文本编号时将列格式设置为“文本”。套装编号如果被 Excel 当成数字处理后续可能出现 0 丢失或科学计数法显示的问题。用带连字符的编号也能减少这种误判但不可完全依赖。5.2 幂等导入脚本“幂等”听起来高级通俗解释就是同一个脚本跑第二次不会因为数据已经存在而报错也不会产生重复记录。这一能力在批量盘点场景中非常有用。创建import_sets.py# file: import_sets.py import csv import sqlite3 import pathlib import sys BASE_DIR pathlib.Path(__file__).resolve().parent DB_PATH BASE_DIR / lego_warehouse.db def parse_int(value): try: return int(value) except (TypeError, ValueError): return None def import_csv(csv_path: str): conn sqlite3.connect(DB_PATH) total 0 try: with open(csv_path, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: code (row.get(set_code) or ).strip() name (row.get(set_name) or ).strip() if not code or not name: print(跳过空记录缺少编号或名称) continue conn.execute( INSERT INTO lego_sets ( set_code, set_name, theme, release_year, pieces, status, place, source, note ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(set_code) DO UPDATE SET set_name excluded.set_name, theme excluded.theme, release_year excluded.release_year, pieces excluded.pieces, status excluded.status, place excluded.place, source excluded.source, note excluded.note, updated_at datetime(now, localtime) , ( code, name, row.get(theme) or 未分类, parse_int(row.get(release_year)), parse_int(row.get(pieces)), row.get(status) or 待拆封, row.get(place) or , row.get(source) or , row.get(note) or , ), ) total 1 conn.commit() print(f导入完成共处理 {total} 条记录) finally: conn.close() if __name__ __main__: if len(sys.argv) ! 2: print(用法: python import_sets.py xxx.csv) sys.exit(1) import_csv(sys.argv[1])写入时使用utf-8-sig编码读取 CSV是为了兼容 Excel 生成的带 BOM 文件避免表头首列出现“\ufeff”乱码。执行导入python import_sets.py sample_sets.csv如果再次执行已经存在的DEMO-001不会被重复插入而是更新为 CSV 中的最新内容。这正是盘点现场修改后重新导入时最需要的特性。5.3 导入后检查导入完成后可以先简单查看总记录数python -c import sqlite3; conn sqlite3.connect(lego_warehouse.db); print(conn.execute(select count(*) from lego_sets).fetchone()[0])如果你把真实 70 余套装整理成 CSV 并导入最终这里的数字应该与手头库存数量一致。如果不一致优先检查 CSV 中是不是有重复编号或空行。6. 用 Rebrickable 接口自动补全套装信息手工整理完 70 余套之后你会发现一个问题很多盒子上虽然有官方编号但要补全套装名称、发售年份、系列和颗粒数仍然需要大量手工录入。此时可以借助公开乐高数据接口来减少重复劳动。6.1 为什么需要自动补全如果只是本地记录“编号 状态 位置”其实不调用接口也能完成。但当你需要知道某一套属于哪个系列、颗粒数是多少、适合在 Vlog 中做怎样定位时手工补全的成本就很高。公开数据集的意义是把“官方套装目录”变成可以程序化查询的信息源。这里以 Rebrickable 为例说明思路。它是一个社区维护的乐高数据库提供套装编号、名称、年份、主题、零件数等字段。需要说明的是第三方公开接口的使用限制和字段定义可能随官方调整本文只演示关键逻辑接入前请以官网文档为准。6.2 注册 key 与请求设置使用该接口前需要注册账号并申请 API Key然后在本地环境变量中设置export REBRICKABLE_API_KEY你的keyWindows PowerShell 中对应$env:REBRICKABLE_API_KEY你的key不要把 API Key 写死在代码中。它相当于你的访问凭证一旦提交到公开仓库别人就可以冒用你的身份调用接口。用环境变量保存更安全。6.3 查询脚本与更新逻辑创建find_sets_api.py# file: find_sets_api.py import os import sys import requests BASE_URL https://rebrickable.com/api/v3/lego/sets/ def make_header(): api_key os.getenv(REBRICKABLE_API_KEY, ) if not api_key: raise RuntimeError(请先设置环境变量 REBRICKABLE_API_KEY) return {Authorization: fkey {api_key}} def search_sets(keyword: str): resp requests.get( BASE_URL, params{search: keyword, page_size: 5}, headersmake_header(), timeout15, ) if resp.status_code ! 200: print(请求失败:, resp.status_code, resp.text[:200]) return data resp.json() for item in data.get(results, []): print(f{item.get(set_num)} | {item.get(name)} | {item.get(year)} | {item.get(num_parts)} 件) def get_set_by_code(set_code: str): resp requests.get( f{BASE_URL}{set_code}/, headersmake_header(), timeout15, ) if resp.status_code 404: print(未找到该套装编号请检查是否包含 -1 后缀) return if resp.status_code ! 200: print(请求失败:, resp.status_code, resp.text[:200]) return data resp.json() print({ set_code: data.get(set_num), set_name: data.get(name), year: data.get(year), num_parts: data.get(num_parts), }) if __name__ __main__: if len(sys.argv) 3: print(用法:) print( python find_sets_api.py search 保时捷) print( python find_sets_api.py code 42115-1) sys.exit(1) command sys.argv[1] value sys.argv[2] if command search: search_sets(value) elif command code: get_set_by_code(value) else: print(未知命令)搜索示例python find_sets_api.py search 保时捷完整编号查询示例python find_sets_api.py code 42115-1特别提醒套装编号一定要写完整。官方数据中的编号通常带版本后缀比如42115-1。如果只写42115接口会返回 404因为对端把编号当成精确主键处理。若你只有外盒上的纯数字编号建议先走search命令搜索确认后再使用。7. 查询、统计与导出Vlog 清单的真正价值数据录入完成之后系统才开始真正产生价值。当你想拍“70 余套乐高合集”Vlog 时与其一个个搬箱子查不如直接查询数据库得到一份清单再按清单去对应位置取货。7.1 按状态统计库存建设一个简单的 CLI 查询工具。创建query_sets.py# file: query_sets.py import sqlite3 import pathlib import argparse import csv BASE_DIR pathlib.Path(__file__).resolve().parent DB_PATH BASE_DIR / lego_warehouse.db def connect(): return sqlite3.connect(DB_PATH) def count_by_status(): conn connect() rows conn.execute( SELECT status, COUNT(*) AS cnt FROM lego_sets GROUP BY status ORDER BY cnt DESC ).fetchall() conn.close() print(f{状态:12}{数量:6}) for status, cnt in rows: print(f{status:12}{cnt:6}) def list_sets(status: str, keyword: str): conn connect() sql SELECT set_code, set_name, status, place FROM lego_sets WHERE (? OR status ?) AND (? OR set_name LIKE ?) ORDER BY set_code like f%{keyword}% rows conn.execute(sql, (status, status, keyword, like)).fetchall() conn.close() print(f{编号:16}{名称:30}{状态:12}{位置:20}) for code, name, st, place in rows: print(f{code:16}{name:30}{st:12}{place:20}) def export_csv(output_path: str): conn connect() rows conn.execute(SELECT * FROM lego_sets ORDER BY set_code).fetchall() cols [desc[0] for desc in conn.execute(SELECT * FROM lego_sets).description] conn.close() with open(output_path, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow(cols) writer.writerows(rows) print(f已导出到 {output_path}共 {len(rows)} 条记录) def main(): parser argparse.ArgumentParser(description乐高收藏管理工具) parser.add_argument(command, choices[status, list, export]) parser.add_argument(--status, default) parser.add_argument(--keyword, default) parser.add_argument(--output, defaultlego_backup.csv) args parser.parse_args() if args.command status: count_by_status() elif args.command list: list_sets(args.status, args.keyword) elif args.command export: export_csv(args.output) if __name__ __main__: main()查看整体状态分布python query_sets.py status预期输出类似状态 数量 待拆封 1 拼搭中 1 拼好展示 1筛选“拼搭中”的套装python query_sets.py list --status 拼搭中按名称搜索python query_sets.py list --keyword 演示7.2 按摆放位置查询当 Vlog 拍摄需要某一类主题时可以先用 location 层面的筛选缩小范围。虽然上面的list命令还没有把位置作为筛选条件但你可以在 SQLite 中直接扩展。更快的做法是利用表格导出然后在表格软件里二次筛选。如果你有 70 多套库存“位置”字段最大的价值不是日常搜索而是在大型整理后快速发现“哪些套装还没有填写位置”。对空位置字段做一次集中排查通常比一套套回忆节省大量时间。7.3 导出为 CSV 或 Excel导出 CSVpython query_sets.py export --output lego_records.csv导出的文件用 Excel 或 WPS 打开后可以直接发给家人协助确认也可以作为 Vlog 文案的底稿。因为代码中使用了utf-8-sig导出文件带 BOM多数表格软件能正常识别中文。如果安装了 pandas还可以临时生成 Excelimport pandas as pd import sqlite3 conn sqlite3.connect(lego_warehouse.db) df pd.read_sql_query(SELECT * FROM lego_sets ORDER BY set_code, conn) df.to_excel(lego_records.xlsx, indexFalse) conn.close() print(Excel 文件已生成)我的建议是优先使用 CSV 作为数据交换格式它足够通用Excel 更适合做一次性交付或发给不熟悉数据的收藏朋友。8. 常见问题与排查思路收藏整理过程中最容易出问题的点往往不在乐高而在编码、重复和接口调用。下面按实际使用频率列出排查清单问题现象可能原因排查方式解决方案导入 CSV 后中文乱码CSV 文件编码与脚本读取方式不一致用文本编辑器查看文件编码读取时使用 utf-8-sigExcel 另存为 CSV UTF-8套装编号被识别为数字0 丢失Excel 自动转换纯数字查看 CSV 原始内容列格式设为文本或编号保留连字符后缀重复导入同一套缺少唯一约束或编号不一致查询表中 set_code 是否包含前后空格在 database 层加 UNIQUE导入前 strip查询结果为空CSV 表头与脚本字段名不一致打印第一行 DictReader 的字段统一表头必须包含 set_code、set_nameRebrickable 返回 401API Key 没设置或写错检查环境变量是否为空重新设置 REBRICKABLE_API_KEYRebrickable 返回 404套装编号缺少版本后缀确认编号是否为完整格式先 search 再 code不要凭记忆拼编号Python 提示找不到模块未激活虚拟环境检查命令行前缀是否有 venv执行 source venv/bin/activate 或 venv\Scripts\activate误删了一条记录手工删除时条件写错检查 sqlite3 命令行日志养成先 SELECT 再 DELETE 的习惯提前备份这里想强调一个安全习惯删除数据是高风险操作。SQLite 数据库虽然很小但也不要直接在生产心态不端正的状态下执行 DELETE。正确流程是先备份再查询确认删除范围最后执行删除。如果给朋友或家人维护不妨在删除前把目标行导出为 CSV。9. 最佳实践给收藏数据建立的几条纪律工具能解决效率但真正的长期价值来自“纪律”。不立规矩再好的数据库三个月后也会变成废库。9.1 统一枚举和命名状态字段要固定使用同一套词。我自己会限制为“待拆封、拼搭中、拼好展示、拆件备用、已转出”五种。不要因为一时顺手写“拼了一半”“展示中”“出掉了”这类自由文本。统一命名的好处在统计时体现得最明显分组查询结果一眼能看懂。9.2 定期备份SQLite 是单文件备份非常简单。可以手动复制也可以用 Python 的 backup API 生成一致性快照# file: backup_db.py import sqlite3 import pathlib import datetime BASE_DIR pathlib.Path(__file__).resolve().parent DB_PATH BASE_DIR / lego_warehouse.db BACKUP_DIR BASE_DIR / backups BACKUP_DIR.mkdir(exist_okTrue) today datetime.date.today().isoformat() backup_path BACKUP_DIR / flego_backup_{today}.db src sqlite3.connect(DB_PATH) dst sqlite3.connect(backup_path) with dst: src.backup(dst) dst.close() src.close() print(f备份完成{backup_path})相比直接复制文件使用 backup API 可以避免在写入过程中产生损坏快照是更稳妥的姿势。建议每次导入大批数据、调整大量记录之后都跑一次备份。9.3 把“来源”当作重要字段很多人在设计表时会忽略“来源”觉得只是顺手记一下。实际操作中来源字段可以帮助你回答几个很有价值的问题今年从不同渠道一共买了多少套哪一个渠道的盒况描述最靠谱某一次石家庄行程带回的 70 余套分别花了多少钱只有当“地点”“渠道”“时间”被结构化记录后复盘才有依据。9.4 数字化不要变成负担这套系统的目标是减少整理焦虑不是制造新的仪式感。你可以一开始只记录编号、名称、状态和位置等信息足够多、确实需要时再加字段。字段越多录入越累放弃概率越高。对大部分收藏者来说一个能坚持维护的简单清单远胜过一个功能完备但逐渐荒废的复杂系统。记录时也注意隐私边界。家中的具体楼层、房间和柜子可以在自己数据库里写但如果要公开分享 Vlog 或截图不建议把详细房间号、门牌号甚至窗外环境一起展示。数据工具应该服务于爱好而不是为不必要的信息泄露留出口。最后说一句实在话拥有一大批乐高不是结束能长期清晰知道每套在哪、处于什么状态才是把爱好活得有条理的表现。从今天的一百来个字、一个 SQLite 文件开始你可以让任何一次“收获满满”都变成真正可控的资产积累也让自己在下次想拍 Vlog 时两分钟就能找到最合适的拍摄主角。