投稿者「sasaki」のアーカイブ

WebPack + Vueで、「Element」導入時にハマった点

前提

以下環境にて確認

要素 バージョン
debian 8.6
node 8.2.1
npm 5.3.0

概要

Webpackを用いてVueアプリを開発している中で、Vue2.0用のUIライブラリであるElementを導入しようとしたら、Webpack周りでハマったので導入手順と解決手順を備忘録に残す。

備忘録が主な目的なので、Webpack,Vue,Elementなどに関する説明は割愛

Elementの導入

npmでインストールする

npm install element-ui -s

エントリポイントでElement関連をimport

import ElementUI  from 'element-ui'
import locale     from 'element-ui/lib/locale/lang/ja'
import 'element-ui/lib/theme-default/index.css'

Elementの利用を宣言。localeを指定することで日本語が有効になるらしい

Vue.use(ElementUI, {locale});

エラー① Element内のCSSファイルが解決できない

この状態でwebpackを回すと、以下のエラーが出る

ERROR in ./node_modules/element-ui/lib/theme-default/index.css
Module parse failed: /root/degulog/node_modules/element-ui/lib/theme-default/index.css Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type.

どうやらElementに付随するCSSファイルをバンドルできないらしい。

原因は単純に、これまでscssだけ使ってたのでcssに対するローダーをwebpackで設定していなかったため。
なのでwebpack.config.jsのloadersに以下を追加すれば解決

{ test: /\.css$/, loader: 'style-loader!css-loader' },

エラー② Element内のwoffファイルが解決できない

続けて以下のエラーが発生した

ERROR in ./node_modules/element-ui/lib/theme-default/fonts/element-icons.woff?t=1472440741
Module parse failed: /root/degulog/node_modules/element-ui/lib/theme-default/fonts/element-icons.woff?t=1472440741 Unexpected character '' (1:4)
You may need an appropriate loader to handle this file type.

問題の本筋は前項と同様。woffを解決するローダが設定されていなかった。

url-loaderを適用すれば良いが、まずそれが入っていなかたのでインストールする。

$ npm install url-loader

で、webpack.config.jsonのloadersに以下を追加。woff意外にも対応できるように汎用的に記述。

{
  test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
  loader: 'url-loader'
},

動作確認

依存解決も上手く行って、以下のようにElementが利用できた。
例はDatetimePickerコンポーネント

<el-date-picker
  v-model="value1"
  type="datetime"
  placeholder="Select date and time">
</el-date-picker>

[Python3] scikit-learnによる機械学習4: 学習が完了したモデルを永続化する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

前回、自動車の情報から価格を予測する学習モデルを構築し、それを評価した。

しかし、予測を行おうとスクリプトを実行するたびに学習モデルを構築しているのは無駄でしかないので、一度作成した学習モデルをシリアライズし、使いまわせるようにする。以降では、シリアライズされた学習モデルをデシリアライズして予測を行うことで、学習作業を省略することができる。

シリアライズ

今回は、scikit-learnのjoblibを用いてシリアライズする。以下のimport文が必要

from sklearn.externals import joblib

学習が完了したモデル(clf)を、cars.plkのファイル名で保存する場合は以下のようにする

joblib.dump(clf, 'cars.pkl')

実行するとcars.plkが生成される。可読性のあるものではないので中身は気にしなくて良い

$ file cars.pkl
cars.pkl: 8086 relocatable (Microsoft)

デシリアライズ

シリアライズされた学習モデルは、load関数でデシリアライズすることができる。以降ではclfを用いれば従来通りpredictメソッドを使って予測を行うことができる

clf = joblib.load('cars.pkl');

ソースコード

自動車の情報と価格をランダムフォレストで学習し、シリアライズするスクリプト

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing
from sklearn.externals import joblib

