人生100年!生涯エンジニア人生!

楽しいエンジニア人生!

アンカータグに入ったJavaScriptスキームのサイトにScrapyで挑む

Scrapyしにくいサイト

ここです。
jinzai.hellowork.mhlw.go.jp
ちょっとアクセスすると判るのですが、hrefJavaScript:に入れまくっている(JavaScriptスキーム)平成初期の臭いが漂うサイトですね。
今回は、Scrapyを使ってスクレイピングしてみます。

Scrapyの下準備

Scrapyの説明も含めて、こちらをごらんください。
qiita.com
非常に解りやすいので、オススメです。

デベロッパーツールはともだち

Scrapyは何でもいけますが、リクエスト内容がわからないと画面遷移ができないので調査します。
サイトにアクセスしてから、デベロッパーツールを開きます。
※POSTMANなどツールもありますが、今回は使わないでいきます。
ご存知だと思いますが [表示] - [開発 / 管理] - [デベロッパーツール]でデベロッパーツールで開きます。(ショートカットキーでもOK)
そしてNetworkに切り替えます。
f:id:hideaki_kawahara:20200511182736p:plain

start_urlsのURLを探索する

調査したいリンクを押下します。
そして、Networkで表示されたリソースの一番上を押下して情報を所得したら、一番下までスクロールします。
f:id:hideaki_kawahara:20200511183452p:plain

Form Dataが必要な情報になります。
Form Dataにトークンらしきものが無ければ、このForm Dataのデータを使ってGet Methodにしてアクセスしてみます。
Get Methodにできればコードが減るので、このURLをとりあえず覚えておきます。
※Post Methodのみだったら、ここからコードを書くことになります。

さらにもっとできないか探索します。
調査を続けます、次に都道府県の東京都をチェックしてから検索を押下します。
同じようにForm Dataを見て、トークンらしきものが無ければForm Dataのデータを使ってGet Methodにしてアクセスをします。
そうすると、アクセスできたので、このURLもとりあえず覚えておきます。

トークンがある

検索結果を全て取得したいので、次ページのページネーションを確認します。
画面の下の2を押下します。
そうすると、ついにトークンらしきものが現れました。
f:id:hideaki_kawahara:20200512060855p:plain

ここからは、Post Methodの方が良さそうなので、さきほど覚えたURLをScrapyのstart_urlsとしてコードを書きます。

コードを書く

会社名を取得するので、items.py はcompany_nameと書く。

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy

class Post(scrapy.Item):
    company_name = scrapy.Field()

spiders/hellowork.py はこんな感じです。

# -*- coding: utf-8 -*-
import re
import scrapy
from .. items import Post

class HelloworkSpider(scrapy.Spider):
    name = 'Hellowork'
    allowed_domains = ['jinzai.hellowork.mhlw.go.jp']
    start_urls = ['https://jinzai.hellowork.mhlw.go.jp/JinzaiWeb/GICB102010.do?action=search&screenId=GICB102010&cbTokyo=1']

    def parse(self, response):
        for post in response.css('table#search tr'):
            name = post.css('span#ID_lbJigyoshoName::text').extract_first()
            if name is None:
                continue
            yield Post(
                company_name = name
            )

        # 現在のページ数を取得する
        for current in response.css('table tr td[style="width: 60%;"]'):
            current_page = current.css('span#ID_lbSearchCurrentPage::text').extract_first()
            if current_page is None:
                return

        # 次のページがあるか確認する
        next_page = int(current_page) + 1
        has_next_page = False
        for id_pager_tag in response.css('table tr td[style="padding:5px;"]'):
            id_pager_href = id_pager_tag.css('a::attr(href)').extract_first()
            if id_pager_href is not None:
                find_array = re.findall('[0-9]+', id_pager_href)
                if len(find_array) == 0:
                    continue
                if int(find_array[0]) == next_page:
                    has_next_page = True
                    next_page = str(next_page)
                    break

        # 次ページが無ければ終了する
        if has_next_page is False:
            return

        # 次のページをリクエストして、解析用parseに渡す
        yield scrapy.FormRequest.from_response(
                    response,
                    formdata = dict( screenId = 'GICB102010', action = 'page', cbTokyo = '1', curPage = current_page, params = next_page),
                    callback = self.parse, dont_click = True 
                )
        return

説明

  1. parseのresponse.css('table#search tr'):から会社名を取得します。
    会社名はID名がsearchのtableタグのの中にあるのでresponse.css('table#search tr')というので検索せます。
    会社名自体はspanタグのID名がID_lbJigyoshoNameのテキスト項目なのでspan#ID_lbJigyoshoName::textで引っ張り出します。
    なお、tableタグの中にはtrには何も定義が無いので、余分なtrも検索してしまうので、spanタグがうまく取れなかったらスルーします。

  2. 次ページみたいのがないので、現在ページを取得+1を計算してページネーションと比較する方法を取ります。
    現在ページ + 1 in ページネーション数値を次ページとしたいので、まずは現在ページを取得します。
    会社一覧の上に小さく現在ページが表示されているので、そこのタグspan#ID_lbSearchCurrentPage::text がページ数らしいですので、それを取得します。

  3. 次はページネーションのところを探索します。
    table tr td[style="padding:5px;"] を配列で取得して、a::attr(href)JavaScriptスキームを取得して、そこから正規表現で数値だけを取り出します。
    なんで、こんな面倒なことをしているのかというと、上記のページネーションは世間一般のページネーションとは異なっているからで、次ページ表記っぽい>>が実は「10*次ページ数+1」という謎ルールで、それなら素直にアンカーダグのJavaScriptスキームを読んだほうが早いからです。

  4. 現在ページ + 1 in ページネーション数値と判定できれば次ページが存在する、無ければ存在しないのでプログラムは終了とする。

  5. 次ページに遷移させるところは、トークンらしきものがあるので、ここはFormRequestを使います。
    formdataに色々とデータを押し込んで、callbackparse再帰呼び出しで次ページが無くなるまで繰り返すようにして、dont_clickをTrueにしてJavaScriptスキームを発火させています。

動作結果

scrapy crawl Helloworkで実行すると、以下のように会社名が取れていることが確認できます。
f:id:hideaki_kawahara:20200512073118p:plain

なお、CSVファイルに出力したいときはscrapy crawl Hellowork -o company_name.csvとします。

今回、このサイトをスクレイピングして思ったのは、JavaScriptスキームも面倒でしたが、それ以上に面倒と思ったのは、タグにID名が振っていないと、スクレイピングしにくいわ!と心底思いました。
そのため、自分でサイトを作るときは、なるべくタグにはID名を振ろうと反省するのでありました。