nprogram’s blog

気ままに、プログラミングのトピックについて書いていきます

Python + Selenium + ChromeでGoogleの検索を自動化する [非headlessモード]

はじめに

本記事は、Python + Selenium + ChromeでGoogleの検索を自動化する手法について記載したものです。

ChromeのWeb操作は、ChromeDriverを用います。

ChromeDriverを導入する方法として、以下の2つの方法がありますが、本記事は前者の方法を使用します。

  • バイナリを直接ダウンロードする
  • pipコマンド(pip install chromedriver-binary)でインストールする

以下のサイトから入手できます。Google Chrome Versionと合わせる必要があるので、ダウンロードするバージョンについては注意してください。

https://chromedriver.chromium.org/

また、ChromeDriverは非headlessモードで動作させます。

(※記事記載時点(2019/10/15)では本記事のコードが動作することを確認していますが、Googleのデザイン等が変われば動作しなくなる可能性があります。ご了承ください)

環境

環境は以下のとおりです。パッケージの管理はAnacondaで実施しています。pythonコードの実行はjupyter Noteboookを使用しました。

  • OS : Windows 10 Pro
  • conda version : 4.7.12
  • conda-build version : 3.18.8
  • python version : 3.7.3.final.0
  • selenium : 3.141.0
  • ChromeDriver : 77.0.3865.40

GoogleのWebサイトを開く

以下のコードでGoogleのWebサイトを開くことができます。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from time import sleep

options = Options()
driver = webdriver.Chrome(chrome_options=options, executable_path="F:\\tool\\ChromeDriver_77_0_3865_40\\chromedriver.exe")
web_site = 'https://www.google.com/'
driver.get(web_site )

from selenium.webdriver.common.keys import Keysは、文字を削除、入力、エンターキーの実行を行うためのキー操作を可能にするための宣言です。

from time import sleepはスリープさせるための宣言になります。

特に、webdriver.Chromeのパス指定(executable_path)は注意してください。Windowsの場合は、フォルダー階層はバックスラッシュ2回にする必要があります。

GoogleのWebサイトで検索を実行する

Googleの検索を自動化しようとした場合、以下のような課題があります。

  • Webページが完全に読み込まれる前にコードを実行しようとするとコードの処理が失敗する
  • Webページの要素が存在しないのに、要素を操作しようとするとコードの処理が失敗する

上記の課題を解決するためには、WebDriverWaitを使って任意のHTMLの要素が特定の状態になるまで待つことで解決可能です。

presence_of_element_locatedを実行することで、指定した要素がDOM上に現れるまで待機することが出来ます。

An expectation to locate an element and check if the selection state specified is in that state. locator is a tuple of (by, path) is_selected is a boolean

[例]

WebDriverWait(driver, MAX_WAIT_TIME_SEC).until(EC.presence_of_element_located((By.CLASS_NAME, INPUT_BOX_CLASS_NAME)))

EC.presence_of_element_located((By.CLASS_NAME, INPUT_BOX_CLASS_NAME))で2重の括弧になっていますが、バグではありません。 内側の括弧がタプル(tuple of (by, path))を示します。byは要素の種類、pathは要素の位置を示します。外側の括弧が関数presence_of_element_locatedの括弧です。

Webページの要素を操作する場合は、Webページの要素にアクセス可能か必ず確認してから操作したほうがよいでしょう。

サンプルコード

以下のサンプルコードでは、以下の操作を自動化しています。

  1. GoogleのWebサイトを開く
  2. 入力フィールドをクリアする (Back Spaceボタンを100回押す)
  3. 入力フィールドに、文字列testを入力する
  4. Enterボタンを押す
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

# ChromeDriverのパスを引数に指定しChromeを起動
CHROME_DRIVER_PATH = "F:\\tool\\ChromeDriver_77_0_3865_40\\chromedriver.exe"
options = Options()
driver = webdriver.Chrome(executable_path=CHROME_DRIVER_PATH, options=options)

# 指定したURLに遷移する
web_site = 'https://www.google.com/'
driver.get(web_site )

