IT技術で仕事を減らしたい!

ITエンジニアのメモ+α

Python Seleniumスクレイピング

どうも、nippa です。

スクレイピングの必要性が出てきたので、Selenium で Web 操作を自動化したいと思います。

Web 開発でのモンキーテストにも使えるので、知っていて損はないかと思います。

Web サイトによってはスクレイピングが禁止されているところもありますので、ご注意ください。

環境

パッケージ管理

パッケージは poetry を使って管理していきます。利用するパッケージは

の2つです。

以前までは、PC にインストールされている Chrome のブラウザのメジャーバージョンを調べ、そのバージョンにあったドライバを用意する必要がありました。

chromedriver-binary-autoは自動でインストールされている Chrome のバージョン取得して、それに合うドライバをインストールしてくれます。

以下のコマンドでインストールを行います。

poetry add selenium chromedriver-binary-auto

スクレイピング

今回は自動で Google 検索を行って、検索内容について 100 件取得することを題材とします。

SeleniumGoogle.com の表示

まずは、Selenium の簡単な使い方になります。 url を指定して、Chrome で開き、5 秒スリーブごにブラウザを閉じるコードになります。

import time

import chromedriver_binary  # noqa
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


if __name__ == "__main__":
     base_url = "https://www.google.com/"

    url = f"{base_url}/"

    options = Options()

    # Browser
    driver = webdriver.Chrome(options=options)
    driver.get(url)

    time.sleep(5)

    driver.close()

SeleniumGoogle 検索

Google 検索で、「python selenium」を検索してみます。

スクレイピングでは html の構造を理解している必要があります。

Chrome であれば、Developer Tools を使うことで、html の構造を確認することができます。

また、beautifulsoup4 ライブラリを使えば html の構造ごと取得することもできます。

import sys
import time

import chromedriver_binary  # noqa
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

if __name__ == "__main__":
    base_url = "https://www.google.com/"

    url = f"{base_url}/"

    options = Options()

    driver = webdriver.Chrome(options=options)
    driver.get(url)

    name = "q"  # elementのname
    search_word = "python selenium"  # 検索するワード
    try:
        marker = driver.find_element(By.NAME, name)  # elementの取得
        marker.send_keys(search_word)  # elementに検索ワードの送信
        marker.send_keys(Keys.ENTER)  # elementにenterのkeyコマンドを送信

    except Exception as err:
        print(err)
        sys.exit(1)

    time.sleep(5)

    driver.close()

Google.com の検索用の input の name はqのため name を指定して、element を取得しています。

取得した element に検索するワードを送り、Enter key コマンドを送付して、検索を行います。

検索結果表示後、5秒間スリーブして、ブラウザを閉じるという動作になっています。

検索結果のタイトルと URL の取得

xpathを使って、検索結果のタイトルと URL を取得します。

google の検索結果の html の構造は以下のようになっています

...
<dvi id="search">
  <dvi>
    ...
    <a hred="リンク">
      <h3>タイトル</h3>
    </a>
    ...
  </dvi>
</dvi>

このときのタイトルを取得するためのxpathの書き方としては、//div[@id='search']//a/h3となります。

//は複数の element 意味します。

また親の element を取得する場合は..で一つ上のxpathを意味します。

import sys
import time

import chromedriver_binary  # noqa
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

if __name__ == "__main__":
    base_url = "https://www.google.com/"

    url = f"{base_url}/"

    options = Options()

    # Browser
    driver = webdriver.Chrome(options=options)
    driver.get(url)

    name = "q"
    search_word = "python selenium"

    try:
        marker = driver.find_element(By.NAME, name)
        marker.send_keys(search_word)
        marker.send_keys(Keys.ENTER)

        xpath = "//div[@id='search']//a/h3"  # 検索結果のtitleのxpath
        title_elements = driver.find_elements(By.XPATH, xpath)  # 結果の取得

    except Exception as err:
        print(err)
        sys.exit(1)

    for title_element in title_elements:
        if len(title_element.text) > 0:
            # 検索結果のtitleの親のxpathを取得
            href_element = title_element.find_element(By.XPATH, "..")
            # 出力
            print(
                f'titele={title_element.text.replace(" ...", "")} url={href_element.get_attribute("href")}'
            )

    time.sleep(5)

    driver.close()

これで検索 1 ページ目の検索結果のタイトル(一部)と URL を取得することができます。

複数のページの検索結果のタイトルと URL の取得

複数のページの検索結果を取得する場合、ページ移動を挟んで取得していきます。

