Log

いろいろ

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: /

「Hatena Antenna」以外は明示的なDenyは無し。

環境

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

BeautifulSoup4はpip installします。

pip install beautifulsoup4

作成したクローラ

フロー

  •  ランキングページから各企業ページのURLを生成
  • BeautifulSoup4を用いてURLからHTMLを取得
  • HTMLを解析して必要なデータを抽出

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

matsuql.hatenablog.com

ただし、今回はSeleniumは使いません。

遷移ボタンクリック時に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系はサポート終了とのこと。

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