requestsを使って、Webサイトのソースコードを取得する

今回はとりあえず単純に httpで getするだけのコードを紹介します。
サンプルとして、yahooのトップページのHTMLを取得します。

利用するのは、 requests というpythonのライブラリです。
ドキュメントにある通り、超手軽に使えます。

こちらのコードで、htmlという変数に結果が入ります。


import requests
url = "https://www.yahoo.co.jp/"
response = requests.get(url)
html = response.text

matplotlibのデフォルトのフォントを変更する

前の記事でmatplotlibで日本語を表示できるフォントをインストールしましたので、
この記事では実際にそのフォントを使う方法を書いておきます。

最初に、デフォルトのフォントのままだと、グラフがどのように表示されるのかを見ておきましょう。


import matplotlib.pyplot as plt
# デフォルトの設定を確認
print(plt.rcParams["font.family"]) # => ['sans-serif']
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(range(10),range(10), label="データ1")
ax.plot(range(10),range(0,20,2), label="データ2")
ax.set_title("タイトル")
ax.set_xlabel("x軸のラベル")
ax.set_ylabel("y軸のラベル")
ax.legend()
plt.show()

こちらのコードを実行した結果がこの画像です。日本語文字が豆腐のようになります。

これを回避する方法の一つは実行するたびにフォントを指定することです。
このように、font.familyを指定することで、日本語の文字も豆腐にならず表示されます。


import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "IPAexGothic"

このように指定したあとに、上と同じプログラムを実行すると、下図のように正しく日本語が表示されます。

ただし、これは毎回書くのは結構面倒です。たった1行なのに。
そこで、対応としてmatplotlibの設定ファイルでフォントを指定します。

最初に下記のコードを実行して、設定ファイルの場所を確認します。


>>> import matplotlib
>>> print(matplotlib.matplotlib_fname()) 

環境によって結果は変わりますが、自分の場合は下記の場所にありました。
/Users/<ユーザー名>/.pyenv/versions/anaconda3-5.3.1/lib/python3.7/site-packages/matplotlib/mpl-data/matplotlibrc

このmatplotlibrcのバックアップを取って編集します。
ファイル内に下記の記載があるのでコメントアウトを解除してIPAexGothicを指定します。
元の記述
#font.family : sans-serif
修正後
font.family : IPAexGothic

これで次回以降はフォントの指定をしなくてもmatplotlibで日本語が使えます。

もしうまく表示されない場合はキャッシュファイルを一度削除する必要があります。
matplotlib.get_cachedir()
でキャッシュのディレクトリがわかるので、ここにあるファイルを消して試してみてください。

MeCab.Tagger()はかなり遅いという話

昔、形態素解析にかかる時間を短縮するために調べた内容のメモです。

以前の記事で、mecab-python3 の使い方を書いたとき、tagger = MeCab.Tagger() という処理を関数の外側で行なっていました。

実は初めてmecab-python3を使った頃、僕は次のように書いてました。


def mecab_tokenizer(text):
    # 関数の中で、MeCab.Tagger()を呼び出す。これが遅い
    tagger = MeCab.Tagger()
    parsed_text = tagger.parse(text)
    parsed_lines = parsed_text.split("\n")[:-2]
    surfaces = [l.split('\t')[0] for l in parsed_lines]
    features = [l.split('\t')[1] for l in parsed_lines]
    bases = [f.split(',')[6] for f in features]
    # ここに、必要な品詞の単語だけ選抜する処理を入れることもある
    result = [b if b != '*' else s for s, b in zip(surfaces, bases)]
    return result

1個や2個のテキストを処理する分にはこの書き方で問題なかったのですが、
数十万件のテキストを処理するとこの関数がとても遅いという問題があり、調査をしていました。

結果わかったことは、タイトルの通り、MeCab.Tagger()が遅いということです。
jupyter で コードの前に %timeit とつけると時間を測れるのでやってみます。