# CSVファイルを読み込む
df = pd.read_csv('cars.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習する
clf = RandomForestClassifier()
clf.fit(data, label)

# 学習結果をシリアライズ
joblib.dump(clf, 'cars.pkl')

シリアライズされた学習モデルをデシリアライズし、予測、評価するスクリプト

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing
from sklearn.externals import joblib

# CSVファイルを読み込む
df = pd.read_csv('cars.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習モデルをデシリアライズ
clf = joblib.load('cars.pkl');

# 評価する
predict = clf.predict(data)
rate_sum = 0
for i in range(len(label)):
  t = int(label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(label))

[Python3] scikit-learnによる機械学習3: 自動車の情報から価格を予測する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

scikit-learnによる機械学習がなんたるかがほんの少しわかってきたので、足し算などという実用性皆無な例でなく、もう少し実用的な使い方をしてみる。

今回は、自動車の各種情報と、その自動車の価格を学習させ、新たに情報を与えると価格を予測してくれるスクリプトを作成する。なお、学習データにはMicrosoftのクラウド機械学習サービスである、Azure Maschine Learningより、そのチュートリアルで使用されているサンプルデータを拝借する。

学習用データについて

今回使用する学習データは以下のようなCSVファイル。

画像をだとわかりづらいが、以下の列で構成されている。列の内容は列名から概ね察せるが、最終列にあるprice列の値(自動車の価格)を、他の列の情報を元に予測するということがわかれば問題ない。

  • symboling
  • normalized-losses
  • make
  • fuel-type
  • aspiration
  • num-of-doors
  • body-style
  • drive-wheels
  • engine-location
  • wheel-base
  • length
  • width
  • height
  • curb-weight
  • engine-type
  • num-of-cylinders
  • engine-size
  • fuel-system
  • bore
  • stroke
  • compression-ratio
  • horsepower
  • peak-rpm
  • city-mpg
  • highway-mpg
  • price

学習用データはいつもどおりPandasを用いてDataFrameに展開する

df = pd.read_csv('cars.csv')

無効な行/列の削除

CSVのスクリーンショットを見てもらうとわかるが、全体的に値が”?”になっているセルが散らばっている。これは情報が無いという意味を表していると思うので、それらを取り除いていく。

まず、’normalized-losses'(2列目)は、列全体で”?”が多いので、思い切ってこの列自体を削除する。列についではdel文で削除できるので削除してしまう。

del df['normalized-losses']

次に、他の”?”についてだが、それらについては一部の行のみ”?”になっているだけなので、列全体の削除はせず、”?”が一つでも含まれている行を削除するという方針にする。

“?”が存在する列は’num-of-doors’, ‘price’, ‘bore’, ‘stroke’, ‘horsepower’, ‘peak-rpm’の6列なので、これらのいずれの列にも”?”が含まれていない行のみ取り出して再代入するという処理を行う。PandasのDataFrameでは、行の抽出に、列を対象とした条件式を指定することができるので、”?”が含まれている行を排除できる。

for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

おそらくPandasを使ってもっとスマートに同じことができると思うので、良い方法があったらご教授お願いします。

文字列を数値化する

今回も前回までと同様、scikit-learnの学習モデルのfitメソッドを用いて学習を行うが、fitメソッドはfloat型の値しか受け付けない。しかし、今回のCSVには文字列も多く含んでいるので、それを数値化する必要がある。

そこで今回は、scikit-learnのLabelEncoderクラスを用いて各文字列を数値に置き換えることで、学習可能のデータに変換する。変換が必要な列は、’make’, ‘fuel-type’, ‘aspiration’, ‘num-of-doors’, ‘body-style’, ‘drive-wheels’, ‘engine-location’, ‘engine-type’, ‘num-of-cylinders’, ‘fuel-system’の10列なので、それぞれに対して、以下のようにLabelEncoderオブジェクトのfit_transformメソッドを適用することで文字列を数値に変換できる。

le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

例として、body-style列の、変換前後を出力すると以下のようになり、文字列が数値に変換されていることがわかる

変換前

0      convertible
1      convertible
2        hatchback
3            sedan
4            sedan
5            sedan
6            sedan
7            wagon
8            sedan
(以下略)

変換後

0      0
1      0
2      2
3      3
4      3
5      3
6      3
7      4
8      3
(以下略)

わざわざ変換する列名を配列でツラツラ書かなくてももっとスマートに書ける方法がありそうです。こちらも良い方法ありましたらご教授お願いします。

学習用データとテストデータの分割

ここは前回一緒なのでコードだけ載せて割愛

train_data, test_data, train_label, test_label = train_test_split(data, label)

ランダムフォレストで学習させる

前回までは線形回帰アルゴリズムを用いて足し算の学習を行わせたが、今回は別の学習アルゴリズムを使うことにする。どのアルゴリズムをどのような場合に使えば良いかはまだ調査中だが、とりあえず線形回帰とは異なるアルゴリズムを使う場合の例として使ってみる。

ランダムフォレスト(英: random forest, randomized trees)は、2001年に Leo Breiman によって提案された[1]機械学習のアルゴリズムであり、分類、回帰、クラスタリングに用いられる。決定木を弱学習器とする集団学習アルゴリズムであり、この名称は、ランダムサンプリングされたトレーニングデータによって学習した多数の決定木を使用することによる。対象によっては、同じく集団学習を用いるブースティングよりも有効とされる。(Wikipedia引用)

といっても、scikit-learnではどの学習アルゴリズムを使おうと、その使い方が共通化されているので、線形回帰の時とほとんど同じコードで動かすことができる。
必要なモジュールをインポートした状態で以下のようにすることランダムフォレストで学習を実行できる。

clf = RandomForestClassifier()
clf.fit(train_data, train_label)

前回までとの違いは、clf変数にどの学習アルゴリズムのオブジェクトを代入するかの違いだけである。どの学習アルゴリズムについてもfitメソッドを使うことができるので、同じように利用することができる。

評価する

学習用データとテスト用データに分割した際の、テスト用データの方を用いて学習モデルの評価を行う。これまで通り、predictメソッドを用いて各自動車の価格を予想する。

predict = clf.predict(test_data)

とりあえず予測した結果と正解を並べて出力してみる。

for i in range(len(test_label)):
  p = int(predict[i])
  t = int(test_label.iloc[i])
  print(p, t)

どうだろうか、かなり近い価格を出せているのではないだろうか。(予測値 正解値)

7898 6918
5572 5572
12764 12964
10198 11694
7299 8249
8949 9549
17450 15250
7198 8358
23875 17710
10245 8495
8238 8058
6338 6488
31600 28248
11199 8449
10295 6785
34028 32528
15985 12940
7299 6849
(以下略)

ただこれだと、どのぐらいの精度が出てるかわかりづらいので、正解に何%近いかを計算して出力するように修正する

for i in range(len(test_label)):
  p = int(predict[i])
  t = int(test_label.iloc[i])
  rate = int(min(t, p) / max(t, p) * 100)
  print(rate)

それなりの精度であることがわかる

$ python cars.py
70
93
79
87
75
97
91
84
90
97
88
89
95
90
100

さらに、個々の精度の平均値を計算し、全体の精度を出力するようにする

for i in range(len(test_label)):
  t = int(test_label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(test_label))

学習データとテストデータの組み合わせによってムラがあるので、何度か実行して確認してみると、概ね87%前後の精度が得られた

$ python cars.py
86.6734693877551
$ python cars.py
87.46938775510205
$ python cars.py
88.59183673469387
$ python cars.py
86.55102040816327
$ python cars.py
86.04081632653062

さらに、アルゴリズムをランダムフォレストから、線形回帰に変更して再評価してみる。変数clfに代入する学習モデルのオブジェクトを、ランダムフォレストから線形回帰に変更するだけで良い

clf = linear_model.LinearRegression()

同じように何度か実行してみると、83%程度と、ランダムフォレストより微妙に精度が下がり、アルゴリズムによって結果が変わることを実感した。

$ python cars.py
82.40816326530613
$ python cars.py
83.79591836734694
$ python cars.py
83.53061224489795
$ python cars.py
85.65306122448979
$ python cars.py
82.46938775510205

なお、学習モデルの評価方法についても、scikit-learnに様々な機能があるが、今回は意図的に自前で計算するようにして感覚を掴んだ。

ソースコード

Github

#
# 車の製品情報と価格を学習し、価格を予測させる
#
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing

# CSVファイルを読み込む
df = pd.read_csv('cars.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習用データとテストデータにわける
train_data, test_data, train_label, test_label = train_test_split(data, label)

# 学習する
clf = RandomForestClassifier()
clf.fit(train_data, train_label)

# 評価する
predict = clf.predict(test_data)
rate_sum = 0

for i in range(len(test_label)):
  t = int(test_label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(test_label))

[Python3] scikit-learnによる機械学習2: 足し算学習モデルを評価する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

前回実装した、足し算を線形回帰学習するモデルがどのような学習をしたかを評価する

線形回帰学習で算出した式を確認する

線形回帰モデルは、目的変数Yを求めるために、説明変数X1,X2,X3…それぞれに偏回帰係数b1,b2,b3…を掛け、最後に切片(誤差)eを付与する。式にすると以下のようになる。

Y = X1*b1 + X2*b2 + X3*b3 … + e

今回の単純な足し算モデルは、結果Yに対して、入力値が2種類なのでX1とX2が存在する。
なので理論上は以下の式が求められれば正確といえる

Y = X1*1 + X2*1 + 0

では各b及びeがどのような値になったか確認する。

学習が完了したモデルのcoef_,intercept_にそれぞれ偏回帰係数と切片が格納されているのでそれを確認する。

print("偏回帰係数1: ", float(model.coef_[0][0]))
print("偏回帰係数2: ", float(model.coef_[0][0]))
print("切片: ", float(model.intercept_))

すると以下のような結果が得られたので

偏回帰係数1:  1.0000000000000002
偏回帰係数2:  1.0000000000000002
切片:  -4.440892098500626e-16

この学習モデルが予測した線形回帰は以下の式で表される

Y = X1*1.0000000000000002 + X2*1.0000000000000002 + -4.440892098500626e-16

小数計算ならともかく、今回は結果をint関数で整数化しているので、このレベルの誤差なら数値が極大出ない限りは誤った答えが出ないことが予想できる。

精度を確認する

前回は標準入力から式を入力して、解答を標準出力していたが、膨大なデータをテストする場合に一々手入力していたらキリがないので、今回は自動で100000件のテストデータを生成して、それぞれについて正しい答えを求められているかを評価する。

まずは以下のような、1~1000のランダムな数値を二種及びその和のセットを戻す関数を実装した

def get_rand_data():
    max = 1000
    a = random.randint(1, max)
    b = random.randint(1, max)
    return [a, b, a + b]

これを、以下のように内包記法を用いて呼び出すことで10000セット、PandasのDataFrame形式で取得する

rand_data = DataFrame([get_rand_data() for i in range(100000)])

ランダムデータの1,2列目が入力になるので、1,2列目を取り出して学習させる

pre = model.predict(rand_data[[0, 1]])

変数preには、1万件分のデータセットに対する和の予測値が格納されているので、それらを順に確認して、本来の解と一致しているかを比較検証する。一致件数と総数から、正解率を算出する。このあたりを勝手に評価してくれるモジュールがあったと思うが、今回は初見なので自前で実装する。

correct_num = 0
for i in range(len(rand_data)):
  if rand_data[2][i] == int(pre[i]):
    correct_num += 1

print("正解率 = ", correct_num / len(rand_data))

すると、以下のように正解率1.0と出力されるので、最低でも1~1000の整数数二種の和は正確に求められていることがわかる。

正解率 =  1.0

備考

  • もちろん計算の桁数が大きくなればなるほど誤差が影響して正解率は落ちる
  • そもそも明確な解のある足し算を機械学習させるのはお門違いだが今回は簡易的に理解できる例として使用した

[Python3] scikit-learnによる機械学習1: 足し算を学習させる

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

機械学習とは

機械学習(きかいがくしゅう、英: machine learning)とは、人工知能における研究課題の一つで、人間が自然に行っている学習能力と同様の機能をコンピュータで実現しようとする技術・手法のことである。(wikipedia引用)

機械学習とは、言語やゲームなどをはじめとした人間の様々な知的活動の中で、人間が自然と行っているパターン認識や経験則を導き出したりするような活動を、コンピュータを使って実現するための技術や理論、またはソフトウェアなどの総称である。(IT用語時点引用)

まぁ人間っぽい学習をコンピュータにもやらせましょうというお話。
本シリーズでは機械学習の定義とかにあまり拘らずに、一般的に考えられる機械学習を自分でもやってみようという緩い方向で進める。

scikit-learnとは

Pythonにおける事実上標準と言っても過言ではない、機械学習のためのライブラリ。
機械学習と言えばTensorFlowというワードの知名度が高いが、TensorFlowは学習コストも高く、ディープラーニングまで幅広くできるので、本シリーズではお手軽に機械学習ができるsckit-learnを用いる

scikit-learnのインストール

sckit-learnはPythonモジュールとして公開されているので、pipなどを用いてインストールする。pipはPythonをインストールすると標準で入ってる可能性が高い。入っていない場合は別途導入する。あるいはAnacondaなどの別な手段を適用する。

$ pip install scikit-learn

Pandas/Numpyについて

Pythonで機械学習を行う上で切っても切れないライブラリに、以下の2つがある

Pandas
データ解析を支援するためのライブラリ。汎用的なデータ構造の構築や、データ内の演算などをサポートする。

Numpy
科学計算用のライブラリ。特に行列に対する計算は普通にPythonで行うより圧倒的に早い。

以上より、これらのライブラリも初めに導入しておく。

$ pip install numpy pandas

足し算の機械学習について

本記事では、機械学習の第一歩として、sckit-learnの線形回帰アルゴリズムを用いて、足し算を学習させる。

流れとしては以下のようになる

  • 足し算の式と、その答えの組み合わせをいくつか用意する
  • sckit-learnに式と答えの組み合わせを学習させ、学習モデルを生成
  • 標準入力から二種類の整数を学習モデルに与え、予測結果を標準出力する

なお、2数の足し算とは当然演算によって求められるものなので、機械学習の利用法としては適していない。
が、ここでは雰囲気を掴むためにこのテーマにした。

教師あり学習について

教師あり学習は、データの組み合わせと、それに対する答えのセットを複数セット学習させることで、データの組み合わせを与えただけでその答えを予測してもらおうという学習モデル。

今回は足し算を学習させるので、データの組み合わせが式で、答えが和と考えれば良い。

線形回帰について

Wikipediaをそのまま掲載すると以下の通り。なるほど。

何を言ってるかよくわからないが、足し算に置き換えれば非常にシンプルになる。

目的変数Yは、足し算における和である。
説明変数X1,X2は、足し算のそれぞれの数値である。

この場合、線形回帰直線は、Y = X1 + X2の形になるので、
この式を予測することが線形回帰学習である(あってるか極めて怪しい)

実装

モジュールのimport

今回は、Pandasによる学習用データの入力と、scikit-learnの線形回帰モデルを用いるので、以下のimport文を記述する。Numpyは今回出番なし。

from pandas import DataFrame
from sklearn import linear_model

学習データの用意

学習データは、PandasのDataFrameの形式で用意する。DataFrameはデータの集合を二次元で表現する。
sckit-learnでは、学習データとその答えを別々に渡す必要があるので、それぞれformulas,answersに代入する。

formulas = DataFrame([
    [0, 0],
    [0, 1],
    [0, 2],
    [1, 0],
    [1, 1],
    [1, 2],
    [2, 0],
    [2, 1],
    [2, 2]
])
answers = DataFrame([0, 1, 2, 1, 2, 3, 2, 3, 4])

コードを見ての通り、それぞれ、0 + 0 = 0, 0 + 1 = 1, 0 + 2 = 2 … のセットを用意している。
ちなみにこの段階でformulasを標準出力すると以下のようになり、DataFrameがどのようにデータ集合を持っているかのイメージが湧く

   0  1
0  0  0
1  0  1
2  0  2
3  1  0
4  1  1
5  1  2
6  2  0
7  2  1
8  2  2

学習データを学習させる

importしたlinear_modelを用いて、先程用意した学習データを予測する。
LinearRegressionメソッドで学習モデルを取得できるので、それに対してfitメソッドで学習データを割り当て、学習を開始する。

model = linear_model.LinearRegression()
model.fit(formulas, answers)

学習結果を確認する

学習が完了したmodelに対して、predictメソッドを用いることで、新たなデータに対する答えの予測値を取得することができる。
この場合、必ず入力、出力ともに配列である必要があるので注意。

例えば以下では、10 + 20の予測結果を取得する。

predected_answer = model.predict([[10, 20]])

これを用いて、標準入力から得られた2数の和の予測結果を出力し、それを繰り返すコードを以下に示す

while True:

    print('> ', end='')
    x, y = list(map(lambda x: int(x), input().split(' ')))

    predected_answer = model.predict([[x, y]])
    print("{0} + {1} = {2}".format(x, y, int(predected_answer[0][0])))

標準入力と標準出力で小難しいことをやってるように見えるが、とりあえず入力した2数の和を予測して出力していることはわかる。

動作確認

これまでのコードを以下のように整理して実行する
コード全文 (Github)

 #
 # 2つの整数の足し算を機械学習し、任意の足し算を回答させる
 #
 from pandas import DataFrame
 from sklearn import linear_model

 # 足し算の例とその答えを用意する
 formulas = DataFrame([
     [0, 0],
     [0, 1],
     [0, 2],
     [1, 0],
     [1, 1],
     [1, 2],
     [2, 0],
     [2, 1],
     [2, 2]
 ])
 answers = DataFrame([0, 1, 2, 1, 2, 3, 2, 3, 4])

 # 式と答えを線形回帰学習させる
 model = linear_model.LinearRegression()
 model.fit(formulas, answers)
 print('学習完了')

 while True:

     # 標準入力から計算式を取得
     print('> ', end='')
     x, y = list(map(lambda x: int(x), input().split(' ')))

     # 学習モデルを用いて回答を取得し、標準出力
     predected_answer = model.predict([[x, y]])
     print("{0} + {1} = {2}".format(x, y, int(predected_answer[0][0])))

実行後、2秒程度で学習が完了する。コレに対して標準入力で適当に2数を入力すると、その予測結果が出力される。

$ python addition.py
学習完了
> 1 1
1 + 1 = 2
> 10 20
10 + 20 = 30
> 777 333
777 + 333 = 1110

良い感じに計算できているように見える。

本モデルの評価について次回行う。

[Ruby] Twitterから画像を自動収集する

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
ruby 2.4.1
gem 2.6.11
twitter_oauth 0.4.94

また、記事内で必要となるTwitterAPI用のトークンは既に取得済みであることを前提とする

概要

Twitter上に特定ワードと共に添付された画像ファイルを、任意の枚数自動でダウンロードをするCUIツールをRubyで実装したお話。
画像収集方法として他に以下を検討したが、それぞれ以下の理由で見送った。

  • Bing Image Search API
    — これまで画像収集でお世話になっていたAPIだが、Azureに統合(?)され、実質有料化したので見送り
  • Instagram API
    — イケると思って調べたが、どうやら他のユーザの投稿写真を取得するためには承認を得る必要がある模様
  • Tumblr API
    — Tumblrがよくわからなかったため
  • Google画像検索などのクローリング
    — Pythonでクローリングしようとも検討したが、あまり美しく無いと思ったので見送り

他にもいくつか調査したが、最終的に普段から利用しているTwitterAPIを使うことにした。TwitterのSearchAPIを用いると、ツイートに添付されている画像に関してもまとめて取得できるようになっているので、比較的簡単にできると踏んだため。

ライブラリのインストール

今回はRubyによるCUIツールの形式で実装する。RubyからTwitterAPIを叩くために、普段から利用しているGemライブラリであるtwitter_oauthを今回も使用する。

以下のコマンドを叩くか、あるいは同様のGemfileを作成する。

$ gem install twitter_oauth

TwitterAPI認証

Twitterクラスを実装し、TwitterAPI認証を行うauthメソッドを実装する。API認証をコンストラクタにしなかったのは、インスタンス生成のタイミングで認証が行われるのが気持ち悪いと個人的に感じたため。

authメソッドでは、前項でインストールしたtwitter_oauthモジュールに含まれるClientクラスのインスタンスを生成する。その際にトークンが必要になるので、今回は環境変数から読み込むようにした。

require 'twitter_oauth'
require 'net/http'
require 'uri'
class Twitter
  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end
end

検索ワードに合致する写真一覧を取得

特定ワードでツイートを検索し、その中に画像ファイルが添付されている場合にはURLを配列に追加。画像ファイルの総数が指定した枚数に達するまで、再帰的にツイートの検索を繰り返す。という一連の処理を行うsearch_picturesメソッドを実装する。

また、以下ページを参考に、TwitterAPIのレスポンスから添付画像のURLを取得するextract_pictures_from_tweetsメソッドを実装し、ツイート取得ごとに実行する。
GET search/tweets – ツイートを検索する

@@SLEEP_TIMEはAPI呼び出し毎に挟む待機時間。APIは時間あたりの利用回数が決まっているので、それに引っかからないように調整する。

#
# 検索ワードに合致する写真一覧を取得
#
def search_pictures(word, num = 10, opt = {})
  @twitter or self.auth
  params = {
    lang:        'ja',
    locale:      'ja',
    result_type: 'mixed',
    count:       200,
  }.merge(opt)
  puts "画像検索中(残り#{num}枚)"

  tweets = @twitter.search(word, params)['statuses']
  max_id = tweets[-1]['id']
  pictures = extract_pictures_from_tweets(tweets)

  if num <= pictures.count
    return pictures.take(num)
  else
    sleep @@SLEEP_TIME
    return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
  end
end
#
# TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
#
def extract_pictures_from_tweets(tweets)
  pictures = tweets.map do |t|
    if media = t['entities']['media']
      media.map {|m| m['media_url']}
    else
      []
    end
  end
  pictures.flatten.uniq
end

画像のダウンロード

前項のsearch_picturesメソッドを用いて取得した画像URL一覧を元に、指定したディレクトリにダウンロードするためのdownload_picturesメソッドを以下のように実装する。

こちらは単純に、個々の画像URLに対してNet::HTTPを用いてダウンロードしている。こちらもマナーとして、ダウンロード1件毎に@@SLEEP_TIME秒待機する。


#
# ツイッター上の画像をまとめてダウンロードする
#
def download_pictures(word, download_dir, num = 10)
  pictures = self.search_pictures(word, num)
  pictures.each_with_index do |picture, idx|
    filename = File.basename(picture)
    filepath = "#{download_dir}/#{filename}"
    open(filepath, 'wb') do |file|
      puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
      file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
    end
    sleep @@SLEEP_TIME
  end
end

動作確認

実行方法

irbなんかを起動して、

Twitter.new.download_pictures('#デグー', '/share/images', 20)

と実行することで、”#デグー”が含まれたツイートに含まれている画像20件をダウンロードする。もちろん’#デグー’が含まれたツイートに画像が添付されてない場合もあるので、写真数が20に達するまで再帰的にツイートを検索し続ける。

デモ

ソースコード

今回は諸事情でGithubにリポジトリを上げていないので、以下に全ソースコードを掲載する

require 'twitter_oauth'
require 'net/http'
require 'uri'

class Twitter2

  @@SLEEP_TIME = 1

  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end

  #
  # 検索ワードに合致するツイート一覧を取得
  #
  def search_pictures(word, num = 10, opt = {})
    @twitter or self.auth
    params = {
      lang:        'ja',
      locale:      'ja',
      result_type: 'mixed',
      count:       200,
    }.merge(opt)
    puts "画像検索中(残り#{num}枚)"

    tweets = @twitter.search(word, params)['statuses']
    max_id = tweets[-1]['id']
    pictures = extract_pictures_from_tweets(tweets)

    if num <= pictures.count
      return pictures.take(num)
    else
      sleep @@SLEEP_TIME
      return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
    end
  end

  #
  # ツイッター上の画像をまとめてダウンロードする
  #
  def download_pictures(word, download_dir, num = 10)
    pictures = self.search_pictures(word, num)
    pictures.each_with_index do |picture, idx|
      filename = File.basename(picture)
      filepath = "#{download_dir}/#{filename}"
      open(filepath, 'wb') do |file|
        puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
        file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
      end
      sleep @@SLEEP_TIME
    end
  end

  private

    #
    # TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
    #
    def extract_pictures_from_tweets(tweets)
      pictures = tweets.map do |t|
        if media = t['entities']['media']
          media.map {|m| m['media_url']}
        else
          []
        end
      end
      pictures.flatten.uniq
    end

end

[Python3] GithubAPI/TwitterAPIを用いて、最新のコミットログをツイートする

前提

以下の環境で実装、動作確認

要素 バージョン
debian 8.6
python 3.6.2

概要

GithubAPIを用いて、自身がGithub上のリモートリポジトリに対してpushした内容からコミットログを取得し、それに関する情報をTwitterAPIを用いてツイートしたお話。

本記事ではTwitterAPIを利用するためのコンシューマキー、アクセストークンは取得済みであることを前提としている。

同じことはGithubのWebhookを使ってできるしむしろ自然だが、その場合サーバーを用意する必要があるので、今回はあえてpull型で非効率なやり方を採用

動作イメージ

リモートリポジトリがGithub上にあるリポジトリに対して適当にコミット/プッシュ

$ git commit -m "コミットテスト"
[master e6bb9e4] コミットテスト
 1 file changed, 1 insertion(+)
$ git push origin master
Counting objects: 3, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.

今回実装したコマンドを実行する

$ python main.py

以下のように、リポジトリ名、コミットメッセージ、diffへのリンクがツイートされる

GithubAPIについて

GithubAPIは、名前の通りGithubの各種読み書きを行うためのAPI。
公開されているデータの取得に関しては認証無しで手軽に利用できる。

例えば、curlコマンドを用いて以下のURLに対してGETリクエストを送信すると

curl -i https://api.github.com/users/sa2knight/events

以下のように、該当ユーザのGithubでのイベントログを取得することができる

[
  {
    "id": "6516862394",
    "type": "PushEvent",
    "actor": {
      "id": 16274215,
      "login": "Sa2Knight",
      "display_login": "Sa2Knight",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Sa2Knight",
      "avatar_url": "https://avatars.githubusercontent.com/u/16274215?"
    },
    "repo": {
      "id": 96615971,
      "name": "Sa2Knight/degulog2",
      "url": "https://api.github.com/repos/Sa2Knight/degulog2"
    },
    "payload": {
      "push_id": 1946817475,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
      "before": "afd8b8647c2b810ceb731a74bad20c7388c19974",
      "commits": [
        {
          "sha": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
          "author": {
            "email": "shingo.sasaki.0529@gmail.com",
            "name": "shingo sasaki"
          },
          "message": "バックアップ追加",
          "distinct": true,
          "url": "https://api.github.com/repos/Sa2Knight/degulog2/commits/4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29"
        }
      ]
    },
    "public": true,
    "created_at": "2017-08-27T15:22:08Z"
  },
  {
    "id": "6516846556",
    "type": "PushEvent",
(以下省略)

ここで取得できるイベントは、Webで言う以下のような、そのユーザのリポジトリに対する操作全般の情報なので、今回はこのAPIを用いることにする

ライブラリの導入

今回はPython3を用いて実装するので、Pythonのパッケージ管理ツールであるpipを用いて、以下の二種類のライブラリを導入する。

  • requests
    — HTTPライブラリ
  • twitter
    — TwitterAPI用

TwitterAPIの利用には認証も絡んでくるので、専用のライブラリを利用する。GithubAPIは特定URLにGETするだけなのでHTTPライブラリで直接実行することに。

$ pip install requests
$ pip install twitter

ソースコード

最新版はこちら

import twitter
import requests
import json
import os

FILE_NAME      = "last_id"
EVENTS_URL     = "https://api.github.com/users/sa2knight/events"
REPOSITORY_URL = "https://api.github.com/repos"

def tweet(text):
  auth = twitter.OAuth(consumer_key=os.environ['TWITTER_CONSUMER_KEY'],
                       consumer_secret=os.environ['TWITTER_CONSUMER_SECRET'],
                       token=os.environ['TWITTER_ACCESS_TOKEN'],
                       token_secret=os.environ['TWITTER_ACCESS_SECRET'])
  t = twitter.Twitter(auth=auth)
  t.statuses.update(status=text)

def tweet_event(event):
  tweet_text = f"""
  @null
  Githubにコミットをプッシュしました。
  [{event['repository']}]
  「{event['commits'][0]['message']}」
  """.strip()
  if 1 < len(event['commits']):
    tweet_text += f"ほか{len(event['commits']) - 1}件"
  tweet_text += f"\n\n{event['commits'][0]['url']}"
  tweet(tweet_text)

def save_id(id):
  with open(FILE_NAME, 'w') as f:
    f.write(id)

def load_id():
  if os.path.exists(FILE_NAME):
    with open(FILE_NAME, 'r') as f:
      return f.readline()
  else:
    return ''

def is_new_id(id):
  return id != load_id()

def parse_commit_log(repo_name, commit):
  return {
    'url': f"https://github.com/{repo_name}/commit/{commit['sha']}",
    'message': commit['message']
  }

def get_repository_description(repo_name):
  url = f"{REPOSITORY_URL}/{repo_name}"
  response = requests.get(url)
  repository = json.loads(response.text)
  return repository['description']

def get_recent_push_event():
  response = requests.get(EVENTS_URL)
  events   = json.loads(response.text)
  recent_event = list(filter(lambda e: e['type'] == 'PushEvent', events))[0]
  repo_name    = recent_event['repo']['name']
  commits      = list(map(lambda c: parse_commit_log(repo_name, c), recent_event['payload']['commits']))
  return {
    'id':         recent_event['id'],
    'repository': get_repository_description(recent_event['repo']['name']),
    'commits':    commits,
  }

event = get_recent_push_event()
if is_new_id(event['id']):
  tweet_event(event)
  save_id(event['id'])
  print('ツイートを投稿しました')
else:
  print('ツイートは不要です')

スクリプトを実行すると概ね以下の手順でツイートされる
1. GithubAPIを叩いて、自身の最新のコミットを含んだイベントログを取得
2. 直近で取得したイベントIDをファイルに保存しておき、そのIDと比較、一致していた場合変更なしとして終了
3. イベントログを元にツイートするテキストを生成
4. TwitterAPIを用いてツイート

所感

  • 本スクリプトを15分おきぐらいにcronで実行すれば、概ねコミット内容がツイートされる
  • Webhookと違ってpull型になるので、リアルタイムにはできない
  • 単にGithubAPIが使いたくて遊んだだけで実用性はあんまり無い

GoogleChrome拡張でチャットワークを使いやすくする

概要

業務でもプライベートでも、GoogleChromeでチャットワークを使用しているが、微妙に使いづらい部分があったり、使わない機能のUIが気になったりしていたので、Chromeの拡張機能でどうにか改善できないかと調査し、改善したお話。

標準の問題点

  • 何か全体的にUIの主張がデカイ
  • メッセージ記法の入力が面倒というか忘れる
  • タスク機能はメモレベルでしか使ってないので不要なUIが多い

Chrome拡張の導入

Chatwork Extension

Chatwork Extensionは、Chatwork全体をいい感じに拡張してくれるが、拡張内容のオン/オフの切替や、任意のスクリプト/スタイルの適用を行えるので実質ユーザの望むままのカスタマイズを行うことができる。

導入後のデフォルト設定だけで、主に以下のような拡張が行われる

ルーム一覧のスマート表示

ルーム一覧の縦幅が狭まりスマートに表示され、検索窓を用いてルームを検索することができる。

コードのシンタックスハイライト化

コードにシンプルな装飾をしてくれるので、チャットワークでコードを共有するレベルでなら充分。

@でToの入力をアシスト

Twitterに近い感覚で、@を入力すると対象ユーザの一覧が表示され、To指定を容易に行えるようになるので、マウスレスな宛先指定ができるようになる。

その他の機能

その他の機能についてはこちらを参照

カスタムスタイル

Chatwork Extensionでは、拡張のオプションから、任意のスタイルを設定することができる。

現状、チャットワークのタスク機能はメモ程度にしか使っていないので、タスク機能周りUIを始めとした、不要な要素を片っ端から非表示にするスタイルを適用した。適用したスタイルは以下の通り

/* チャットルームごとのタスク数を非表示 */
li.roomListBadges__taskBadge {
  display: none !important;
}

/* ヘッダーメニューのタスク一覧ボタンを非表示 */
li#_openTaskWindow {
  display: none !important;
}

/* ヘッダーメニューのチャットワークアシスタントボタンを非表示 */
div.globalHeaderAssistant {
  display: none !important;
}

/* チャット一覧のメニューを非表示 */
div#_chatFilterMenu {
  height: 25px !important;
}
div#_chatFilterMenu > div {
  display: none !important;
}
div#_chatFilterMenu > ul {
  display: none !important;
}
div#_chatFilterMenu > input  {
  width: 97% !important;
  position: static !important;
}

