Amazon Linux上で、Python + Selenium + Headless Chromeを使用してWEBスクレイピング

Amazon Linux上で、Python + Selenium + Headless Chromeを使用してWEBスクレイピングをしていきます。

前準備

まず、Amazon EC2で「Amazon Linux 2 AMI (HVM), SSD Volume Type – ami-009d6802948d06e52」を立ち上げます。

Chromeをインストールします。

sudo yum -y install https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm

ChromeDriverをインストールします。

wget https://chromedriver.storage.googleapis.com/2.45/chromedriver_linux64.zip
unzip chromedriver_linux64.zip -d bin/

Python3とSeleniumをインストールします。

sudo yum install python3
sudo pip3 install Selenium

スクレイピング

test.py
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time

def _main():
    options = webdriver.ChromeOptions()
    options.add_argument('--headless')
    driver = webdriver.Chrome(options=options)

    driver.get('https://www.google.co.jp/')
    search = driver.find_element_by_name('q')
    search.send_keys('Python')
    search.send_keys(Keys.RETURN)

    time.sleep(3)
    driver.save_screenshot('search_results.png')

    driver.quit()

if __name__ == '__main__':
    _main()

Googleで「Python」を検索して、検索結果のスクリーンショットを取ります。

python3 test.py

以下のようにスクリーンショットが取得できました。

日本語の文字化け

ただし、この状態で日本語を検索すると、以下のように文字化けしてしまいます。

search.send_keys('パイソン')

そこで、日本語フォントをインストールします。

sudo yum install ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts

再度実行すると、文字化けすることなく、スクリーンショットを取得できました。

Python: 文章中の単語出現回数をカウントするときに、defaultdictを使うとシンプルに書ける

例として以下の文章内(words)で出現する単語の頻度をカウントします。

words = ‘Python is an interpreted, high-level, general-purpose programming language. Created by Guido van Rossum and first released in 1991, Python has a design philosophy that emphasizes code readability, notably using significant whitespace. It provides constructs that enable clear programming on both small and large scales. In July 2018, Van Rossum stepped down as the leader in the language community.’

面倒臭いので 話をシンプルにするため、「,」や「.」、大文字小文字は考えないことにします。
普通に書くとこんな感じでしょうか。

count_word.py

word_count = {}
for word in words.split():
    if word in word_count:
        word_count[word] += 1
    else:
        word_count[word] = 1
print(word_count)

こっちの方がわかりやすいでしょうか。

word_count = {}
for word in words.split():
    if not word in word_count:
        word_count[word] = 0
    word_count[word] += 1
print(word_count)

いずれにしても、word_countディクショナリに存在しないwordが出てきたら、初期化をしています。
得られる結果は以下の通りです。

python count_word.py 
{'and': 2, 'the': 2, 'code': 1, 'Van': 1, '1991,': 1, 'is': 1, 'both': 1, 'an': 1, 'down': 1, 'as': 1, 'design': 1, 'in': 2, 'readability,': 1, 'It': 1, 'van': 1, 'Python': 2, 'Rossum': 2, 'interpreted,': 1, 'that': 2, 'provides': 1, 'July': 1, 'has': 1, 'leader': 1, 'stepped': 1, 'enable': 1, 'Created': 1, 'philosophy': 1, 'constructs': 1, 'emphasizes': 1, 'general-purpose': 1, 'notably': 1, 'released': 1, 'high-level,': 1, 'significant': 1, 'Guido': 1, 'using': 1, 'by': 1, 'a': 1, 'on': 1, 'language': 1, 'whitespace.': 1, 'clear': 1, 'programming': 2, 'language.': 1, 'large': 1, 'community.': 1, 'In': 1, 'small': 1, '2018,': 1, 'scales.': 1, 'first': 1}

初期化が面倒くさいので、以下のように書きたくなってしまいますが、これではエラーになってしまいます。

word_count = {}
for word in words.split():
    word_count[word] += 1
print(word_count)
python count_word.py 
Traceback (most recent call last):
  File "count_word.py", line 7, in 
    word_count[word] += 1
KeyError: 'Python'

一つ目のword「Python」がword_countディクショナリに無いためです。
そこで、defaultdictの出番です。defaultdictを使用することで、あらかじめ初期値を設定することができます。

from collections import defaultdict
word_count = defaultdict(int)
for word in words.split():
    word_count[word] += 1
