基于PID与SQLite的画师作品资源管理方案
发布时间:2026/9/9 2:50:56来源:尧图网络
1. 为什么“知道一个画师PID”还不够追更画师这件事很多人的第一步是打开Pixiv找到喜欢的画师点收藏。收藏多了以后问题就来了今天收了一张、明天收了三十张几个月下来文件夹里全是12345678_p0.jpg这种文件名放到网盘里基本等于乱葬岗。等到想找某张图做壁纸、做素材、或者做二创参考时完全不知道去哪翻。画师“悟之心”的作品PID:143758591放在这个场景里就特别有代表性。这个PID并不是一个普通的数字它是这张作品在Pixiv平台上的唯一身份标识。知道这个ID就能通过平台的接口、网页或者第三方工具快速定位到原始作品页面确认作者、发布时间、尺寸、标签甚至追溯官方的授权范围。可以说PID本身就是数字艺术资源管理里的主键。但这篇文章真正要解决的问题不是“怎么去下载一张图”而是当你面对一位画师的大量作品或者自己积累了几百个PID之后如何用工程化的方式把这些数字资产管理起来。很多文章讲画师、讲PID往往停留在“去哪看、怎么存”的层面很少从技术角度拆解作品资源的结构化整理。实际上这件事的底层逻辑和开发一个数据系统是一样的先定义元数据模型再设计文件命名规则然后通过脚本批量归档最后用数据库做检索和校验。如果你是一个画师本人这篇文章能帮你建立自己的作品版本管理体系如果你是一个开发者可以跑通一套从“零散文件”到“可检索资源库”的最小工程流程如果你只是收藏爱好者也能从此告别“文件淹没在文件夹”的焦虑。整个过程不依赖任何付费工具用Python 3和SQLite就能完成代码和命令都会给出完整可复制的版本。2. PID到底是什么以及作品资源管理为什么依赖它PID在Pixiv语境下指的是作品IDIllustration ID。每一张上传到平台的作品从发布的那一刻起就拥有一个全局唯一的数字编号。画师本人也有自己的用户ID但作品PID和画师用户ID是两个不同维度的标识一位画师可以发布几千张作品每张作品有独立的PID而画师ID则是对应账号本身。不要小看这个区别。在资源管理场景里这是最容易被混淆、也最容易埋坑的地方。比如你写爬虫、做本地索引如果只用画师ID去关联文件那么同一个画师的几百张作品就只能靠文件名里的数字区分。一旦有两个PID接近的作品同时出现在目录里143758591和143758592很容易被误判为“同一张图的重复下载”实际上它们是平台上的两张不同作品。反过来如果你只记录作品PID而不记录画师ID后续做归属统计时就只能靠记忆非常不工程化。更合理的做法是把作品资源视为一条记录PID是主键作品文件是这条记录关联的物理资产画师信息、标签、发布日期是这条记录的属性字段。这里借用数据库设计的一句话先设计Schema再管理数据。在本地归档中对应关系应该是作品PID143758591 所属画师悟之心 本地文件WZX_143758591_p0.png 元数据title、tag、publish_date、resolution、source_url这样设计之后后续所有脚本都能围绕“PID 文件路径 元数据”三者展开。无论是做去重、做校验、做检索都有了可靠的基础。有位开发者在做画师作品批量整理时踩过的坑就是“以画师名建文件夹、不记录PID”结果画师改了一次昵称整个目录结构就乱了。后来他把作品PID写进文件名把画师ID和昵称都存进SQLite才发现这才是真正稳定的一对多关系。3. 环境准备与目录规范设计在动手写脚本之前先把运行环境和目录规范定下来。这部分不需要高端配置普通开发电脑即可重点在于统一规格避免“不同设备、不同命名、不同结构”的混乱。本文示例使用以下环境操作系统Windows 10/11、macOS、Linux 均可Python版本3.8 及以上数据库SQLitePython 内嵌支持无需单独安装所用Python模块os、pathlib、hashlib、sqlite3、shutil均为标准库无需pip install在正式项目中版本请以实际环境为准但本文使用的全是标准库兼容性风险很低。推荐的目录结构如下D:/Gallery/ ├── archive/ # 归档后的作品原图 │ ├── WZX_143758591_p0.png │ └── WZX_143758600_p0.png ├── database/ │ └── gallery.db # SQLite 数据库文件 ├── scripts/ │ ├── scan_works.py # 扫描目录并登记文件 │ └── verify_files.py # 校验文件完整性 └── records/ └── import_works.json # 批量导入时的元数据文件几个命名惯例要在这时候定下来文件前缀建议使用画师姓名的首字母拼音缩写如WZX代表“悟之心”。PID必须完整出现在文件名中这是唯一主键。同一PID可能有多张分页图因此文件名里加_p0、_p1这类页码标识。文件名不建议包含标签和中文全称因为中文名在不同系统间复制时容易出乱码标签应存入数据库而不是塞进文件名。如果你已经有一个凌乱的下载目录先不要急着删文件先把它们放到archive/暂存区后续用脚本统一改名并登记。这个过程是“有序迁移”不是“手动维护”。4. 核心数据模型用 SQLite 登记作品清单资源管理最关键的一步是把文件系统里的“物理文件”和数据库里的“逻辑记录”绑定。SQLite 是目前最轻量、最稳妥的选择单文件即可保存全部作品元数据不需要安装数据库服务也很容易迁移到其他机器。4.1 建表设计作品基本信息表artworks负责存储作品维度的元数据CREATE TABLE IF NOT EXISTS artworks ( pid INTEGER PRIMARY KEY, artist_id INTEGER NOT NULL, artist_name TEXT NOT NULL, title TEXT NOT NULL, tags TEXT DEFAULT , publish_date TEXT DEFAULT , resolution TEXT DEFAULT , source_url TEXT DEFAULT , created_at TEXT DEFAULT CURRENT_TIMESTAMP );这张表里的pid是平台的作品数字ID示例中就是143758591。artist_id是画师的用户IDartist_name是当前昵称。注意昵称会变ID不会变所以artist_name只作为展示字段关联统计一律走artist_id。再建一张分页文件表work_files用于记录一张作品对应的多个物理文件CREATE TABLE IF NOT EXISTS work_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, pid INTEGER NOT NULL, page_no INTEGER DEFAULT 0, file_path TEXT NOT NULL, file_hash TEXT DEFAULT , file_size INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (pid) REFERENCES artworks(pid) );两张表对应关系一个artworks记录可以关联多个work_files记录。比如PID:143758591是一张多页图那么work_files中就会出现两行page_no分别是 0 和 1。4.2 为什么要单独建 work_files 表很多人在做本地作品归档时喜欢把所有信息全部放在一张表里。但如果一张作品有多个分页或者同一张图在不同时间下载了不同分辨率版本单表就会显得很僵硬。拆成“主表 文件表”之后主表只关心作品属性文件表只关心物理存储后续做“该作品是否有原图”“该文件是否损坏”这类校验时逻辑会非常清晰。如果你不需要分页信息也可以只使用artworks表但从工程规整度来说保留分页表更符合长期维护的需要。4.3 用 Python 完成建表和基础插入下面是一个可直接运行的 Python 脚本它会在database/目录下创建gallery.db并插入一条示例作品记录。示例使用用户提供的PID:143758591和画师“悟之心”# 文件路径scripts/init_database.py import sqlite3 from pathlib import Path DB_PATH Path(__file__).resolve().parents[1] / database / gallery.db def get_connection(): DB_PATH.parent.mkdir(parentsTrue, exist_okTrue) conn sqlite3.connect(DB_PATH) conn.execute(PRAGMA foreign_keys ON) return conn def init_db(): conn get_connection() cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS artworks ( pid INTEGER PRIMARY KEY, artist_id INTEGER NOT NULL, artist_name TEXT NOT NULL, title TEXT NOT NULL, tags TEXT DEFAULT , publish_date TEXT DEFAULT , resolution TEXT DEFAULT , source_url TEXT DEFAULT , created_at TEXT DEFAULT CURRENT_TIMESTAMP ); ) cursor.execute( CREATE TABLE IF NOT EXISTS work_files ( id INTEGER PRIMARY KEY AUTOINCREMENT, pid INTEGER NOT NULL, page_no INTEGER DEFAULT 0, file_path TEXT NOT NULL, file_hash TEXT DEFAULT , file_size INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (pid) REFERENCES artworks(pid) ); ) conn.commit() conn.close() print([OK] 数据库初始化完成位置, DB_PATH) if __name__ __main__: init_db()运行指令cd D:/Gallery python scripts/init_database.py如果能在控制台看到[OK] 数据库初始化完成说明SQLite环境正常。这里真正容易踩坑的地方是路径拼接Path(__file__).resolve().parents[1]在部分场景下会因为脚本存放层级不同而指向错误目录。建议所有路径统一从项目根目录用绝对路径推导不要依赖“当前工作目录”。5. 完整示例批量登记作品文件与校验数据库初始化完成之后下一步是将archive/目录中的实际文件扫描进数据库。这个过程的实质是让数据库知道“有哪些文件它们属于哪张作品哈希值是多少”。5.1 文件名与PID解析我们首先假设归档目录中的文件名已经符合约定形如WZX_143758591_p0.png。脚本需要从文件名中提取PID和页码。这个步骤看起来琐碎实际上是后续所有自动化流程的基石。# 文件路径scripts/scan_works.py import sqlite3 import re import hashlib from pathlib import Path ROOT Path(__file__).resolve().parents[1] ARCHIVE_DIR ROOT / archive DB_PATH ROOT / database / gallery.db FILENAME_PATTERN re.compile(r^[A-Za-z]_(\d)_p(\d)\.(png|jpg|jpeg|webp)$, re.IGNORECASE) def get_connection(): conn sqlite3.connect(DB_PATH) conn.execute(PRAGMA foreign_keys ON) return conn def compute_hash(file_path: Path) - str: sha256 hashlib.sha256() with open(file_path, rb) as f: for chunk in iter(lambda: f.read(8192), b): sha256.update(chunk) return sha256.hexdigest() def scan(): conn get_connection() cursor conn.cursor() if not ARCHIVE_DIR.exists(): print([ERROR] 找不到 archive 目录, ARCHIVE_DIR) return files list(ARCHIVE_DIR.iterdir()) if not files: print([WARN] archive 目录为空没有可扫描文件。) return for file_path in files: if not file_path.is_file(): continue match FILENAME_PATTERN.match(file_path.name) if not match: print(f[SKIP] 文件名不符合规范跳过{file_path.name}) continue pid int(match.group(1)) page_no int(match.group(2)) # 计算文件哈希 file_hash compute_hash(file_path) file_size file_path.stat().st_size # 插入 work_files 记录 cursor.execute( INSERT INTO work_files (pid, page_no, file_path, file_hash, file_size) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET file_path excluded.file_path, file_hash excluded.file_hash, file_size excluded.file_size , (pid, page_no, str(file_path), file_hash, file_size)) print(f[OK] {file_path.name} - PID {pid}, page {page_no}, size {file_size}) conn.commit() conn.close() print([OK] 扫描完成。) if __name__ __main__: scan()这段代码做了几件事用正则表达式从文件名提取PID和页码。通过hashlib分块计算大文件的 SHA256 值避免一次读入内存占用过高。使用ON CONFLICT处理重复扫描保证脚本可以重复执行而不产生重复记录。5.2 插入作品主表记录的示例文件表登记完成后还需要把作品维度的信息补进artworks表。这些信息需要从合法渠道获取。以下代码展示如何手动插入一条PID:143758591的记录# 文件路径scripts/insert_artwork.py示例脚本可按需修改 import sqlite3 from pathlib import Path DB_PATH Path(__file__).resolve().parents[1] / database / gallery.db def insert_artwork(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() # 具体字段值请根据实际授权获取的信息填写 cursor.execute( INSERT INTO artworks (pid, artist_id, artist_name, title, tags, publish_date, resolution, source_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(pid) DO UPDATE SET artist_name excluded.artist_name, title excluded.title, tags excluded.tags, publish_date excluded.publish_date, resolution excluded.resolution, source_url excluded.source_url , ( 143758591, # pid 0, # artist_id请替换为实际画师ID 悟之心, # artist_name 未知标题, # title请替换为实际作品标题 示例标签, # tags , # publish_date , # resolution # source_url )) conn.commit() conn.close() print([OK] 作品主表信息已写入。) if __name__ __main__: insert_artwork()这里特意保留了artist_id 0和title 未知标题的占位形式目的是提醒读者不要为了完整而编造数据。如果暂时没有合法获取到画师ID、标题等字段就留空而不是随便写一个值进去。从工程角度看一个“字段不全但真实”的记录远比一个“字段齐全但错误”的记录更可靠。5.3 查询与验证示例录入完成后可以用一条查询验证数据是否一致# 文件路径scripts/query_works.py import sqlite3 from pathlib import Path DB_PATH Path(__file__).resolve().parents[1] / database / gallery.db def query(pidNone): conn sqlite3.connect(DB_PATH) cursor conn.cursor() if pid is not None: cursor.execute( SELECT a.pid, a.artist_name, a.title, f.page_no, f.file_path FROM artworks a LEFT JOIN work_files f ON a.pid f.pid WHERE a.pid ? ORDER BY f.page_no , (pid,)) else: cursor.execute( SELECT a.pid, a.artist_name, a.title, f.page_no, f.file_path FROM artworks a LEFT JOIN work_files f ON a.pid f.pid ORDER BY a.pid, f.page_no ) rows cursor.fetchall() if not rows: print([INFO] 没有查询到记录。) else: for row in rows: print(row) conn.close() if __name__ __main__: import sys if len(sys.argv) 1: query(int(sys.argv[1])) else: query()运行方式python scripts/query_works.py 143758591这个查询会同时查出主表信息和文件路径直接验证“PID、画师、文件”三者是否已经正确关联。6. 文件完整性校验与批量去重资源管理只做“登记”还不够。长期积累之后一个常见的风险是同一张作品下载了两遍、不同文件名指向同一内容或文件在传输过程中损坏。解决这个问题需要从哈希入手。6.1 使用 hashlib 检查重复文件两个文件即使文件名不同只要内容一致SHA256 就一致。基于这个原理可以快速找出重复资源# 文件路径scripts/check_duplicates.py import hashlib from pathlib import Path from collections import defaultdict ROOT Path(__file__).resolve().parents[1] TARGET_DIR ROOT / archive def compute_hash(file_path: Path) - str: sha256 hashlib.sha256() with open(file_path, rb) as f: for chunk in iter(lambda: f.read(8192), b): sha256.update(chunk) return sha256.hexdigest() def find_duplicates(): hash_map defaultdict(list) for file_path in TARGET_DIR.iterdir(): if not file_path.is_file(): continue file_hash compute_hash(file_path) hash_map[file_hash].append(file_path) found False for file_hash, paths in hash_map.items(): if len(paths) 1: found True print(f[DUP] hash{file_hash[:12]} 文件数{len(paths)}) for p in paths: print( , p) if not found: print([OK] 未发现重复文件。) if __name__ __main__: find_duplicates()这个脚本的价值在于它不依赖文件名只看文件内容。对于“同一个PID在多个目录下被重复下载”的情况能一次性找出来。如果你直接把archive/下所有文件扔进一个目录那么重名覆盖、漏下载、多下载的问题就会很容易提前暴露。6.2 在命令行中快速校验文件数量如果你不习惯写Python脚本也可以用系统命令快速查看归档目录下的文件数量find archive -type f | wc -l再把数据库里work_files表的记录数查出来对比sqlite3 database/gallery.db SELECT COUNT(*) FROM work_files;两个数字相等说明当前目录下“见到的文件”和“数据库登记的记录”一致如果不相等就要看是“文件多但记录少”还是“记录多但文件少”。这是最初步的健康检查。7. 常见问题与排查思路问题现象可能原因排查方式解决方案扫描时大量文件被跳过文件名不符合正则匹配规则先查看跳过日志再抽查文件名样式统一命名规范必要时写一次性改名脚本数据库插入报“UNIQUE constraint failed”主表重复插入同一PID且未使用冲突处理检查是否真的遇到同一PID还是PID解析错误使用ON CONFLICT更新逻辑或先查询再插入文件明明存在但查询不到数据库记录的file_path是绝对路径移动目录后失效检查路径是否仍然存在在verify_files.py中对每条记录做Path.exists()检查哈希计算速度很慢文件过大或数量过多确认是否一次性扫描了超大图包对超大文件使用分块读取定期增量扫描画师改昵称后历史数据全对不上昵称作为主键存储没有使用画师ID检查artworks表结构重建表时增加artist_id昵称只做展示字段archive目录下文件重名下载时没有拼接PID只保留了默认文件名看文件列表是否有同名覆盖从源头规范文件名配合哈希查重脚本找回被覆盖前的副本如果还有备份排查时记住一个原则先看数据库和文件系统的一致性再改数据不要在没确认一致性的情况下手动删文件。以PID:143758591为例如果查询主表时有记录但work_files中没有对应文件路径说明当初只登记了作品信息没有归档物理文件。这时候最稳妥的方法是重新获取授权文件而不是把数据库记录强行指向其他文件。8. 最佳实践与工程建议8.1 把“元数据录入”和“文件归档”分开新手最容易犯的错误是下载一张图就立刻在数据库里写一行结果文件路径混乱、元数据残缺。更推荐的做法是分阶段处理第一阶段把所有新文件批量拷入一个新增目录统一扫描哈希并按PID重命名。第二阶段整理作品元数据批量导入数据库。第三阶段对历史数据做一致性校验清理重复文件和无效记录。三个阶段互不干扰。文件没整理完不要急着写作品主表。8.2 备份策略参考“3-2-1”原则作品原图是不可再生资源一旦删除很难找回。参考数据库备份领域的“3-2-1”原则比较稳妥至少保留3份副本。保存在2种以上不同介质本地硬盘、移动硬盘、网盘/对象存储。至少1份存放在异地。管理 PID 和画师资源时数据库文件gallery.db也要纳入备份范围。数据库本身很小但它是所有文件的“索引核心”。8.3 对待账号与版权信息要谨慎在记录artist_id、source_url等信息时只保存自己合法获取到的公开信息不要尝试通过非正常手段获取私密数据或绕过平台限制。批量下载画师作品前务必确认画师的分享许可和平台的服务条款。文中所有代码都应在符合规则的前提下用于个人学习、备份和研究。8.4 脚本要支持“可重复执行”归档脚本建议全部设计成“幂等操作”。所谓幂等就是执行一遍和执行两遍的结果相同。比如用ON CONFLICT处理重复插入用哈希做唯一性判断用exists()做文件存在性检查。这样每天定时执行扫描和校验脚本时不会产生重复数据和脏数据。8.5 推荐加入“变更记录表”当画师改昵称、作品被删除、文件重新归档时建议记录一条变更历史CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, action TEXT NOT NULL, pid INTEGER, old_value TEXT DEFAULT , new_value TEXT DEFAULT , changed_at TEXT DEFAULT CURRENT_TIMESTAMP );所有人工作业都有迹可循能在多次整理后依然知道“这个PID为什么改过文件名”“这个画师为什么从 A 改名到 B”。这层审计信息在个人项目中看似多余但当数据量到了几千条之后价值会立刻显现。9. 总结与延伸方向围绕PID:143758591和画师“悟之心”其实可以带出一整套数字艺术资源管理方案先理解 PID 是作品唯一主键画师 ID 是账号唯一标识两者不能混淆然后通过目录命名规范、SQLite 数据模型、扫描脚本、哈希去重和一致性校验把零散图片变成可检索的资源库。读完这篇文章建议你亲自动手做三件事选定一个测试目录放入三五张命名规范的作品文件或任意测试图片跑通扫描和查询脚本。把数据库文件备份到移动硬盘或网盘验证“删除本地文件后能从记录中知道缺失了什么”。如果你有大量历史文件先运行一遍哈希去重脚本清理重复内容后再重新登记。到这一步你已经不再是一个随手存图的收藏者而是在用工程思维管理自己的数字资产。下一步可以研究的方向包括用 FastAPI 做一个本地 Web 相册服务、把 SQLite 数据迁移到 PostgreSQL、或者对接离线 OCR 和图像标签模型为作品自动生成语义标签。但无论走哪个方向基于 PID 画师ID 文件哈希的元数据底座都不会变。”
网站建设高端定制企业官网