/*タスクの期限を非表示に*/
ul#_subRoomTaskList .taskListItem__meta {
  display: none !important;
}

/* 自分のタスクのみ表示 を非表示 */
div#_subRoomMyTaskList {
  display: none !important;
}

適用した結果、チャット一覧はよりスマートに

タスク一覧はただのメモとして表示されるようになった

ChatWork Quick Info Input

Chatwork Extensionとは別にもう一種、ChatWork Quick Info Inputも導入した。

こちらは地味ながらかなり大きな役割をもっており、入力ウィンドウに、title/info/codeのメッセージ記法を入力するためのUIを追加してくれる。

所感

  • チャットワーク用のChrome拡張は本記事で紹介した2種類の他にも様々ある。個々人のチャットワークの使い方や拘りに応じて柔軟にカスタマイズすると良さそう
  • Chatwork Extensionは、任意のスクリプトを仕込むこともできるので、やりようによっては凄いことできそう
  • Slackはもっとカスタマイズがしやすいとの噂が気になる

Python3 + Selenium + Firefox でチャットワークの表示名を自動で切り替える

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

概要

非常にニッチな目的に感じる題だが、以下の流れでこうなった

  • 諸事情でチャットワークの表示名を自動で変更する手段を作りたい
  • チャットワークAPIを使えばいけそう
  • ユーザ情報を変更するAPIが存在しなかった(見落としているだけであったら教えてください)
  • 仕方ないのでスクレイピングで強引に変更しよう
  • チャットワークにログインする必要もあるのでSeleniumを使うのが良いのかな
  • せっかくだから最近勉強してるPython3を使おう

