スクレイピングの基本

3.タウンワークの検索結果を最後のページまで取得する

前の記事のおさらいと本記事でやること

前の記事では検索結果の1ページ目の要素の取得方法を解説しました。

この記事では、次のページのURLを取得し、ループ文によって検索結果のすべてのページの要素の取得方法を解説します。

次のページのURLを取得する

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)
    url = get_nextpage(soup)
    return df, url

def get_nextpage(soup):
    return url

次のページのURLを取得する関数get_nextpage()を作成するために、33行目から関数を定義していきます。ここでは、HTMLコードを入力すると次のページのURLを出力する関数にしたいので、関数get_nextpage()は、HTMLのソースコード(変数soup)を引数とし、返り値をURLとするように定義します。

関数get_nextpage()は、30行目に追記したように関数get_items()内で、ページ内の要素を取得する流れで最後に次のページのURLを取得するかたちで利用します。

最終的には、関数get_items()をループさせるようなプログラムを作成することを目標とするため、31行目でdfとurlを返り値として指定します。

「次のページへ」ボタンのHTMLからURLを取得する

URLを取得するために、まずはブラウザ上から次のページに移るためのHTMLを確認します。

<div class="pager-next-btn">
<div class="btn-wrap">
<a class="linknone grd-gray btn-gray-h30 i-btn-next ico-arrow-a" href="/...">次のページへ</a>
</div>
</div>

するとこのように<div class="pager-next-btn">タグに囲まれたdiv > aタグの中のhref属性にURLが含まれていることが分かります。

def get_nextpage(soup):
    url = soup.select_one("div.pager-next-btn > div.btn-wrap > a").get("href")
    url = "https://townwork.net" + url
    return url

これは前の記事で解説したように、select_one()メソッド(またはfind()メソッド)によりタグを指定した後にget()によりhref属性を参照すると取得できます。

タウンワークのリンクはHTML上だとドメイン名(https://townwork.net)が省略されているので、35行目で忘れずに付加するようにします。

最後のページについてエラー処理をする

これだけでも一見十分なようですが、このままだと次のページを永遠に取得し続けるコードで、「次のページへ」ボタンが存在しないと思われる最後のページになると、エラーが発生する可能性が高いので、try~except文によるエラー処理を行います。

最後のページを確認すると、そもそも<div class="pager-next-btn">というタグが存在しないことがわかります。そのため、select_one()メソッドにより得られる結果がNoneとなり、get()メソッドを実行するとhref属性が参照できずAttributeErrorが発生します。

def get_nextpage(soup):
    try:
        url = soup.select_one("div.pager-next-btn > div.btn-wrap > a").get("href")
    except AttributeError:
        print("最後のページです")
        return
    url = "https://townwork.net" + url
    print(url)
    return url

そこでtry~except文によりAttributeErrorが発生した場合=最後のページの場合は、「最後のページです」とprint文によりコンソールに表示させて、returnにより処理を終了します。

40行目で、次のページのURLが取得できた時はコンソール上にそのURLを表示させています。

38行目のように、関数内で返り値を指定せずにreturnのみを書くと、「None」が返り値となり、関数内の処理はそこで終了します。関数からの返り値を処理するときのために覚えておきましょう。

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)
    url = get_nextpage(soup)
    return df, url

def get_nextpage(soup):
    try:
        url = soup.select_one("div.pager-next-btn > div.btn-wrap > a").get("href")
    except AttributeError:
        print("最後のページです")
        return
    url = "https://townwork.net" + url
    print(url)
    return url

これで、次のページのURLを取得する関数get_nextpage()が完成しました。

ループ文によりすべてのページの要素を取得する

ここまでで検索結果の一ページ目の要素と次のページのURLを取得するコードが完成しました。

次にmain()関数にget_items()をループ文で回して全ページの要素を取得するコードを追記していきます。

def main():
    url = "https://townwork.net/joSrchRsltList/?fw=プログラミング"
    columns = ["rid", "company", "title", "salary", "access", "term", "timelimit"]
    df = pandas.DataFrame(columns=columns)
    while True:
        df, url = get_items(url, df, columns)
        time.sleep(5)
        if url is None:
            break
    print(df)

