【スクレイピングの基本】タウンワークから求人情報を自動取得しよう!
業務効率化

2.タウンワークの検索結果の1ページ目を取得する

         

スクレイピングのコードの全体像

スクレイピングは以下のような流れで行う場合がほとんどです。

  1. ページ内の欲しい要素を取得
  2. 次のページや詳細ページに移動(移動先のURLを取得)
  3. 1~2の繰り返し
  4. データの保存・出力

今回は、 1番/2~3番/4番の3ページに分けて、本ページでは1番のページ内の欲しい要素を取得するコードを書いていきます。

このような、いくつかの部分(数行から数十行のコード)に分けられるプログラムを書く場合、自作関数(def...)を作ってコードを書くのが一般的です。関数を作ると以下のようなメリットがあります。

  • コードが見やすくデバッグがしやすくなる
    • まとまりが分かりやすい
    • 条件分岐によるネスト(入れ子)を浅く抑えられる
  • 繰り返し使う場合に再利用できる

コードを自分で全く書いたことがない人にはややハードルが高いかもしれませんが、最初のうちから関数を作ってプログラムを書くほうがいいでしょう。

APIの基本では初歩の初歩のプログラムであり、また非常にシンプルなプログラムだったため、関数は作りませんでしたが、今後のプロダクトの解説では毎回関数を作って解説を行います。

ライブラリのインポートと実行コードの導入部分

import requests
from bs4 import BeautifulSoup
import pandas

def get_items(url, df, columns):
    return

def main():
    url = "https://townwork.net/joSrchRsltList/?fw=プログラミング"
    columns = ["rid", "company", "title", "salary", "access", "term", "timelimit"]
    df = pandas.DataFrame(columns=columns)
    results = get_items(url, df, columns)

if __name__ == '__main__':
    main()

まず、1~3行目で今回のプログラムに使用するライブラリをインポートします。前のページで解説した通り、requests、beautifulsoup、pandasの3つになります。

次に5~6行目で本ページで解説の中心となる「検索結果の1ページ目の要素を取得する」関数を定義します。関数内のコードはこれから作成するので、現時点では書いていても書いてなくても関係ありませんが、ひとまずreturnとだけ書いています。

引数には取得先のURLであるurl、取得したデータを保存しておくための容器であるpandasのデータフレームdf、dfの列名(会社名、タイトル、給与などの取得する各データの名前)となるcolumnsの三つを指定します。

関数の引数や返り値は、コードを作成しながら決めていくことも多いので、最初はあまり深く考えずにとりあえず必要そうなものを書いておけば十分です。

8行目以降でコードの実行部分をmain()関数にまとめています。

9行目はURLの指定。

10行目でdfの列名(詳細ページのURLをid、会社名をcompany、求人タイトルをtitle、給与をsalary、交通をaccess、勤務時間をterm、掲載期間終了日時をtimelimit)の指定。

11行目でcolumnsを列名とするデータフレームdfの作成。

12行目で1ページ目の要素を取得する5行目で定義した関数get_items()に各引数を渡して結果をresultsに代入するコードを暫定的に挿入。

最後の14~15行目ではmain()関数を実行しています。

def main():として実行部分をmain()関数を定義してまとめることにより、バグを未然に防ぐことができます。仮に実行部分を関数にまとめずに、

import requests
from bs4 import BeautifulSoup
import pandas

def get_items(url, df, columns):
    return

url = "https://townwork.net/joSrchRsltList/?fw=プログラミング"
columns = ["rid", "company", "title", "salary", "access", "term", "timelimit"]
df = pandas.DataFrame(columns=columns)
results = get_items(url, df, columns)

と書くと、関数(get_items)の中と外で同じ名前の変数に値が代入されたとき、関数内で代入されたものはローカル変数(その関数内でのみ有効な変数)、関数外で代入されたものはグローバル変数(すべての範囲で有効な変数)として変数名は同じの異なる変数として扱われるため、不具合が発生することがあります。