以上より、Seleniumを用いてFirefoxを自動操作し、チャットワークにログイン、表示名を任意のに変更するスクリプトを実装する。

なお、今回はGUIを持たない環境で実装するので、Xvfbを用いた仮想画面を使って動かす(この辺理解が怪しい)

下準備

まずFirefoxを入れる。画面無くて良いならPhantomJSとかヘッドレスブラウザ使えばいいじゃんと思い最初はそうしたのだが、どうしてもチャットワークが正しく表示できなかったので安定を取ってFirefoxにした。その分容量が大きいけど仕方ない。

$ apt-get install firefox

Firefoxのレンダリングエンジンであるgeckoをseleniumから利用するためのドライバーが別途必要。
こちらからダウンロードできるので、OSの種類に応じてダウンロードする。
今回は64ビット版のLinuxなので以下のように。パスが通ってるところで展開したほうが後々楽。

$ cd /usr/local/bin
$ wget https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz
$ tar -zxvf geckodriver-v0.18.0-linux64.tar.gz
$ rm geckodriver-v0.18.0-linux64.tar.gz

仮想モニタが必要になるのでインストール

$ apt-get install xvfb

必要なPythonモジュールをpipでインストール

$ pip install selenium
$ pip install PyVirtualDisplay

ログイン情報を定義

