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

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

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

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

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

Firefoxに加えChromeの対応を追加しました。更新:2021-08-18

目次

■成果物

成果物として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に出力する

■必要なパッケージ

必要なパッケージ

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

■【 Selenium 】で検索の自動化

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

□Webドライバの取得

更新:2021-07-04
Seleniumスクレイピングするには、Webドライバが必要です。
ブラウザとしてChromeFirefoxに対応しています。1
お使いのブラウザに合わせてWebドライバを取得します。
Chrome用Webドライバ(実際にはchromedriver)、または
Firefox用Webドライバ(実際にはgeckodriver)をダウンロードして解凍し、exeを任意のフォルダに保存します。

◎取得先

こちらから取得します。

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

□サイト呼出

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

【処理】

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

  2. ChromeまたはFirefoxインスタンス(外部Doc⇒)

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

  4. 画面が表示されるのを待機する (外部Doc⇒)

    • 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
    # ブラウザーを起動
    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=settings.executable_path, options=options)  # ブラウザインスタンス作成
    else:
        options.add_argument("--disable-software-rasterizer")   # Chromeではこれを付けないとエラー(kFatalFailure)になる(理由はよくわからない)
        browser = webdriver.Chrome(executable_path=settings.executable_path, options=options)  # ブラウザインスタンス作成

    # ハローワーク検索画面にアクセス
    print("start browsing")
    url = "https://www.hellowork.mhlw.go.jp/kensaku/GECA110010.do?action=initDisp&screenId=GECA110010"
    browser.get(url)

    # 待機
    wait = WebDriverWait(browser, 15)  # Timeout 15秒(最大待ち時間)
    # 求人情報検索画面が表示されるまで待機
    wait.until(Ec.title_contains("求人情報検索"))
    print("got url, start selecting")

□基本検索条件設定

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

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")))

□職種の設定

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

【実操作】

  • 「職種を選択」ボタンをクリック
  • 表示される「職種 選択画面」で大分類のドロップダウンメニューから選択
  • 必要なら、詳細を選択

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

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

  • 設定ファイルに検索したい大分類と詳細を設定
  • 設定値は、画面に表示されている文字列をそのまま設定
  • selenium には表示されている文字列で選択するメソッド select_by_visible_text() があります。このメソッドで設定ファイルで指定した項目を選択

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

  • 「職種の選択」ボタンは input タグで value を「職種を選択」として実装されている(HTML から)
  • 従って、selenium でそのタグをすべて取得
  • 「職種 選択画面」の大分類、詳細は、個別のIDが振られたselect タグで実装されている(新たな画面のHTML から)
  • select タグが使用されているは、「大分類」だけなのを確認
  • 従って、selenium で IDを指定してselect タグをすべて取得し、大分類、詳細のタグを取得

【処理】

  1. ボタンのタグを取得

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

    1. 「職種を選択」ボタンをクリック
    2. 「職種 選択画面」が出るのを待機
    3. 「大分類」から設定ファイルで指定したものを選択
      select タグをすべて取得(select タグは大分類のみ)
      (本来、待機すべきかもしれません)
    4. 「詳細項目」が出るのを待機
      詳細項目は大分類に何を選んでも「こだわらない」というテキストが存在するので、このテキストが表示されるまで待機
      (待機がなくてChrome対応でエラーになってしまった。手抜きはダメですね)
      追加:2021-07-04
    5. 「詳細項目」から設定ファイルで指定したものを選択
    6. OK ボタンを押す
    7. 画面が閉じるまで待機

考え方に戻る⤴

【設定ファイルの一部】

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

【この部分のソース】

    # 職種
    # inputタグでvalueが「職種を選択」を抽出、onclick属性でソート
    btns = [x for x in buttons if x.get_attribute("value") == "職種を選択"]
    btns = sorted(btns, key=lambda x: x.get_attribute("onclick"))
    # 職種3か所の設定
    for  _btn, _sksu in zip(btns, settings.sksus):
        _btn.click()    # 職種選択画面表示
        # 選択画面が表示されるまで待つ
        wait.until(Ec.element_to_be_clickable((By.ID, "ID_rank1Code")))
        _sel = browser.find_element_by_id("ID_rank1Code")
        Select(_sel).select_by_visible_text(_sksu[0])
        # 詳細が出るまで待つ
        wait.until(Ec.text_to_be_present_in_element((By.ID, "ID_rank2Codes"), "こだわらない"))
        element = browser.find_element_by_id("ID_rank2Codes")
        for _city in _sksu[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")))

□詳細検索条件設定

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

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

考え方に戻る⤴

【この部分のソース】

    # 「詳細検索条件」をクリック
    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とセクション管理ができる

□待機

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

◎待機一覧

待機方法 メソッド
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

□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
else:
    from selenium.webdriver.chrome.options import Options

更新:2021-07-04

□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="" 詳細を表示

■全体のソース

全体のソースはこちらから取得できます。
リンク:GitHub juu7g/Python-Scraping4HW

□バイナリ(アプリ)

ソースはいいから動かしたい方には、exeファイルも用意しています。
別記事『ハローワークの求人情報を自動で検索するアプリ(スクレイピング)』を参照してください。

■バイナリ作成(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 を使って検査の自動化を作った経験が役に立った気がします。
自動化は処理の待ちをいかにうまくコントロールするかが鍵だと思います。

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

■参考

投稿: 、更新:

  1. 他のブラウザを使う場合はこちらから