プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

ハローワーク求人情報のスクレイピング(Selenium)【Python】

このエントリーをはてなブックマークに追加

ハローワークの求人情報を自動で検索するアプリをPythonの勉強のために作成しました。
SeleniumBeautifulSoup でWebスクレイピングをします。
HTML を解析して操作方法を検討し、その操作に対する応答を待機するようにしています。
結果はcsvファイルに出力します。

Firefox に加え Chrome の対応を追加しました(更新:2021-08-18)
Web ドライバの取得を自動化しました(更新:2023-02-21)
職種選択のHTMLの変更に対応しました(更新:2023-08-22)

アプリはお使いいただけます 📖 ハローワークの求人情報を自動で検索するアプリ(スクレイピング) 🔗

目次

◆成果物

成果物としてcsvファイルを出力します。エクセルで見ると次のようになります。

検索結果

◆できること

SeleniumBeautifulSoup の次のような機能を使用してWebスクレイピングをします。

  • Selenium

    • 画面の要素(ボタンなど)をクリックする操作
    • 画面の入力フィールドに値を入力する操作
    • 画面の選択肢から値を選択する操作
    • 画面の操作に対する応答の待機
  • BeautifulSoup

    • ページ情報の解析と取得
  • その他

追加:2021-08-18

◆考え方

ハローワークサイトを呼び出して、必要な条件を設定し検索します。
検索結果を解析して、csvファイルに出力します。
具体的には、次の通りです。

  1. Selenium】で検索の自動化

    1. ハローワーク求人情報検索サイト 』を呼び出す
    2. 「基本検索条件」を設定する(就業場所、職種以外)
    3. 「就業場所」を設定する
    4. 「職種」を設定する
    5. 「詳細検索条件」を設定する
    6. 検索する
  2. BeautifulSoup】で解析

    1. 検索結果を解析する
    2. 求人情報サマリーを取得する(tableタグ)
    3. 見出し情報を抽出する
    4. 1件ごとに求人情報を抽出する
  3. csv】に出力

    1. 抽出結果をcsvに出力する

◆必要なパッケージ

必要なパッケージ

  • beautifulsoup4
  • selenium
  • tqdm
  • webdriver-manager(追加:2023-02-21)

ここでは、PythonPython パッケージのインストールなど基本的な使い方は諸先輩に譲ります。

◆【 Selenium 】で検索の自動化

Seleniumハローワークサイトの検索の自動化を行うには次のようにします。

◇Webドライバの自動取得(webdriver-manager)

削除:2023-02-21

手動での取得方法は削除しました(参考のために残してあります) Seleniumスクレイピングするには、Webドライバが必要です。
ブラウザとしてChromeFirefoxに対応しています。
お使いのブラウザに合わせてWebドライバを取得します。
Chrome用Webドライバ(実際にはchromedriver)、または
Firefox用Webドライバ(実際にはgeckodriver)をダウンロードして解凍し、exeを任意のフォルダに保存します。

◎取得先

こちらから取得します。

お手数ですが… Webドライバが再配布可能なのかどうか、良く理解できなかったので、別途ダウンロードをお願いします。

Seleniumスクレイピングするには、Webドライバが必要です。
本アプリは、ブラウザとしてChromeFirefoxに対応しています。1
使用するブラウザに合わせて Web ドライバを取得します。
Web ドライバは、webdriver-manager パッケージを使用すると使用するブラウザのバージョンに合ったドライバを自動でダウンロードしてくれます。
Chrome ブラウザは、頻繁にバージョンアップされ、そのバージョンに合った Web ドライバが必要になりますが、それも自動で取得します。
ここでは、webdriver-manager パッケージを使用します。
Web ドライバは、ユーザーのホーム\.wdm フォルダに保存されます。
更新:2023-02-21

【参考のソース(Chromeの場合)】

# selenium 3
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())

# selenium 4
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))

◇サイト呼出

ここからは実装方法の説明です。
まず、Webドライバのインスタンスを作成し、ハローワークサイトを呼び出します。