チャットワークにログインするためのメールアドレスをパスワードが必要になるが、ソースコード中にハードコーディングしてしまうとコードの共有ができなくなってしまうので、今回はJSONファイルに別途書き出すようにする。

以下のようなsecret.jsonを作成した。

{
  "email":    "hogehoge@fugafuga.com",
  "password": "qawsedrftgyhujikol"
}

実装

今回は一つのファイルで全て完結させるので、前項で作成したsecret.jsonと同じフォルダに、main.pyを作成する。

モジュールインポート

今回使うモジュールは以下の通りなのでimportする。

import time
import json
import sys
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from pyvirtualdisplay import Display

ログイン情報を取得する

secret.jsonを開いて、jsonモジュールでデシリアライズ。メールアドレスとパスワードを抜き出す

with open('secret.json', 'r') as file:
  secret   = json.load(file)
  email    = secret['email']
  password = secret['password']

Seleniumの初期設定

仮想モニタを立ち上げ、SeleniumDriverをFirefoxで初期化。画面サイズがやたらと大きいのはデバッグ時のスクリーンショットを見やすくするためなので最終的に意味はない。

display = Display(visible=0, size=(1920, 1080))
display.start()
driver = webdriver.Firefox()

チャットワークへのアクセス

SeleniumDriverのgetメソッドで簡単にWebサイトにアクセスできる。