print(word_count)

defaultdict(int)としていますが、これは初期値を0にするということです。

python count_word.py 
defaultdict(, {'and': 2, 'the': 2, 'code': 1, 'Van': 1, '1991,': 1, 'is': 1, 'both': 1, 'an': 1, 'down': 1, 'as': 1, 'design': 1, 'in': 2, 'readability,': 1, 'It': 1, 'van': 1, 'Python': 2, 'Rossum': 2, 'interpreted,': 1, 'that': 2, 'provides': 1, 'July': 1, 'has': 1, 'leader': 1, 'stepped': 1, 'enable': 1, 'Created': 1, 'philosophy': 1, 'constructs': 1, 'emphasizes': 1, 'general-purpose': 1, 'notably': 1, 'released': 1, 'high-level,': 1, 'significant': 1, 'Guido': 1, 'using': 1, 'by': 1, 'a': 1, 'on': 1, 'language': 1, 'whitespace.': 1, 'clear': 1, 'programming': 2, 'language.': 1, 'large': 1, 'community.': 1, 'In': 1, 'small': 1, '2018,': 1, 'scales.': 1, 'first': 1})

無事結果が得られました。
defaultdictを使用することで、for文の中身がすっきりしてわかりやすくなったかと思います。
ちなみに、以下のような書き方も良い感じです。(あまり見かけない気がしますが…)

word_count = {}
for word in words.split():
    word_count[word] = word_count.get(word, 0) + 1
print(word_count)

ディクショナリのgetメソッドを使用して、第1引数に取得したい要素のキーを、第2引数にキーが存在しない場合のデフォルト値を指定しています。
今回の場合には、collections.Counterを使用するのもいいですが、先に全部リスト化する必要があるので、長い文章になった場合にはメモリが足りなくなる恐れがあることに注意です。

Python: boto3を使用してAWSのEC2インスタンスを立ち上げる

まずは、最小限のオプションだけで起動してみます。
ec2.py

import boto3

ec2 = boto3.resource('ec2')
instance = ec2.create_instances(
    ImageId="ami-009d6802948d06e52",
    MinCount=1,
    MaxCount=1
)

print(instance)
python ec2.py 
[ec2.Instance(id=‘[インスタンスID]’)]

立ち上がったインスタンスタイプはm1.smallになります。デフォルトのインスタンスタイプがデフォルトでm1.smallになっているためです。
次は、t2.microを指定して起動します。

instance = ec2.create_instances(
    ImageId="ami-009d6802948d06e52",
    MinCount=1,
    MaxCount=1,
    InstanceType="t2.micro"
)

t2.microのインスタンスが立ち上がりました。
ただ、この状態だと、AWS マネジメントコンソールから「接続」を開くと以下のように表示されます。

インスタンスがキーペアに関連付けられていません。
このインスタンスがキーペアに関連付けられていません。キーペアがない場合、有効なユーザー名とパスワードの組み合わせを使用してこのインスタンスにログインする必要があります。

次は、キーペアを指定して起動します。

instance = ec2.create_instances(
    ImageId="ami-009d6802948d06e52",
    MinCount=1,
    MaxCount=1,
    InstanceType="t2.micro",
    KeyName="[キーペア]"
)

これで、ログインできるかと思いきや、以下のエラーが出力されます。

ssh -i "[pemファイル]" ec2-user@ドメイン名
ssh: connect to host [ドメイン名] port 22: Operation timed out

22番ポートが開放されていないようです。
次は、セキュリティグループIDを指定して起動します。

instance = ec2.create_instances(
    ImageId="ami-009d6802948d06e52",
    MinCount=1,
    MaxCount=1,
    InstanceType="t2.micro”,
    KeyName="[キーペア]",
    SecurityGroupIds=["[セキュリティグループID]"]
)

無事ログインできました。

Python: ArgumentParserを使用してコマンドライン引数を取得するときのチートシート

Pythonの実行時にコマンドライン引数を取得したい場合には、Pythonの標準モジュールであるArgumentParserを使用すると便利です。

ArgumentParserのシンプルな使用方法

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('arg1')
parser.add_argument('arg2')
parser.add_argument('arg3')
args = parser.parse_args()

print(args.arg1, args.arg2, args.arg3)
python test.py -h
usage: test.py [-h] arg1 arg2 arg3

