ハローワークの求人情報を自動で検索するアプリをPythonの勉強のために作成しました。
Selenium と BeautifulSoup でWebスクレイピングをします。
HTML
を解析して操作方法を検討し、その操作に対する応答を待機するようにしています。
結果はcsvファイルに出力します。
※Firefox に加え Chrome の対応を追加しました。(更新:2021-08-18)
※Web ドライバの取得を自動化しました。(更新:2023-02-21)
目次
- ◆成果物
- ◆できること
- ◆考え方
- ◆必要なパッケージ
- ◆【 Selenium 】で検索の自動化
- ◆【 BeautifulSoup 】で解析
- ◆【 CSV 】出力
- ◆ハローワーク求人検索結果のHTML構造
- ◆全体のソース
- ◆バイナリ作成(pyinstaller)
- ◆あとがき
- ◆参考
◆成果物
成果物としてcsvファイルを出力します。エクセルで見ると次のようになります。
◆できること
Selenium
と BeautifulSoup
の次のような機能を使用してWebスクレイピングをします。
-
- 画面の要素(ボタンなど)をクリックする操作
- 画面の入力フィールドに値を入力する操作
- 画面の選択肢から値を選択する操作
- 画面の操作に対する応答の待機
BeautifulSoup
- ページ情報の解析と取得
その他
追加:2021-08-18
◆考え方
ハローワークサイトを呼び出して、必要な条件を設定し検索します。
検索結果を解析して、csvファイルに出力します。
具体的には、次の通りです。
◆必要なパッケージ
必要なパッケージ
- beautifulsoup4
- selenium
- tqdm
- webdriver-manager(追加:2023-02-21)
ここでは、Python や Python パッケージのインストールなど基本的な使い方は諸先輩に譲ります。
◆【 Selenium 】で検索の自動化
Selenium
でハローワークサイトの検索の自動化を行うには次のようにします。
◇Webドライバの自動取得(webdriver-manager)
◎取得先
こちらから取得します。
- Chrome用
ChromeDriver - WebDriver for Chrome
Latest stable releaseを選択してください。 - Firefox用
Release x.xx.x · mozilla/geckodriver · GitHubサイト
環境にあった版を選択してください。
お手数ですが…
Webドライバが再配布可能なのかどうか、良く理解できなかったので、別途ダウンロードをお願いします。
Selenium
でスクレイピングするには、Webドライバが必要です。
本アプリは、ブラウザとしてChromeとFirefoxに対応しています。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ドライバのインスタンスを作成し、ハローワークサイトを呼び出します。
【処理】
selenium
パッケージをインポート(from selenium import webdriver
)ChromeまたはFirefoxをインスタンス化(参考:ブラウザーのドライバーをインストールする | Selenium )
- Webドライバの場所を指定(
executable_path=
) - オプションがある場合、オプションクラスのインスタンスを指定(
options=
)- ヘッドレスモード(ブラウザ画面を出さないモード)はオプションで指定
(
options.headless = True
) - オプションにはインポート
import Options
が必要
- ヘッドレスモード(ブラウザ画面を出さないモード)はオプションで指定
(
- Webドライバの場所を指定(
get(url)
メソッドでWebを開く(参考:Browser interactions | Selenium )画面が表示されるのを待機する(参考:待機 | Selenium )
【この部分のソース】
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から操作対象のタグを抽出し、辞書として作成しておきます。具体的には、こちら⤵を参照
【処理】
クリック操作で設定する項目の設定
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()
キーボード入力操作で設定する項目の設定
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)
要素を選択する操作で設定する項目の設定(詳細検索条件設定でのみ実装)
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
タグが使用されているのは、「就業場所」だけなのを確認- 従って、
selenium
でselect
タグをすべて取得し、就業場所のタグを取得
【処理】
select
タグをすべて取得し、ID順にソートsels = browser.find_elements_by_tag_name( "select" )
sels = sorted(sels, key=lambda x: x.get_attribute("id"))
ボタンのタグを取得
- ボタンのタグをすべて取得(「市町村選択」ボタン以外にも存在する)
find_elements_by_css_selector( "input.button" )
- ボタンのタグから「市町村選択」ボタンを抽出(value属性が「選択」のもの)
[x for x in buttons if x.get_attribute("value") == "選択"]
-
onclick
属性順にソート(selectタグと同期させるため)
sorted(btns, key=lambda x: x.get_attribute("onclick"))
- ボタンのタグをすべて取得(「市町村選択」ボタン以外にも存在する)
「市町村選択」ボタンのタグと設定ファイルの都道府県を同期させて、ループ処理
for _sel, _btn, _tdk in zip(sels, btns, settings.tdks):
- 都道府県を選択
Select( _sel ).select_by_visible_text( _tdk[0] )
_tkd
の先頭が都道府県
インポートimport Select
が必要
Select
クラスのインスタンスを作成してメソッドで選択する - 「市町村選択」ボタンをクリック
_btn.click()
- 「市町村選択画面」が出るのを待機
wait.until( Ec.element_to_be_clickable( (By.ID, "ID_rank1CodeMulti")))
インポートimport By
が必要 - 市町村選択項目から設定ファイルで指定したものを選択
OK
ボタンを押す- 画面が閉じるまで待機
- 都道府県を選択
【設定ファイルの一部】
# 都道府県 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
タグをすべて取得し
【処理】
ボタンのタグを取得
input
タグでvalue
が「職種を選択」を抽出onclick
属性順にソート(selectタグと同期させるため)
「職種選択」ボタンのタグと設定ファイルの職種を同期させて、ループ処理
- 「職種を選択」ボタンをクリック
- 「職種 選択画面」が出るのを待機
- 「大分類」から設定ファイルで指定したものを選択
select
タグをすべて取得(select
タグは大分類のみ)
(本来、待機すべきかもしれません) - 「詳細項目」が出るのを待機
詳細項目は大分類に何を選んでも「こだわらない」というテキストが存在するので、このテキストが表示されるまで待機
(待機がなくてChrome対応でエラーになってしまった。手抜きはダメですね)
追加:2021-07-04 - 「詳細項目」から設定ファイルで指定したものを選択
OK
ボタンを押す- 画面が閉じるまで待機
【設定ファイルの一部】
# 職種 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件の場合、表示内容が異なるため、それを考慮します。
【処理】
「検索」ボタンをクリック
検索完了を待機
2つの条件で待機する場合、until()
メソッドは引数にメソッドを取るためlamda
式で2つの待機メソッドのor
を取るメソッドにします。- 検索結果が0件でない場合:「表示件数」が表示されるのを待機
- 検索結果が0件の場合:「ご希望の条件に合致する情報は見つかりませんでした。」が表示されるのを待機
検索結果が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
:""
- 属性
radio
:False
- 属性
checkbox
:False
- 属性
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 セレクタを用いて要素を指定 |
◇待機
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()
メソッドの第一引数はタプルなのに注意
◇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
で取得した HTML
を BeautifulSoup
に渡すだけです。
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件目だけ見出し情報を抽出します。
BeautifulSoup
の get_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")]
◎プログレスバーの出力
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")]
◇求人情報の抽出
BeautifulSoup
の get_text()
メソッドは。取得したタグに含まれる全ての文字を文字列で返します。従って、単純に table
タグに対して get_text()
メソッドを実行すると見出しと内容が両方含まれた文字列が返ります。
ハローワークサイトの HTML
を見ると(本ブログのソースには???
と記述しています)、求人情報には、クラス属性が設定されているものとないものがあります。こちらを参照⤵
従って、場合分けしてタグを取得します。
職種の取得
m13
クラス属性を持つtd
タグを取得
head = [job.find("td", class_="m13").get_text()]
求人区分から公開範囲までの取得
親が
border_new
属性を持つtr
タグで、子がクラス属性を持たないtd
タグを取得
body1 = [x.get_text(strip=False) for x in job.select("tr.border_new td:not([class])")]
※クラス属性を持たないタグの指定方法::not([class])
(見つけるのに苦労しました)期限、特徴などの取得
親がクラス属性を持たない
tr
タグで、子がクラス属性を持たないtd
タグを取得
body2 = [x.get_text(" ", strip=True) for x in job.select("tr:not([class]) > td:not([class])")]
詳細情報の「必要な経験等」を取得し、「仕事の内容」に追記する。
詳細は別途「詳細情報の抽出」で⤵
抽出した求人情報をまとめてリストにし、リスト(行列の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)
◇詳細情報の抽出
求人情報から詳細情報のリンクを取得
foot = [url0 + job.find("a", id="ID_dispDetailBtn")['href'][1:]]
リンクのa
タグは、「求人票を表示」と「詳細を表示」があるのでIDを指定してfind()
メソッドを実施
属性値の取得は、辞書のように扱います。
リンクは./…
というように相対パスなので.
を外してURLを加えて絶対パスに変換します。詳細情報のサイトを呼び出す
ここは、複数回実施されるのでメソッドにしています。
再びSelenium
を使ってURLを取得します。仕事の内容を取得
ID
ID_shigotoNy
で要素を取得し、text
プロパティで文字列を取得します。必要な経験などを取得
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="" 詳細を表示
◆全体のソース
全体のソースはこちらから取得できます。
- ソース:Scraping4HW.py
- 設定ファイル:settings.py
- 取得先:GitHub juu7g/Python-Scraping4HW
◆バイナリ作成(pyinstaller)
バイナリ( exe
) ファイルは、pyinstaller
で作成します。
- 作成コマンド:
pyinstaller -F --exclude-module settings ファイル名
◇設定ファイルがある時のpyinstaller
設定ファイル(仮にsettings.py)をバイナリでも使えるようにします。
考え方の基本は、バイナリを作るソースから設定ファイルを除外して、exe と同じパスにある設定ファイルを読めるようにします。
- import settingsを記述しているソースの変更
pyinstallerで作成したexeファイルがあるディレクトリから import できるように修正
sys.executable
が該当ディレクトリなのでsys.path
に追加
例:sys.path.append( os.path.dirname( sys.executable))
import settings
- settings.pyはexeに含めないようにpyinstallerを実行
--exclude-module
オプションを指定
例:--exclude-module settings
※拡張子は指定しないので注意 - 配布はexeとsettings.pyを渡し、同じディレクトリにおいて起動
◆あとがき
初めてのスクレイピングで、少し泥臭い感じです。
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
◆参考
- ハローワーク:ハローワーク インターネットサービス 求人情報検索・一覧
- スクレイピング解説:ハローワークの求人情報をスクレイピング(Python + Selenium + BeautifulSoup)
- スクレイピング解説:ハローワークから求人情報をスクレイピングする
- スクレイピング解説:データ解析、プログラミング学習記録: ハローワークインターネットサービスから求人情報を取得する(python,pandas,スクレイピング)
- Seleniumのマニュアル:Seleniumブラウザー自動化プロジェクト :: Seleniumドキュメント
- Seleniumがよくまとまっていて便利:Selenium API(逆引き)
- Chrome Webドライバ:ChromeDriver - WebDriver for Chrome
- Firefox Webドライバ:Release x.xx.x · mozilla/geckodriver · GitHub
- Beautiful Soupマニュアル:kondou.com - Beautiful Soup 4.2.0 Doc. 日本語訳 (2013-11-19最終更新)
- Beautifull Soup解説:10分で理解する Beautiful Soup - Qiita
- Beautifull Soupマニュアル(英文):Beautiful Soup Documentation — Beautiful Soup 4.9.0 documentation
- webdriver-manager:webdriver-manager · PyPI
- 他のブラウザを使う場合はこちらから:対応ブラウザ | Selenium ↩