driver.get('https://www.chatwork.com/login.php?lang=ja&args=')

メールアドレス/パスワードを入力

find_element_by_css_selectorメソッドで、CSSセレクターを用いてDOMを取得し、それに対してsend_keysメソッドを用いてキー入力を行うことができる。JSONファイルから取得しておいたメールアドレス/パスワードを入力する。今後他にも出てくるけど、SeleniumDriverって何かとメソッド名が冗長だなと思った。

driver.find_element_by_css_selector('form[name="login"] input[name="email"]').send_keys(email)
driver.find_element_by_css_selector('form[name="login"] input[name="password"]').send_keys(password)

ログインする

同じくfind_element_by_css_selectorメソッドを用いてログインボタンを取得し、clickメソッドでクリックすることでログインする。

driver.find_element_by_css_selector('form[name="login"] input[name="login"]').click()

ログイン処理には時間がかかる上、チャットワークはJavaScriptで画面を構築していくので、構築の完了まで待機する必要がある。
Seleniumでは、以下のようにWebDriverWaitを用いることで、「特定の何かが確認できるまで待機する」という記述を行うことができる。
以下では、IDが_myStatusNameのDOMが確認できるまで待機するという処理(10秒でタイムアウト)

wait = WebDriverWait(driver, 10)
wait.until(expected_conditions.visibility_of_element_located((By.ID, "_myStatusName")))