positional arguments:
  arg1
  arg2
  arg3

optional arguments:
  -h, --help  show this help message and exit
python test.py aaa bbb ccc
('aaa', 'bbb', 'ccc')

オプション引数を取りたい場合

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('arg1')
parser.add_argument('--arg2')
parser.add_argument('-a', '--arg3', default="ccc")
args = parser.parse_args()

print(args.arg1, args.arg2, args.arg3)
python test.py -h
usage: test.py [-h] [--arg2 ARG2] [-a ARG3] arg1

positional arguments:
  arg1

optional arguments:
  -h, --help            show this help message and exit
  --arg2 ARG2
  -a ARG3, --arg3 ARG3
python test.py aaa
('aaa', None, 'ccc')
python test.py aaa --arg2 bbb -a ddd
('aaa', 'bbb', 'ddd')

複数のコマンドライン引数を受け取る

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('arg1', nargs='*')
parser.add_argument('arg2')
args = parser.parse_args()

print(args.arg1, args.arg2)
python test.py -h
usage: test.py [-h] [arg1 [arg1 ...]] arg2

positional arguments:
  arg1
  arg2

optional arguments:
  -h, --help  show this help message and exit
python test.py aaa bbb ccc ddd
(['aaa', 'bbb', 'ccc'], 'ddd')

コマンドライン引数でファイル名を受け取る

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('arg1', type=argparse.FileType('r'))
args = parser.parse_args()

print(args.arg1.read())
python test.py aaa
usage: test.py [-h] arg1
test.py: error: argument arg1: can't open 'aaa': [Errno 2] No such file or directory: 'aaa'
python test.py test.txt
[test.txtの中身]

Python: 内包表記のチートシート

リスト内包表記

リスト内の全ての要素を2倍にする

mylist = [1, 2, 3, 4, 5, 6, 7, 8, 9]
newlist = [n*2 for n in mylist]
print(newlist)
[2, 4, 6, 8, 10, 12, 14, 16, 18]

リスト内の偶数のみ取り出す

mylist = [1, 2, 3, 4, 5, 6, 7, 8, 9]
newlist = [n for n in mylist if n%2==0]
print(newlist)
[2, 4, 6, 8]

リスト内の偶数は”even”、奇数は”odd”とする

mylist = [1, 2, 3, 4, 5, 6, 7, 8, 9]
newlist = ["even" if n%2==0 else "odd" for n in mylist]
print(newlist)
['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd']

辞書内包表記

辞書の要素を全て2倍にする

mydict = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
newdict = {k:v*2 for k,v in mydict.items()}
print(newdict)
{'a': 2, 'c': 6, 'b': 4, 'e': 10, 'd': 8}

辞書の要素が偶数であるもののみ取り出す

mylist = [1, 2, 3, 4, 5, 6, 7, 8, 9]
newdict = {k:v for k,v in mydict.items() if v%2==0 }
print(newdict)
{'b': 2, 'd': 4}

Python: リスト内の全ての要素を2倍する

シンプルにfor文を使用する方法

mylist = [1,2,3,4,5,6,7,8,9]
newlist = []
for n in mylist:
    newlist.append(n*2)
print(newlist)
[2, 4, 6, 8, 10, 12, 14, 16, 18]

わかりやすいですが、行数が多くなります。

python内包表記を使用した方法

mylist = [1,2,3,4,5,6,7,8,9]
newlist = [n*2 for n in mylist]
print(newest)
[2, 4, 6, 8, 10, 12, 14, 16, 18]

個人的には一番好きです。最もPythonらしい書き方かと思います。

map関数を使用した方法1

def double(n):
     return n*2

mylist = [1,2,3,4,5,6,7,8,9]
newlist = map(double, mylist)
print(newlist)
[2, 4, 6, 8, 10, 12, 14, 16, 18]

Pythonではあまりmap関数は多用しないかと思いますが、他の言語から入ってきた方にはわかりやすいかもしれません。

map関数を使用した方法2(ラムダ式で無名関数を定義する)

mylist = [1,2,3,4,5,6,7,8,9]
newlist = map(lambda x: x*2, mylist)
print(newlist)
[2, 4, 6, 8, 10, 12, 14, 16, 18]

適用する関数の内容がシンプルな場合には、ラムダ式で十分かと思います。