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

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

G2R2018は現時点で399作品（失格含む）登録されており、二次登録でさらなる増加が見込まれます。
一個一個ダウンロードしていくのはかなり大変なことと思われます。

有志によるパッケージング（ありがたいことです）を待つ人もいるでしょう。
ですが、待ちに待ったBMSを一刻も早くプレイしたいという人も多いのではないでしょうか。

僕もその一人でした。
しかし1つずつダウンロードするのはとても手間がかかる。
そこで、プログラミングで一括ダウンロードが出来ないか、試行錯誤を重ねました。

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

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

# 大まかな手順

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

# 下準備
　まず、Python3をインストール（2でも出来るかもしれませんが保証はない）
　また、以下のライブラリをインストール
・BeautifulSoup
・Requests
・lxml
・urllib
　pip installを用いてinstallできます。

```shell
pip install requests
pip install beautifulsoup4
pip install lxml
pip install urllib
```

コード

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

```python
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ライブラリを用いて手元にウェブページを持ってきます。

```python
url = "http://manbow.nothing.sh/event/event.cgi?action=More_def&num=45&event=123"
r = requests.get(url)
```

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

```python
soup = BeautifulSoup(html, 'lxml')
blockquote = soup.find("blockquote")
hrefs = blockquote.find_all("a") 
```

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

```python
>>> 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版を取るという方針でいきました。

```python
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の登録番号とリストのインデックスとを合わせるためです。

```python
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

以下のようにして

```python
#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に変更します

```python
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上にある場合のコードは以下のようになります。さっきより長いです。

```python
#まず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とみなし、拡張子を取ってくることにしました。その他は諦めます。

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

### まとめ

コードとしては長くなりますが、以下のような感じで一括ダウンロードします。
例外処理や分岐が多いので長いです。何とか短く出来ないかなー

```python
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

```python
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"というコマンドで利用出来ます。
ダバァファイルも勝手にまとめてくれる、優秀な奴です。

```shell
brew install unar
```

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

```shell
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ファイルが入ってないフォルダを見つけ、個別に対処します。

```shell
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ライフを！