_myStatusNameは、この部分。これが表示されたら読み込み完了と捉える。

読み込み完了でようやくチャットワークの画面に到達する。

プロフィール画面を開く

ヘッダのプルダウンメニューを開いて、編集メニューをクリックしてプロフィール編集画面を表示する。
かなりまどろっこしい事してる気がする。可能であれば最初からプロフィール編集画面を表示するためのJavaScriptコードを実行させれば一発だと思うが、Seleniumの練習も含めて地道に行う。

0.5秒のスリープを毎回挟んでるのは、UIのアニメーションがあるのでスグに次のUIを操作できないため。これもっと良い方法あったら教えて下さい。

driver.find_element_by_css_selector('#_myStatusName').click()
time.sleep(0.5)
driver.find_element_by_css_selector('#_myProfile .myAccountMenu__anchor').click()
time.sleep(0.5)

表示名を編集する

編集ボタンをクリックし、テキストボックスの内容を書き換え、保存ボタンをクリックする。
変更後の表示名は、コマンドライン引数から取ることを想定し、sys.argvを参照する。

driver.find_element_by_css_selector('#_profileContent ._profileEdit').click()
time.sleep(0.5)
driver.find_element_by_id('_profileInputName').clear()
driver.find_element_by_id('_profileInputName').send_keys(sys.argv[1])
driver.find_element_by_css_selector("div[aria-label='保存する']").click()