# 指定された要素(検索テキストボックス)がDOM上に現れるまで待機する (最大でMAX_WAIT_TIME_SEC秒待つ)
MAX_WAIT_TIME_SEC = 100
INPUT_BOX_CLASS_NAME = "gLFyf"
element = WebDriverWait(driver, MAX_WAIT_TIME_SEC).until(EC.presence_of_element_located((By.CLASS_NAME, INPUT_BOX_CLASS_NAME)))

# 検索テキストボックスをクリアする
for item in range(0,100) :
    element.send_keys(Keys.BACK_SPACE)

# 検索テキストボックスに文字列を入力する
code = "test"
element.send_keys(code)

# Enterボタンを押す
element.send_keys(Keys.RETURN)

参考ページ

あとがき

Webページの要素の指定をさらに簡単にしたいので、「XPath」で指定できるようにしたいと思います。また、GoogleのUIを使用しないHeadless Chromeでの自動化ができるようにしたいと思います。

Python データ構造学習

リストを使って記載してみる

def names(name_and_grouplist, group_name):
    _names = []

    for name_and_group in name_and_group_list:
        if name_and_group[0] == group_name:
            name = name_and_group[1]
            if name not in _names:
                _names.append(name)

    return _names

name_and_group_list = [
    ["Aグループ", "Tom"],
    ["Aグループ", "Jerry"],
    ["Bグループ", "Mike"],
    ["Bグループ", "Ichiro"],
    ["Bグループ", "Mike"],
]

result_list = names(name_and_group_list, "Bグループ")

print(result_list)
['Mike', 'Ichiro']

辞書を使って記載してみる

def get_name_and_group(name_and_group_list):

    # 辞書宣言
    _names_by_group = {}

    print(type(_names_by_group))

    for name_and_group in name_and_group_list:
        
        group = name_and_group[0]
        name = name_and_group[1]

        print(type(group))
        print(type(name))

        if group not in _names_by_group:
             _names_by_group[group] = []

        if name not in _names_by_group:
            _names_by_group[group].append(name)

    return _names_by_group


name_and_group_list = [
    ["A", "Tom"],
    ["A", "Jerry"],
    ["B", "Mike"],
    ["B", "Ichiro"],
    ["B", "Mike"],
]

print(type(name_and_group_list))

result_list = get_name_and_group(name_and_group_list)

print(result_list["B"])
['Mike', 'Ichiro', 'Mike']

defaultdictを使用して、辞書を改善 (listで初期化)

from collections import defaultdict

def get_names_by_group(someList):
    _names_by_group = defaultdict(list)
    
    for item in someList:
        group = item[0]
        name = item[1]
        
        if name not in _names_by_group[group]:
            _names_by_group[group].append(name)
    
    return _names_by_group



name_and_group_list = [
    ["A", "Tom"],
    ["A", "Jerry"],
    ["B", "Mike"],
    ["B", "Ichiro"],
    ["B", "Mike"],
]


result_list = get_names_by_group(name_and_group_list)

print(result_list["B"])
['Mike', 'Ichiro']

defaultdictを使用してみる

defaultdictは標準ライブラリのcollectionsモジュールに含まれるデータ型です。 辞書に存在しないキーにアクセスしたとき、デフォルト値が設定されたキーを自動で作ってくれるのがメリットです。 defaultdict の引数には、「初期化時に実行する関数」を記述します。

defaultdictを使用して、辞書を改善 (setで初期化)

from collections import defaultdict

def get_names_by_group(someList):
    
    _names_by_group = defaultdict(set)
    
    for group, name in someList:
        _names_by_group[group].add(name)
    
    return _names_by_group



name_and_group_list = [
    ["A", "Tom"],
    ["A", "Jerry"],
    ["B", "Mike"],
    ["B", "Ichiro"],
    ["B", "Mike"],
]


result_list = get_names_by_group(name_and_group_list)

print(result_list["B"])
{'Mike', 'Ichiro'}

リンク

C++のコードレビューまとめ

C++のコードレビューのまとめを行います

派生される可能性があるクラスの場合、デストラクタにvirtualをつけよう

ポリモーフィズムを利用すべく作った基底クラスのデストラクタはvirtualが必要であるためです。 基底クラスのデストラクタにvirtualを付けない場合、派生クラスのデストラクタが呼ばれずメモリリークが起きる

ルール厳守なもの

[Guilty Code]

class Test
{
    Test(){}
}

[Correct Code]

class Test
{
    Test(){}
    virtual ~Test() = default;
}