ページ移動にはリンクの URL を取得して、そのページをロードする形にし、そのページが表示されたら、データを取得します。

xpath = "//div[@id='botstuff']//a[@id='pnnext']"
next_element = driver.find_element(By.XPATH, xpath)
next_page_url = next_element.get_attribute("href")
driver.get(next_page_url)

実際に pythonスクリプト化すると以下のようになります。

以下ではpython seleniumという検索ワードで 10 ページ分の検索結果をタイトル、url を表示するものになっています。

また、--headlessオプションを利用しています。

from typing import List, TypedDict, Optional
import sys
import time

import chromedriver_binary  # noqa
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys


class ResultData(TypedDict):
    title: Optional[str]
    url: Optional[str]


def get_title_and_url(driver: webdriver.Chrome) -> List[ResultData]:
    data: List[ResultData] = []

    try:
        xpath = "//div[@id='search']//a/h3"
        title_elements = driver.find_elements(By.XPATH, xpath)
    except Exception as err:
        print(err)
        sys.exit(1)

    for title_element in title_elements:
        if len(title_element.text) > 0:
            href_element = title_element.find_element(By.XPATH, "..")
            result_data: ResultData = {
                "title": title_element.text.replace(" ...", ""),
                "url": href_element.get_attribute("href"),
            }

            data += [result_data]

    return data

if __name__ == "__main__":
    base_url = "https://www.google.com/"

    url = f"{base_url}/"

    options = Options()
    options.add_argument("--headless")

    # Browser
    driver = webdriver.Chrome(options=options)
    driver.get(url)

    search_word = "python selenium"
    max_page = 10

    try:
        name = "q"
        marker = driver.find_element(By.NAME, name)
        marker.send_keys(search_word)
        marker.send_keys(Keys.ENTER)

    except Exception as err:
        print(err)
        sys.exit(1)

    data: List[ResultData] = []

    for i in range(max_page):
        data += get_title_and_url(driver)

        time.sleep(1)
        if i < max_page - 1:
            xpath = "//div[@id='botstuff']//a[@id='pnnext']"
            next_element = driver.find_element(By.XPATH, xpath)
            next_page_url = next_element.get_attribute("href")
            driver.get(next_page_url)

    driver.close()
    for _data in data:
        print(f'titele={_data["title"]} url={_data["url"]}')

感想

今回、Google の検索結果をスクレイピングselenium を使って書いてみました。

Web テスト用として利用する準備として、今回試しに使ってみています。

スクレイピングはサイトによっては禁止されていますので、サイトのルールに則ってください。

ではでは、また次回。

Docker Swarmモードの設定

どうも、nippa です。

docker をマルチサーバで共通して使いたいと思い、 swarm モードを利用してみようと思います。

今回は swarm モードの設定をになります。

Docker Swarm モードとは

docker エンジンを使った、クラスター管理モードです。マルチサーバ上で、docker を利用する場合に適しています。

また、docker ベースなので、サーバ環境の移行や、スケールリングが比較的容易にできる点が非常に便利です。

環境

Virtual Box 上に Linux サーバを 2 台構築

  • ubuntu 20.04
  • メモリ:2GB
  • ストレージ: 10GB

サーバは

  • Manager サーバ(manager node + work node)
  • Worker サーバ(work node)

と定義します。()の中は swarm モードでの node 構成を示しています。

ホストの登録

サーバが相互でホスト参照できるように/etc/hostsにホスト情報を登録しておきます。

sudo vim /etc/hosts

# 以下の形式で両サーバに登録する
[ホスト1のIP] [ホスト名1]
[ホスト2のIP] [ホスト名2]

Manager サーバ Swarm モードの設定

Swarm がアクティブになっているかを確認します。

以下のコマンドを実行して、Swarm: activeになっているかを確認します。

docker system info | grep Swarm

Swarm: inactiveの場合は、以下のコマンドで swarm モードをアクティブにします。

docker swarm init

ネットワーク接続が 2 つ以上の場合は、swarm モードで利用する IP を指定する必要があります。

docker swarm init --advertise-addr [IPアドレス]

swarm モードがアクティブになると、以下のような出力がされます。

Swarm initialized: current node ([node ID]) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token [SWARMトークン] [IPアドレス]:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

node の設定状況は

docker node ls

で確認できます。

これで Manager サーバの設定は完了になります。

Worker サーバの追加

ホスト間の通信プロトコルは以下のようになっています。

