LoginSignup
0
0

More than 3 years have passed since last update.

ゲーム攻略サイトからデータを抽出するコツ(pandas、正規表現、Python)

Posted at

 ゲームの情報等はWiki等によくまとめられていますが、データ解析に扱いやすい形になっているとは限りません。それを解析しやすい形にするために、スクレイピングやpandas等を使ってアレコレしていったので、その手順やコツをまとめてみました。整列したデータを使って実際に解析した記事はこれになりますが、ゲームの内容そのものへの言及を多分に含むため、自分のブログに投稿しました。本記事で扱う内容は、飽くまでプログラミングに関わる内容になり、Wikiからデータフレームを作成し、各種解析をするまでの、Pythonにおける手順になります。

 本記事で対象とするのはこんな感じのサイトです。内容としてはこのサイトに特化していますが、他のサイト(ゲーム)であっても、Wikiであったりすれば特に内容は変わらないと思いますし、最悪ぜんぜん別のレイアウトであっても、正規表現でゴリゴリすれば対応可能かと思います。

ライブラリのインポート

import csv
import os
import pickle
import re

import numpy as np
import pandas as pd
import requests
from matplotlib import pyplot as plt
from matplotlib import rcParams
from scipy.spatial import distance
from sklearn.decomposition import PCA

 後、カテゴリだけ手動で設定しておきます。

params = ['food', 'leaf', 'topic', 'gift']

インデックスの取得

image.png

 ソースを覗きます。

image.png

 一括で抽出するのは大変そうなので、この段落だけ正規表現で抽出(pattern = r'>各ユニット(.*?)</div>')した後、更に個別の情報を正規表現で絞り込む(pattern = r'"">(.*?)</a>')、という2段階の作戦でいきます。基本的に、定型文hogefugaに挟まれた中身、つまりhoge+x+fugaからxを抽出した場合、r'hoge(.*?)fugaという正規表現をぶつければなんとかなります。


if not os.path.exists('units.pickle'):
    url = 'https://www.pegasusknight.com/wiki/fe16/?%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E4%B8%80%E8%A6%A7'
    get = requests.get(url)
    text = get.text.replace('\n', '')
    pattern = r'>各ユニット(.*?)</div>'
    unit_text = re.findall(pattern, text)[0]
    pattern = r'"">(.*?)</a>'
    units = re.findall(pattern, unit_text)
    units.remove('テンプレート')
    units.remove('主人公男')
    units.remove('主人公女')
    pickle.dump(units, open('units.pickle', 'wb'))
units = pickle.load(open('units.pickle', 'rb'))

 これをリストとして保存し、これから作るデータフレームのインデックスとしていきます。以降の作業でもそうですが、ローカルにpickleファイルがなければスクレイピングしてデータをローカルに作成、あればそのまま読み込む、という形にして、無駄なスクレイピングの量を減らしています(if not os.path.exists('units.pickle'):による分岐)。

ソースの取得

 インデックスに対応したデータソースをスクレイピングします。

if not os.path.exists('profile_data.pickle'):
    profile_data = {}

    for unit in units:
        print(f'searching {unit}...')
        url = f'https://www.pegasusknight.com/wiki/fe16/?%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88/{unit}#ff12631d'
        get = requests.get(url)
        text = get.text.replace('\n', '')
        profile_data[unit] = text

    pickle.dump(profile_data, open('profile_data.pickle', 'wb'))
profile_data = pickle.load(open('profile_data.pickle', 'rb'))

データフレームの作成

 このような形でデータがまとめられているので、それぞれ解析していきます。
image.png

 ソースを覗いてみます。

image.png

 pattern = r'<th class="style_th" style="text-align:center;">(.*?)</th><td class="style_td" style="text-align:center;">(.*?)</td>'で絞り込めば、項目名と好き嫌いのタプルが抽出できそうです。○と×では扱いづらいので、pair = {'◯': 1, '': 0, '×': -1}なる辞書を用意して数値に変換します。

 また、違うレイアウトのデータもあります。

image.png

 ソースを覗いてみます。

image.png

 下はhoge+x+fugaなので今までの作戦でいけますが、上はそうはいきません。ソースを観察すると、<br class="spacer" />なるタグで区切られていることがわかります。つまりx + hoge + y + hoge + ... + hoge + zという形です。このような時にはsplitメソッドが役立ちます。

 以上を踏まえたデータ作成手順が以下になります。

