笔者自高中到现在,游走于 Pixiv 若干载,不慎收藏了许多名家雅作。
独乐乐不如众乐乐!笔者想做一个 web 页面来随机访问我的收藏,不过在此之前,可以先实现服务端上的内容。再之后做网页时,不过是简单的读取数据库罢了!
最初,笔者以为得将我的库存全部放到服务器上项目中去,然后随机访问其中的图片实现功能,但这样做很难得同步,遂搁置。不过,笔者在最近发现有一个 Pixiv 图片代理网站 可以快速下载到图片,大喜,于是开始了这个小工程。
实现此功能分为两个阶段:一,为本地的图片生成数据库索引条目。二,开发 Telegram Bot 接口,随机从数据库索引中获取一张图片转发给聊天。
为本地的 Pixiv 图片建立索引 初始化数据库 Pixiv 图片索引信息 如果使用 Pixiv 图片代理的方法,只需要将 Artwork 的基本信息上传给我们的数据库即可。
根据官方文档 ,当发送图片文件时,Telegram API 对图片的大小有限制:直接使用 HTTP URL 的方式不超过 5 MB,服务器上传图片的方式不超过 10 MB(以文件的格式 发送时,不超过 50 MB,本文不采用文件的格式发送)。因此,在设计数据库时,还需要考虑图片的大小。
设计 Sequenlize 数据库模型 ServicePixivCollection.js
如下:
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 const { DataTypes } = require ("sequelize" );module .exports = { id : { type : DataTypes.INTEGER, autoIncrement : true , primaryKey : true , }, picId : { type : DataTypes.INTEGER, allowNull : false , }, picIndex : { type : DataTypes.INTEGER, allowNull : false , }, picType : { type : DataTypes.TEXT, allowNull : false , }, picSize : { type : DataTypes.FLOAT, allowNull : false , }, picCreatedAt : { type : DataTypes.DATE, allowNull : false , }, };
如采用最小实现,也可以将上面的 picId
, picIndex
和 picType
合并为一个数据项,例如 picName
,直接保存图片的名字。笔者考虑到后续可能会增添新的功能,于是将它们单独拎出来储存。
处理不同路径下的图片时,可能需要保存执行的情况。例如 A 目录在当前时间点进行维护,获取了所有的图片,下次读取 A 目录时,应当从上次维护的时间开始获取最新的图片。现在新加了 B 目录,如果从上次维护 A 的时间点开始读取图片的话,在时间点之前的图片将无法上传。因此,针对不同文件夹的维护,可以分别建立一个单独的数据项。
设计数据库模型 ServiceProcess.js
如下:
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 const { DataTypes } = require ("sequelize" );module .exports = { id : { type : DataTypes.INTEGER, autoIncrement : true , primaryKey : true , }, serviceId : { type : DataTypes.UUID, defaultValue : DataTypes.UUIDV4, allowNull : false , }, serviceName : { type : DataTypes.TEXT, allowNull : false , }, serviceConfig : { type : DataTypes.TEXT, }, serviceSharedData : { type : DataTypes.TEXT, }, lastExecAt : { type : DataTypes.DATE, }, haveExecTime : { type : DataTypes.INTEGER, allowNull : false , defaultValue : 0 , }, };
为 Sequelize 添加该模型,向 sequelize.js
添加如下代码:
1 2 3 4 5 const servicePixivCollectionModel = require ("path/to/ServicePixivCollection" );const serviceProcessModel = require ("path/to/ServiceProcess" );sequelize.define("ServicePixivCollection" , servicePixivCollectionModel); sequelize.define("ServiceProcess" , serviceProcessModel);
配置扫描图片的间隔时间和本地图片路径,编写 config.js
如下:
1 2 3 4 5 6 7 8 module .exports = { pixiv : { randomGetFromCollection : { duration : 3600 , path : ["C:\\path\\to\\collection" ], }, }, };
扫描指定目录的图片文件,图片文件名称应满足从 Pixiv 下载图片的名称格式。例如:95400283_p0.jpg
,95400283
为数据库中的 picId
,_p0
的 0
为 picIndex
,.jpg
中的 jpg
为 picType
。编写生成图片索引的代码 pixiv.js
如下:
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 113 114 115 116 117 118 119 const { readdir, stat } = require ("fs/promises" );const path = require ("path" );const config = require ("path/to/config" ).pixiv;const Sequelize = require ("path/to/sequelize" );const generateCollectionIndex = async function ( ) { const serviceName = "Generate Collection Index" ; const bToMB = 1024 * 1024 ; const fileSizeReservedDecimalPlace = 3 ; const fileSizeReservedDecimalNum = 10 ** fileSizeReservedDecimalPlace; const sequelize = await Sequelize(); const ServicePixivCollection = sequelize.models.ServicePixivCollection; const ServiceProcess = sequelize.models.ServiceProcess; let collectionPaths = config.generateCollectionIndex.path; if (!Array .isArray(collectionPaths)) { collectionPaths = [collectionPaths]; } let allFiles = []; for (const collectionPath of collectionPaths) { let files = []; files = files.concat(await readdir(collectionPath)); const reg = /^\d+_p\d+.(jpg|png|gif)$/ ; files = files.filter((filename ) => { return reg.test(filename); }); for (let i = 0 ; i < files.length; i++) { const filename = files[i]; const filePath = path.join(collectionPath, filename); const picIdSplitArr = filename.split("_p" ); const picId = Number (picIdSplitArr[0 ]); const picIndexSplitArr = picIdSplitArr[1 ].split("." ); const picIndex = Number (picIndexSplitArr[0 ]); const picType = picIndexSplitArr[1 ]; const picStat = await stat(filePath); const picSize = Math .floor((picStat.size / bToMB) * fileSizeReservedDecimalNum) / fileSizeReservedDecimalNum; const picCreatedAt = picStat.mtimeMs; files[i] = { picName : filename, picId, picIndex, picType, picSize, picCreatedAt, }; } const serviceProcess = await ServiceProcess.findOne({ where : { serviceName, serviceConfig : collectionPath }, }); if (serviceProcess) { let lastUpdateIndexTime = serviceProcess.dataValues.lastExecAt; if (lastUpdateIndexTime) { lastUpdateIndexTime = new Date (lastUpdateIndexTime).getTime(); files = files.filter((pic ) => { return pic.picCreatedAt > lastUpdateIndexTime; }); } } else { await ServiceProcess.create({ serviceName, serviceConfig : collectionPath, }); } allFiles = allFiles.concat(files); } const updateIndexAt = new Date ().toISOString(); for (const picFile of allFiles) { await sequelize.updateOrCreate( ServicePixivCollection, { picId : picFile.picId, picIndex : picFile.picIndex, }, picFile ); } ServiceProcess.update( { lastExecAt : updateIndexAt, }, { where : { serviceName }, } ); ServiceProcess.increment("haveExecTime" , { where : { serviceName } }); }; module .exports = { generateCollectionIndex, };
设置定时扫描图片,基于 toad-scheduler
库编写服务代码如下:
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 const { ToadScheduler, SimpleIntervalJob, AsyncTask, } = require ("toad-scheduler" ); const pixivTask = require ("path/to/pixiv" );const config = require ("path/to/config" ).pixiv;const initService = async function ( ) { const taskGenerateCollectionIndex = new AsyncTask( "Generate Pixiv Collection Index" , async () => { await pixivTask.generateCollectionIndex(); }, (error ) => { console .error(error); } ); const jobGenerateCollectionIndex = new SimpleIntervalJob( { seconds : config.generateCollectionIndex.duration, runImmediately : true , }, taskGenerateCollectionIndex ); const scheduler = new ToadScheduler(); scheduler.addSimpleIntervalJob(jobGenerateCollectionIndex); }; module .exports = initService;
程序启动时,运行 initService()
即可。服务器将自动读取指定目录下的 Pixiv 图片文件,并在数据库中建立索引。下一步 ,我们将随机从数据库中读取一张作品的信息。
思路:爬取 Pixiv 图片的源文件地址 如果使用亲自获取图片并转发的方法,除了前面需要初始化数据库的图片索引信息外,还需要构建 Axios 请求,爬取网页源代码,从中读取 Artwork 的下载链接等信息,再上传到数据库。
例如,对于下载 95400283_p0.jpg
,最少需要获取其源文件链接 https://i.pximg.net/img-original/img/2022/01/09/07/27/17/95400283_p0.jpg
中的 /2022/01/09/07/27/17
部分,并上传到数据库中。
更多的,在爬取源代码时,可以记录下图片的作者信息,图片是否可以在工作时安全观看(登录后才能爬取此类内容,可能需要在请求时添加个人账户信息)等,对日后处理展示内容大有裨益。
处理网页源代码时,可以使用 cheerio 库增加效率。
为 Telegram Bot 添加随机获取 Pixiv 图片的接口 随机获取数据库中的一个 Pixiv 图片访问地址 编写 randomGetPixivCollection.js
代码如下:
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 const Sequelize = require ("path/to/sequelize" );const randomGetPixivCollection = async function ( ) { try { const sequelize = await Sequelize(); const ServicePixivCollection = sequelize.models.ServicePixivCollection; const artworksCount = await ServicePixivCollection.count(); const randomArtworkId = Math .floor(Math .random() * artworksCount) + 1 ; const artwork = await ServicePixivCollection.findOne({ where : { id : randomArtworkId }, }); const data = artwork.dataValues; const picId = data.picId; const picIndex = data.picIndex; const picType = data.picType; data.picName = `${picId} _p${picIndex} .${picType} ` ; data.picNameMD = `${picId} \\_p${picIndex} \\.${picType} ` ; data.picUrl = `https://www.pixiv.net/artworks/${picId} ` ; let picProxyUrlParam; if (picIndex > 0 ) { picProxyUrlParam = `${picId} -${picIndex + 1 } .${picType} ` ; } else { picProxyUrlParam = `${picId} .${picType} ` ; } data.picProxyUrl = `https://pixiv.cat/${picProxyUrlParam} ` ; return { ok : true , data, error : undefined , }; } catch (error) { return { ok : false , data : undefined , error, }; } }; module .exports = randomGetPixivCollection;
根据 Pixiv.cat 网站的使用说明,对单张图片的 Pixiv 作品,应访问 https://pixiv.cat/${picId}.${picType}
。而对于多张图片的 Pixiv 作品(漫画),应访问 https://pixiv.cat/${picId}-${picIndex + 1}.${picType}
。
可能会影响体验,需要改进的地方是:当作品名为 ${picId}_p0.${picType}
时,我们不知道该作品是否为单张图片,还是漫画作品,无法正确判断应该访问的 URL 链接。在后面 ,我们将针对此情况做处理。
添加 Telegram Bot 命令 接下来为 Bot 添加指令,以调用随机获取图片的接口。同样,这里给出使用 Pixiv 图片代理的具体实现,以及亲自获取图片并转发的可能思路。
直接使用 Pixiv 图片代理 注意,使用这种方式上传的图片不超过 5 MB 。
编写 Bot 的配置如下:
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 bot.onText(/\/random_pixiv/ , async (msg) => { const chatId = msg.chat.id; const res = await randomGetPixivCollection(); if (res.ok === true ) { const placeholderMessage = await bot.sendMessage( chatId, `Geeeeting a random Pixiv artwork ...` ); const data = res.data; const caption = `Pixiv Artwork: ${data.picNameMD} \n[source](${data.picUrl} ) \\| powered by [pixiv\\.cat](https://pixiv.cat/)` ; if (data.picSize >= 5 ) { await bot.sendMessage(chatId, caption, { parse_mode : "MarkdownV2" , disable_web_page_preview : false , }); } else { const sendPhotoOptions = { caption, parse_mode : "MarkdownV2" , disable_web_page_preview : true , }; try { await bot.sendPhoto(chatId, data.picProxyUrl, sendPhotoOptions); } catch (err) { try { const picProxyUrl = `https://pixiv.cat/${data.picId} -1.${data.picType} ` ; await bot.sendPhoto(chatId, picProxyUrl, sendPhotoOptions); } catch (err) { await bot.sendMessage(chatId, caption, { parse_mode : "MarkdownV2" , disable_web_page_preview : false , }); } } } bot.deleteMessage(chatId, placeholderMessage.message_id); } else { bot.sendMessage( chatId, "Get random pixiv artwork failed. You may try to call it again later!" ); } });
在上面这段代码中,当以 bot.sendPhoto()
的方法尝试发送 5 MB 以下的图片失败时,首先重新构建请求 URL 为 https://pixiv.cat/${data.picId}-1.${data.picType}
,再次进行发送。如果还是失败(可能图片被作者删除),则发送简单的链接文本。由此,解决了前面提到的无法判断作品是否为漫画作品的问题。
与 Telegram 上的机器人对话,发送命令 \random_pixiv
,结果如下:
思路:获取 Pixiv 图片并转发 注意,使用这种方式上传的图片不超过 10 MB 。
我们无法直接在 bot.sendPhoto()
方法中使用 Pixiv 源站图片的获取链接,这是因为 Pixiv 设置了反爬虫机制,只接收请求头的 Referer 包含 https://www.pixiv.net/
的请求。
因此,我们需要在自己的服务器上构造 Axios 请求,设置 Referer 请求头,然后发送请求向 Pixiv 服务器获取图片,再将图片转为 multipart/form-data
格式发送给 Telegram 会话。
为 Koa 添加随机获取 Pixiv 图片的接口 Koa 应用程序本身也提供了路由功能,在这里可以很轻松地将获取随机 Pixiv 图片的服务对接到 Koa 上去。
编写路由文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const router = require ("koa-router" )();const randomGetPixivCollection = require ("path/to/randomGetPixivCollection" );router.get("/random" , async function (ctx ) { const res = await randomGetPixivCollection(); if (res.ok === true ) { ctx.redirect(res.data.picProxyUrl); } else { ctx.body = "Get random Pixiv artwork failed." ; } }); module .exports = router;
然而,这里并没有解决前面提到的漫画作品问题,留待用户在跳转后自行操作。
参考文章