ポート 通信方式 用途
2377 TCP クラスター管理における通信
7946 TCP ノード間の通信のため
4789 UDP オーバーレイネットワークのトラフィックのため
50 EPP オーバーレイネットワークを暗号化(--opt encrypted)で利用する場合

ホスト間でのプロトコルとポートの開放

上記については、サーバ間の通信の許可しておく必要があります。

worker サーバで、swarm モードがアクティブになった際に表示されたコマンドを実行して、swarm モードに worker サーバを追加します。

# workerサーバ
docker swarm join --token  [SWARMトークン] [IPアドレス]:2377

worker サーバが複数ある場合は、各サーバで swarm モードへの追加をコマンドを実行してください。

トークン、IP の確認は、Manager サーバで

# managerサーバ
docker swarm join-token manager

を実行すると確認できます。

また、node が追加されたことは、manager サーバで確認できます。

# managerサーバ
docker node ls

# 出力
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
rnlr2ria5qdkz07uspzgnm5lq *   manager    Ready     Active         Leader           20.10.14
z73grf2bdca0mkzz8j44ep7sm     worker     Ready     Active                          20.10.14

サービスのデプロイ

docker service createでサービスを作成します。

docker service create --replicas 1 --name helloworld alpine ping docker.com

manager サーバでサービスの状態の確認をします。

# managerサーバ
docker service ls

サービスの詳細は以下で確認します。

docker service inspect --pretty [サービス名]

サービスの詳細は以下で確認します。

docker service ps helloworld

サービスのスケール変更

サービスのスケール変更は以下のコマンドで行います。

# スケールの変更
docker service scale <サービスID>=<タスク数>

# helloworld を実行する node を5とする場合
docker service scale helloworld=5

サービスのノードが変わったことは以下で確認できます。

docker service ps helloworld

サービスの削除

以下のコマンドでサービスを削除します。

docker service rm helloworld

サービスが削除されるとサービス詳細を確認すると、エラーになることを確認します。

docker service inspect helloworld

# 出力
[]
Error: no such service: helloworld

感想

今回 docker swarm モードについてまとめました。

クラスターで利用する場合に、非常に便利なモードです。

swarm モードの実践的なデプロイ方法やネットワーク関連をまとめたいと思います。

ではでは、また次回。

Python Poetry AssertionErrorエラー解消

どうも、nippa です。

Poetry のパッケージ管理で、AssertionError が起きたので解決策をまとめておきます。

環境

  • macOS 11.6
  • poetry 1.1.14

AssertionError の原因

今回起きたのは、pyproject.tomlnameがパッケージ名と同じだったため、AssertionErrorが起きました。

name = "selenium"のときに、selenium をインストールしようとすると起きました。

[tool.poetry]
name = "selenium" <--ここ
version = "0.1.0"
description = ""
authors = ["authors <0000000+authors@users.noreply.github.com>"]

[tool.poetry.dependencies]
python = "^3.9"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

以下のように変更して、再度 selenium をインストールするとインストール可能になりました。

[tool.poetry]
name = "selenium-test" <--変更後
version = "0.1.0"
description = ""
authors = ["authors <0000000+authors@users.noreply.github.com>"]

[tool.poetry.dependencies]
python = "^3.9"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

感想

Poetry のAssertionErrorで悩まれている方の役に立てればと思い、記事に残しておきます。

ではでは、また次回。

Docker 一般ユーザーでのdockerコマンドの利用

どうも、nippa です。

Linux で docker をインストールすると初期設定では root ユーザーでのみでしか利用できません。

今回は、一般ユーザーで利用する方法をまとめておきたいと思います。

環境

  • ubuntu 20.04(VM 上にインストール)

docker のインストール

iso ディスクでの ubuntu 20.04 をインストールすると、インストール時のオプションとして docker をインストールすることができます。その場合、sanp でインストールされます。

snap でインストール場合は、以下のコマンドで docker をインストール可能です

sudo snap install docker

docker グループの作成・確認

docker グループが作成されているかを以下のコマンドで確認します。

cat /etc/group | grep docker

docker グループが作成されていない場合は、以下のコマンドで作成します。id は指定しなくても問題ありません。

sudo groupadd docker --gid [グループID]

グループの確認をします。

cat /etc/group | grep docker

ユーザーに docker グループを追加する

ユーザーのグループを確認します。

groups [ユーザー名]

docker グループに含まれてない場合は、

sudo usermod -aG docker [ユーザー名]

で追加します。

一般ユーザーで docker を実行する

グループ情報を変更したので、セッションを接続したまま情報を以下のコマンドで更新します。

newgrp docker

docker を実行します。

