目次
はじめに
原神(ゲーム)を遊んだことがある人ならご存知だと思いますが、「パイモンの絵」という絵文字シリーズがあります。原神のゲーム内チャットや、運営会社であるmiHoYoの公式コミュニティであるHoYoLABで使用されています。
原神の新キャラクターの追加やバージョンアップの度に新しいボリュームがリリースされるので、記事執筆時点で既にVol. 38まで登場しています。
とっても可愛い絵柄なので、ダウンロードしてコレクションしたくなりますね。
パイモンの絵シリーズの絵文字は、原神のファンWikiなどを見ると、ボリュームごとに画像がまとめられていて公開されています。しかし、再配布奴からダウンロードするのも何だか気分がよくありません。せっかくなら公式ソースからダウンロードしたいです。
そこで、公式のサーバからなんとかして絵文字を一括ダウンロードする方法を探してみました。本記事はその記録です。
HoYoLABの通信を覗いてみる
パイモンの絵シリーズの絵文字が実際に使われている公式コミュニティHoYoLABを見てみます。HoYoLABの通信を覗けば、一括ダウンロードのヒントがあるかもしれない。
HoYoLABでは、自分のコミュニティ投稿や返信のメッセージに、miHoYoゲームのキャラクターの絵文字を使うことができます。HoYoLABの投稿編集画面から「スタンプ」の項目をクリックすると、スタンプショップ(無料)から原神関連の絵文字の絵文字の一覧を取得できます。
なんだか、絵文字一覧取得用の内部API的な何かが動いてそうな雰囲気が漂っています。
ブラウザの開発者ツールのネットワークパネル(どのURLにリクエスト飛ばしたとかが見られるやつ)でリクエストの履歴を見てみると、list
という「いかにも」な以下のURLにGETリクエストを飛ばしていることが確認できます。
https://bbs-api-os.hoyolab.com/community/pendant/wapi/pendant/list?last_id=0&page_size=50&series_id=70&type=3
試しに上記のURLにブラウザでアクセスしてみると、レスポンスとして以下のJSONが返ってきます。
どうやら予想通り、絵文字一覧取得用の内部APIが動いているようです。
レスポンスのJSONには、絵文字のセットとそれに含まれる絵文字の画像URLが含まれているので、これらのURLの内容を片っ端からダウンロードしていけばよさそう。
リクエストの内容を見てみる
さっきのリクエストURLを見てみると、何やらURLにパラメータ的なものが見えます。
last_id: 0
page_size: 50,
series_id: 70,
type: 3
いろいろリクエストの内容をいじって試してみた結果、各パラメータは以下の内容を意味していることがわかりました。
パラメータ | 説明 |
---|---|
last_id | 指定した値より小さい絵文字セットのみを取得 0で全件取得 |
page_size | デカいほど一度に取得する量が増える |
series_id | 取得する絵文字のカテゴリを指定する 0: すべて 66: 公式 67: ファンアート 68: 崩壊3rd 69: 未定事件簿 70: 原神 71: スターレイル 72: HoYoLAB 104: ゼンゼロ |
type | 謎(3じゃないとダメっぽい) |
今回の場合は、すべての絵文字を一括でダウンロードしたいので、page_size
をできるだけ大きい値にして、series_id
は原神の70
、他はそのままでよさそうです。
レスポンスを見てみる
先述の通り、レスポンスで絵文字の画像のURLの一覧が返ってきています。
https://upload-os-bbs.hoyolab.com/upload/2025/01/26/b4830ca95c57aca4aaa183eebbb7a4b6_8563542812292528506.png?x-oss-process=image/auto-orient,0/interlace,1/format,webp/quality,q_70
この画像のURLにも、何やらパラメータらしきものがくっついています。
?x-oss-process=image
/auto-orient,0
/interlace,1
/format,webp
/quality,q_70
これは調べるとすぐに出てきて、Alibabaのオブジェクトストレージにおける配信時の画像処理を指示するパラメータのようです。
Image Processing - Object Storage Service - Alibaba Cloud Documentation Center
例えば、quality,q70
などとありますが、これは画像の画質を70%に落として配信する設定で、やりとりするデータ量を削減し読み込みを高速化することができます。
今回は一番良い状態の画像を保存したいので、これらの画像処理は不要です。よって、ダウンロードの際は、これらのパラメータをすべて削除したURLにアクセスします。
コードを書く
やることが決まればあとはスクリプトを書くだけです。
まずさっきのURLから絵文字の一覧を取得し、パイモンの絵(Paimon’s Paintings)のものだけフィルタし、ボリュームごとにフォルダを作り、画像処理パラメータを除去したURLから画像をダウンロードし、ボリュームのフォルダの中に保存していくだけですね。
言語は何でもいいのですが、個人的に慣れているのと非同期処理が楽に書けるのでDeno (TypeScript)で書きます。
……
出来上がったスクリプトがこちらです(料理番組並感)。個人的に、Denoはペライチのスクリプトをササッと書いて動かすのに適していると思います。
import ky from "npm:ky"
import { extname, join } from "jsr:@std/path"
type ResponseBody = {
data: {
list: [{
id: string
title: string
details: [{
id: string
url: string
}]
}]
}
}
async function main() {
const baseSaveDir = "paimons_paintings"
const res = await ky<ResponseBody>(
"https://bbs-api-os.hoyolab.com/community/pendant/wapi/pendant/list",
{
searchParams: {
last_id: 0,
page_size: 999, // 50 → 999 に変更
series_id: 70,
type: 3,
},
},
).json()
res.data.list.forEach(async ({ title, details }) => {
// パイモンの絵シリーズのみをフィルタしてボリューム番号を取得
const volume = title.match(/Paimon's Paintings\D*(\d+)/)?.at(1)
if (!volume) return
// ボリュームごとにフォルダを作る
const saveDir = join(baseSaveDir, volume.padStart(2, "0"))
await Deno.mkdir(saveDir, { recursive: true })
details.forEach(async ({ url }, i) => {
// クエリパラメータ(URLの`?`以降の文字列)を削除
const emojiUrl = url.split("?")[0]
const fileName = String(i + 1).padStart(2, "0") + extname(emojiUrl)
const savePath = join(saveDir, fileName)
// 画像をダウンロードしてボリュームのフォルダに保存
const blob = await ky(emojiUrl, { timeout: 60_000 }).blob()
await Deno.writeFile(savePath, blob.stream())
console.log(savePath)
})
})
}
import.meta.main && main()
上の内容をまるっと全部コピーして、適当にmain.ts
とかを作って保存します。
このへんを見てDenoをインストールしたら、以下のコマンドでさっきのスクリプトを実行します。
deno -NW main.ts
スクリプトの実行が完了すると、main.ts
がある場所と同じ階層にpaimons_paintings
フォルダが作られます。その中にボリュームごとにフォルダ分けされて連番で保存されるようになっています。
ちゃんと全部ダウンロードできました。めでたしめでたし。
おわりに
無事今日の依頼を達成しましたね(デイリー任務並感)。
これで今後新しい「パイモンの絵」がリリースされても、スクリプトをたった1回実行するだけで全部ダウンロードできます。
上に載せたコードは一応ここに置いています。たった数十行なので、リポジトリにするほどでもないですが……。
MateChan/paimons_paintings_downloader
あと、記事を書いていて思ったけど、「絵文字」じゃなくて「スタンプ」が正しいかもしれない。どっちでもいいか。