作成者別アーカイブ: python-tksssk

timeitで実行時間計測

「この関数の実行時間を計測して、もっと高速化しよう」といった時に実行時間計測を便利に行うモジュール。

import timeit

# 計測対象の関数
def number_join_by_forloop():
    s = "-"
    for n in range(10000):
        s += str(n)
    return s

# 1000回実行を繰り返した秒数
t1 = timeit.timeit('number_join_by_forloop()', number=1000, globals=globals())
print("for loop: %.3f sec" % t1)

timeit関数での実行は、関数を実行するのとは別の名前空間で実行するため、そのままでは計測対象の関数を参照できない。globals引数で実行時のグローバル領域をコピーして指定すると呼び出し側で定義した関数が参照できる(globals引数はPython3.5から)。

呼び出し側の名前空間を使用しない、単純なPythonコードであればglobals引数は不要。

Python3.5以前のglobals引数が使えない環境では、以下のように自身のモジュールをインポートすることで参照できる。

t2 = timeit.timeit('from __main__ import number_join_by_forloop; number_join_by_forloop()', number=1000)
print("map join: %.3f sec" % t2)

ワイルドカードインポート(import *)は推奨されない

from hoge import *とすることで、hogeモジュールのすべてをインポートできますが、これは雑なインポートの指定方法で意図しない変数、関数までこのモジュールの名前空間にインポートしてしまうので推奨されない。

pythonの公式のスタイルガイドであるPEP8でも非推奨となっている。

http://pep8-ja.readthedocs.io/ja/latest/#import
ワイルドカードを使った import (from import *) は避けるべきです。なぜなら、どの名前が名前空間に存在しているかをわかりにくくし、コードの読み手や多くのツールを混乱させるからです。

ワイルドカードインポートによって混乱する例

あるモジュールmain.pyがfirst.pyに依存しfirst.pyはsecond.pyに依存するとする。

main --> first --> second

first.pyでは、second.pyのすべてをワイルドカードインポートした場合、secondモジュールのすべてのグローバル変数、関数がfirst.pyの名前空間に入る。

# second.py
var_x = 1
def func1():
    pass
# first.py
from second import *
var_y = 2
def func2():
    pass

first.pyからはsecond.pyのfunc1関数を使いたいだけだったが、var_xの変数もインポートされる。

同様にmain.pyはfirst.pyをワイルドカードインポートすると、first.pyの全変数、関数がインポートされる。これにはfirst.pyがインポートしていたsecond.pyの内容までもインポートされる。

# main.py
from first import *
var_z = 3
def func3():
    pass

print(dir())

dir関数で定義された名前を確認すると、func1, func2, func3, var_x, var_y, var_zすべてが参照可能になっている。

[ ...中略..., 'func1', 'func2', 'func3', 'var_x', 'var_y', 'var_z']

意図しないモジュールからのインポートも行われるため定義位置を把握するのがとても難しくなる。
(main.pyでvar_xが参照できるのは、first.pyを参照し、second.pyのインポートからたどることでやっとvar_xの存在がわかる)

これはシンプルな例だが、カジュアルにワイルドカードインポートを使うとこれが肥大化しソースコードの可読性が著しく悪くなる。

second.pyに関数を一つ追加すると、main.pyでも自動的に参照できるようになる点も危険。

インポートする順番によって意図せず上書きする可能性がある

func1という関数が定義されたhogeモジュール。

# hoge.py
def func1():
    print("func1 called")

main.pyでは、円周率πを使用したかったために、mathモジュールのpi定数をインポートしていた。
ここから、hogeモジュールの関数を使いたいために、hogeモジュールをワイルドカードインポートをした。

# main.py
from math import pi
from hoge import *

func1()
print(pi)

main.pyスクリプトの実行結果は、以下のようになり円周率πの値が出力される。

func1 called
3.141592653589793

この状態で、hoge.pyで何気なくpiというグローバル変数を定義すると、main.pyがインポートしたpi変数を上書きしてしまうことになる。

# hoge.py
def func1():
    print("func1 called")
pi = 12345
func1 called
12345

hoge.pyで記述した内容は、piというグローバル変数を作っただけであり、mathモジュールのpi定数を上書きしたわけではない。
しかしmain.pyではワイルドカードインポートしているため、main.pyの名前空間ではpi変数は、hogeモジュールのpi変数を参照することになる。

