LoginSignup
18

More than 3 years have passed since last update.

posted at

updated at

JavaScriptでコンテンツが生成されるサイトのスクレイピング【Selenium+PhantomJS】

はじめに

JavaScriptによってコンテンツが生成されるサイトは、よく使用されるBeautifulSoup4だけではスクレイピングできません。
例えば「最後までスクロールすると次のコンテンツが表示される」といったサイトです。
URLが変化するわけでもないし、どうすればいいのでしょうか…。
そんなときに登場するのがSelenium+PhantomJSです。

背景

さまざまなケースでWebスクレイピングができるようになりたいという想いのもと、今回も『Pythonクローリング&スクレイピング』という本を参考に実践していきます。
単純に、Webスクレイピングは楽しいです。

やること

note」というサイトのスクレイピングを行います。
トップページに表示される投稿の

  • タイトル
  • URL
  • 概要

を抽出し、MongoDBやcsv、RSSで保存します。

何ができるのか

以下のようなページからスクレイピングすることで
pre.PNG
※「note」のキャプチャ

以下のような投稿一覧が得られます。
post.PNG
※csvファイルをExcelで読み込んだ画面のキャプチャ

流れ

  1. SeleniumとPhantomJSでJavaScriptを用いるサイト(今回は「note」)からHTMLを抽出
  2. 抽出したHTMLをBeautifulSoup4でパースし、必要な情報を抽出1
  3. MongoDBにデータを保存
  4. csv形式でデータを保存
  5. RSS形式でデータを保存

環境

  • Windows10 64bit
  • Python3.7
  • MongoDB 4.0.2
  • Selenium 3.14.0
  • PhantomJS 2.1.1
  • beautifulsoup4 4.6.3

前準備

コード

0. logのためのおまじない

参考:ログ出力のための print と import logging はやめてほしい - Qiita

from logging import getLogger, StreamHandler, DEBUG
logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBUG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

1. SeleniumとPhantomJSでJavaScriptサイトからHTMLをスクレイピング

AttributeError: 'NoneType' object has no attribute 'attrs'が出る場合、sleep_timeの数を大きくして再実行してみてください。

import sys
import time
from selenium import webdriver # pip install selenium
from selenium.webdriver.support.ui import WebDriverWait
from bs4 import BeautifulSoup

# スクレイピングするサイトのurlを指定
url = 'https://note.mu/'

# 読み込む投稿の多さを指定(amount * 10 投稿程度)
amount = 2

# 読み込み時に待機する時間の指定
sleep_time = 5


def main():
    """
    メイン処理
    """

    # PhantomJS本体のパスを指定
    pjs_path = r"C:\phantomjs-2.1.1\bin\phantomjs.exe"
    driver = webdriver.PhantomJS(executable_path=pjs_path)

    # ページの読み込み
    navigate(driver, url, amount=amount, sleep_time=sleep_time)

    # データの抽出
    posts = scrape_posts(driver, url)

    return posts


def navigate(driver, url, amount=1, sleep_time = 5):
    """
    目的のページに遷移する
    amount >= 1
    """

    logger.debug('Navigating...')
    driver.get(url)
    assert 'note' in driver.title

    # 指定した回数分、ページ下部までスクロールしてコンテンツの生成を待つ
    for i in range(1, amount+1):
        driver.execute_script('scroll(0, document.body.scrollHeight)')
        logger.debug('Waiting for contents to be loaded...({0} times)'.format(i))
        time.sleep(sleep_time)


def scrape_posts(driver, url):
    """
    投稿のURL、タイトル、概要のdictをリスト形式で取得
    """

    posts = []

    # Seleniumで取得したHTMLをBeautifulSoup4に読み込む
    html = driver.page_source
    bsObj = BeautifulSoup(html,"html.parser")

    for post_html in bsObj.findAll("div",{"class":"c-card__body"}):
        # 記事のURLを取得
        content_url = post_html.find("h3").find('a').attrs['href']
        content_full_url = url + content_url[1:]

        # 記事タイトルを取得
        title = post_html.find("h3").find('a').find('span').get_text()
        title = title.replace('\n', '') # 改行を削除

        # 記事の概要を取得
        try:
            description = post_html.find("p", {'class':'p-cardItem__description'}).get_text()
            description = description.replace('\n', '') # 改行を削除
        except AttributeError as e:
            description = '-no description-'
            logger.debug("「{0}」 has no description: {1}".format(title, e))

        posts.append({
            'url': content_full_url,
            'title': title,
            'description': description,
            })

    return posts

実行

posts = main()

2. MongoDBにデータを保存

データベース用のディレクトリを作成し、コマンドプロンプト上で

