Python
BMS

Python3を用いてG2R2018のBMSを一括ダウンロードしようと奮闘した話

こんにちは。BMSプレイヤーの皆さん、G2R2018楽しんでいますでしょうか。

G2R2018は現時点で399作品(失格含む)登録されており、二次登録でさらなる増加が見込まれます。

一個一個ダウンロードしていくのはかなり大変なことと思われます。

有志によるパッケージング(ありがたいことです)を待つ人もいるでしょう。

ですが、待ちに待ったBMSを一刻も早くプレイしたいという人も多いのではないでしょうか。

僕もその一人でした。

しかし1つずつダウンロードするのはとても手間がかかる。

そこで、プログラミングで一括ダウンロードが出来ないか、試行錯誤を重ねました。

正直僕自身Pythonに関しては初心者なので、手探り状態です。多分もっと良いコード書ける人大量にいます。

なのでこれは参考程度にお納めください。


大まかな手順

 まずBMSファイルへのリンクURLを一括ダウンロードします。その後、実物をダウンロード、解凍という流れです。


下準備

 まず、Python3をインストール(2でも出来るかもしれませんが保証はない)

 また、以下のライブラリをインストール

・BeautifulSoup

・Requests

・lxml

・urllib

 pip installを用いてinstallできます。

pip install requests

pip install beautifulsoup4
pip install lxml
pip install urllib

コード

pythonを立ち上げる際に必要なライブラリをimportすると楽です。

import urllib.request

from bs4 import BeautifulSoup
import os
import requests


URLを収集

G2R2018会場において各イベントページは、

http://manbow.nothing.sh/event/event.cgi?action=More_def&num=<登録番号>&event=123

の形で記述されます。numのパラメータが登録番号を表し、eventのパラメータが各イベントのIDになります。

参考までに僕のBMSのページを用いると、

http://manbow.nothing.sh/event/event.cgi?action=More_def&num=45&event=123

のようになります。

このページをまずrequestsライブラリとBeautifulSoupを用いてスクレイピング・解析します。

以下、Beautifulsoupの参考: https://qiita.com/matsu0228/items/edf7dbba9b0b0246ef8f

requestsライブラリを用いて手元にウェブページを持ってきます。

url = "http://manbow.nothing.sh/event/event.cgi?action=More_def&num=45&event=123"

r = requests.get(url)

ダウンロードURLはblockquoteタグの領域に存在するので、BeautifulSoupを使ってその中に範囲を絞り、またその中から更にハイパーリンクを全て取り出します。

soup = BeautifulSoup(html, 'lxml')

blockquote = soup.find("blockquote")
hrefs = blockquote.find_all("a")

こうすることで、全てのダウンロード用URLが出てきます

>>> print(hrefs)