【処理】

  1. seleniumパッケージをインポート(from selenium import webdriver

  2. ChromeまたはFirefoxインスタンス化(参考:ブラウザーのドライバーをインストールする | Selenium

    • Webドライバの場所を指定(executable_path=)
    • オプションがある場合、オプションクラスのインスタンスを指定(options=)
      • ヘッドレスモード(ブラウザ画面を出さないモード)はオプションで指定 (options.headless = True)
      • オプションにはインポートimport Optionsが必要
  3. get(url)メソッドでWebを開く(参考:Browser interactions | Selenium

  4. 画面が表示されるのを待機する(参考:待機 | Selenium

    • WebDriverWaitクラスのインスタンスを作成
    • 第2引数で最大待ち時間を指定すると良い
    • インポート import WebDriverWait が必要
    • 求人情報検索画面が表示されるまで待機(wait.until( Ec.title_contains( "求人情報検索")))
      インポート import expected_conditions as Ec が必要
      どうやって待つかここでは、指定したテキストがタイトルに表示されるまで待ちます。待ち方はいろいろあります。他の待ち方はこちら⤵
      考え方に戻る⤴

【この部分のソース】

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as Ec
from selenium.webdriver.support.ui import WebDriverWait
# Webドライバーに依り対象ブラウザを変える
if settings.executable_path.endswith("geckodriver.exe"):
    from selenium.webdriver.firefox.options import Options
    from webdriver_manager.firefox import GeckoDriverManager
else:
    from selenium.webdriver.chrome.options import Options
    from webdriver_manager.chrome import ChromeDriverManager

# ブラウザーを起動
options = Options()             # オプションインスタンス作成
if not (flag_b or settings.flag_b):
    options.headless = True     # ヘッドレスモード(ブラウザを見せない)
# Webドライバーに依り対象ブラウザを変える
if settings.executable_path.endswith("geckodriver.exe"):
    browser = webdriver.Firefox(executable_path=GeckoDriverManager().install(), options=options)  # ブラウザインスタンス作成
else:
    options.add_argument("--disable-software-rasterizer")   # Chromeではこれを付けないとエラー(kFatalFailure)になる(理由はよくわからない)
    browser = webdriver.Chrome(executable_path=ChromeDriverManager().install(), options=options)  # ブラウザインスタンス作成

# 待機
wait = WebDriverWait(browser, 15)  # Timeout 15秒(最大待ち時間)

# ハローワーク検索画面にアクセス
print("start browsing")
browser.get(url)
# 求人情報検索画面が表示されるまで待機
wait.until(Ec.title_contains("求人情報検索"))
print("got url, start selecting")

更新:2023-02-21

◇基本検索条件設定

ハローワークサイトの「基本検索条件」に条件を設定します。

selenium では、次の操作などを実行することができます。

  • 要素をクリックする操作
  • キーボード入力する操作
  • 要素を選択する操作

ハローワークサイトでは、入力項目に対してHTMLのタグにIDが振られています。
これらを利用して基本検索条件を設定します。
また、一つずつ設定するのではなく、同じ操作の条件をまとめて設定します。
(同じ処理は何度も書きたくないですからね)

前処理としてHTMLから操作対象のタグを抽出し、辞書として作成しておきます。具体的には、こちら⤵を参照

【処理】

  1. クリック操作で設定する項目の設定

    • HTMLから抽出したIDでクリック操作対象のものを辞書として抽出
      クリック操作の対象のものは、辞書から型が bool のものを抽出
      _kensaku = {k: v for k, v in settings._kensaku.items() if v and type(v) is bool}
      辞書のitems()メソッドは、キーと値を返す

    • 抽出した辞書で繰り返し設定
      IDを指定して要素を見つけて、その要素のクリック操作を行う
      find_element_by_id( id ).click()

  2. キーボード入力操作で設定する項目の設定

    • HTMLから抽出したIDでキーボード入力操作対象のものを辞書として抽出
      キーボード入力操作の対象のものは、辞書から型が str のものを抽出
      _kensaku = {k: v for k, v in settings._kensaku.items() if v and type(v) is str}

    • 抽出した辞書で繰り返し設定
      IDを指定して要素を見つけて、その要素のキーボード入力操作を行う
      find_element_by_id( id ).send_keys(v)

  3. 要素を選択する操作で設定する項目の設定(詳細検索条件設定でのみ実装)

    • HTMLから抽出したIDで要素選択操作対象のものを辞書として抽出
      要素選択操作の対象のものは、辞書から型が list のものを抽出
      _detail = {k: v for k, v in settings._shosai_settei.items() if v and type(v) is list}

    • 抽出した辞書で繰り返し設定
      IDを指定して要素を見つけて、その要素の選択操作を行う
      前の二つと区別するためにリスト型にしたので値は第1要素で指定
      Select( browser.find_element_by_id( id ) ).select_by_visible_text( v[0])

辞書からの抽出… 辞書からの抽出は、内包表記を使用しています。Pythonの勉強を始めてから、内包表記を気に入って使っています。余談でした。

考え方に戻る⤴

【この部分のソース】

    # 検索条件の設定 クリックするもの
    # sttingsから値がTrueで設定されているもので辞書を作成
    _detail = {k: v for k, v in settings._kensaku.items() if v and type(v) is bool}
    for id in _kensaku:
        browser.find_element_by_id(id).click()  # チェックボックスをオンにする

    # 検索条件の設定 キー入力するもの
    # sttingsから値が文字列で設定されているもので辞書を作成
    _detail = {k: v for k, v in settings._kensaku.items() if v and type(v) is str}
    for id, v in _kensaku.items():
        browser.find_element_by_id(id).send_keys(v)     # 設定文字列をセットする

◇就業場所の設定

ハローワークサイトの「基本検索条件」の「就業場所」を設定します。

【実操作】

  • 都道府県をドロップダウンメニューから選択し、次に必要なら、市町村を選択
  • 市町村は、「選択」ボタンをクリックし、「住所 選択画面」が出て、そこで選択

この操作に合わせた処理にします。

都道府県と市町村の選択方法】

  • 設定ファイルに検索したい都道府県と市町村を設定
  • 設定値は、画面に表示されている文字列をそのまま設定
  • selenium には表示されている文字列で選択するメソッド select_by_visible_text() がある。
    このメソッドで設定ファイルで指定した項目を選択

【「就業場所」タグの抽出方法】

  • 「就業場所」は select タグで実装されている(HTML から)
  • select タグが使用されているのは、「就業場所」だけなのを確認
  • 従って、seleniumselect タグをすべて取得し、就業場所のタグを取得

【処理】

  1. select タグをすべて取得し、ID順にソート

    sels = browser.find_elements_by_tag_name( "select" )
    sels = sorted(sels, key=lambda x: x.get_attribute("id"))

  2. ボタンのタグを取得

    1. ボタンのタグをすべて取得(「市町村選択」ボタン以外にも存在する)
      find_elements_by_css_selector( "input.button" )
    2. ボタンのタグから「市町村選択」ボタンを抽出(value属性が「選択」のもの)
      [x for x in buttons if x.get_attribute("value") == "選択"]
    3. onclick 属性順にソート(selectタグと同期させるため)
      sorted(btns, key=lambda x: x.get_attribute("onclick"))
  3. 「市町村選択」ボタンのタグと設定ファイルの都道府県を同期させて、ループ処理

    for _sel, _btn, _tdk in zip(sels, btns, settings.tdks):

    1. 都道府県を選択
      Select( _sel ).select_by_visible_text( _tdk[0] )
      _tkd の先頭が都道府県
      インポート import Select が必要
      Select クラスのインスタンスを作成してメソッドで選択する
    2. 「市町村選択」ボタンをクリック _btn.click()
    3. 「市町村選択画面」が出るのを待機
      wait.until( Ec.element_to_be_clickable( (By.ID, "ID_rank1CodeMulti")))
      インポート import By が必要
    4. 市町村選択項目から設定ファイルで指定したものを選択
    5. OK ボタンを押す
    6. 画面が閉じるまで待機

考え方に戻る⤴

【設定ファイルの一部】

# 都道府県 3つまで 市町村は5つまで
tdks = [
        ["東京都", "千代田区"]
        , ["埼玉県"]
        , ["千葉県"]
        ]

【この部分のソース】

    # 就業場所の設定
    # 都道府県のselectタグを取得して、ID順にする。
    sels = browser.find_elements_by_tag_name("select")          # selectタグは都道府県のみ
    sels = sorted(sels, key=lambda x: x.get_attribute("id"))    # id属性でソート
    # 市町村選択ボタンのタグを取得して、onclick属性順にする。selectタグと同期させるため。
    # inputタグでvalueが選択を抽出、onclick属性でソート
    buttons = browser.find_elements_by_css_selector("input.button")         # 他のボタンも含まれる
    btns = [x for x in buttons if x.get_attribute("value") == "選択"]       # valueで選別可能
    btns = sorted(btns, key=lambda x: x.get_attribute("onclick"))           # onclick属性にIDが含まれる
    # 就業場所3か所の設定
    for _sel, _btn, _tdk in zip(sels, btns, settings.tdks):
        Select(_sel).select_by_visible_text(_tdk[0])
        if len(_tdk) > 1:   # 市町村を選択する場合
            _btn.click()    # 市町村選択画面表示
            # 選択画面が表示されるまで待つ
            wait.until(Ec.element_to_be_clickable((By.ID, "ID_rank1CodeMulti")))
            element = browser.find_element_by_id("ID_rank1CodeMulti")
            for _city in _tdk[1:]:
                Select(element).select_by_visible_text(_city)
            browser.find_element_by_id("ID_ok").click()    # OKをクリック
            # 選択画面が閉じるまで待つ
            wait.until(Ec.invisibility_of_element_located((By.ID, "ID_ok")))

◇職種の設定

ハローワークサイトの「基本検索条件」の「希望する職種」を設定します。

職種選択のHTMLの変更に対応しました(本節は全体的に書き換えました)
更新:2023-08-22

【実操作】

  • 「職種を選択」ボタンをクリック
  • 表示される「職種 選択画面」で大分類の項目の「✚」をクリック
  • 開いた選択肢(チェックボックス)にチェックを付ける

この操作に合わせた処理にします。

【大分類と詳細の選択方法】

  • 設定ファイルに検索したい大分類と詳細を設定
  • 設定値は、画面に表示されている文字列をそのまま設定
  • 大分類は ac_header クラスのタグのテキストで選択します
  • 詳細は、大分類と兄弟のタグの子孫に label タグがあり、そのテキストで選択します

【「職種の選択」ボタンに対応したタグ、大分類、詳細の対応タグの抽出方法】

  • 「職種の選択」ボタン
    1. buttom クラスを持つ button タグでテキストを「職種を選択」として実装されています(HTML から)
    2. buttom クラスを持つ button タグを抽出
    3. 更にテキストが「職種を選択」のものを抽出
  • 「職種 選択画面」の大分類
    1. ac_header クラスを持つ button タグにテキストを大分類名として実装されています(HTML から)
    2. ac_header クラスを持つタグを抽出
    3. 更にテキストが指定した大分類のものを抽出
    4. 「✚」をクリックして拡大するために、子孫から i_box クラスを持つタグを抽出し、クリックします
  • 「職種 選択画面」の詳細
    1. 大分類をクリックすると、ac_headeropen クラスを持つタグが現れます
      その兄弟タグに ac_inner クラスを持つタグが現れます
      その子孫に詳細な項目名を持った label タグとして実装されています(HTML から)
    2. ac_headeropen クラスを持ち(一つだけしかない)、その兄弟タグで ac_inner クラスを持つタグを抽出
    3. その子孫から label タグでテキストが指定した詳細項目のものを抽出
    4. チェックを付けるために、子孫から input タグを抽出し、クリックします

【処理】

  1. ボタンのタグを取得

    1. button クラスを持つ button タグで text が「職種を選択」を抽出
    2. onclick 属性順にソート(selectタグと同期させるため)
  2. 「職種選択」ボタンのタグと設定ファイルの職種を同期させて、ループ処理

    1. 「職種を選択」ボタンをクリック
    2. 「職種 選択画面」が出るのを待機
      CSS セレクタmodaltop クラスを持つタグの表示を待つ
    3. 「大分類」から設定ファイルで指定したものを開く
    4. 「詳細項目」が出るのを待機
      CSS セレクタac_headeropen クラスを持つタグの表示を待つ
    5. 「詳細項目」から設定ファイルで指定したものにチェックを付ける
  3. OK ボタンを押す

  4. 画面が閉じるまで待機

考え方に戻る⤴

【設定ファイルの一部】

# 職種 3つまで
sksus = "技術職(建設、開発、IT)、専門職", "ソフトウェア開発技術者、プログラマー", "その他の情報処理・通信技術者"]
        , ["事務、管理職", "一般事務、事務補助"]
        ]

【この部分のソース】

    # 職種
    # buttonタグでtextが「職種を選択」を抽出、onclick属性でソート
    buttons = browser.find_elements_by_css_selector("button.button")         # 他のボタンも含まれる
    btns = [x for x in buttons if x.text == "職種を選択"]
    btns = sorted(btns, key=lambda x: x.get_attribute("onclick"))

    # 職種3か所の設定
    for  _btn, _sksu in zip(btns, settings.sksus):
        _btn.click()    # 職種選択画面表示
        # 選択画面が表示されるまで待つ
        wait.until(Ec.text_to_be_present_in_element((By.CSS_SELECTOR, '.modal.top'), '職種選択画面'))
        # 大分類選択 「✚」をクリック
        elems = browser.find_elements_by_class_name("ac_header")    # 大分類のタグを取得
        _elem_oya = [x for x in elems if x.text == _sksu[0]][0]     # 指定した大分類を抽出
        _elem = _elem_oya.find_element_by_class_name('i_box')       # eventの割り付いている子要素を取得
        _elem.click()   # クリックして開く
        # 詳細が出るまで待つ
        wait.until(Ec.presence_of_element_located((By.CSS_SELECTOR, ".ac_header.open")))
        # 詳細は大分類と兄弟のタグ 開いている大見出しはクラスが「ac_header open」になる(一度に一つだけ)
        _elem_oya = browser.find_element_by_css_selector(".ac_header.open + .ac_inner")
        for _item in _sksu[1:]:
            # labelタグのテキストで項目を探し、子要素のinputタグをクリックしてチェックを付ける
            _elem = [x for x in _elem_oya.find_elements(By.TAG_NAME, 'label') if x.text == _item][0]
            _elem = _elem.find_element(By.TAG_NAME, 'input')
            _elem.click()
        browser.find_element_by_id("ID_ok3").click()    # OKをクリック
        # 選択画面が閉じるまで待つ
        wait.until(Ec.invisibility_of_element_located((By.ID, "ID_ok3")))

◇詳細検索条件設定

ハローワークサイトの「詳細検索条件」に条件を設定します。

詳細検索条件の設定は、詳細検索条件画面を出した後、基本検索条件の設定と同じように対応します。基本検索条件を参照⤴

考え方に戻る⤴

【この部分のソース】

    # 「詳細検索条件」をクリック
    browser.find_element_by_id("ID_searchShosaiBtn").click()
    # 選択画面が表示されるまで待つ
    wait.until(Ec.visibility_of_element_located((By.ID, "ID_saveCondBtn")))

    # 詳細検索条件の設定
    # 詳細検索条件の設定 クリックするもの
    # sttingsから値がTrueで設定されているもので辞書を作成
    _detial = {k: v for k, v in settings._shosai_settei.items() if v and type(v) is bool}
    for id in _detial:
        browser.find_element_by_id(id).click()  # チェックボックスをオンにする

    # 詳細検索条件の設定 キー入力するもの
    # sttingsから値が文字列で設定されているもので辞書を作成
    _detial = {k: v for k, v in settings._shosai_settei.items() if v and type(v) is str}
    for id, v in _detial.items():
        browser.find_element_by_id(id).send_keys(v)     # 設定文字列をセットする

    # 詳細検索条件の設定 要素選択するもの
    # sttingsから値がリストで設定されているもので辞書を作成
    _detial = {k: v for k, v in settings._shosai_settei.items() if v and type(v) is list}
    for id, v in _detial.items():
        Select(browser.find_element_by_id(id)).select_by_visible_text(v[0])     # 設定文字列をセットする

◇検索

検索を開始し、検索が終わるのを待機します。
検索結果が0件の場合、表示内容が異なるため、それを考慮します。

【処理】

  1. 「検索」ボタンをクリック

  2. 検索完了を待機
    2つの条件で待機する場合、 until() メソッドは引数にメソッドを取るため lamda 式で2つの待機メソッドの or を取るメソッドにします。

    • 検索結果が0件でない場合:「表示件数」が表示されるのを待機
    • 検索結果が0件の場合:「ご希望の条件に合致する情報は見つかりませんでした。」が表示されるのを待機
  3. 検索結果が0件の場合、以降の処理を実施しないように例外を立てる。

考え方に戻る⤴

【この部分のソース】

    # 「OK」をクリック
    browser.find_element_by_id("ID_saveCondBtn").click()
    # 選択画面が閉じるまで待つ
    wait.until(Ec.invisibility_of_element_located((By.ID, "ID_saveCondBtn")))
    print("selected, start job search")

    # 「検索」をクリック
    browser.find_element_by_id("ID_searchBtn").click()
    # 0件の時「ご希望の条件に合致する情報は見つかりませんでした」が出る
    # 表示件数選択肢がクリックできるようになるまで待つ
    wait.until(lambda x: 
        Ec.element_to_be_clickable((By.ID, "ID_fwListNaviDispTop"))
                or Ec.text_to_be_present_in_element((By.ID, "msg_area"), "ご希望の条件に合致する情報は見つかりませんでした。"))

    try:    # 検索結果が0件の場合、ID_fwListNaviDispTopが見つからないので例外を出す
        _sel = browser.find_element_by_id("ID_fwListNaviDispTop")
    except NoSuchElementException:
        raise Exception("ご希望の条件に合致する情報は見つかりませんでした")

◇設定項目を整理する

ハローワークサイトのHTMLを確認すると、<input>タグとtype属性で部品を指定していることが分かります。
また、IDが一意に振られています。
これらを利用して基本検索条件をひとつずつコードで指定するのではなく、ロジックで繰り返し処理できるようにします。
そのために、HTMLから<input>タグとtype属性を抽出したものを用意し、それを辞書として作成します。
また、抽出したものは、設定ファイルとして作成します。そうすれば、実行前に変更できます。
設定ファイルは、一意であるIDをキーとします。
設定値はtype属性を元に初期値を設定します。

  • 属性 text""
  • 属性 radioFalse
  • 属性 checkboxFalse
  • 属性 select[]

ハローワークサイトのHTMLの一部】

<div class="fs1_5">
    <div>基本検索条件</div>

</div>
<table class="normal mb1">
    <tr>
        <th scope="row">求人区分<span
                  class="nes_label nes1 nes">必須</span></th>
        <td class="nes2"><span
                  class="nes_label nes">必須</span></td>
        <td class="iew">
            <div id="ID_kjKbnRadioBtn"
                 name="kjKbnRadioBtn"
                 class="flex input align_center mb03">
                <div>
                    <div class="flex align_center mb05">
                        <div class="radio"><label
                                   id="ID_LkjKbnRadioBtn1"
                                   for="ID_kjKbnRadioBtn1"><input
                                       type="radio"
                                       id="ID_kjKbnRadioBtn1"
                                       name="kjKbnRadioBtn"
                                       value="1"
                                       checked>一般求人</label>
                        </div>
                        <div id="ID_ippanCKBox"
                             name="ippanCKBox"
                             class="flex input align_center mb03">
                            <div></div>

                            <div class="checkbox"><label
                                       id="ID_LippanCKBox1"
                                       for="ID_ippanCKBox1"><input
                                           type="checkbox"
                                           id="ID_ippanCKBox1"
                                           name="ippanCKBox"
                                           value="1">フルタイム</label>
                            </div>

【設定ファイルの一部】

_kensaku = {   
      "ID_kSNoJo": ""                     # text 求人番号
    , "ID_kSNoGe": ""                     # text 求人番号
    , "ID_kjKbnRadioBtn1": False          # radio 一般求人
    , "ID_ippanCKBox1": True              # checkbox フルタイム
    , "ID_ippanCKBox2": False             # checkbox パート

◎設定ファイルについて

アプリケーションが使用する設定ファイルの管理には次のようなものがあります。
ここでは、settings.pyを使う方法で実現しています。

なぜか… jsonもiniファイルも設定ファイルのためにロジックが必要になるのでsettings.pyにしました。


管理方法の種類 説明 記述方法
pyファイルから読み込む(settings.py) import settingsと入れるだけ pythonの文法で記述
jsonファイルから読み込む(.json) ファイルをopenし、json.load()を実行 json形式
iniファイルから読み込む(ConfigParser) ConfigParserライブラリを使用
Defaultとセクション管理ができる

◇ロケータ

Selenium では待機や値の取得などに要素を使用します。
要素はロケータで指定します。
ロケータはその種類と値のタプルで指定します。
種類は次のインポートを行うと選択できます。
from selenium.webdriver.common.by import By

種類の名前 内容
ID "id"属性を用いて要素を指定
XPATH "xpath"を用いて要素を指定
LINK_TEXT a要素の内容を用いて要素を指定
PARTIAL_LINK_TEXT a要素の内容に含まれるかを判断して要素を指定
NAME "name"属性を用いて要素を指定
TAG_NAME HTML のタグ名を用いて要素を指定
CLASS_NAME クラス名を用いて要素を指定
CSS_SELECTOR css セレクタを用いて要素を指定
追加:2022-05-08

◇待機

WebDriverWaitクラスの until メソッドを用い、引数で指定した条件になるまで待機します。
今回使用した待機方法は次の通りです。
他にもいくつか待機方法があります。詳しくはこちらを参照:Selenium API(逆引き)

◎待機一覧

待機方法 メソッド 引数
Alertが表示されるまで待機 alert_is_present なし
要素がチェックONまたはチェックOFFになるまで待機 element_selection_state_to_be エレメント、セレクト状態
要素がクリック出来る状態になるまで待機 element_to_be_clickable ロケータ
指定した要素が表示されるまで待機 visibility_of_element_located ロケータ
指定した要素が非表示になるまで待機 invisibility_of_element_located ロケータ
指定したテキストが表示されるまで待機 text_to_be_present_in_element ロケータ、文字列
特定文字列を含むページタイトルを取得するまで待機 title_contains 文字列
特定文字列を含むURLを取得するまで待機 url_contains 文字列

▽使用例
wait.until(Ec.text_to_be_present_in_element((By.ID, "ID_rank2Codes"), "こだわらない"))
text_to_be_present_in_element() メソッドの第一引数はタプルなのに注意

更新:2022-05-08

◇import

文章中では、省略して書いていたのでここにまとめておきます。

◎importのまとめ

from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as Ec
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import TimeoutException

# Webドライバーに依り対象ブラウザを変える
if settings.executable_path.endswith("geckodriver.exe"):
    from selenium.webdriver.firefox.options import Options
    from webdriver_manager.firefox import GeckoDriverManager
else:
    from selenium.webdriver.chrome.options import Options
    from webdriver_manager.chrome import ChromeDriverManager

更新:2023-02-21

◇Chomeブラウザの対応

当初、Firefoxだけの対応だったので、トップシェアのChromeにも対応しました。
その時の対応内容です。

  • Optionsのインポートの切り分け
  • Webドライバインスタンス作成の切り分け
  • 職種設定に待機を追加
    ブラウザの処理が遅いのかエラーになってしまったため対応

追加:2021-07-04

◆【 BeautifulSoup 】で解析

Beautifull Soupを使用し find() メソッドでタグを取得し、 get_text() メソッドで文字列を取得します。

◇検索結果の解析

Beautifull SoupでHTMLの処理を行うための前処理としての検索結果の解析を行います。
Selenium で取得した HTMLBeautifulSoup に渡すだけです。

BeautifulSoup(browser.page_source, "html.parser")

◇求人情報サマリーの取得

ハローワークサイトの検索結果の HTML は検索結果を求人ごとに一つの table タグで実装しています。このタグを求人の数だけ列挙しています。
また、そのタグには kyujin クラス属性が設定されています。

クラス属性について HTMLを見ると class="kyujin mt1 noborder" と記述されています。
クラス属性は、空白で区切って列挙できるため、
kyujinクラス属性、mt1クラス属性、noborderクラス属性を持つということになります。
この意味を知るのに結構手間取りました。(汗)

従って、kyujin クラス属性を持つ table タグを取得します。
find_all() メソッドで複数ある table タグを取得します。

soup.find_all("table", class_="kyujin")
find()find_all() メソッドでクラス属性を指定する場合、class_= と指定します。
考え方に戻る⤴

ハローワークサイトのHTMLの一部】

<!-- 求人情報(サマリ) -->

<div class="flex align_end last_right mt05 ">
    …途中省略
</div>
<table class="kyujin mt1 noborder">
    …途中省略
</table>
<table class="kyujin mt1 noborder">
    …途中省略
</table>

【この部分のソース】

    # 今見ているページをBeautifulSoupで解析
    soup = BeautifulSoup(browser.page_source, "html.parser")

    # 「求人」のテーブルを検索
    jobs = soup.find_all("table", class_="kyujin")

    _table = []
    _csv_header = []
    # jobsの要素を処理し、プログレスバーを表示する。
    for job in tqdm(jobs, unit="件", ncols=75):
        # 1度だけCSVのヘッダーとなる情報を取得する
        # 見出しとなるタグにはfbクラスが設定されている。
        if not _csv_header:
            _csv_header = [x.get_text(strip=True) for x in job.select("td.fb")]

        # 職種の取得
        head = [job.find("td", class_="m13").get_text()]
        # 求人区分から公開範囲の取得
        body1 = [x.get_text(strip=False) for x in job.select("tr.border_new td:not([class])")]
        # 期限、特徴などの取得
        body2 = [x.get_text(" ", strip=True) for x in job.select("tr:not([class]) > td:not([class])")]
        # 詳細情報のURL取得 相対アドレスが返るので、外部アドレスに変える。
        foot = [url0 + job.find("a", id="ID_dispDetailBtn")['href'][1:]]
        # 詳細情報の仕事を取得し、置き換える。仕事の内容は、4番目
        shigoto = [get_job_dt(foot[0])]
        body1[3:4] = shigoto
        # 1件分のデータを設定
        _table.append(head + body1 + body2 + foot)

◇見出し情報の抽出

求人情報サマリーの各1件分のデータには、見出し情報が含まれています。

csvに出力する際は、最初だけ見出しがあればよいので、1件目だけ見出し情報を抽出します。

BeautifulSoupget_text() メソッドは。取得したタグに含まれる全ての文字を文字列で返します。従って、単純に table タグに対して get_text() メソッドを実行すると見出しと内容が両方含まれた文字列が返ります。

ハローワークサイトの HTML を見ると、見出しには、fb クラス属性が設定されていることが分かります。こちらを参照⤵

従って、fb クラス属性を持つ td タグを取得します。
ここでは、find() メソッドに代えて、select() メソッドを使用しています。select() メソッドは css セレクタを使用してタグを取得します。

_csv_header = [x.get_text(strip=True) for x in job.select("td.fb")]

はずかしながらここで初めてcssセレクタを勉強したので、使い方がぎこちないかもしれません。(笑)
cssセレクタは、勉強のため使用しました。本当は、cssセレクタでないと選択できないと思い、始めたのですがそうではなかったようです。
ここでも内包表記を使っています。

プログレスバーの出力

for 文で、プログレスバーの出力をしています。
プログレスバーの出力には tqdm を使用しています。
tqdm の使用には、インポート import tqdm が必要です。
tqdm は、for 文で対象にしているイテレータtqdm のコンストラクタに変えてあげるだけで実装できます。

  • ブログレスバーなし:for job in jobs:
  • プログレスバーあり:for job in tqdm(jobs, unit="件", ncols=75):

考え方に戻る⤴

【この部分のソース】

    _table = []
    _csv_header = []
    # jobsの要素を処理し、プログレスバーを表示する。
    for job in tqdm(jobs, unit="件", ncols=75):
        # 1度だけCSVのヘッダーとなる情報を取得する
        # 見出しとなるタグにはfbクラスが設定されている。
        if not _csv_header:
            _csv_header = [x.get_text(strip=True) for x in job.select("td.fb")]

◇求人情報の抽出

BeautifulSoupget_text() メソッドは。取得したタグに含まれる全ての文字を文字列で返します。従って、単純に table タグに対して get_text() メソッドを実行すると見出しと内容が両方含まれた文字列が返ります。

ハローワークサイトの HTML を見ると(本ブログのソースには???と記述しています)、求人情報には、クラス属性が設定されているものとないものがあります。こちらを参照⤵

従って、場合分けしてタグを取得します。

  1. 職種の取得

    m13 クラス属性を持つ td タグを取得
    head = [job.find("td", class_="m13").get_text()]

  2. 求人区分から公開範囲までの取得

    親が border_new 属性を持つ tr タグで、子がクラス属性を持たない td タグを取得
    body1 = [x.get_text(strip=False) for x in job.select("tr.border_new td:not([class])")]
    ※クラス属性を持たないタグの指定方法::not([class]) (見つけるのに苦労しました)

  3. 期限、特徴などの取得

    親がクラス属性を持たない tr タグで、子がクラス属性を持たない td タグを取得
    body2 = [x.get_text(" ", strip=True) for x in job.select("tr:not([class]) > td:not([class])")]

  4. 詳細情報の「必要な経験等」を取得し、「仕事の内容」に追記する。

    詳細は別途「詳細情報の抽出」で⤵

  5. 抽出した求人情報をまとめてリストにし、リスト(行列の2次元のリスト)に追加

考え方に戻る⤴

【この部分のソース】

        # 職種の取得
        head = [job.find("td", class_="m13").get_text()]
        # 求人区分から公開範囲の取得
        body1 = [x.get_text(strip=False) for x in job.select("tr.border_new td:not([class])")]
        # 期限、特徴などの取得
        body2 = [x.get_text(" ", strip=True) for x in job.select("tr:not([class]) > td:not([class])")]
        # 詳細情報のURL取得 相対アドレスが返るので、外部アドレスに変える。
        foot = [url0 + job.find("a", id="ID_dispDetailBtn")['href'][1:]]
        # 詳細情報の仕事を取得し、置き換える。仕事の内容は、4番目
        shigoto = [get_job_dt(foot[0])]
        body1[3:4] = shigoto
        # 1件分のデータを設定
        _table.append(head + body1 + body2 + foot)

◇詳細情報の抽出

  1. 求人情報から詳細情報のリンクを取得

    foot = [url0 + job.find("a", id="ID_dispDetailBtn")['href'][1:]]
    リンクの a タグは、「求人票を表示」と「詳細を表示」があるのでIDを指定して find() メソッドを実施
    属性値の取得は、辞書のように扱います。
    リンクは ./… というように相対パスなので . を外してURLを加えて絶対パスに変換します。

  2. 詳細情報のサイトを呼び出す

    ここは、複数回実施されるのでメソッドにしています。
    再び Selenium を使ってURLを取得します。

  3. 仕事の内容を取得

    ID ID_shigotoNy で要素を取得し、text プロパティで文字列を取得します。

  4. 必要な経験などを取得

    ID ID_hynaKikntShsi で要素を取得し、text プロパティで文字列を取得します。
    要素が存在しない場合もあるので例外処理を付けます。
    仕事の内容に追記します。

考え方に戻る⤴

【この部分のソース】

        # 詳細情報のURL取得 相対アドレスが返るので、外部アドレスに変える。
        foot = [url0 + job.find("a", id="ID_dispDetailBtn")['href'][1:]]
        # 詳細情報の仕事を取得し、置き換える。仕事の内容は、4番目
        shigoto = [get_job_dt(foot[0])]
        body1[3:4] = shigoto


def get_job_dt(url):
    """
    求人情報詳細を開いて「仕事の内容」と「必要な経験など」を取得する

    Args:
        url:    求人情報詳細のURL

    Return:
        「仕事の内容」と「必要な経験など」の文字列
    """
    browser.get(url)
    wait.until(Ec.url_contains(url))
    _text = browser.find_element_by_id("ID_shigotoNy").text # 仕事の内容
    try:    # 必要な経験などがあれば追加する
        _text = _text + "\n【必要な経験など】\n" + browser.find_element_by_id("ID_hynaKikntShsi").text
    except NoSuchElementException:
        pass
    # browser.close()
    return _text

◆【 CSV 】出力

csv出力

csv出力は、書き込みデータが文字列か数値のイテラブルのイテラブルになっていると writerows() メソッドで全データ書き込めるので便利です。

【この部分のソース】

# CSVに出力
output_path = '{}.csv'.format(datetime.datetime.now().strftime("%m%d_%H%M_%S"))

try:
    with open(output_path, encoding="cp932", mode="w", newline="") as f:
        _writer = csv.writer(f)
        _writer.writerow(_csv_header)   # 見出しを出力
        _writer.writerows(_table)       # データを出力
except Exception as e:
    print("CSVエラー", e)

ハローワーク求人検索結果のHTML構造

【1件分の検索結果】

<table class="kyujin">
    <tr class="kyujin_head>
      <td>
          <table class="noborder">
              <tr>
                  <td class="fb">                 職種
                  <td class="m13">                ???
  <tr>
      <td>                                      受付年月日???  紹介期限日???
  <tr class="kyujin_body">
      <td>
          <table class="noborder">
              <tr class="border_new">
                  <td class="fb">                 求人区分
                  <td>                          ???
              <tr class="border_new">
                  <td class="fb">                 事業所名
                  <td>                          ???
              <tr class="border_new">
                  <td class="fb">                 就業場所
                  <td>                          ???
              <tr class="border_new">
                  <td class="fb">                 仕事の内容
                  <td>                          ???

          <table class="noboder">
              <tr class="border_new">
                  <td class="fb">                 就業時間
                  <td>                          ???

  <tr>
      <td>
          <span>                                    学歴不問など
  <tr>
      <td>                                      求人数
  <tr class="kyujin_foot">
      <td>                                      賃金は…
  <tr class="kyujin_foot">
      <td>
          <div class="flex jus_end">
              <a id="ID_kyujinhyoBtn" href=""      求人票を表示
              <a id="ID_dispDetailBtn" href="" 詳細を表示

◆全体のソース

全体のソースはこちらから取得できます。

◆バイナリ作成(pyinstaller)

バイナリ( exe ) ファイルは、pyinstaller で作成します。

  • 作成コマンド: pyinstaller -F --exclude-module settings ファイル名

◇設定ファイルがある時のpyinstaller

設定ファイル(仮にsettings.py)をバイナリでも使えるようにします。
考え方の基本は、バイナリを作るソースから設定ファイルを除外して、exe と同じパスにある設定ファイルを読めるようにします。

  1. import settingsを記述しているソースの変更
    pyinstallerで作成したexeファイルがあるディレクトリから import できるように修正
    sys.executableが該当ディレクトリなので sys.path に追加
    例:sys.path.append( os.path.dirname( sys.executable))
      import settings
  2. settings.pyはexeに含めないようにpyinstallerを実行
    --exclude-moduleオプションを指定
    例:--exclude-module settings
    ※拡張子は指定しないので注意
  3. 配布はexeとsettings.pyを渡し、同じディレクトリにおいて起動
追加:2022-01-08

◆あとがき

初めてのスクレイピングで、少し泥臭い感じです。
HTMLも良くわからない状態から始めたので、もう少しスマートな方法があるのかもしれません。
在職中に uwsc を使って検査の自動化を作った経験が役に立った気がします。
自動化は処理の待ちをいかにうまくコントロールするかが鍵だと思います。

他のサイトをスクレイピングする時の参考になれば幸いです。

◇ご注意

本記事は次のバージョンの下で動作した内容を基に記述しています。

  • Python 3.8.5
  • beautifulsoup4 4.9.3
  • selenium 3.141.0
  • tqdm 4.56.0
  • webdriver-manager 3.8.5(追加:2023-02-21)

ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
更新:2023-02-21

◆参考

投稿: 、更新:

  1. 他のブラウザを使う場合はこちらから:対応ブラウザ | Selenium