#oyasuminase

駆け出しオタクエンジニア

PythonとBeautifulSoup4でスクレイピング

PythonのライブラリであるBeautifulSoup4を用いてwebサイトからスクレピングをするメモ。今回対象となるwebサイトは企業価値検索サービスUlletです。

Ulletとは

決算情報や従業員数などの上場企業のデータが掲載されているwebサイトです。

www.ullet.com

近年の小洒落たwebサイトと違い懐かしさのある無骨なデザイン。ドメイン+企業IDで遷移できるためSelenium使わなくていいかなとなりました。robots.txtは下記の通り。

User-agent: *
Sitemap: http://www.ullet.com/sitemap.xml

User-agent: Hatena Antenna
Disallow: /

サイトマップを提示してクローリングを促進しているのでOKでしょう。Hatena Antennaがなぜブロックされいるのか気になりますが。

環境

  • macOS:10.15.2
  • Python:3.8.0
  • BeautifulSoup4:4.8.2

BeautifulSoup4はpip installします。

pip install beautifulsoup4

作成したクローラ

ランキングページから各企業ページのURLを生成してBeautifulSoup4に与えHTMLを取得します。後はHTMLから必要なデータを抽出していくだけです。

ページ遷移は昨日書いたように遷移後のURLを叩いても意味がないので別の方法を用います。

oyasuminase.hatenablog.com

今回は遷移ボタンクリック時にhttp://www.ullet.com/search/page/{対象のページ番号}.htmlへリクエストが投げられていることを確認したのでこのURLを動的に生成します。

メイン

def scrapy_ullet():
    rows = []

    # 総ページ数の取得
    res = requests.get(URL_ULLET_SEARCH)
    soup = BeautifulSoup(res.text, 'html.parser')
    li_all = soup.find('ul', class_='mg_menu_tab mg_menu_tab_top_reverse').find_all('li')
    max_page_txt = li_all[len(li_all)-1].span.a.get_text()
    max_page_num = int(re.search('\d+', max_page_txt).group())

    for i in range(1, max_page_num+1):
        read_page(i, rows)

    write_csv(rows)

総ページ数分read_page()を呼び出して最後に取得結果をcsvに出力します。

ページ読み込み

def read_page(page, rows):
    print(datetime.now().strftime('%Y/%m/%d %H:%M:%S'), 'page%d'%page)
    
    res = requests.get(URL_ULLET_PAGE%page)
    soup = BeautifulSoup(res.text, 'html.parser')
    a_all = soup.find('div', id='ranking').find_all('a', class_='company_name')

    for a in a_all:
        rows.append(get_company_detail(URL_ULLET_TOP+a.get('href')))

対象ページの企業情報を取得してrowsに格納します。

企業情報取得

def get_company_detail(url):
    res = requests.get(url)
    soup = BeautifulSoup(res.text, 'html.parser')

    # サーバー負荷軽減のため一定時間待機する
    time.sleep(REQUEST_WAIT_TIME)

    row = []
    row.append(re.search('\d+', url).group()) #コード
    row.append(soup.find('a', id='company_name0').get_text()) #企業名
    row.append(trim_indicator(soup, 0)) #売上高
    row.append(trim_indicator(soup, 1)) #純利益
    row.append(trim_indicator(soup, 2)) #営業CF
    row.append(trim_indicator(soup, 3)) #総資産
    row.append(soup.find('table', class_='company_outline').tbody.find_all('tr')[2].td.get_text()) #従業員数(単独)
    row.append(soup.find('table', class_='company_outline').tbody.find_all('tr')[3].td.get_text()) #従業員数(連結)
    row.append(soup.find('table', class_='company_outline').tbody.find_all('tr')[4].td.get_text()) #平均年齢(単独)
    row.append(soup.find('table', class_='company_outline').tbody.find_all('tr')[5].td.get_text()) #平均勤続年数(単独)
    row.append(soup.find('table', class_='company_outline').tbody.find_all('tr')[7].td.get_text()) #業種

    return row

企業詳細ページの至るところから正規表現を使い気合でデータを取得します。BeautifulSoup.find()は最初に適合した要素のみを返却するので条件に気をつけること。

また、短時間で大量のリクエストを投げないようにRUQUEST_WAIT_TIMEで指定した時間待機しています。今回は1秒を設定してあり2020年1月3日アクセス時点で3,736社掲載されているため実行に1時間以上かかります。これはもう仕方ない。

データ整形

def trim_indicator(soup, index):
    i = soup.find('tbody').find_all('tr')[1].find_all('td')[index].get_text()
    r = re.search('.*円', i)
    if r != None:
        return r.group()
    else:
        return ""

経営指標の4つはHTML構成の都合でget_text()に余分な値が含まれたり、そもそも値が存在しなかったりするのでget_company_detail()から切り出して処理を書いています。

csv出力

def write_csv(rows):
    with open("ullet.csv", "w", encoding='utf-8') as f:
        writer = csv.writer(f, delimiter=",", quotechar='"', quoting=csv.QUOTE_ALL)
        for row in rows:
            writer.writerow(row)

quotechar='"'quoting=csv.QUOTE_ALLを指定することで全ての値をダブルクォーテーションで囲っています。

以下コード全文。

github.com

あとがき

わりと短くまとまった気がしています。今回使わなかったSeleniumを取り入れたり結果をDBに保存したりlambdaに置いて実行したりすると大変ですが結構勉強になります。もう既に忘れかけているのでまた今度やり直して記事にまとめられたらいいなあ。

それとPCを購入したばかりなのでちまちま環境構築しているのですがMacに最初からPythonが入ってることに気づいて驚きました。2系なのでさらに驚きました。ちょうど2020年1月1日でPython2系はサポート終了とのこと。

環境構築は面倒ですね。早く整えたいです。