そのため、実行部分のコードはまとめてmain()関数として定義し、定数の定義など意識的にグローバル変数を利用したい場合を除いて、関数外はmain()とのみ書いて実行する習慣をつけておくと良いでしょう。なお、慣例的にdef main():として定義しますが、規則があるわけではなく自由に関数名を付けることが可能です。

また、main()関数の実行は、 if __name__ == '__main__': とあわせて

if __name__ == '__main__':
    main()

と書かれることがよくあります。 if __name__ == '__main__': は「もし、ファイルが直接実行されたときは」という意味で、その.pyファイルが別の.pyファイルから呼び出されたときは実行されません。

複数の.pyファイルに分けてプログラムを書くとき以外は、if文があってもなくても動作は変わらないので

main()

とだけ書いて、if文を省略しても構いません。

取得先のURLと通信を行う

URLの決定

https://townwork.net/joSrchRsltList/?fw=プログラミング

スクレイピングを行うときは、まず取得先のURLを決めます。今回は「プログラミング」というワードでタウンワークを検索したときのURLを指定しています。fw=の先が検索ワードとなるようです。

ここらへんの仕様はサイトによってすべて異なるので、ブラウザで試してどの検索ページがどのようなURLになるかを確認してください。

タウンワークの場合は、例えば東京都で絞ってプログラミングで検索すると

https://townwork.net/joSrchRsltList/?ac=041&fw=プログラミング

飲食業だけで検索すると

https://townwork.net/joSrchRsltList/?jc=001

などのようなURLになります。

検索結果をスクレイピングしたい場合は、1ページの表示件数も指定できることがほとんどなので、サイトへの負荷を減らし取得時間も短縮するために、なるべく多くの件数を表示する設定にすることをおすすめします。

https://example.com/search?query=検索ワード&num=100

のように指定できるところがほとんどですが、タウンワークは指定できないようなので今回解説するプログラムでは未指定となっています。

requestsライブラリで通信を行う

まずはAPIとの通信と同じように、requestsライブラリを利用して取得先のURLにアクセスします。

def get_items(url, df, columns):
    res = requests.get(url)
    res_text = res.text

URLの定義等は関数外で既に行っており引数により値を受け取っているので、6行目でget通信を行うコードを書くだけで基本的には完了です。

7行目で変数名.textとすることにより通信先のページのHTMLソースが取得されます。

APIの基本で解説したようなエラー処理はここでは入れていません。

簡単のため、シンプルにしてコードの解説をしやすくするため、という理由もありますが、スクレイピングはAPIと異なり定期実行などの処理は相手の負荷等を考えると好ましくないものなので、ほとんど一回限りという前提でプログラムを作成しているためです。

1ページ目の要素を取得

本題のHTMLソースから各データを取得する解説です。

HTMLのパース

def get_items(url, df, columns):
    res = requests.get(url)
    res_text = res.text
    soup = BeautifulSoup(res_text, "html.parser")

8行目でBeautifulSoup(HTMLソース, "parser")と書くことによりHTMLコードをパースします。パースというのは解析して扱いやすいデータに変換することで、ここではHTMLコードを解析してBeautifulSoupオブジェクトというbeautifulsoupライブラリで扱うためのデータ構造に変換しています。

第2引数で指定しているのはパースするためのツール名で、ここでは「html.parser」というツールを使用しています。

パーサー(ツール)にはhtml.parser、lxml、html5libなど様々な種類があります。

それぞれ特徴がありますが、html.parserはPythonに標準で付属している最も一般的なものです。

lxmlは高速動作、html5libは構文エラーがあるHTMLにも幅広く対応できるなどのメリットがありますが、いずれも外部ライブラリなのでimportする必要があります。

検索結果の1件1件をリスト形式で取得する

取得先のページのHTMLを確認すると

<html>
<head>...</head>
<body>
...
<div class="job-cassette-lst-wrap">
...
<div class="job-lst-main-cassette-wrap">...</div>
<div class="job-lst-main-cassette-wrap">...</div>
<div class="job-lst-main-cassette-wrap">...</div>
...
<div class="job-lst-main-cassette-wrap">...</div>
<div class="job-lst-main-cassette-wrap">...</div>
</div>
...
</body>
</html>