std::cinとstd::coutへのアクセスは副作用があるため呼び出す場所は限定すること

std::cinとstd::coutは副作用があるため呼び出す場所は限定すること (main関数内を強く推奨)

不要な#include文は削除しましょう

使用していない#include文は削除しましょう

コードを意味のあるまとめまりにしよう

単体テストを活用しよう

#include <cassert>
#include <string>
#include "Main.h"

bool isIPAddr(const std::string& inputConsoleLine);

class Test
{
public:
    Test()
    {
        ExecuteTest();
    }
private:    
    static void ExecuteTest()
    {
        // ---- 正常パターン単体テスト ---- //
        // 正規表現ロジック上の下限値・上限値確認
        assert(isIPAddr("0.0.0.0"));
        assert(isIPAddr("255.255.255.255"));
        assert(isIPAddr("9.9.9.9"));
        assert(isIPAddr("10.99.10.99"));
        assert(isIPAddr("100.199.100.199"));
        assert(isIPAddr("200.249.200.249"));
        assert(isIPAddr("250.255.250.255"));
        
        // ---- 異常パターン単体テスト ---- //
        // 前置きの0は失敗判定させる
        assert(!isIPAddr("00.00.00.00"));
        assert(!isIPAddr("000.000.000.000"));
        assert(!isIPAddr("0000.0000.0000.0000"));
        
        // 異常値を与える
        assert(!isIPAddr("."));
        assert(!isIPAddr(","));
        assert(!isIPAddr("2147483647"));
        
        // コロンを挿入する位置を変更する
        assert(!isIPAddr("12.3..4"));
        assert(!isIPAddr("..1.2.3.4"));
        assert(!isIPAddr("1.2.3.4.."));
        
        // 誤った桁数を与える
        assert(!isIPAddr("1"));
        assert(!isIPAddr("1.2"));
        assert(!isIPAddr("1.2.3"));
        assert(!isIPAddr("1.2.3.4.5"));
        
        // 範囲外の値を与える
        assert(!isIPAddr("-1.-1.-1.-1"));
        assert(!isIPAddr("256.256.256.256"));
        assert(!isIPAddr("2147483647.2147483647.2147483647.2147483647"));  // __int32の+側最大値
    }
};

const static Test Test;

intよりint32_tを使おう

型エイリアス宣言を用いて、クラス側でクラスで使用する型を決定することで、ソフトウェアの堅牢性を向上させる

型エイリアスは以前に定義された型を参照する名前です (typedef と同様です)

以下のように型を別名で宣言します。 using TestData = std::array<int, 10>;

【修正前】 [Main.cpp]

#include <iostream>
#include "Test.hpp"

int main(void)
{
    std::array<int, 10> testData{1,2,3,4,5,6,7,8,9,10};
    
    Test test{testData};
    
    for (const auto& i : test.GetMultiplyData(5))
    {
        std::cout << i << std::endl;
    }
}

[Test.hpp]

#include <array>
#include <vector>
#include <algorithm>    // std::transform

class Test
{

private:
    std::array<int,10> m_inputData;

public:
    Test(std::array<int,10> inputData) : m_inputData{inputData}
    {}
    
    // 倍数したデータをvectorで取得する
    std::vector<int> GetMultiplyData(int times)
    {
        std::vector<int> resultData;
        
        std::transform(std::begin(m_inputData), std::end(m_inputData), std::back_inserter(resultData), [times](int data){return data * times;});
        
        return resultData;
    }
};

【修正後】 [Main.cpp]

#include <iostream>
#include "Test.hpp"

int main(void)
{
    Test::TestData testData{1,2,3,4,5,6,7,8,9,10};
    
    Test test{testData};
    
    for (const auto& i : test.GetMultiplyData(5))
    {
        std::cout << i << std::endl;
    }
}

[Test.hpp]

#include <array>
#include <vector>
#include <algorithm>    // std::transform

class Test
{
    // クラス側でクラスで使用する型を決定することで、ソフトウェアの堅牢性を向上させる
public:
    using TestData = std::array<int, 10>;
    
private:
    TestData m_inputData;

public:
    Test(TestData inputData) : m_inputData{inputData}
    {}
    