if not os.path.exists('fav_data.pickle'):
    data = {}
    for p in params:
        data[p] = pd.DataFrame(index=units)

    for d in profile_data:
        text = profile_data[d]
        # food_searching
        pattern = r'>食事の好み(.*?)</div>'
        food_text = re.findall(pattern, text)[0]
        pattern = r'<th class="style_th" style="text-align:center;">(.*?)</th><td class="style_td" style="text-align:center;">(.*?)</td>'
        fav_foods = re.findall(pattern, food_text)
        print(fav_foods)
        pair = {'◯': 1, '': 0, '×': -1}
        for f in fav_foods:
            data['food'].at[d, f[0]] = pair[f[1]]
        # leaf_searching
        pattern = r'>お茶会(.*?)</div>'
        tea_text = re.findall(pattern, text)[0]
        pattern = r'<tr><td class="style_td" colspan="4" style="text-align:center;">(.*?)</td></tr>'
        result = re.findall(pattern, tea_text)[0]
        fav_leaves = result.split('<br class="spacer" />')
        print(fav_leaves)
        for f in fav_leaves:
            data['leaf'].at[d, f] = 1
        # topic_searching
        pattern = r'<td class="style_td">(.*?)</td>'
        fav_topics = re.findall(pattern, tea_text)
        print(fav_topics)
        for f in fav_topics:
            data['topic'].at[d, f] = 1
        pattern = r'">贈り物(.*?)</div'
        # gift_searching
        gift_text = re.findall(pattern, text)[0]
        pattern = r'<th class="style_th" style="text-align:center;">(.*?)</th><td class="style_td" style="text-align:center;">(.*?)</td>'
        fav_gifts = re.findall(pattern, gift_text)
        print(fav_gifts)
        pair = {'◯': 1, '': 0, '×': -1}
        for f in fav_gifts:
            data['gift'].at[d, f[0]] = pair[f[1]]

    for p in params:
        data[p] = data[p].fillna(0).astype('int64')

    # special_process_for_topic
    sum_data = data['topic'].sum()
    data['topic'] = data['topic'].loc[:, sum_data > 1]

    pickle.dump(data, open('fav_data.pickle', 'wb'))
data = pickle.load(open('fav_data.pickle', 'rb'))

 カテゴリごとに、インデックスだけを用意した空のデータフレームを作成します。その後、データを抽出するごとに、data.at[d,f]でデータを入力していきます。atメソッドは、マッチする行(インデックス)と列(カラム)があればそこのマスにデータを記入し、なければ新たに行または列を作成してくれるという便利な命令です。この時、指定されていないマスにはすべてNaNが記入されるので、後でそこを置換しておく必要があります。

解析

 複数のデータに似たような手順を適用するので、データフレームと操作をひとまとめにしたクラスを作っておきます。ここではAnalyzerクラスと名付けています。

class Analyzer:

    def __init__(self, data=None):
        self.data = data

    def summarize(self):
        sum_data = self.data.sum().sort_values(ascending=False)
        print(sum_data)
        sum_data2 = self.data.sum(axis=1).sort_values(ascending=False)
        print(sum_data2)

    def visualize(self):
        num_data = np.array(self.data)
        units = self.data.index.values

        dist_M = distance.cdist(num_data, num_data, metric='euclidean')
        all_min = np.min(dist_M[dist_M > 0])
        all_max = np.max(dist_M)

        pca = PCA(n_components=2).fit_transform(num_data)
        t_pca = pca.transpose()

        rcParams['font.family'] = 'MS Gothic'
        fig, ax = plt.subplots(figsize=(10, 10))
        ax.scatter(t_pca[0], t_pca[1], alpha=0.1)
        for unit, PC in zip(units, pca):
            ax.annotate(unit, PC)
        plt.show()

    def partner(self):
        num_data = np.array(self.data)
        units = self.data.index.values

        dist_M = distance.cdist(num_data, num_data, metric='euclidean')
        all_min = np.min(dist_M[dist_M > 0])
        all_max = np.max(dist_M)

        for unit, d in zip(units, dist_M):
            where_min = np.where(d == all_min)
            if len(where_min[0]) > 0:
                best_partner = units[where_min[0][0]]
                print(f'{unit}{best_partner} が最も趣味が合いそうです')
            where_max = np.where(d == all_max)
            if len(where_max[0]) > 0:
                worst_partner = units[where_max[0][0]]
                print(f'{unit}{worst_partner} はあまり趣味が合わないかも……')

summarizeでは、行、列ごとに合計値を集計値しソートしています。今回の例では、好き嫌いが多いユニットや、逆に人を選ぶ食事などがわかります。

 visualizeでは、2次元の主成分分析を起こった後にそれをグラフにすることにより、各人の趣味の近さを図示することができます。

 partnerでは、各人の趣味の近さを計算し、最も趣味が合いそうな二人と、逆に最も趣味が合わなさそうな二人を決定します。

analyzer = Analyzer()

for p in params:
    analyzer.data = data[p]
    analyzer.summarize()

all_data = pd.concat([data[p] for p in params], axis=1)
analyzer.data = all_data
analyzer.visualize()
analyzer.partner()

 例えば、visualizeの結果はこのような感じになります。

image.png

全ソースコード

import csv
import os
import pickle
import re

import numpy as np
import pandas as pd
import requests
from matplotlib import pyplot as plt
from matplotlib import rcParams
from scipy.spatial import distance
from sklearn.decomposition import PCA

# define_all_params

params = ['food', 'leaf', 'topic', 'gift']

# data_making