mongod --dbpath "${データベース用のディレクトリへのパス}"

を実行。MongoDBを起動しておく。

from pymongo import MongoClient, DESCENDING # pip install pymongo

# MongoDBとの接続
mongo_client = MongoClient('localhost', 27017) # MongoDBと接続
db = mongo_client.note
collection = db.recomend # noteデータベース -> recomendコレクション
collection.delete_many({}) # 既存の全てのドキュメントを削除しておく

def save_to_mongodb(collection, items):
    """
    MongoDBにアイテムのリストを保存
    """

    result = collection.insert_many(items) # コレクションに挿入
    logger.debug('Inserted {0} documents'.format(len(result.inserted_ids)))

# 実行
save_to_mongodb(collection, posts)

3. csv形式でデータを保存

csvファイルで保存しておけば、非エンジニアの方もExcelでソート等の分析ができて便利です。
MongoDBをインストールする手間も省けます。
そこで、ここでは

  • MongoDBを経由する場合
  • MongoDBを使用しない場合

の2種類の方法を掲載します。

※作成したcsvファイルは文字コードがutf-8のため、Excelで普通に開くと文字化けします。以下サイトの手順で開きましょう。
 「Excelで開くと文字化けするUTF-8のCSVを文字コードを変換せずに開く方法」

import csv

def save_as_csv(posts, csv_name):
    # 列名(1行目)を作成
    ## [タイトル、URL、概要]
    col_name = ['title', 'url', 'description']

    with open(csv_name, 'w', newline='', encoding='utf-8') as output_csv:
        csv_writer = csv.writer(output_csv)
        csv_writer.writerow(col_name) # 列名を記入

        # csvに1行ずつ書き込み
        for post in posts:
            row_items = [post['title'], post['url'], post['description']]
            csv_writer.writerow(row_items)
# =====
# MongoDBを経由する場合
# from pymongo import MongoClient, DESCENDING # pip install pymongo
# mongo_client = MongoClient('localhost', 27017) # MongoDBと接続
# db = mongo_client.note
# collection = db.recomend # noteデータベース -> recomendコレクション
# posts = collection.find()

# MongoDBを使用しない場合
# postsはmain()の返り値
# =====

# ファイル名を指定
csv_name = 'note_list.csv'

save_as_csv(posts, csv_name)

4. RSS形式でデータを保存

import feedgenerator # pip install feedgenerator

def save_as_feed(f, posts):
    """
    コンテンツリストをRSSフィードとして保存
    """

    feed = feedgenerator.Rss201rev2Feed(
        title='おすすめノート',
        link='https://note.mu/',
        description='おすすめノート')

    for post in posts:
        feed.add_item(title=post['title'], link=post['url'],
                     description=post['description'], unique_id=post['url'])

    feed.write(f, 'utf-8')

# postsは「3. csv形式でデータを保存」を参照
with open('note_recommend.rss', 'w', encoding='utf-8') as f:
    save_as_feed(f, posts)

まとめ

「最後までスクロールすると次のコンテンツが表示される」
といった、一見するとどうやってスクレイピングすればよいのか分からないサイトでも、SeleniumPhantomJSを使うとスクレイピングできることが分かりました。

参考

コードは主に以下を参考にさせていただきました。2 3

自分の過去記事も参考にしました。

GitHub

JupyterNotebookをGitHub上で公開しています。
https://github.com/kokokocococo555/JavaScriptScraping-demo

課題

  • 「1. SeleniumとPhantomJSでJavaScriptサイトからHTMLをスクレイピング」実行時にAttributeError: 'NoneType' object has no attribute 'attrs'が出ることがあるが、原因は未解明。
    • 一応の対処法も記載しているが、どこまで効果があるのやら…。
    • (2018-09-18追記)エラーが出る理由が書籍のサポートページに載っていた(「P.198, 5.6.2 noteのおすすめコンテンツを取得する」)。対応策も本記事と同じく、time.sleep(2)を入れるという方法。これ以上は放置、という方向でしょうか。

注意

Webスクレイピングでは著作権等で気をつけるべきことがあります。
以下記事を参考に、ルール・マナーを意識しておきましょう。

関連記事【スクレイピング・自動化】


  1. 参考にした書籍ではSeleniumでパース・情報抽出まで行っていましたが、私の環境では上手くできなかったため、ここの処理は慣れているBeautifulSoup4で行いました。 

  2. noteのHTML構成が本書出版時と異なっていたため、スクレイピングのコードは自力で書きました。 

  3. 本記事では「スクレイピングする投稿数を指定できるようにする」などの工夫を加えています。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
18