    // 倍数したデータをvectorで取得する
    std::vector<int> GetMultiplyData(int times)
    {
        std::vector<int> resultData;
        
        std::transform(std::begin(m_inputData), std::end(m_inputData), std::back_inserter(resultData), [times](int data){return data * times;});
        
        return resultData;
    }
};

リンク

pandasを使って株価情報を取得しましょう

はじめに

pandasを使うと、webページの表(tableタグ)のスクレイピングが簡単にできます。

環境

  • Windows version : 1903 (Windows 10 Home)
  • conda version : 4.7.12
  • conda-build version : 3.18.8
  • python version : 3.7.3.final.0
  • selenium : 3.141.0

手順

(1) 必要なモジュールをインストールします

conda install pandas

conda install beautifulsoup4

conda install html5lib

(2) コードをjupyter notebookに入力します

import pandas as pd
tables = pd.read_html('https://info.finance.yahoo.co.jp/ranking/?kd=4', flavor='bs4', header=0)

first_five_data = tables[0].head()
last_five_data = tables[0].drop(len(tables[0])-1).tail()

display(first_five_data.append(last_five_data))

f:id:nprogram:20191022231420p:plain

コード説明

pandasのread_htmlメソッドを用いて、webページの表(tableタグ)のスクレイピングを行います。

上記のメソッドを呼び出すとページ内の表をすべて取得しDataFrameのリストとして返します。今回の場合は表が1つしかないため、tables[0]で表データを参照できます

read_html型の戻り値は、リスト(<class 'list'>)を作成します。引数はの意味は以下のとおりです。

# flavor
## 解析に使用するパッケージ種別
# 引数 : header
# 表タイトルに指定する行番号
tables = pd.read_html('https://info.finance.yahoo.co.jp/ranking/?kd=4', flavor='bs4', header=0)

以下のコードで表の最初の5行分のデータを表示します。

display(tables[0].head())

f:id:nprogram:20191022223757p:plain

以下のコードで表の最後の5行分のデータを表示します。しかし、最終行にゴミデータが入っています。

display(tables[0].tail())

f:id:nprogram:20191022223923p:plain

上記のように最終行にゴミがある場合は、dropメソッドを呼び出し、最後の行を指定して削除した後に、最後の5行を取り出せば問題ありません。

tables[0].drop(50).tail()

f:id:nprogram:20191022230954p:plain

上記のコードは以下でも代用できます

display(tables[0].drop(len(tables[0])-1).tail())

csvファイルに保存・csvファイルから読み込み

次に取得した情報をcsvファイルに保存しましょう。csvファイルに保存することで、取得したデータを永続化できます。

前回で、dropメソッドで不要な行を削除しましたが、今回は別な方法で削除します。

tables[0][:-1].to_csv("./stock_data_list.csv")

df_csv = pd.read_csv('./stock_data_list.csv', index_col=0)
display(df_csv.head().append(df_csv.tail()))

以下のコードで、一番目のテーブルの最後の行を除去して、csvファイルとして保存します。

[:-1]で最初の行から最後から一行を意味します。ここで、0行目はタイトル行です。

次のread_csv関数で、0行目をインデックス行(タイトル行)(index_col=0)と判断して、csvファイルをリードして、DataFrameに変換します。

Yahooサイトから、株価情報を取得します

Yahooサイトはスクレイピングでの高速アクセスを禁止しています。

そのため、高速アクセスを禁止するため、一度の処理の間にはウェイト(スリープ)を必ず入れたいと思います

参考リンク

あとがき

株価の予想をAIを用いて実行できるようにしたいです。まずは、スクレイピング(ネットから情報を取得)をマスターしたいと思います。

ただ、スクレイピングは国内サイトの場合、規制が厳しいです。海外のサイトから株価情報を取得することも考慮したいと思います。

機械学習で未来を予測する - scikit-learn の決定木で未来の株価を予測 - Qiita

pythonで東証から株価をAPI取得して、データをChartに表示させる - Qiita

Anaconda3で作成した仮想環境上で、Seleniumを使ってブラウザを操作する [Windows]

はじめに

PythonとSeleniumを使ってブラウザを操作する作業を行いたいと思います。

Pythonのバージョンやインストールするモジュールのバージョンによって、プログラム動作が変わる可能性があります。

そのため、仮想環境上でPythonやSeleniumのモジュールをインストールして実行したいと思います。

