はじめに
大学の授業で、入力されたアーティストと近しいアーティストのネットワーク図を作成するシステムを作ったので、今回は個人的な趣味で、そのシステムを発展させてWebアプリケーションを開発しました。その際の備忘録を残します。
part1では、バックエンド側の実装についてまとめます。
Webアプリケーションの大まかな流れ
- 初期画面で入力フォームを表示し、ユーザにアーティスト名を入力させる
- 入力されたアーティストに対して、Spotipyの
artist_related_artists()
を使用して、そのアーティストと関連のあるアーティスト情報を取得する -
networkx
ライブラリを使用して、関連のあるアーティストを芋づる式にノードで結んでネットワーク図を作り、ページランクの値が高い順に取り出す(この時、人気曲の情報も取得する) - リザルト画面で関連のあるアーティスト10人と取得した人気曲の情報を表示する
今回の備忘録では、2.と3.のバックエンド側の実装についてまとめます。
バックエンドの開発
初期設定
from flask import Flask, render_template, request, redirect, url_for
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import networkx as nx
import pandas as pd
import os
from dotenv import load_dotenv
app = Flask(__name__)
# .envファイルから環境変数を読み込む
load_dotenv()
# 環境変数からSpotify APIのクライアントIDとシークレットを取得
client_id = os.getenv('SPOTIFY_CLIENT_ID')
client_secret = os.getenv('SPOTIFY_CLIENT_SECRET')
auth_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)
sp = spotipy.Spotify(auth_manager=auth_manager)
- 必要なモジュールをインポートします
-
app
変数にFlaskクラスのインスタンスを格納します -
.env
ファイルから環境変数を取得し、Spotipyを使えるようにします。.envの具体的な扱い方は次回以降の記事で書きます
ネットワーク図作成にあたり必要な関数の作成
# アーティスト名のURIを取得
def get_artist_uri(name):
results = sp.search(q='artist:' + name, type='artist')
if results['artists']['items']:
artist = results['artists']['items'][0]
uri = artist['uri']
return uri
else:
return None
# アーティストのURIを入力し,そのアーティストと関連があるアーティスト情報を出力
def get_related_artist_info(uri):
df = pd.DataFrame()
for artist in sp.artist_related_artists(uri)['artists']:
tmp = pd.Series([], name=artist['name'])
for key in ['popularity', 'uri']:
tmp[key] = artist[key]
# アーティストのトップトラックを取得
top_tracks = sp.artist_top_tracks(artist['uri'])['tracks']
top_track_names = [track['name'] for track in top_tracks]
# トップトラックをデータフレームに追加
tmp['top_tracks'] = ', '.join(top_track_names)
df = pd.concat([df, pd.DataFrame(tmp).T])
return df
Spotipyでは、アーティスト情報はURIを使って表されるので、
- アーティスト名のURIを取得する関数
- 上記のURIを使って、そのアーティストと関連がある(似ている)アーティスト情報を取得する関数
を作成します。これらの関数を使ってネットワーク図を作ります。
ネットワーク図の作成、関連アーティストと人気曲情報の取得
# 以下,ページランクをもとに上位10個のアーティストを取得する関数
#最初にアーティストネットワークを作成する
#次に,そのネットワークに対してpagerankを計算し,pagerankの上位10個を出力する.
def get_top_artists(artist_name):
G = nx.Graph() # ページランク計算用の新しいグラフオブジェクトを生成
uri = get_artist_uri(artist_name)
if not uri:
return None, None
df = get_related_artist_info(uri)
G.add_node(artist_name)
for name in df.index.tolist():
G.add_node(name)
G.add_edge(artist_name, name)
pr = nx.pagerank(G)
sorted_pr = sorted(pr.items(), key=lambda x: x[1], reverse=True) #sorted_prリストは(アーティスト名, PageRank値)のタプル構造になっている.
top_artists_list = [artist[0] for artist in sorted_pr[:11]] #artist[0]はsorted_prリストの「アーティスト名」を参照している.
if artist_name in top_artists_list: #上位10人の中で入力されたアーティスト名と同じ名前のものが入っていた場合は,除去する
top_artists_list.remove(artist_name)
else: #top_artistリストの1番目は入力されたアーティスト名と同じなので,else文に来たときはpopして除去する.
top_artists_list.pop()
# アーティストごとに有名曲を5曲取得
top_tracks_dict = {}
for artist in top_artists_list:
uri = get_artist_uri(artist)
if uri:
top_tracks = sp.artist_top_tracks(uri)['tracks'] #人気順になってる
top_tracks_info = []
for track in top_tracks[:6]:
track_info = {
'name': track['name'], # 曲名を取得
'release_date': track['album']['release_date'].split('-')[0], # リリース年を取得
'image_url': track['album']['images'][0]['url'] # アルバムの画像URLを取得
}
top_tracks_info.append(track_info)
# トップトラック情報をアーティスト名をキーにして辞書に追加
top_tracks_dict[artist] = top_tracks_info
return top_artists_list, top_tracks_dict
get_artist_uri(name)
、get_related_artist_info(uri)
、networkx
ライブラリを使用してネットワークを作り、その中からページランクの高い順に関連のあるアーティストを10人取り出して、top_artists_list
辞書に格納しています。
一般的に、G = nx.Graph()
はグローバル変数として関数の外で定義しますが、今回のアプリケーションでそのように定義してしまうと、ユーザが1回目に入力したアーティスト情報のネットワーク結果が2回目以降の入力にも影響を与えてしまいます。今回はそれを防ぐため、関数内に定義しました。これにより、ユーザの入力ごとにget_top_artists(artist_name)
を利用してネットワーク図を作成することができ、以降の入力時に影響を与えないようにしています。
また、今回は取り出したアーティストの人気曲を6曲取り出し、それも出力するようにしました。具体的には、artist_top_tracks(uri)['tracks']
で人気曲の情報を取得し、「曲名」・「アルバム収録年」・「アルバム画像」をtop_tracks_dict
辞書に格納しています。
Webアプリケーションの初期画面とリザルト画面の設定
#初期画面
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
artist_name = request.form['artist_name']
return redirect(url_for('result', artist_name=artist_name))
return render_template('index.html')
#リザルト画面
@app.route('/result/<artist_name>')
def result(artist_name):
top_artists_list, top_tracks_dict = get_top_artists(artist_name)
if not top_artists_list:
return render_template('error.html', artist_name=artist_name)
return render_template('result.html', artist_name=artist_name, top_artists_list=top_artists_list, top_tracks_dict=top_tracks_dict)
初期画面(index.html
)とリザルト画面(result.html
とerror.html
)のバックエンド側を上記のように実装しました。HTMLテンプレートの中身は次回以降の記事で書きます。
初期画面は、以下のように実装しています。
①ユーザーが初めてページにアクセスしたときや、フォームを送信せずにページをリロードしたときindex.html
テンプレートをレンダリングして表示する。
②クライアント側からPOSTリクエストを受け取ったらartist_name
パラメータを使用してresultという名前のルート(/)にリダイレクトする
一方で、リザルト画面は、以下のように実装しています。
①artist_name
はURLパスから取得され、関数の引数として渡す
②top_artists_list
が空(つまり、関連アーティストが見つからない)場合、error.html
テンプレートをレンダリングして表示する。その際、artist_name
をコンテキストとしてテンプレートに渡す。
③top_artists_list
が空でない場合、result.html
テンプレートをレンダリングして表示する。その際、artist_name
、top_artists_list
、top_tracks_dict
をコンテキストとしてテンプレートに渡す。
終わりに
今回は、Webアプリケーションのバックエンド側の実装についてまとめました。次回以降では、HTMLテンプレートの中身やCSSの定義、.envファイルについてを書こうと思います。
参考文献
以下の記事を参考にしました。ありがとうございました。