となっており、一つ一つの検索結果は「job-lst-main-cassette-wrap」をclass名とするdivタグに囲まれていることが分かります。

このような場合、

def get_items(url, df, columns):
    res = requests.get(url)
    res_text = res.text
    soup = BeautifulSoup(res_text, "html.parser")
    ret = soup.find_all("div", class_="job-lst-main-cassette-wrap")

と9行目のように書き、BeautifulSoupライブラリのfind_all関数によってリスト形式で1個1個の要素を取得することができます。

ひとつの検索結果内の要素を取得

次にひとつの検索結果に着目し、取得したい項目を抽出するコードを書いていきます。

まずは、<div class="job-lst-main-cassette-wrap>内のHTMLを見て、どの部分がどのHTMLに対応しているかを確認します。

<a href="URL" class="job-lst-main-box-inner job-lst-main-pr-box-inner">

のhref属性に詳細ページURL、

<h3 class="job-lst-main-ttl-txt">会社名</h3>

に会社名、

<p class="job-lst-main-txt-lnk">求人タイトル</p>

に求人タイトル、

<tr class="job-main-tbl-inner">
 <th class="job-main-tbl-detail">
  <span class="ico-tbl-salary">項目名</span>
 </th>
 <td>
  <p>
    <span>文章1</span><span>文章2</span><span>文章3</span>
  </p>
 </td>
</tr>

となっているような<tr class="job-main-tbl-inner">内の<td>内の<p>内が給与、

さらに <tr class="job-main-tbl-inner"> は計3つ並んでいて、二番目が交通、三番目が勤務時間となっています。

<p class="job-lst-main-period-limit"><span>日時</span>に掲載期間が終了</p>

のspan内に掲載期間終了日時がそれぞれ表示されています。

これらの要素をそれぞれPythonで取得していきます。

    ret = soup.find_all("div", class_="job-lst-main-cassette-wrap")
    ret.pop(0)  # 最初と最後の要素は広告欄なので削除
    ret.pop(-1)
    for item in ret:
        rid = ...
        title = ...

まずはリスト形式になっている変数ret内の要素をfor文により一つ一つ変数itemとして取り出します。その後各項目を変数に代入していきます。一つ一つの項目を詳細に見ていきましょう。

タウンワークの検索結果の1件目と最後はどのページでもPICKUP欄となっており、検索結果のうちのひとつが重複されて表示されるようなので、10行目と11行目でPythonの組み込み関数であるpop()により最初と最後の要素を削除しています。

このように取得段階でデータを選別するのも一つの手段ですが、タウンワークのようにHTMLタグの構造が通常の求人とPICKUP枠の求人で全く変わらない場合は、エラーが発生することなく要素を取得できるので、すべてのデータの取得・保存後に後から重複チェックをして除外操作を行うことも可能です。

URL(求人ID)

rid = item.find("a", class_="job-lst-main-box-inner").get("href")

まずは詳細ページのURLを求人IDとして変数ridに代入します。BeautifulSoupのfind ()メソッド でタグ名とクラス名を指定します。これだけだと、<a>や</a>を含んだ文字列がすべて含まれてしまうので、さらにそこからhref属性のみを抽出するために、.get("href")と指定します。

会社名

company = item.find("h3", class_="job-lst-main-ttl-txt").text.strip()

次に、会社名を同様にfind ()メソッド により抽出します。今回は属性内ではなくタグ内なので、.textにより抽出します。通常はこれだけでも十分ですが、今回はh3タグ内の要素に改行や余分な空白等が含まれていたため、Pythonの組み込み関数であるstrip()をさらに書いています。

求人タイトル

title = item.find("p", class_="job-lst-main-txt-lnk").text.strip()

求人タイトルも会社名と同様です。

給与・交通・勤務時間

trs = item.select("tr.job-main-tbl-inner > td > p")
salary = trs[0].text
access = trs[1].text
term = trs[2].text