main()関数の5行目からループ文を追記しています。

5行目のwhile Trueは決まった書き方で、途中で終了させない限りwhile節の中を無限ループします。

6行目で変数dfとurlにそれぞれ関数get_items()の返り値を代入しています。

関数の返り値が二つ以上ある場合、

df, url = get_items()

と書くことで1行で二つの変数に二つの返り値をそれぞれ代入させることができます。

results = get_items()
df = results[0]
url = results[1]

変数一つに代入させる書き方だと、関数の返り値はタプルとして代入されます。この場合、あとからresults[0]のように参照しなければならずコードが冗長になるため、タプルとして使いたい理由があるとき以外は、一行にまとめると良いでしょう。

import requests
from bs4 import BeautifulSoup
import pandas
import time

7行目では、Python標準のtimeモジュールのsleep()メソッドを使うことにより、5秒間一時停止しています。sleep()メソッドを使うにはtimeモジュールをimportしなければならない点に注意しておきましょう。

8~9行目では、最後のページの処理を書いています。最後のページでは先ほど作ったget_nextpage()の返り値がNoneとなるため、変数urlもNoneとなります。このときループも終了させたいので、breakと書くことにより処理をそこで終了させます。

time.sleep(一時停止したい秒数)

スクレイピングを行う際は、通信先のサイトへの負荷を軽減するために、次のページに遷移する前に必ず一時停止するコードを挿入しましょう。

while True:
    get_items()
    if loop == "last"
        break

また、今回のように無限ループwhile Trueを書くときは、ループを終了させる処理を必ず入れましょう。特に通信先などに迷惑のかかる可能性がある状況で初めてコードを動かすときは、不具合によりループが止まらないことが考えられるため、コードの稼働状況を確認し、ループが止まることを必ず確認してください。

コードのまとめ

import requests
from bs4 import BeautifulSoup
import pandas
import time

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)
    url = get_nextpage(soup)
    return df, url

def get_nextpage(soup):
    try:
        url = soup.select_one("div.pager-next-btn > div.btn-wrap > a").get("href")
    except AttributeError:
        print("最後のページです")
        return
    url = "https://townwork.net" + url
    print(url)
    return url

def main():
    url = "https://townwork.net/joSrchRsltList/?fw=プログラミング"
    columns = ["rid", "company", "title", "salary", "access", "term", "timelimit"]
    df = pandas.DataFrame(columns=columns)
    while True:
        df, url = get_items(url, df, columns)
        time.sleep(5)
        if url is None:
            break

if __name__ == '__main__':
    main()

これですべてのページの要素を取得し、dfにデータを格納するコードが完成しました。

>>> print(df)
                                                   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
3                /detail/clc_3288029002/joid_Y003WBGX/  ...  12月23日 07:00
4                /detail/clc_0359862001/joid_Y003WFY3/  ...  12月23日 07:00
..                                                 ...  ...           ...
375  https://www.hatalike.jp/h/r/H103010s.jsp?RQ=Y0...  ...            不明
376  https://www.hatalike.jp/h/r/H103010s.jsp?RQ=Y0...  ...            不明
377  https://www.hatalike.jp/h/r/H103010s.jsp?RQ=Y0...  ...            不明
378  https://www.hatalike.jp/h/r/H103010s.jsp?RQ=Y0...  ...            不明
379                    /detail/clc_0287030008/?opf=PR4  ...  12月23日 07:00

[380 rows x 7 columns]

dfをprintして中身を確認すると、すべての要素が取得できていることがわかります。

今回は、HTML内の次のページへのリンクを順次取得してループで回す、という方法を実行しましたが、URLを操作してループで回すという方法もあります。

例えば今回のタウンページの検索結果の2ページ目は、

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

と、page=2のようにURLの後ろにページ番号を付加することで該当のページへと遷移することができます。

これを利用して、

page_number = ... #合計のページ数を取得するコード

for i in range(page_number):
    url = "https://townwork.net/joSrchRsltList/?fw=プログラミング&page={}".format(i+1)
    df = get_items(url)
    time.sleep(5)

のようにして、ページ数分だけURLをループさせるコードにより、すべてのページの要素を取得することも可能です。

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