環境

  • Windows version : 1903 (Windows 10 Home)
  • conda version : 4.7.12
  • conda-build version : 3.18.8
  • python version : 3.7.3.final.0
  • selenium : 3.141.0

手順

(1) Anaconda3をインストールする

(2) Anaconda3で仮想環境を作成する

Anaconda3のGUIから行う方法とAnaconda Promptから行う方法があります。 (3) Pythonをインストールする

(4) Pythonのインストールを確認する

(5) Seleniumをインストールする

上記の手順(3) - (5) は、Anaconda Promptでは以下の手順で実施可能です。

  • conda create --name myenv python=3.7

    • 仮想環境作成 (仮想環境名をmyenv、使用Python versionを3.7にする) (※Seleniumを使う場合はPython3.7が必要と思われます)
  • activate [仮想環境名]

    • 仮想環境を切り替える
  • conda install selenium

    • Seleniumをインストールする

(6) WebDriverをインストールする ダウンロードしてきた実行ファイル(chromedriver.exe)をPC内の任意の場所に保存してください。(本コード例ではF:/tool/ChromeDriver_77_0_3865_40/chromedriver.exeに保存)

(7) jupyter notebook上でコードを動作させる

早速ブラウザを自動操作しましょう

以下のコードをjupyter notebook上で動作させます。

コード例

from selenium import webdriver
 
driver = webdriver.Chrome("F:/tool/ChromeDriver_77_0_3865_40/chromedriver.exe")
driver.get("http://www.yahoo.co.jp")
 
elem_search_word = driver.find_element_by_id("srchtxt")
elem_search_word.send_keys("test")

elem_search_btn = driver.find_element_by_id("srchbtn")
elem_search_btn.click()

コード説明

  • 1行目:webdriverモジュールをインポートしています
  • 3行目:WebDriverのパスを指定してChromeを起動します
  • 4行目:Yahooのページをブラウザで開きます
  • 6行目は入力したいテキストフィールドの要素を取得しています。このページのHTMLソースを見ると、検索語を入力するテキストフィールドのIDがsrchtxtということがわかるので、find_element_by_idメソッドを使ってこの要素を取得します。
  • 7行目は取得した要素にsend_keysメソッドを使ってtestという文字を入力しています。同様にして8行目は検索ボタン要素の取得、9行目は検索ボタンをクリックします

自動テストイメージ

f:id:nprogram:20190918172354p:plain

参考

リンク

あとがき

比較的、簡単にAnaconda3で作成した仮想環境上で、Seleniumを使ってブラウザを操作できました。

Anaconda環境でパッケージの管理を行う場合は、可能な限りconda installコマンドでパッケージをインストールすることをお勧めします。

pip installでもパッケージのインストールは可能ですが、Anaconda環境と競合する可能性があるため。

template関数でsplit関数を実装する (空白文字列区切り)

template関数でsplit関数を実装する (空白文字列区切り)

template関数を使用することで、複数の型を同じ関数で扱うことができるようになります。

使い方

関数呼び出し時には、通常の関数呼び出しとは異なり、型を指定する必要があります。

const std::vector<std::string> result1 = SplitWords<std::string>(input1);

コード例1

#include <iostream>
#include <vector>
#include <sstream>


template <class T>
std::vector<T> SplitWords(const std::string &line)
{
    std::vector<T> words;
    std::istringstream ss{line};
    std::string buf;
    
    T t;
    int count = 0;
    
    while(ss >> t)
    {
        words.emplace_back(t);
        count++;
    }
    
    std::cout << "Loop count : " << count << std::endl;
    
    return words;
}

int main (void)
{
    const std::string input1 = "abc\n def\n\n ghi\n";
    const std::vector<std::string> result1 = SplitWords<std::string>(input1);
    std::cout << "result 1 size : " << result1.size() << std::endl;
    
    for (const auto & item : result1)
    {
        std::cout << item << std::endl;
    }

    const std::string input2 = "12.3\n 45.6\n 78.9\n";
    const std::vector<double> result2 = SplitWords<double>(input2);
    std::cout << "result 2 size : " << result2.size() << std::endl;
    
    for (const auto & item : result2)
    {
        std::cout << item << std::endl;
    }
    
    const std::string input3 = "1\n 222\n\n 333\n 44\n\n\n";
    const std::vector<double> result3 = SplitWords<double>(input3);
    std::cout << "result 3 size : " << result3.size() << std::endl;
    
    for (const auto & item : result3)
    {
        std::cout << item << std::endl;
    }
    
    EXIT_SUCCESS;
}
Loop count : 3
result 1 size : 3
abc
def
ghi
Loop count : 3
result 2 size : 3
12.3
45.6
78.9
Loop count : 4
result 3 size : 4
1
222
333
44

