-- WARNING! --
本記事は、アイマス成分をそれなりに扱っています。
アレルギーをお持ちの閲覧者様はブラウザバックをおすすめします。
🎄🎅🎁 Merry Christmas! 🍗🍰🎄
ついにやってきました ハンズラボ Advent Calendar 2019 最終日は、
@Angelan1720がお送りします。
筆者について軽く紹介。
本業はアイドルプロデューサーで、
主に765Productionの我那覇響を担当としてプロデューサー(以下Pと略称)をしており、
副業でハンズラボのエンジニアしています(という気持ちでいます。(ここ重要))
本日もいつも通り変わらずプロデュースに忙しくしています。
という自己紹介をした時点で本記事の内容について、
「この人の記事は... (察し)」(色んな意味で)
と思った方が多くいると思いますが、そういう記事です。
今回筆者がお話しすることは、
タイトルの通り、im@sparqlなるものを使って作成した趣味アプリを出来る限りアイマスワールドを展開してお話しします。
#そもそもim@sparqlって何よ
本家サイトim@sparql.docの「1.im@sparqlとは」より引用
提供するものはデータベースそのものです。
im@sparqlは、オープンデータプラットフォームです。
すでに誰かの手によって見やすい形に整えられた表ではありません。
RDFで記述されたLinked Open Data(LOD)です。
LODを操作するには、「SPARQL」というクエリ言語を用います。
つまり、RDF
で記述してアイドルマスター関連のデータを集約したLinked Open Data(LOD)
です。
データは公式から出ているものを有志の方々によってまとめられたものになっています。
オープンデータであることが、とてもありがたいポイントですね!(ありがサンキュー)
と、いきなり分からない技術用語が出てきてしまっているので浅く説明します。
簡単に深堀り説明
Linked Open Data (LOD)
Linked Data + Open Data
つまり、Linked Data
をオープンデータとして公開されたものを指す。
Linked Data
Webの仕組み(一意のURLを持たせて場所を特定させること)を用いて 相互にリンクされたデータ
のこと。
データ(URI)を参照された場合には、RDF
やSPARQL
を標準仕様として用いて情報を提供出来るようにする。
Resource Description Framework (RDF)
Webページ上の情報のメタデータ(データの属性や関係を表すためのデータ)を記述するための構造を指す。
メタデータは、主語(subject)述語(predicate)目的語(object)の3つの要素でリソースに関する関係情報を表現する。
SPARQL
引用にも書いてある通り、RDF問合せ言語の1つ。
この後に挙げている、データを抽出する際に用いたSQLライクに記述されたクエリが、これに該当する。
筆者は、以下の記事がとてもイメージしやすく参考になりましたので、ぜひ一読してみて下さい。
LOD : https://www.slideshare.net/KoujiKozaki/linked-open-datalod-77040726
RDF,SPARQL : https://qiita.com/maoringo/items/4742b5cd01c9e698260d
では早速本題の作ったアプリ......に入る前に
実は、こんなめでたい日に生まれた誕生日アイドルがアイマス世界線にいますので、上記のチュートリアル的な実行結果の紹介も兼ねて、
1人のPとして、クリスマスに生まれた誕生日アイドルを全員抽出してお祝いしましょう!
(余興) 今日の誕生日アイドルはだれがいるの?
極めたPであれば、アイドル全員の誕生日を覚えるなんてことは朝飯前のことだと思いますが、
筆者はそのようなエクストリーム脳になっていないので調べないといけません。しかし、1人ずつ全シリーズのアイドルプロフィールを確認していたら、クリスマスが過ぎ去ってしまいます。
そんな時に解決してくれるのが、im@sparqlです。
ここのエンドポイントに対して、欲しいデータを抽出する条件を記述したSPARQLクエリをリクエストとして投げることで、アイマス関連のデータを取得することができます。
その日の誕生日アイドルを出すクエリは、トップページの一番下に当サイトお誕生日お祝い編
で例として上がっているので、こちらを使わせて頂きましょう。以下はJavaScript文中のurl
変数にあるURLパラメーターをエンコードする前の形にしたものです。
PREFIX schema: <http://schema.org/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT (sample(?o) as ?date) (sample(?n) as ?name)
WHERE {
?s schema:birthDate ?o;
rdfs:label ?n;
FILTER(regex(str(?o), "12-25")).
}
group by(?n)
order by(?name)
これを、UTF-8ベースでエンコードしたものをURLパラメーターにして渡すと、
{
"head": {
"vars": [ "date" , "name" ]
} ,
"results": {
"bindings": [
{
"date": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#gMonthDay" , "value": "--12-25" } ,
"name": { "type": "literal" , "value": "大崎甘奈" }
} ,
{
"date": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#gMonthDay" , "value": "--12-25" } ,
"name": { "type": "literal" , "value": "大崎甜花" }
} ,
{
"date": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#gMonthDay" , "value": "--12-25" } ,
"name": { "type": "literal" , "value": "望月聖" }
} ,
{
"date": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#gMonthDay" , "value": "--12-25" } ,
"name": { "type": "literal" , "value": "柊志乃" }
}
]
}
}
上記のようなJSON文字列が結果として返ってきます。
これが今回の主役技術の一連の流れになります。
というわけで、今日のクリスマス誕生日アイドルは
アイドルマスターシャイニーカラーズ より、大崎甘奈と大崎甜花
アイドルマスターシンデレラガールズ より、望月聖と柊志乃の4人でした。
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
✨✨✨ 🎉🎉HAPPY BIRTHDAY!!🎉🎉✨✨✨
✨✨✨✨みんなお誕生日おめでとう!! ✨✨✨✨
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
(やっと本題) そんな仕組みを利用して作った成果物
リポジトリは以下にあります。
https://github.com/Suzuka-Samidare/IDOLMASTER-CHARACTER-PROFILE-SEARCH
こういうアプリだよ
上のgifは、筆者の上位担当アイドル3人を検索したものです。
他にも、シンデレラガールズ・961プロ所属系・ディアリースターズ・sideMのシリーズも検索でしっかり抽出表示されます。
仕様
見た目
docker-composeでコンテナを2つ用意。
1つはフロントエンドでフレームワーク勉強も兼ねてAngularを使っています。(まだチュートリアル程度の知識)
また、UIコンポーネントライブラリでAngular Materialを使用。
もう1つはキャラ画像取得用のバックエンドで、簡易的にPython(Bottle)で組んだものからCustom Search APIを叩いて、カスタム検索結果で画像が添付されているサイトの最初の1枚をURLで読んでいます。
SPARQLクエリ
PREFIX schema: <http://schema.org/>
PREFIX imas: <https://sparql.crssnky.xyz/imasrdf/URIs/imas-schema.ttl#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT
?birthDate ?birthPlace ?gender ?height ?weight ?age ?bloodType ?color ?constellation ?handedness
?title ?cv ?familyName ?givenName ?alternateName ?familyNameKana ?givenNameKana ?alternateNameKana
?bust ?waist ?hip ?attribute ?division ?description ?type ?category ?shoeSize
(GROUP_CONCAT(distinct ?Favorite;separator=",") as ?favorite)
(GROUP_CONCAT(distinct ?Hobby;separator=",") as ?hobby)
(GROUP_CONCAT(distinct ?Talent;separator=",") as ?talent)
WHERE {
?data rdfs:label ?label;
schema:gender ?gender;
imas:Title ?title;
imas:cv ?cv .
OPTIONAL { ?data foaf:age ?age . }
OPTIONAL { ?data schema:birthDate ?birthDate . }
OPTIONAL { ?data schema:birthPlace ?birthPlace . }
OPTIONAL { ?data imas:Constellation ?constellation . }
OPTIONAL { ?data imas:Hobby ?Hobby . }
OPTIONAL { ?data schema:height ?height . }
OPTIONAL { ?data schema:weight ?weight . }
OPTIONAL { ?data imas:BloodType ?bloodType . }
OPTIONAL { ?data imas:Color ?color . }
OPTIONAL { ?data imas:Handedness ?handedness . }
OPTIONAL { ?data schema:familyName ?familyName . FILTER(LANG(?familyName) = 'ja') }
OPTIONAL { ?data schema:givenName ?givenName . FILTER(LANG(?givenName) = 'ja') }
OPTIONAL { ?data schema:alternateName ?alternateName . FILTER(LANG(?alternateName) = 'ja') }
OPTIONAL { ?data imas:familyNameKana ?familyNameKana . }
OPTIONAL { ?data imas:givenNameKana ?givenNameKana . }
OPTIONAL { ?data imas:alternateNameKana ?alternateNameKana . }
OPTIONAL { ?data imas:Bust ?bust . }
OPTIONAL { ?data imas:Waist ?waist . }
OPTIONAL { ?data imas:Hip ?hip . }
OPTIONAL { ?data imas:Talent ?Talent . }
OPTIONAL { ?data imas:Attribute ?attribute . }
OPTIONAL { ?data imas:Division ?division . }
OPTIONAL { ?data imas:Favorite ?Favorite . }
OPTIONAL { ?data schema:description ?description . }
OPTIONAL { ?data imas:Type ?type . }
OPTIONAL { ?data imas:Category ?category . }
OPTIONAL { ?data imas:ShoeSize ?shoeSize . }
FILTER(regex(str(?label), '^${name}$'))
FILTER(LANG(?cv) = 'ja')
}
GROUP BY
?birthDate ?birthPlace ?gender ?height ?weight ?age ?bloodType ?color ?constellation ?handedness
?title ?cv ?familyName ?givenName ?alternateName ?familyNameKana ?givenNameKana ?alternateNameKana
?bust ?waist ?hip ?attribute ?division ?description ?type ?category ?shoeSize
うわっ…私の考えたクエリ、長すぎ…?
もしかしたら、もう少しまとまった書き方があるかもしれない...
見づらい場合は筆者の勉強不足です。
ちなみに、OPTIONAL
という文はマッチするデータがあった場合にレスポンスを生成する的なもので、
ほとんどのデータにはこれを適用せざるを得なくなりました。
なぜかというと、
- 公式から提示されているアイドルのプロフィールの項目が、シリーズ作品ごとに違いがある。
- あだ名しか公表されていないアイドル(ジュリア、アスラン=BBⅡ世 etc...)がいる。
- そもそも、961プロの玲音というアイドルが、公式設定で
不明
という項目が多いゆえにデータベース上にほとんど定義出来ない。
よって、全シリーズを通して確実に存在する共通プロフィール項目が、
- 性別
- 所属プロダクション(コンテンツ)
- 担当声優
の3つのみ(筆者調べ)で、残りは全てOPTIONAL
で項目が定義されていれば取得する条件になっています。
全ての項目を考慮して合わせるのは少し苦労したよ...
(というか、玲音の項目数最小プロフィールに最終的にはほとんど収束される形となるという後の祭り状態)
で、最終的に以下のように取得出来るので、JSON文字列データを読み取って整形などしてHTMLテンプレートに値を流し込みました。
{
"head": {
"vars": [ "birthDate" , "birthPlace" , "gender" , "height" , "weight" , "age" , "bloodType" , "color" , "constellation" , "handedness" , "title" , "cv" , "familyName" , "givenName" , "alternateName" , "familyNameKana" , "givenNameKana" , "alternateNameKana" , "bust" , "waist" , "hip" , "attribute" , "division" , "description" , "type" , "category" , "shoeSize" , "favorite" , "hobby" , "talent" ]
} ,
"results": {
"bindings": [
{
"birthDate": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#gMonthDay" , "value": "--10-10" } ,
"birthPlace": { "type": "literal" , "xml:lang": "ja" , "value": "沖縄" } ,
"gender": { "type": "literal" , "value": "female" } ,
"height": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#float" , "value": "152.0" } ,
"weight": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#float" , "value": "41.0" } ,
"age": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#integer" , "value": "16" } ,
"bloodType": { "type": "literal" , "value": "A" } ,
"color": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#hexBinary" , "value": "01ADB9" } ,
"constellation": { "type": "literal" , "xml:lang": "ja" , "value": "天秤座" } ,
"handedness": { "type": "literal" , "value": "right" } ,
"title": { "type": "literal" , "xml:lang": "en" , "value": "765AS" } ,
"cv": { "type": "literal" , "xml:lang": "ja" , "value": "沼倉愛美" } ,
"familyName": { "type": "literal" , "xml:lang": "ja" , "value": "我那覇" } ,
"givenName": { "type": "literal" , "xml:lang": "ja" , "value": "響" } ,
"familyNameKana": { "type": "literal" , "xml:lang": "ja" , "value": "がなは" } ,
"givenNameKana": { "type": "literal" , "xml:lang": "ja" , "value": "ひびき" } ,
"bust": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#float" , "value": "83.0" } ,
"waist": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#float" , "value": "56.0" } ,
"hip": { "type": "literal" , "datatype": "http://www.w3.org/2001/XMLSchema#float" , "value": "80.0" } ,
"attribute": { "type": "literal" , "value": "Da" } ,
"division": { "type": "literal" , "xml:lang": "en" , "value": "Princess" } ,
"favorite": { "type": "literal" , "value": "散歩,動物" } ,
"hobby": { "type": "literal" , "value": "編み物,卓球" } ,
"talent": { "type": "literal" , "value": "家事全般" }
}
]
}
}
「クエリ苦労したんだね・・・でもさぁ」
と、SPARQLがなんとなくわかってきた人の中で今の苦労話を聞いたところで、
なんとなく気づいたり、閃いたという方がいるのではないでしょうか。
「全ての項目をオプション値で条件抽出すれば良くないっすかね?」
と、初見なのに直感であっさりと解決策を見出してしまう芹沢あさひが言いそうなことを思ってしまう人もいると思います。
やっぱり、それが一番手っ取り早い解決になりますよね。そうですよね。
でも、筆者は出来る限り必ず取れる項目を洗い出したかったのでやりました。(これで合っているかは未検証)
まとめ
LODはとても便利です。
今回はアイドルマスターのデータが集約されているLOD、im@sparqlに定義されている全アイドルを網羅する的なことをやってしまったので、クエリが少し大変なことになってしまいましたが、
パブリックな形であらかじめデータ集約されているので、あとは何のデータを取りたいかを指定する上記のようなクエリを投げるだけで多くのデータを取ることができ、繋げていろんな開発に用いることが出来ます。
im@sparql意外にも、wikipediaの情報をLODとしてまとめたDBpediaや、
e-Statが国勢調査などの統計データをまとめた統計LODがあるので、
「私はアイドルよりも、もっと表向きでグローバルなデータに興味があります」
という方は、まずはこちらで試してみると良いと思います。
後書き
当日になって気づいたこと。
現在進行形でゲーム展開しているアイマスシリーズは、
各アプリ内でその日の初ログイン時に誕生日お知らせムービーが流れたり、
誕生日特設ページが用意されていたりと充実しているので、その日の誕生日アイドルを知るだけでオッケーなら公式だけで十分かと思ってしまったのはここだけの内緒。
Link Appeal
あと、筆者よりも先にAdventCalendar2019で似たようなプロフィール検索をやっている方がいたのでLinkいたします。
https://qiita.com/Adacchi3/items/3819b726fafa8bab4387
_人人人人人人人人_
> 先を越された <
 ̄Y^Y^Y^Y^Y^Y^Y ̄