%timeit tagger=MeCab.Tagger()
```
結果:
217 µs ± 6.17 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

ちなみに、形態素解析自体(parse)の実行時間はこちら


# 100文字のテキストを事前に用意しておきます
print(len(text))
```
100
```
%timeit parsed_text = tagger.parse(text)
```
結果:
26.9 µs ± 151 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
```

テキストがもっと長くなると話も変わるのですが、100文字くらいのテキストであれば、
parseにかかる時間よりも、Taggerのオブジェクトを作るのにかかる時間の方が8くらいかかっています。

対象のテキスト数(=関数が呼び出される回数)が数十万〜数百万件になってくると、
体感スピードがかなり違うので、
tagger = MeCab.Tagger()
は関数の中ではなく、事前に行うようにしておきます。

名前空間を汚染したりすることが気になる場合は、 class化するなどの対応をとりましょう。
また、形態素解析するテキストの数が少ない場合はあまり気にしなくても大丈夫です。

完全に余談ですが、この記事を書くために私物のMacで時間を計測したとき、職場のMacよりはるかに速いので感動しました。
職場の端末だとMeCab.Tagger()に 1.2ms (6倍!)かかります。
端末が5年物とそこそこ古いだけでなく、辞書指定などの問題もあるかもしれません。

matplotlibで等高線

折れ線グラフや散布図に比べると利用頻度が落ちますが、
2次元から1次元への写像の可視化として等高線を使うことがあるので、そのメモです。

使う関数は、線を引く場合は、contour,色を塗る場合は contourf を使います。

サンプルの関数は何でもいいのですが、今回はこれを使います。
$$
1-\exp(-x^2+2xy-2y^2)
$$
まずはデータの準備です。


import matplotlib.pyplot as plt
import numpy as np

# 関数の定義
def f(x, y):
    return 1- np.exp(-x**2 + 2*x*y - 2*y**2) 

# プロットする範囲のmeshgridを作成する。
X = np.linspace(-2,2,41)
Y = np.linspace(-2,2,41)
xx, yy = np.meshgrid(X, Y)

そして可視化してみます。まずは等高線から。


fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.contour(xx,yy,f(xx, yy))
plt.show()

次に、色を塗る場合。


fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.contourf(xx,yy,f(xx, yy), alpha=0.5)
plt.show()

色の指定などをきちんとしていないのですが、まあまあみやすく可視化できていますね。
機械学習の決定境界の可視化などでも、これと同じ方法を使うことがあります。

データフレームの特定の文字列を含む行を抽出するときに None の列があってもエラーにならないようにする

自然言語処理をよくやっているので、データフレームに格納されたテキストデータから、
特定の文字列を含むものを抽出する作業は非常に頻繁に発生します。
そのときには、 pandas.Series.str.contains という非常に便利な関数を使うのですが、
Series の中に None や Nan があるとエラーになるのが地味に不便でした。


print(df)
```
出力結果:
     col
0  あいうえお
1  かきくけこ
2   None
3  たちつてと
```
df[df["col"].str.contains("く")]
```
出力結果:
(略)
ValueError: cannot index with vector containing NA / NaN values
```

まあ、空白文字列か何かで埋めてあげれば何の問題もないのですが、このエラーが出ると嫌な気持ちになります。
気をつけていてもデータフレーム同士を結合したりするとすぐ None は発生しますし。

これはしょうがないと思っていたのですが、ドキュメントを見ていると、na というパラメーターがー準備されているのを見つけました。

contains の引数に na=True を指定すると,Noneの列も抽出され、na=Falseとすると、Noneの列は含みません。


print(df[df["col"].str.contains("く",na=False)])
```
出力結果:
     col
1  かきくけこ
```
print(df[df["col"].str.contains("あ",na=True)])
```
出力結果:
     col
