投稿者「python-tksssk」のアーカイブ

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を文字列で取得

JSONを返すようなWebAPIのようなリクエストではjson()メソッドでjsonをパースしてdictやlistで返してくれる。

response = requests.get('https://httpbin.org/get')
print(response.json())  # レスポンスのjsonをdict化して返す

POSTリクエスト(requests版)

POSTの場合はpostメソッドでリクエストしてdata属性にdictを渡す。
このときは data属性で渡したときはHTMLのフォームからのリクエストのように application/x-www-form-urlencoded で送信される。

import requests

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

application/jsonでリクエストしたいときは、dataではなくjson属性でdictを渡す。

response = requests.post('https://httpbin.org/post', json={'foo': 'bar'})

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

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

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

GETリクエスト(python3 urllib版)

得られるレスポンスはbyte文字列なので、適切なエンコーディングでデコードする必要がある。(正確につくるならレスポンスヘッダContent-Typeを確認してどのようにdecodeするかを決定する)

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 — 型ヒントのサポート

pythonでのflatmap(flattenする)

他の言語で言うところのflatMapだとかflattenする方法。

複数のイテレータなどから連続した要素を得る場合の方法

itertools.chainで複数のiterableなオブジェクトを連結

import itertools

r1 = range(0, 3)
r2 = range(3, 6)

stream = (r1, r2)
print(stream)


flat = itertools.chain(r1, r2)
for n in flat:
    print(n)

実行結果

(range(0, 3), range(3, 6))
0
1
2
3
4
5

chainの場合は複数のiterableなオブジェクトを列挙して指定するが、すでにそれがiterableなコンテナに入っている場合は、
itertools.chain.from_iterableが使用可能。

stream = (r1, r2)
for n in itertools.chain.from_iterable(stream)
    print(n)

参考

10.1. itertools — 効率的なループ実行のためのイテレータ生成関数

pythonで並列処理 (thread)

threadを使って簡易的に並列処理を実装することができる。

threading.Threadクラスの生成時にtarget引数に指定する関数を、別スレッドで並列的に実行できる。

※並列して処理が動作するため、複数のスレッドでデータを共有する場合にはロックなどを使用してスレッドセーフにする必要がある。

# 1秒おきと2秒おきにprintする処理をthreadで実行
import threading
import time

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

th1 = threading.Thread(name="a", target=worker, args=(1,))
th2 = threading.Thread(name="b", target=worker, args=(2,))

th1.start()
th2.start()

参考:17.1. threading — スレッドベースの並列処理

※threadで実行しても、完全に同時に2つの処理が動作するわけではなく、CPython実装ではマルチコアを活かした並行処理はmultiprocessingを使わないとできない。

可変長引数の設定と呼び出し

通常の引数を可変長引数に

pythonで可変長引数を指定するには、仮引数に*を指定する。関数の実装側では、仮引数をタプルとして扱える。

def func1(a, *others):
    print(a, others)

func1(1)        # 1, ()
func1(1, 2)     # 1, (2,)
func1(1, 2, 3)  # 1, (2, 3)

逆の使い方として、タプルやリストなどのシーケンス型に*をつけると実引数に展開して呼び出すことができる。

以下の例ではa, b, cの引数をそれぞれ指定するのではなく、リストの要素を展開してa,b,cに割り当てている。要素数が引数の数と合わない場合はエラーになる(多くても少なくても)。

def func2(a, b, c):
    print("a={}, b={}, c={}".format(a, b, c))

params = [1, 2, 3])
func2(*params)    # a=1, b=2, c=3

キーワード引数を可変長引数に

pythonでは引数を名前付きで指定することもできる。任意のキーワードで引数を受け付けるには、**をつける。

この場合はdictで扱える。

def func3(a, **others):
    print(a, others)

func3(1, b=2, c=3)   # 1 {'b':2, 'c': 3}

通常の引数と同じように実引数のほうに **をつけることで、dict型をキーワード引数に展開できる。

def func2(a, b, c):
    print("a={}, b={}, c={}".format(a, b, c))

params = {'a':1, 'b':2, 'c':3}
func2(**params)    # a=1, b=2, c=3

任意の引数を受け取れる関数

以下のように リスト形式の可変長引数*argsと、 dict形式の引数**kwargs を受け取るようにすると、どのような引数でも受け付けられるようになる。

def func(*args, **kwargs):
    print(args, kwargs)

文字列からbool値に変換する

文字列からbool値に変換するのは、一見bool()関数でできそうですが、これは真偽値判断をしてくれる関数であって、空文字列がFalse。それ以外がTrueになってしまいます。

単純に文字列→boolとなる関数は標準ライブラリには用意されていないようです。

実際に変換したいのはいろいろな文字列→bool値の仕様があると思いますが、以下のような仕様での実装例を考えてみます。

文字列からboolへの変換の例

文字列 bool値
TRUE True
true True
T True
yes True
1 True
それ以外 False

Trueとなる文字列を列挙してマッチするかどうか

def str2bool(s):
     return s.lower() in ["true", "t", "yes", "1"]

# 確認コード
for s in ["TRUE", "FALSE", "true", "false", 
        "T", "F", "yes", "no", "1", "0"]:
    print(s, str2bool(s))

True対象でなければFalseとなるので、これで文字列からboolへの変換が実装できます。

distutils.util.strtoboolを使用

標準ライブラリにはないと書きましたが、別の用途で使用するモジュールにひっそり似たようなものがあります。
これを使用するのも楽です。

from distutils.util import strtobool

# 確認コード
for s in ["TRUE", "FALSE", "true", "false",
         "T", "F", "yes", "no", "1", "0"]:
    print(s, " ==> ", strtobool(s)) 
        # Trueのとき1, Falseのとき0を返す

1と0の数値で返却されますが、数値の真偽値判別では0はFalse, 0以外はTrueとなるので問題ありません。先ほど挙げた変換例ともマッチするようです。