給与・交通・勤務時間は同じテーブル内のjob-main-tbl-innerをclass名とするtrタグに含まれているので、セットで取得してしまいます。

find_all("tr", class_="job-main-tbl-inner")で要素をすべて取得したのち、for文やリストの要素指定を行い取得することも可能ですが、select ()メソッドを使うとCSSセレクターで指定することができるので便利です。

select()メソッドは、find_all()のようにリストとして取得されるので、trs[0].textと書くことによりそれぞれの要素が取得できます。

掲載終了日時

try:
timelimit = item.select_one("p.job-lst-main-period-limit > span").text except AttributeError: timelimit = "不明"

掲載終了日時はselect_one()メソッドによって取得します。select_one()メソッドはselect()メソッドと異なり一つだけ取得するものなので、for文やリストの要素指定は必要はりません。

なお、掲載終了日時に関しては、求人の案件によって設定されいないものもあるようなので、.textでエラーが発生した際は「不明」と入力するようにしました。

BeautifulSoupでHTMLタグ内の要素を取得する場合、find()・find_all()と、select_one()・select()の二組の同じ機能を持つメソッドがあります。

select_one()・select()を使うほうが複雑なタグの指定が柔軟にできるため、個人的には指定方法に悩むことなくよりスムーズに開発ができるのではないかと思います。

場合によってはselect系を利用するとコードが冗長になることもあるので、好みに応じて使ってみてください。

データフレームに要素を格納

def get_items(url, df, columns):
res = requests.get(url) res_text = res.text soup = BeautifulSoup(res_text, "html.parser") ret = soup.find_all("div", class_="job-lst-main-cassette-wrap") ret.pop(0) # 最初と最後の要素は広告なので削除 ret.pop(-1) for item in ret: rid = item.find("a", class_="job-lst-main-box-inner").get("href") company = item.find("h3", class_="job-lst-main-ttl-txt").text.strip() title = item.find("p", class_="job-lst-main-txt-lnk").text.strip() trs = item.select("tr.job-main-tbl-inner > td > p") salary = trs[0].text access = trs[1].text term = trs[2].text try: timelimit = item.select_one("p.job-lst-main-period-limit > span").text except AttributeError: timelimit = "不明" print("{0}番目の情報({1}):{2}をDataFrameに追加します...".format(len(df)+1, rid, title)) print("会社名:{0} タイトル:{1} 給与:{2} 交通:{3} 勤務時間:{4} 掲載終了日時:{5}" \ .format(company, title, salary, access, term, timelimit)) se = pandas.Series([rid, company, title, salary, access, term, timelimit], columns) df = df.append(se, ignore_index=True)

ここまでのまとめと取得した要素をデータフレームに格納するコードです。

25~27行目ではprint文により取得した情報をコンソール上に表示させています。

28行目で、変数columnsを列名とするpandasのSeries(シリーズ)に取得したデータを格納し、

29行目でそのシリーズをデータフレームに追加しています。(name属性のない)シリーズをデータフレームに追加するときは、ignore_index=Trueの指定が必須なのでご注意ください。

DataFrameは2次元の表のようなデータ構造であるのに対し、Seriesは1次元のデータ構造です。

DataFrameにデータを追加していく際は、まず1次元のSeriesにデータを格納し、そのSeriesをDataFrameに追加していく流れが一般的です。

コードのまとめ

import requests
from bs4 import BeautifulSoup
import pandas

def get_items(url, df, columns):
    res = requests.get(url)
    res_text = res.text
    soup = BeautifulSoup(res_text, "html.parser")
    ret = soup.find_all("div", class_="job-lst-main-cassette-wrap")
    ret.pop(0) # 最初と最後の要素は広告なので削除
    ret.pop(-1)
    for item in ret:
        rid = item.find("a", class_="job-lst-main-box-inner").get("href")
        company = item.find("h3", class_="job-lst-main-ttl-txt").text.strip()
        title = item.find("p", class_="job-lst-main-txt-lnk").text.strip()
        trs = item.select("tr.job-main-tbl-inner > td > p")
        salary = trs[0].text
        access = trs[1].text
        term = trs[2].text
        try:
            timelimit = item.select_one("p.job-lst-main-period-limit > span").text
        except AttributeError:
            timelimit = "不明"

        print("{0}番目の情報({1}):{2}をDataFrameに追加します...".format(len(df)+1, rid, title))
        print("会社名:{0} タイトル:{1} 給与:{2} 交通:{3} 勤務時間:{4} 掲載終了日時:{5}" \
              .format(company, title, salary, access, term, timelimit))
        se = pandas.Series([rid, company, title, salary, access, term, timelimit], columns)
        df = df.append(se, ignore_index=True)
    return df