0  あいうえお
2   None
```

これは便利ですね。
空文字列と、Noneを区別したい場面も結構あるのでNoneをそのまま残せるのはありがたいです。

また、ついでですが、 regex というパラメーターで、正規表現の使用未使用を切り替えられることにも気づきました。
デフォルトで正規表現が使えるのでいつも便利だと思っていたのですが、
完全一致のみにすることもできたのですね。

numpy の数値を表示するときの桁数を指定する

当然ですが、numpyを使っていると数値をprintして値を確認する機会が多々あります。
そこで問題になるのが、表示形式です。
本来は利便性のためだと思うのですが、小数点以下の桁が何桁も表示されたり、突然指数表記になったりします。
正直言って、配列内のどの値が大きくてどの値が小さいのか、ぱっと見でわかりにくいです。

表示例。


>>> import numpy as np
>>> ary = np.random.randn(3, 5)
>>> print(ary)
[[-8.69277072e-01 -4.72662319e-01  5.48868554e-01 -6.03789326e-01 1.95117216e-01]
 [-1.46386861e+00  9.92037075e-01  8.04045031e-01 -1.43756938e+00 7.46898368e-02]
 [-1.05065247e+00  3.72571551e-04 -1.15836779e-01 -5.80949053e-03 1.59979389e+00]]

numpy のドキュメントによると、絶対値が一番大きいものと一番小さいものの差が一定値を超えると指数表記になるそうです。

そこで、値を確認するときは、適当なくらいで四捨五入して表示したりしていたのですが、
実はnumpyのオプションで表示桁数を指定できることがわかりました。

設定を変える前に、デフォルトの設定は下記の関数で見ることができます。
(numpyのバージョンによって設定可能項目は変わります。)


>>> np.get_printoptions()
{'edgeitems': 3, 'threshold': 1000, 'floatmode': 'maxprec', 'precision': 8, 'suppress': False, 'linewidth': 75, 'nanstr': 'nan', 'infstr': 'inf', 'sign': '-', 'formatter': None, 'legacy': False}

各設定値の意味はこちら。set_printoptions
(get_printoptionsのページにはset_printoptions を見ろと書いてある。)

これらの設定値を、set_printoptions関数で変更することができます。
この中で、よく使うのはこの二つ。
precision = 3 # 小数点以下の表記を
suppress = True # 指数表記を禁止

設定してみたのがこちら。


>>> np.set_printoptions(precision=3, suppress=True)
>>> ary = np.random.randn(5,3)
>>> print(ary)
[[ 1.611 -2.259  0.022]
 [-1.937 -0.394  2.011]
 [-0.01  -0.162 -0.823]
 [-1.818 -2.474  0.341]
 [ 0.363 -2.018 -0.667]]

見やすくなりました。

mecab-python3をつかってみる

前回の記事でインストールした mecab-python3 の使い方を書いておきます。
MeCabについてはWikiがあるのですが、このライブラリについては詳細なマニュアルはなく、
リポジトリの test.py を読むようにとそっけなく書いてあります。

ただ、実際のところつかのは非常に簡単です。
次の例のようにMeCab.Tagger() と parse を呼び出すだけで結果を得られます。


>>> import MeCab
>>> text = 'すもももももももものうち'
>>> tagger = MeCab.Tagger()
>>> print(tagger.parse(text))
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

各行の出力結果は次の形です。

表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

注意点としては、 parseした戻り値は一つのテキストなので非常に使いにくいことです。

多くの場合、必要なのは原型の列です。
そこでプログラムでこのテキストから原形の情報を取り出すことになります。
僕はいつも下記のような関数を作って実行しています。


tagger = MeCab.Tagger()


def mecab_tokenizer(text):
    parsed_text = tagger.parse(text)
    parsed_lines = parsed_text.split("\n")[:-2]
    surfaces = [l.split('\t')[0] for l in parsed_lines]
    features = [l.split('\t')[1] for l in parsed_lines]
    bases = [f.split(',')[6] for f in features]
    # ここに、必要な品詞の単語だけ選抜する処理を入れることもある
    result = [b if b != '*' else s for s, b in zip(surfaces, bases)]
    return result

jupyter notebook 上で pycodestyle を使う

テキストエディタで.py ファイルを使う時はコードを綺麗にするために、
pycodestyle (pep8の新しい名前)をよく使います。名前変更の経緯はこちら

これを jupyter notebook でも使えるようにします。
その際に必要になるのが、こちらの pycodestyle_magic というツールです。
flake8というライブラリも必要になるので、一緒に入れます。
(pycodestyleがない場合はそれも必要なのでpipインストールしてください。)

pip install flake8 pycodestyle_magic

使う時は、notebookで事前に読み込み、フォマットをチェックしたいセルで、マジックコマンドを使います。


# magicコマンドを使えるように読み込む
%load_ext pycodestyle_magic

そして、チェックしたいコードが書かれたセルの一番上の行にマジックコマンドを入れて実行します。


%%pycodestyle
#ここにチェックしたいプログラムが書かれている。

問題があれば下記のように警告が表示されます。# の後ろにスペースがありませんでしたね。


2:1: E265 block comment should start with '# '

注意として、この時プログラム自体は実行されないようです。
そのため、スタイルをチェックし終わったら、
マジックコマンドを外して改めてセルを実行する必要があります。