当初main.pyを記述したときには、hogeモジュールを確認しpi変数がなく上書きされなかったのかもしれない。だがあとからhogeモジュールだけを更新するだけでmainモジュールにも影響を与えてしまっている。

意図しないインポートをしないためには、main.pyからは from hoge import func1 という形で必要なものだけインポートするのがよい。

BeautifulSoup4のチートシート(セレクターなど)

BeautiflSoup4でスクレイピングして要素を抽出するときに、よく使うセレクタをチートシート的にまとめておく。

BeautifuSoup4の使い方

スクレイピングする時にBeautifulSoup4を使うことは多いと思い。よく使うAPIやセレクターの記述方法をまとめます。
ちなみに、よく忘れてしまって「どうするんだっけ?」となるんですが、BeautifulSoup4ではxpathを使ったセレクタは存在しない。urlをわたしてHTTPリクエストを投げてくれるような機能はない。

インストール

beautifulsoup4 もしくは別名の bs4でpipからインストールする。
pip install BeautifulSoupとすると古いBeautifulSoup3になるので注意。

$ pip install beautifulsoup4
or 
$ pip install bs4

BeautifuSoupオブジェクト生成のチートシート

説明 コード例
soupオブジェクト生成(HTML文字列) BeautifulSoup('html文字列', 'html.parser')
soupオブジェクト生成(ファイル) BeautifulSoup(file_handle, 'html.parser')

第1引数はHTML文字列かファイルハンドル。
第2引数はParserライブラリを指定する。よく使用するのは html.parserlxml

html.parser はPython標準ライブラリなのでインストール不要ですぐ使えるが、速度は遅い。
lxml は高速だが別途インストールの必要なC依存ライブラリ。実行環境に依存する。

from bs4 import BeautifulSoup
# HTML文字列コンテンツ引数に生成
soup = BeautifulSoup('<html><body>hoge</body></html>', 'html.parser')

# ファイルハンドルを引数に生成
with open('index.html') as html_file:
    soup = BeautifulSoup(html_file, 'html.parser')

# URLからHTTPリクエストを投げて取得するようなAPIは無い
# requestsなどの別モジュールを使って取得する
import requests
res = requests.get('http://b.hatena.ne.jp/hotentry')
soup = BeautifulSoup(res.content, 'html.parser')

BeautifulSoup APIチートシート

説明 コード例
子要素 soup.head
タグ全検索 soup.find_all('li')
1件検索 soup.find('li')
属性検索 soup.find('li', href="html://www.google.com/")
class検索 soup.find('a', class_="first")
属性取得 first_link_element['href']
テキスト要素 first_link_element.string
親要素 first_link_element.parent
from bs4 import BeautifulSoup

soup = BeautifulSoup('''
<html>
    <head><title>example</title></head>
    <body>
        <ul>
            <li><a href="http://example.com/" class="first">example.com</a></li>
            <li><a href="http://www.google.com/" data="123">google.com</a></li>
        </ul>
    </body>
</html>''', 'html.parser')

# 子要素をたぐる
print(soup.head)        # <head><title>example</title></head>
# headの下のtitle
print(soup.head.title)  # <title>example</title>

# find_allは全検索してリストで返す
li_elements = soup.find_all("li")
print(li_elements)  # [<li><a class="first" href="http://example.com/">example.com</a></li>,
                    #  <li><a href="http://www.google.com/">google.com</a></li>]
# 見つからない場合は空のリスト
nothing_images = soup.find_all('img')
print(nothing_images)   # []

# findは検索して最初に見つかった1つ目
first_li_element = soup.find('li')
print(first_li_element) # <li><a class="first" href="http://example.com/">example.com</a></li>

# 見つからない場合はNone
nothing_image_element = soup.find('img')
print(nothing_image_element)    # None

#属性も検索条件につける
anchors_to_google = soup.find_all("a", href="http://www.google.com/")
print(anchors_to_google)    # [<a href="http://www.google.com/">google.com</a>]

# classは予約後なのでアンダースコア付加
first_link_element = soup.find("a", class_="first")
print(first_link_element)   # <a class="first" href="http://example.com/">example.com</a>