def main():
    url = "https://townwork.net/joSrchRsltList/?fw=プログラミング"
    columns = ["rid", "company", "title", "salary", "access", "term", "timelimit"]
    df = pandas.DataFrame(columns=columns)
    results = get_items(url, df, columns)
    print(results)

if __name__ == '__main__':
    main()
出力結果
1番目の情報(/detail/clc_3730189001/):[A][P]週1日、3h~ok!子供向けプログラミング教室の講師をDataFrameに追加します...
会社名:子供向けロボットプログラミング教室ダヴィンチ・ラボ タイトル:[A][P]週1日、3h~ok!子供向けプログラミング教室の講師 給与:時給950円~ ☆無資格・未経験者OK! 交通:北習志野駅徒歩7分 勤務時間:平日/16:30~20:30 土曜/10:30~18:30 ●研修3ヶ月:時給930円 シフト時間前後OK→ご相談下さい☆ 掲載終了日時:12月23日 07:00
2番目の情報(/detail/clc_0155737006/joid_Y003W1VM/):[A][P]子ども向けロボットプログラミング教室補助staffをDataFrameに追加します...
会社名:学校法人西野学園 タイトル:[A][P]子ども向けロボットプログラミング教室補助staff 給与:時給1000円+交通費支給(規定による)☆長期歓迎 交通:勤務地による *オープニングスタッフ募集 勤務時間:<週1日/1コマ~>土・日→9:00-17:30水・木・金→15:30-20:30*土日/平日のみok *上記内で1コマ(50分)以上 *シフト制 *1日4~5hでOK! *帰省… 掲載終了日時:12月23日 07:00
(中略)
29番目の情報(/detail/clc_1671115102/joid_U01CA7LB/):[派]★時給2000円~★大手重工メーカーJAVAでのシステム開発 岩塚をDataFrameに追加します...
会社名:株式会社KDDIエボルバ/EA020044 タイトル:[派]★時給2000円~★大手重工メーカーJAVAでのシステム開発 岩塚 給与:時給:2000円 交通:地下鉄「岩塚」徒歩20分 勤務時間:8:00-17:00 土日祝お休み(企業カレンダーあり) 掲載終了日時:不明
30番目の情報(/detail/clc_0224261030/joid_U01B7DFK/):[A][P]レゴ(R)スクールの運営業務(受付・事務等)をDataFrameに追加します...
会社名:(株)山野楽器 レゴ(R)スクール武蔵小杉 タイトル:[A][P]レゴ(R)スクールの運営業務(受付・事務等) 給与:時給1060円 交通:武蔵小杉駅より徒歩4分 勤務時間:9:45~18:30/12:30~21:15※週4~5日勤務・シフト制 ・土日含む(時間要相談) 掲載終了日時:不明

                                      rid  ...     timelimit
0                 /detail/clc_3730189001/  ...  12月23日 07:00
1   /detail/clc_0155737006/joid_Y003W1VM/  ...  12月23日 07:00
2                 /detail/clc_0287030008/  ...  12月23日 07:00
(中略)
27  /detail/clc_2318615001/joid_U01CGX9B/  ...            不明
28  /detail/clc_1671115102/joid_U01CA7LB/  ...            不明
29  /detail/clc_0224261030/joid_U01B7DFK/  ...            不明

[30 rows x 7 columns]

これで一ページ目の検索結果を抽出し、データフレームに格納するコードが完成しました。

タイトルとURLをコピーしました