docker run hello-world

以下のようなエラーが出る場合は、sock の権限が正しくありません。

docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create": dial unix /var/run/docker.sock: connect: permission denied.

以下のコマンドで sock の権限を変更してください。

sudo chmod 666 /var/run/docker.sock

権限変更後、docker のテストコマンドを実行し、以下の結果が表示されれば、一般ユーザーでの実行が可能となったことになります。

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

感想

Linux を docker をインストールするといつも設定を忘れてしまうので、記事にまとめておきました。

一般ユーザーで使えた方が便利ですので、設定してみてください。

ではでは、また次回。

Python 人名データセットを使ってみる

どうも、nippa です。

膨大なテストデータを生成するために、人名のデータセットを使ってみようと思います。

今回試してみるのは、names-datasetになります。

README を見る限り、結構充実してるように思います。

このデータセットFacebook からリークされた情報から生成されていますが、個人が特定できません。

ライセンス部分には、一般的には”名前の一覧”には著作権はないとのことですが、必要なら弁護士に確認取ってねと書かれてます。

環境

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

  • pip インストール
pip install names-dataset
  • poetry インストール
poetry add names-dataset

依存関係は pycountry のみです。

使い方

データセットを初期化して、名前の検索、データの詳細確認、国ごとの名前を上位から表示ができます。

コードを見る限り、他の使い方はなさそうです。

名前の検索

from names_dataset import NameDataset, NameWrapper

# データセットの初期化
nd = NameDataset()

# 名前の検索
print(nd.search("Taro"))

結果 (整形済)は以下のようになります。

{
    "first_name": {
        "country": {
            "France": 0.05,
            "United Kingdom": 0.063,
            "Hong Kong": 0.046,
            "Iraq": 0.031,
            "Italy": 0.059,
            "Japan": 0.463,
            "Malaysia": 0.091,
            "Netherlands": 0.044,
            "Singapore": 0.041,
            "United States": 0.112,
        },
        "gender": {"Female": 0.11, "Male": 0.89},
        "rank": {
            "United Kingdom": 9740,
            "Hong Kong": 6121,
            "Japan": 156,
            "Malaysia": 13690,
            "Netherlands": 7182,
            "Singapore": 7227,
            "France": None,
            "Iraq": None,
            "Italy": None,
            "United States": None,
        },
    },
    "last_name": {
        "country": {
            "France": 0.106,
            "India": 0.054,
            "Italy": 0.147,
            "Japan": 0.038,
            "Morocco": 0.059,
            "Malaysia": 0.439,
            "Nigeria": 0.03,
            "Saudi Arabia": 0.036,
            "Singapore": 0.032,
            "United States": 0.059,
        },
        "gender": {},
        "rank": {
            "India": 9676,
            "Japan": 1748,
            "Malaysia": 4161,
            "Singapore": 6564,
            "France": None,
            "Italy": None,
            "Morocco": None,
            "Nigeria": None,
            "Saudi Arabia": None,
            "United States": None,
        },
    },
}

日本での first_name で 156 位と表示されています。first_name で性別の割合も出てます。

名前を Wrapper して詳細表示

NameWrapper を使うと、

print(NameWrapper(nd.search("Taro")).describe)

# 出力
Male, Japan

Taro は日本人男性というような形で表示されます。

国別の名前ランキング

日本人の男性トップ 5 を表示します。

print(nd.get_top_names(n=5, gender='Male', country_alpha2='JP'))

# 出力 (整形済)
{
    "JP": {
        "M": [
            "Takashi",
            "Hiroshi",
            "Yusuke",
            "Daisuke",
            "Akira",
        ]
    },
}

日本の男女トップ 3 をそれぞれ表示すると、

print(nd.get_top_names(n=3, country_alpha2="JP"))

# 結果(整形済)
{
    "JP": {
        "M": ["Takashi", "Hiroshi", "Yusuke"],
        "F": ["Yuki", "Yuka", "Yuko"],
    },
}

のようになります。

フルデータセット

インストール時は、50MB 程度のファイルしかないためフルデータを利用する場合は、

Github の README のフルデータセットのダウンロードからデータをダウンロードして、ライブラリの中の

names_dataset/v3/に配置すればフルサイズ(2.3GB)で利用できます。

ただし、50MB でも実行時間が少しかかるので、フルサイズは時間がかかることが予想されるのでご注意ください。

感想

データセットから、ランダムで人名生成するときには使えそうなライブラリです。

テストデータ生成などで役に立ちそうです。

ではでは、また次回。