# 属性取得
print(first_link_element['href'])   # http://example.com/
# テキスト要素
print(first_link_element.string)    # example.com

# 親要素
parent = first_link_element.parent
print(parent)   # <li><a class="first" href="http://example.com/">example.com</a></li>

BeautifulSoup cssセレクタ チートシート

説明 コード例
タグ検索 soup.select('li')
1件検索 soup.select_one('li')
属性検索 soup.select('a[href="http://www.google.com"]')
属性存在 soup.select('a[data])
class検索 soup.select('a.first')
# タグ全検索してリストで返す
li_elements = soup.select("li")
print(li_elements)  # [<li><a class="first" href="http://example.com/">example.com</a></li>,
                    #  <li><a href="http://www.google.com/">google.com</a></li>]

# select_oneは検索して最初に見つかった1つ目
first_li_element = soup.select_one('li')
print(first_li_element) # <li><a class="first" href="http://example.com/">example.com</a></li>
# 見つからない場合はNone
nothing_image_element = soup.select_one('img')
print(nothing_image_element)    # None

#属性検索
anchors_to_google = soup.select('a[href="http://www.google.com/"]')
print(anchors_to_google)    # [<a href="http://www.google.com/">google.com</a>]
#data属性存在
print(soup.select('a[data]'))  # [<a data="123" href="http://www.google.com/">google.com</a>]

# class指定
first_link_element = soup.select_one("a.first")
print(first_link_element)   # <a class="first" href="http://example.com/">example.com</a>

requestsとBeautifulSoup4でシンプルなスクレイピング

はてなブックマークのホットエントリーから記事URL,タイトルを取得する例。

import requests
from bs4 import BeautifulSoup

res = requests.get('http://b.hatena.ne.jp/hotentry')
soup = BeautifulSoup(res.content, 'html.parser')

for a in soup.select('a.entry-link'):
    print(a['href'], a.text)

BeautifuSoupはインスタンス生成時にParserを指定できる。上記の例はhtml.parserを指定してあり、これはPython標準ライブラリで提供されるため、手軽に利用できるが速度的には遅い。高速なParserとしてはlxmlなどを別途インストールして指定することができる。

soup.selectではCSSセレクタを使用して走査している。

Scrapyとの比較

Scrapyでの例と比べると、単純な処理であればrequestsとBeatifulSoup4で実装したほうが簡単に書ける。

Scrapyはクローリングやスクレイピングのフレームワークで、多数のページを連鎖的にクローリングする手法などが組み込まれていて、複雑なスクレイピングが簡単に実装できる。

requestsとBeatifulSoup4で作成する際には、フレームワークの決まりごとがないぶんシンプルに記述できるが、上記のような多ページを取得することなどをやろうとすると複雑になってくる。

スクレイピングの注意事項

スクレイピングはブラウザと同じようにHTTPリクエストをサーバーに送り、データを取得するプログラムだが、プログラムの記述方法次第では、短時間に大量のリクエストをサーバーに送りつけ攻撃のようになってしまう事がある。

スクレイピングを実装する作法としては、繰り返しリクエストを送る間隔にsleep処理を入れて(最低でも1秒以上)短時間に大量のリクエストを送らないように注意する必要がある。

scrapyでシュッとスクレイピングする

Scrapyはスクレイピング用のフレームワークで、scrapyプロジェクトを作って大規模なクローリング・スクレイピングを作ることができるが、ちょこっと使い捨てなスクレイピングをするだけのコードも簡単に作ることができる。

ここでは、scrapyのプロジェクトを作らずスクレイピングのコードを作成する一連の流れを紹介する。

目的

scrapyのexampleページを対象とする。
https://doc.scrapy.org/en/latest/_static/selectors-sample1.html
上記URLがレスポンスするHTMLは以下のようになる。

<html>
 <head>
  <base href='http://example.com/' />
  <title>Example website</title>
 </head>
 <body>
  <div id='images'>
   <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
   <a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
   <a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
   <a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
   <a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
  </div>
 </body>
</html>

上記ページのaタグ内テキストの一覧を取得する。

Name: My image 1 
Name: My image 2 
Name: My image 3 
Name: My image 4 
Name: My image 5 

スクレイピングの動作確認

いきなりコードを書いてから動作確認してもよいが、スクレイピングのセレクターが正しいかどうか確認してみるには対話コンソールで確認するのが楽。まずは目的のデータがどのようにスクレイピングすれば取得できるかを手作業で確認する。

scrapy shellを起動して確認。

$ scrapy shell

>>> fetch('https://doc.scrapy.org/en/latest/_static/selectors-sample1.html')
2017-12-10 11:30:37 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://doc.scrapy.org/en/latest/_static/selectors-sample1.html> (referer: None)
>>> response.css('#images a::text').extract()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']

fetchコマンドでURLを渡して、ページの情報を取得する。取得結果はresponseという変数に自動的に保存される。
resposen.cssで、CSSセレクタを使ってaタグ内のテキストを取得する。結果、目的のデータが取得できた。

scrapy shellでの作業はこれから書くコードの動きを確認するためのもので、コードの作成には関係しない。「CSSセレクタをこのように記述すれば目的のデータが取れるんだな」ということを試行錯誤しながら試すことができる。

セレクタの記述については、xpathやcssセレクタが使用可能。使い方のチートシートはこちらの記事を参照。
scrapyでよく使うxpath,cssのセレクタ

Spiderの作成

どのようにスクレイピングすればよいかは、scrapy shellで試行錯誤することで確認できた。あとはこれを実行するコードを書けば良い。ScrapyではWebからページをクローリングして、スクレイピングする部分はSpiderというプログラムで記述する。
scrapyのツールでSpiderの雛形が作成できる。

scrapy genspiderコマンドでSpiderの雛形を作成する。Spiderの名前とドメインを指定する。

$ scrapy genspider first_scrapy doc.scrapy.org

生成された雛形のSpiderファイル「first_scrapy.py」を編集して開始URLとレスポンスの処理を修正する。

# -*- coding: utf-8 -*-
#first_scrapy.py

import scrapy

class FirstScrapySpider(scrapy.Spider):
    name = 'first_scrapy'
    allowed_domains = ['doc.scrapy.org']
    start_urls = ['https://doc.scrapy.org/en/latest/_static/selectors-sample1.html']

    def parse(self, response):
        for name in response.css('#images a::text').extract():
            print(name)

Spiderの実行

作成したSpiderを実行するには、scrapy runspiderコマンドを実行する。

$ scrapy runspider --loglevel=WARN first_scrapy.py
Name: My image 1
Name: My image 2
Name: My image 3
Name: My image 4
Name: My image 5

コードで実行しているアンカータグの中身が出力されていることが分かる。--loglevelを指定しないとデバッグの出力が得られる。

データの返却

本来、Spiderはクローリング、スクレイピングした結果をデータとして返却する役割を担う。

先ほどのスクリプトはスクレイピングした結果をそのまま出力することで処理を終えてしまっている。簡単な処理ならばそれで十分だが「データの取得」と「取得したデータを活用する」部分は分けたほうがよいこともある。
Spiderはデータを取得して返すことを目的とした実装にできる。その場合はdict型でデータをyieldで逐次返却するようにする。

スクレイピングの処理を修正し、aタグを走査してhref属性とtext要素を取得するように修正。

# -*- coding: utf-8 -*-
#first_scrapy.py

import scrapy

class FirstScrapySpider(scrapy.Spider):
    name = 'first_scrapy'
    allowed_domains = ['doc.scrapy.org']
    start_urls = ['https://doc.scrapy.org/en/latest/_static/selectors-sample1.html']

    def parse(self, response):
        for a in response.css('#images a'):    #aタグの走査
            yield {
                "href": a.css('::attr(href)').extract_first(),
                "name": a.css('::text').extract_first(),
            }

runspiderを実行する際に、出力先としてhoge.jsonというファイルを指定する。

$ scrapy runspider --loglevel=WARN -o hoge.json first_scrapy.py

hoge.jsonの中身はこのようになる。

[
{"href": "image1.html", "name": "Name: My image 1 "},
{"href": "image2.html", "name": "Name: My image 2 "},
{"href": "image3.html", "name": "Name: My image 3 "},
{"href": "image4.html", "name": "Name: My image 4 "},
{"href": "image5.html", "name": "Name: My image 5 "}
]

jsonファイルを出力先に指定したことで、自動的にjson形式で出力されている。明示的に出力形式を指定する場合は、-t でフォーマットを指定する。(csv, json, jsonlines, xmlなどが指定可能)

また出力先の指定に-を指定すると標準出力に出力できる。

$ scrapy runspider --loglevel=WARN -t csv -o - first_scrapy.py
href,name
image1.html,Name: My image 1
image2.html,Name: My image 2
image3.html,Name: My image 3
image4.html,Name: My image 4
image5.html,Name: My image 5

スクレイピングの注意事項

スクレイピングはブラウザと同じようにHTTPリクエストをサーバーに送り、データを取得するプログラムだが、プログラムの記述方法次第では、短時間に大量のリクエストをサーバーに送りつけ攻撃のようになってしまう事がある。

スクレイピングを実装する作法としては、繰り返しリクエストを送る間隔にsleep処理を入れて(最低でも1秒以上)短時間に大量のリクエストを送らないように注意する必要がある。

pythonで並列処理(multiprocessing)

multiprocessingモジュールを使って、threadingモジュールと同じように並列処理を記述できる。

multiprocessing.Processクラスの生成時にtarget引数に指定する関数を、別プロセスで並列的に実行できる。

import multiprocessing
import time


def worker(interval):
    for n in range(3):
        time.sleep(interval)
        print("%s --> %d" % (multiprocessing.current_process().name, n))

p1 = multiprocessing.Process(name="a", target=worker, args=(1, ))
p2 = multiprocessing.Process(name="b", target=worker, args=(2, ))

p1.start()
p2.start()

threadingと同じように書けるように、作られているためほとんど同じように記述できる。生成するのがthreading.Threadか、multiprocessing.Processかの違い。

multiprocessingを使った場合は、サブプロセスが生成されて実行できる。(cpythonの場合)threadingではマルチコアを活かした並行処理ができないが、multiprocessingの場合は独立したプロセスになるのでマルチコアを活かした実行が可能になる。

では、threadingではなくいつもmultiprocessingを使えばいいのかというとそうはならない。
同じように記述はできるが、worker関数を実行するのは別プロセスになるので、メインプロセスとサブプロセスの間で変数を共有したりすることができない。サブプロセスで実行した結果をメインプロセスで受け取ったり、サブプロセスの実行中の状態を取得したりするには、プロセス間通信の仕組みが必要になって複雑になる。

threadの場合は、同一のプロセスで動くのでメモリ空間は共有してあり、ロックなどの処理は必要になるもののデータのやり取りは簡単にできる。

参考:17.2. multiprocessing — プロセスベースの並列処理

scrapyでよく使うxpath, cssのセレクタ

scrapyでxpath, cssを使って要素を抽出するときに、よく使うセレクタをチートシート的にまとめておく。

抽出の元となるHTMLはこんな感じとする。

<html>
<body>
    <div id="main">
        <ul>
            <li data="1">list1</li>
            <li data="2" class="even">list2</li>
            <li data="3">list3</li>
        </ul>
        <div>aaa<div>bbb</div></div>
    </div>
    <div id="sub">
        <ul>
            <li data="1">list4</li>
            <li data="2" class="even">list5</li>
            <li data="3">list6</li>
        </ul>
        <div>ccc<div>ddd</div></div></div>
</body>
</html>

xpath, cssチートシート

xpath css 説明
//li/text() li::text text要素
//div[@id="main"] div#main id指定フィルタ
//li[@class="even"] li.even class指定フィルタ
//li[@data="3"] li[data="3"] 属性指定フィルタ
//div[@id="main"]/div div#main>div 直下の子取得
//li/@data li::attr(data) 属性取得

text要素をextract

res.xpath('//li/text()').extract()
res.css('li::text').extract()

→ [‘list1’, ‘list2’, ‘list3’, ‘list4’, ‘list5’, ‘list6’]

最初の要素をextract

res.xpath('//li/text()').extract_first()     # extract_first
res.xpath('//li/text()')[0].extract()     # selectorの1要素目からextract
res.css('li::text').extract_first()
res.css('li::text')[0].extract()

→ ‘list1’

id指定でフィルタ

res.xpath('//div[@id="main"]//li/text()').extract()  # [@id="main"]
res.css('div#main li::text').extract()               # #main

→ [‘list1’, ‘list2’, ‘list3’]

class指定でフィルタ

res.xpath('//div[@id="main"]//li[@class="even"]/text()').extract()  # [@class="even"]
res.css('div#main li.even::text').extract()               # .even

→ [‘list2’]

属性指定でフィルタ

res.xpath('//div[@id="main"]//li[@data="3"]/text()').extract()  # [@data="3"]
res.css('div#main li[data="3"]::text').extract()               # [data="3"]

→ [‘list3’]

直下の子要素指定でフィルタ

res.xpath('//div[@id="main"]/div/text()').extract()  #  //divなら["aa", "bb"]になる
res.css('div#main>div::text').extract()  # div#main divなら["aa", "bb"]になる 

→ [‘aaa’]

属性取得

res.xpath('//li/@data').extract()
res.css('li::attr(data)').extract()

→ [‘1’, ‘2’, ‘3’, ‘1’, ‘2’, ‘3’]

Elementを取得してループ

# xpath
for elm in res.xpath("//li"):
    #liの要素まで取得してからさらにそこからxpathで取得 
    data = elm.xpath('@data').get()
    text = elm.xpath('text()').get()
    print("{}\t{}".format(data, text))
#css
for elm in res.css("li"):
    #liの要素まで取得してからさらにそこからcssで取得 
    data = elm.css('::attr(data)').get()
    text = elm.css('::text').get()
    print("{}\t{}".format(data, text))


1 list1
2 list2
3 list3
1 list4
2 list5
3 list6

全体的に、xpathのほうが冗長ですが理論的である気がしますね。xpathはhtmlだけではなくxmlに対して使える汎用的なものだからですね。
一方cssの方はjqueryやcssを書くことに慣れていれば、簡潔に(id指定やclass指定など)書けますが、選択ではなく取得になると「あれ?どうやるの?」って気になります。

HTTPのPOST/GETリクエストをする

requestsモジュールを使う(Python2系, 3系共通)

pythonの標準ライブラリでもHTTPリクエストを送ることはできるが、煩雑で使いにくいためrequestsというサードパーティ製のライブラリが使われることが多い。
requestsモジュールを使った場合は直感的で簡単なコードでHTTPリクエストができる。

requestsモジュールのインストール。

$ pip install requests

GETリクエスト(requests版)

getメソッドでURLを指定してリクエスト。

import requests

response = requests.get('http://www.example.com')
print(response.status_code)    # HTTPのステータスコード取得
print(response.text)    # レスポンスのHTMLを文字列で取得

POSTリクエスト(requests版)

POSTの場合はpostメソッドでリクエストしてdata属性にdictを渡す。

import requests

response = requests.post('http://www.example.com', data={'foo': 'bar'})
print(response.status_code)    # HTTPのステータスコード取得
print(response.text)    # レスポンスのHTMLを文字列で取得

python3系の標準ライブラリ urllibを使う

python3とpython2では標準ライブラリのパッケージが異なるので注意(使い方はほぼ同じ)

requestsモジュールと違って、送信データ受信データをバイナリで送って、バイナリから読み込むという操作が必要になる。

GETリクエスト(python3 urllib版)

得られるレスポンスはbyte文字列なので、適切なエンコーディングでデコードする必要がある。(汎用的につくるならレスポンスヘッダを確認するなどする)

import urllib.request

response = urllib.request.urlopen('http://www.example.com')
print(response.getcode())
html = response.read()
print(html.decode('utf-8'))

POSTリクエスト(python3 urllib版)

POSTリクエストする場合はRequestオブジェクトを作ってdataを渡す必要がある。
dataはURLエンコードをしなければならないし、送信データ文字コードを指定してバイト文字列を作る必要がある。

import urllib.request
import urllib.parse

data = urllib.parse.urlencode({'foo': 'bar'}).encode('utf-8')
request = urllib.request.Request('http://www.example.com', data)
response = urllib.request.urlopen(request)
print(response.getcode())
html = response.read()
print(html.decode('utf-8'))

python2系の標準ライブラリ urllib2を使う

python2の場合はpython3のurllibとほぼ同じだが、パッケージ構成が異なるので注意。
python2の時はurllibが複数あったが、python3で整理されている。

python2では文字列リテラルはバイト文字列になるため、エンコードが不要なようにみえるがユニコード文字列を扱いたい場合にはやはりエンコード・デコードが必要にある。

GETリクエスト(python2 urllib2版)

import urllib2
response = urllib2.urlopen('http://www.example.com')
print(response.getcode())
html = response.read()
print(html)

POSTリクエスト(python2 urllib2版)

import urllib
import urllib2

data = urllib.urlencode({'foo': 'bar'})
request = urllib2.Request('http://www.example.com', data)
response = urllib2.urlopen(request)
print(response.getcode())
html = response.read()
print(html)

python3.5の型ヒントでジェネリクス型を定義

型ヒントによる構文チェックの基本については、こちらを参照。Python3.5以降のみ使用可能。

python3.5以降の型ヒント(typeing)の使用 – 基本

ジェネリクス(コンテナ内の型明示)

Java言語のようにジェネリクスを使ってコンテナ内のオブジェクトの型情報を明示することができる。

from typing import List

a:List[str] = []   # 文字列を格納するlistであることを明示
a.append("hello")
a.append(1)        #! Argument 1 to "append" of "list" has incompatible type "int"; expected "str"

型変数

TypeVarを使って型変数を定義できる。

intのListを渡すと返り値はintになるので型チェックできる。

from typing import TypeVar, Sequence
T = TypeVar('T')
def firstElement(l: Sequence[T]) -> T:   # List[T]の最初の要素を返すので戻り値はT型
    return l[0]
firstElement([1, 2, 3])+" " # Unsupported operand types for + ("int" and "str")

クラス定義時のジェネリッククラス

型変数をクラス定義に設定し、インスタンス生成時に型定義を明示する。

from typing import TypeVar, Generic

T = TypeVar('T')
class MyContainer(Generic[T]):   # MyContainerはT型のジェネリックスクラスとする
    def set(self, v:T):
        self.value:T = v
    def get(self) -> T:
        return self.value

container = MyContainer[int]()   # int型のMyContainerを生成
container.set(1)
container.get() + 3

container.set("Hello")      # ! Argument 1 to "set" of "MyContainer" has incompatible type "str"; expected "int" 
container.get().index("l")  # ! "int" has no attribute "index"

参考:26.1.4. ジェネリクス

python3.5以降の型ヒント(typeing)の使用 – 基本

python3.5以降から使用可能な型ヒントを使う。これを使えばpythonでも静的言語なるように見えるが、実行時には完全に無視されチェックはなにも行われない。
現状では型ヒントのチェックツールでの構文チェックや、PyCharmなどの開発環境でのコード補完のためのヒントとなる点に注意する。

チェックツールmypyのインストール

これを使わないと型ヒントの情報はなにも活かせない。

$ pip install mypy
$ mypy -V
mypy 0.511

型ヒントなしのコード

型ヒントなしのコードとして、こんな関数があったとする。

def concat(a, b):
    return a + b

a,bの双方に文字列を渡すケースを想定しているが、数値を渡すと(意図した操作ではないが)加算した結果が得られる。文字列と数値を渡すと実行時エラーになる。

concat("Hello", "World")    # "HelloWorld"
concat(1, 2)                # 3
concat("s", 2)              # TypeError: must be str not int

型ヒントの付加

意図してない操作を除外したい。実行する前に静的に構文エラーとしたい。そのために型ヒントをつける。このように記述できるのはpython3.5以降。

def concat(s:str, n:str)->str:
    return s + n

引数の型。関数の戻り値の型が情報として付加された。

mypyでの静的チェック

これをmypyでチェックすることで、プログラムの実行前に関数の使用法のエラーとして検出可能になる。

$ mypy type-hint.py
type-hint.py:8: error: Argument 1 to "concat" has incompatible type "int"; expected "str"
type-hint.py:8: error: Argument 2 to "concat" has incompatible type "int"; expected "str"
type-hint.py:9: error: Argument 2 to "concat" has incompatible type "int"; expected "str"

PyCharmなどのIDEでは型ヒントの情報から、あらかじめ警告表示するサポートがある。

また戻り値が文字列であることが分かるため、関数の実行結果に対しての操作が、文字列に対する操作になるように補完される。

その他にもジェネリック型など他の静的言語と比べても遜色ない型ヒント情報が指定できる。

参考:26.1. typing — 型ヒントのサポート