[<a href="https://drive.google.com/open?id=1xwEcnoLe0Qzda5YorkfcRzcWB2lgcUaW">https://drive.google.com/open?id=1xwEcnoLe0Qzda5YorkfcRzcWB2lgcUaW</a>, <a href="https://drive.google.com/open?id=1akTcw-F-5P6yCSEpacXEMkxhLwDSZw8b">https://drive.google.com/open?id=1akTcw-F-5P6yCSEpacXEMkxhLwDSZw8b</a>]

ダウンロードURLが複数存在することもあります。

僕の場合はogg版とwav版ですが、中には追加差分があったりすることもあり、それらを全て見分けるのはかなり難しいです。

なので基本的に一番上のものを取ってきて、ogg版とwav版がある場合に関してはogg版を取るという方針でいきました。

if len(hrefs) > 1: #oggとwavがどっちもある場合の処理

oggLoc = max(blockquote.text.find("ogg"), blockquote.text.find("OGG"))
wavLoc = max(blockquote.text.find("wav"), blockquote.text.find("WAV"))
if (oggLoc > wavLoc) and (wavLoc > -1):
bmsUrl = hrefs[1].get("href")
else:
bmsUrl = hrefs[0].get("href")

以上を全てのBMSに対して適用し、URLを一括取得します。

最初に空文字列をlistに入れているのは、BMSの登録番号とリストのインデックスとを合わせるためです。

bmslist =[""]

bmsNum = 399 #取ってくるBMSの数

for i in range(bmsNum):
number = i + 1
url = "http://manbow.nothing.sh/event/event.cgi?action=More_def&num=" + str(number) + "&event=123"
r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')
blockquote = soup.find("blockquote")
hrefs = blockquote.find_all("a")
if len(hrefs) > 1: #oggとwavがどっちもある場合の処理
oggLoc = max(blockquote.text.find("ogg"), blockquote.text.find("OGG"))
wavLoc = max(blockquote.text.find("wav"), blockquote.text.find("WAV"))
if (oggLoc > wavLoc) and (wavLoc > -1):
bmsUrl = hrefs[1].get("href")
else:
bmsUrl = hrefs[0].get("href")
elif len(hrefs) > 0: #存在しない場合
bmsUrl = hrefs[0].get("href")
else:
bmsUrl = ""
bmslist.append(bmsUrl)


ダウンロード

URLを集めたからと言ってすぐにダウンロード出来るわけではありません。寧ろここからが本番でした。

ダウンロードにはurllib.urlretrieveを用います。

参考: https://qiita.com/n_kats_/items/38512513c8978a7ab4b4

以下のようにして

#urlにあるファイルを、手元のfileNameにダウンロードする

urllib.request.urlretrieve(url, fileName)

これを使うにはダウンロードすべきファイルURLと、ファイルの拡張子が必要となります。

ただし、URLによってはダウンロードすべきファイルの場所ではなくプレビュー画面を指定していたりするのが厄介だったりします。

G2Rに用いられているアップローダを大別すると

・Dropbox

・Google Drive

・その他(OneDrive, 個人など)

に分かれます。

それぞれについてダウンロード方法が違うので、見ておきましょう。


Dropbox

有能アップローダ。

DropboxのファイルURLは以下の形を取ります。

https://www.dropbox.com/s/<ファイルID>/<ファイル名>?dl=<ダウンロードのon,off>

例として僕のチームメイトのあとぅすさんのURLを貼ります。

https://www.dropbox.com/s/hv2zwqd9ebe5k70/%5B%E3%81%82%E3%81%A8%E3%81%85%E3%81%99%5DCall%20of%20Abyss_ogg.zip?dl=1

なので、ファイル名から拡張子(この場合はzip)を取り出すことが出来ます。

また、URLの末尾のdlパラメータが0の場合は直接ダウンロード出来るURLになっておらずプレビュー画面を表示してしまうので、1に変更します

url = "" #目的のURL

paramDivide = url.split("?") #URLのパラメータ部で区切る
dotDivide =paramDivide[0].split(".") #ファイル名
expansion = dotDivide[len(dotDivide)-1] #拡張子を取得
url = url.replace("?dl=0", "?dl=1") #ダウンロードURLに変更


Google Drive

Google Drive上のファイルはそれぞれ固有のIDで指定されています。

例えば私のファイルの場合は"1xwEcnoLe0Qzda5YorkfcRzcWB2lgcUaW"です。

ダウンロードURLは以下の形で現されます。

https://drive.google.com/uc?id=<ファイルID>

ただし、普通に共有URLを貼ろうとすると大体以下の2パターンになります。

https://drive.google.com/open?id=<ファイルID>

https://drive.google.com/file/d/<ファイルID>/view?usp=sharing

したがって、この2パターンの場合はうまいことIDを切り出して、ダウンロードURLに変換してやる必要があります。

また、Dropboxと違って拡張子をURLから推定できないのも面倒です。

そこで、一旦プレビュー画面を開いて、拡張子を取得した後という方法を取ります。プレビューURLは先ほど上に示したうちの

https://drive.google.com/open?id=<ファイルID>

となっています。これはHTMLファイルなので、BeautifulSoupを用いて解析することが出来ます。

また、共有方法によってはフォルダが指定されていたりするので、そう言った時は諦めるしかないでしょう。

ファイルが大きすぎる(100MB以上?)の場合、ウイルススキャンが行えない旨のページが先に開かれてしまうため、ダウンロード出来ないのも厄介です。

したがって、Google Drive上にある場合のコードは以下のようになります。さっきより長いです。

#まずIDを取得

if url.find("file") > -1: #https://drive.google.com/file/d/<ID>/view?usp=sharingの場合
id = url.split("/")[5].split("&")[0]
elif url.find("id") > -1: #https://drive.google.com/open?id=<ID>等、idが指定されている場合
id = url.split("id=")[1].split("&")[0]

#次に拡張子を取得
r = requests.get("https://drive.google.com/open?id="+id) #拡張子を得るためにまずプレビュー画面を開く
soup = BeautifulSoup(r.text, 'lxml')
title = soup.find("head").find("title").text #プレビュー画面のうち、ファイル名がある部分
if title.find(".rar") > -1:
expansion = "rar"
elif title.find(".zip") > -1:
expansion = "zip"

#ダウンロード用URLに変換
url = "https://drive.google.com/uc?id=" + id #download用のurlに変換

Google Driveには専用のAPIがあるため、それを用いた方がよりスマートに対応出来るかもしれません。


その他のアップローダ

その他のアップローダは色々あります。が、ほとんど上の2つで賄えてしまうため雑です。

URLに拡張子(rar, zip)が含まれている場合は直接ファイルを指定しているURLとみなし、拡張子を取ってくることにしました。その他は諦めます。

if url.find(".rar") == len(url) - 4:

expansion = "rar"
elif url.find(".zip") == len(url) - 4:
expansion = "zip"


まとめ

コードとしては長くなりますが、以下のような感じで一括ダウンロードします。

例外処理や分岐が多いので長いです。何とか短く出来ないかなー

othersList =[] #ダウンロード出来なかったやつ

tooSmallLislt = [] #あまりにも小さくて正確にダウンロード出来てないであろうやつ

for i in range(bmsNum):
num = i+1
url = bmslist[num]
if url.find('dropbox') > -1: #Dropboxの場合
paramDivide = url.split("?")
dotDivide =paramDivide[0].split(".")
if (len(dotDivide[len(dotDivide)-1]) > 3): #個別対応
othersList.append(num)
print("missed: " + str(num))
continue
expansion = dotDivide[len(dotDivide)-1]
url = url.replace("?dl=0", "?dl=1")
elif url.find('drive.google') > -1: #Google Driveの場合
if url.find("file") > -1:
id = url.split("/")[5].split("&")[0]
elif url.find("id") > -1:
id = url.split("id=")[1].split("&")[0]
else: #個別対応
othersList.append(num)
print("missed: " + str(num))
continue
r = requests.get("https://drive.google.com/open?id="+id) #拡張子を得るためにまずファイルを開く
soup = BeautifulSoup(r.text, 'lxml')
title = soup.find("head").find("title").text
if title.find(".rar") > -1:
expansion = "rar"
elif title.find(".zip") > -1:
expansion = "zip"
else: #個別対応
othersList.append(num)
print("missed: " + str(num))
continue
url = "https://drive.google.com/uc?id=" + id #download用のurl
elif url.find(".rar") == len(url) - 4:
expansion = "rar"
elif url.find(".zip") == len(url) - 4:
expansion = "zip"
else: #個別対応
othersList.append(num)
print("missed: " + str(num))
continue
path = str(num) + "." + expansion
urllib.request.urlretrieve(url, path)
if (os.path.getsize(path) < 200000): #あまりにも小さい場合を除きたい
tooSmallList.append(num)


個別対応行きとなったものたち

othersListやtooSmallListに入ってるメンツは個別対応組です。

webbrowserを使って個別にイベントページを開き、対処します。

参考: https://docs.python.org/ja/3/library/webbrowser.html

import webbrowser

for i in range(len(othersList)):
url = "http://manbow.nothing.sh/event/event.cgi?action=More_def&num=" + str(othersList[i]) + "&event=123"
webbrowser.open(url)


解凍

解凍にはMac付属のUnarchiverがめちゃくちゃ便利でした。

コマンドラインからは"unar"というコマンドで利用出来ます。

ダバァファイルも勝手にまとめてくれる、優秀な奴です。

brew install unar

lsでファイルを全て取ってきて、全て解凍します。ファイル名にスペースが含まれている場合、そこが区切り目だと勘違いしてしまうなので、環境変数IFSを一時的に改行文字にしてからfor文を回します。

IFS_bak=$IFS

IFS=$'\n'

aho=`ls`
for f in ${aho}
do
unar "${f}" >> log.txt
done

IFS=$IFS_bak

フォルダを全部指定して、解凍します。

また、うまくいっていない場合があるので、log.txt内で"Couldn't"など文字列で検索すると、それを見つけることが出来ます。

二重フォルダがある場合は、以下の形で直下にBMSファイルが入ってないフォルダを見つけ、個別に対処します。

IFS_bak=$IFS

IFS=$'\n'
folders=`ls -F | grep /`
for f in ${folders[@]}
do
bmfiles=`ls "${f}" | grep \.bm`
if [ -z "$bmfiles" ]; then
echo $f
fi
done
IFS=$IFS_bak

Windowsの民(大半はこっちでしょうけど)は良くわからないのですが、Explzhというアプリケーションが良いと聞きました。


反省点・課題

今回、80%ほどのファイルをダウンロードすることに成功しました。

ただし、20%ほどは手動でダウンロードしなければならず、面倒なことには変わりなかったです。

また、基本的に「一番上にあるURLを取ってくる」というアドホックな方法を取っているため、修正差分ファイルなどを入手することが出来ません。

大体の場合URLが2つ以上ある時は「修正」「BGA」などのワードが入っているので、それを頼りに修正差分など追加出来たりはすると思いますが、そこまでやる元気はなかったので今のままで。

では皆さん、良いBMSライフを!