if not os.path.exists('units.pickle'):
    url = 'https://www.pegasusknight.com/wiki/fe16/?%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88/%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88%E4%B8%80%E8%A6%A7'
    get = requests.get(url)
    text = get.text.replace('\n', '')
    pattern = r'>各ユニット(.*?)</div>'
    unit_text = re.findall(pattern, text)[0]
    pattern = r'"">(.*?)</a>'
    units = re.findall(pattern, unit_text)
    units.remove('テンプレート')
    units.remove('主人公男')
    units.remove('主人公女')
    pickle.dump(units, open('units.pickle', 'wb'))
units = pickle.load(open('units.pickle', 'rb'))

if not os.path.exists('profile_data.pickle'):
    profile_data = {}

    for unit in units:
        print(f'searching {unit}...')
        url = f'https://www.pegasusknight.com/wiki/fe16/?%E3%83%A6%E3%83%8B%E3%83%83%E3%83%88/{unit}#ff12631d'
        get = requests.get(url)
        text = get.text.replace('\n', '')
        profile_data[unit] = text

    pickle.dump(profile_data, open('profile_data.pickle', 'wb'))
profile_data = pickle.load(open('profile_data.pickle', 'rb'))

if not os.path.exists('fav_data.pickle'):
    data = {}
    for p in params:
        data[p] = pd.DataFrame(index=units)

    for d in profile_data:
        text = profile_data[d]
        # food_searching
        pattern = r'>食事の好み(.*?)</div>'
        food_text = re.findall(pattern, text)[0]
        pattern = r'<th class="style_th" style="text-align:center;">(.*?)</th><td class="style_td" style="text-align:center;">(.*?)</td>'
        fav_foods = re.findall(pattern, food_text)
        print(fav_foods)
        pair = {'◯': 1, '': 0, '×': -1}
        for f in fav_foods:
            data['food'].at[d, f[0]] = pair[f[1]]
        # leaf_searching
        pattern = r'>お茶会(.*?)</div>'
        tea_text = re.findall(pattern, text)[0]
        pattern = r'<tr><td class="style_td" colspan="4" style="text-align:center;">(.*?)</td></tr>'
        result = re.findall(pattern, tea_text)[0]
        fav_leaves = result.split('<br class="spacer" />')
        print(fav_leaves)
        for f in fav_leaves:
            data['leaf'].at[d, f] = 1
        # topic_searching
        pattern = r'<td class="style_td">(.*?)</td>'
        fav_topics = re.findall(pattern, tea_text)
        print(fav_topics)
        for f in fav_topics:
            data['topic'].at[d, f] = 1
        pattern = r'">贈り物(.*?)</div'
        # gift_searching
        gift_text = re.findall(pattern, text)[0]
        pattern = r'<th class="style_th" style="text-align:center;">(.*?)</th><td class="style_td" style="text-align:center;">(.*?)</td>'
        fav_gifts = re.findall(pattern, gift_text)
        print(fav_gifts)
        pair = {'◯': 1, '': 0, '×': -1}
        for f in fav_gifts:
            data['gift'].at[d, f[0]] = pair[f[1]]

    for p in params:
        data[p] = data[p].fillna(0).astype('int64')

    # special_process_for_topic
    sum_data = data['topic'].sum()
    data['topic'] = data['topic'].loc[:, sum_data > 1]

    pickle.dump(data, open('fav_data.pickle', 'wb'))
data = pickle.load(open('fav_data.pickle', 'rb'))

# analysis


class Analyzer:

    def __init__(self, data=None):
        self.data = data

    def summarize(self):
        sum_data = self.data.sum().sort_values(ascending=False)
        print(sum_data)
        sum_data2 = self.data.sum(axis=1).sort_values(ascending=False)
        print(sum_data2)

    def visualize(self):
        num_data = np.array(self.data)
        units = self.data.index.values

        dist_M = distance.cdist(num_data, num_data, metric='euclidean')
        all_min = np.min(dist_M[dist_M > 0])
        all_max = np.max(dist_M)

        pca = PCA(n_components=2).fit_transform(num_data)
        t_pca = pca.transpose()

        rcParams['font.family'] = 'MS Gothic'
        fig, ax = plt.subplots(figsize=(10, 10))
        ax.scatter(t_pca[0], t_pca[1], alpha=0.1)
        for unit, PC in zip(units, pca):
            ax.annotate(unit, PC)
        plt.show()

    def partner(self):
        num_data = np.array(self.data)
        units = self.data.index.values

        dist_M = distance.cdist(num_data, num_data, metric='euclidean')
        all_min = np.min(dist_M[dist_M > 0])
        all_max = np.max(dist_M)

        for unit, d in zip(units, dist_M):
            where_min = np.where(d == all_min)
            if len(where_min[0]) > 0:
                best_partner = units[where_min[0][0]]
                print(f'{unit}{best_partner} が最も趣味が合いそうです')
            where_max = np.where(d == all_max)
            if len(where_max[0]) > 0:
                worst_partner = units[where_max[0][0]]
                print(f'{unit}{worst_partner} はあまり趣味が合わないかも……')


analyzer = Analyzer()

for p in params:
    analyzer.data = data[p]
    analyzer.summarize()

all_data = pd.concat([data[p] for p in params], axis=1)
analyzer.data = all_data
analyzer.visualize()
analyzer.partner()
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0