コード例2(istringstreamのeofメソッドを使った実装)

ifs.eof()の場合は、入力文字列に改行コードがある場合は、ループカウンタが余計に一回実行された。 この方法では、行中の改行文字列'\n'は正しく認識できないため注意してください。

#include <iostream>
#include <vector>
#include <sstream>


template <class T>
std::vector<T> SplitWords(const std::string &line)
{
    std::vector<T> words;
    std::istringstream ss{line};
    std::string buf;
    
    int count = 0;
    
    while(!ss.eof())
    {
        T t;
        ss >> t;
        words.emplace_back(t);
        count++;
    }
    
    std::cout << "Loop count : " << count << std::endl;
    
    return words;
}

int main (void)
{
    const std::string input1 = "abc\n def\n\n ghi\n";
    const std::vector<std::string> result1 = SplitWords<std::string>(input1);
    std::cout << "result 1 size : " << result1.size() << std::endl;
    
    for (const auto & item : result1)
    {
        std::cout << item << std::endl;
    }

    const std::string input2 = "12.3\n 45.6\n 78.9\n";
    const std::vector<double> result2 = SplitWords<double>(input2);
    std::cout << "result 2 size : " << result2.size() << std::endl;
    
    for (const auto & item : result2)
    {
        std::cout << item << std::endl;
    }
    
    const std::string input3 = "1\n 222\n\n 333\n 44\n\n\n";
    const std::vector<double> result3 = SplitWords<double>(input3);
    std::cout << "result 3 size : " << result3.size() << std::endl;
    
    for (const auto & item : result3)
    {
        std::cout << item << std::endl;
    }
    
    EXIT_SUCCESS;
}
Loop count : 4
result 1 size : 4
abc
def
ghi

Loop count : 4
result 2 size : 4
12.3
45.6
78.9
78.9
Loop count : 5
result 3 size : 5
1
222
333
44
44

リンク

*【C/C++】 streamクラスのeofメンバ - http://murakan.cocolog-nifty.com/blog/2009/12/cstreameof-7401.html

Modern_C++_leet文字列変換

#include <string>
#include <iostream>
#include <map>
#include <algorithm> // transform
#include <vector>
#include <numeric> //iota, accumulate

// leetマップ変数 (グローバル変数)
std::map<char, char> leet {
    {'A', '4'},
    {'E', '3'},
    {'G', '6'},
    {'I', '1'},
    {'O', '0'},
    {'S', '5'},
    {'Z', '2'}
};
        
/**
 * @fn
 *  入力文字列をLeet文字列に変換する関数
 * @brief 要約説明
 * @param input 入力文字列
 * @return  Leet変換後の文字列
 * @detail 変換規則はグローバル変数に記載したleetマップ変数に記載
 */
std::string ConvertLeetString(std::string input)
{
    
    std::vector<char> result;
    
    // Map(変換)
    // 入力文字列を1文字ずつLeet文字列に変換する。変換結果はcharを格納するvectorに格納する
    std::transform(input.begin(), input.end(), std::back_inserter(result),
    [=](char item) -> char 
    {
        std::map<char, char>::iterator it;
        
        it = leet.find(item);

        if (it != leet.end())
        {
            // 探索文字(item)と等価なキーがleetマップ変数にある場合はそのvalue値を返す
            return leet[item];
        }
        return item;
    });
    
    // Accumulate(集約)
    return  std::accumulate(result.begin(), result.end(), std::string());
}

int main(void)
{
    // インプット文字列を取得
    std::string input;
    std::cin >> input;
    
    // ConvetLeetStringメソッドを保有するUtilityクラスにしたほうがよかった
    std::string result = ConvertLeetString(input);
    
    std::cout << result << std::endl;

    return EXIT_SUCCESS;
}