実行

以下コマンドで実装できる。ネットワーク環境の影響を大きく受けるが、手元の環境で実行時間は10秒程度。APIがあれば1秒で終わるというのに。

$ python main.py "ふー ばーのすけ"

最新のソースコードはこちら

所感

  • Seleniumの入門としてはえらくニッチな使い方になったが、個人的にやりたかったことが実現できたので良かった
  • 全体的冗長なコードになりやすそう。ラッパーライブラリが多いのも頷ける
  • JavaScriptで動いてるページをうまく制御したければ関連ライブラリ/フレームワークを使うべきか
  • このやり方だとチャットワーク側がちょいとHTML変更したら動かなくなったりするのでやっぱりAPIが欲しい

RubyプログラマがPython3を勉強した話

概要

Rubyを使い始めて2年弱の私が、前々から気にはなっていたが使ったことのないPythonにようやく手を出したお話。
私がひと夏の間に行った、基本的な勉強についての備忘録を残す。

Pythonのバージョンについて

Pythonのバージョンは2.x系(Python2)と3.x系(Python3)が存在するが、2と3では互換性のない変更が多く含まれており、実質別言語として扱われることが多い。

既存システムの多くはPython2で書かれていること、Python3では動かない有用なライブラリが存在することなどから、単に新しいという理由でPython3を採用することはできないが、今回は以下の理由からPython3を採用する。

  • Python2は2010年以降更新されていないため
  • Python2は2020年で公式のサポートが打ち切られるため
  • Python3は現在も積極的なバージョンアップが行われており、今後は完全に3に以降する流れのため

Python3.6.2のインストール

今回は現在の最新のPythonである3.6.2を、Debianにインストールした。
インストールには、Pythonのバージョン管理ツールであるpyenvを用いることにした。インストール手順については、以下記事にまとめた

Python3.6.2をインストールして、Wikipediaをクロールするスクリプトを書く | QSのウェブ開発とか

Python3の基本の勉強

はじめに、Python3の基本的な使い方、文法などの勉強には以下ページを利用した。広く浅く、かつ丁寧にPython3の基本が書かれた良い記事だったので、自身を持ってオススメできる。

Python3基礎文法 – Qiita

Rubyなどのスクリプト言語をある程度経験している人なら、Python3の基本的な範囲については特に戸惑うこと無く馴染めると思う。

Python3のトレーニング

前項で基本的なPython3の書き方、文法は抑えられた。
しかし、まだまだPythonの感覚は掴めていないので、競技プログラミングの問題をPython3を用いて沢山解くことで、Pythonの感覚を掴むことにした。

今回は、私が個人的にもよく利用していたpaizaのスキルチェックを利用した。

paizaのスキルチェック問題は、以下の5段階の難易度がある

  • Dランク: ウォーミングアップレベル
  • Cランク: 初級レベル
  • Bランク: 中級レベル
  • Aランク: 上級レベル
  • Sランク: 超上級レベル

Aランク以上は、言語の使い方よりも、高度なアルゴリズム設計が求められる問題(そもそも解けないのが殆ど)となるので対象外とし、D,C,BランクをPython3を用いて解くことにした。

実際に問題を解いてみると、多言語ではすんなり書けることがPython3ではなかなか書けない。やりたいことをPython3で実現する方法が全然わらないのだ。そういった壁に直面するたびに、実現方法を調べては、Python節を身につけることを繰り返し、D,C,Bランク合わせて30問ぐらい解いたところである程度の理解は出来たと判断して終了した。

Python3によるWebスクレイピング

次に、Pythonと言えばWebスクレイピング/クローリングなので(偏見?)、HTTPライブラリと、XMLパーサライブラリを用いて色々遊んだ。
その中で作成した、Wikipediaをランダムにクロールするスクリプトについては、以下記事にまとめた

Python3.6.2をインストールして、Wikipediaをクロールするスクリプトを書く | QSのウェブ開発とか