はじめに
JavaScriptによってコンテンツが生成されるサイトは、よく使用されるBeautifulSoup4
だけではスクレイピングできません。
例えば「最後までスクロールすると次のコンテンツが表示される」といったサイトです。
URLが変化するわけでもないし、どうすればいいのでしょうか…。
そんなときに登場するのがSelenium
+PhantomJS
です。
背景
さまざまなケースでWebスクレイピングができるようになりたいという想いのもと、今回も『Pythonクローリング&スクレイピング』という本を参考に実践していきます。
単純に、Webスクレイピングは楽しいです。
やること
「note」というサイトのスクレイピングを行います。
トップページに表示される投稿の
- タイトル
- URL
- 概要
を抽出し、MongoDBやcsv、RSSで保存します。
何ができるのか
以下のようなページからスクレイピングすることで
※「note」のキャプチャ
以下のような投稿一覧が得られます。
※csvファイルをExcelで読み込んだ画面のキャプチャ
流れ
- SeleniumとPhantomJSでJavaScriptを用いるサイト(今回は「note」)からHTMLを抽出
- 抽出したHTMLをBeautifulSoup4でパースし、必要な情報を抽出1
- MongoDBにデータを保存
- csv形式でデータを保存
- RSS形式でデータを保存
環境
- Windows10 64bit
- Python3.7
- MongoDB 4.0.2
- Selenium 3.14.0
- PhantomJS 2.1.1
- beautifulsoup4 4.6.3
前準備
- PhantomJSをダウンロードし、解凍し、PATHを通しておく必要があります。
- コードを実行する前にMongoDBをインストールし、起動させておく必要があります。
コード
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)
まとめ
「最後までスクロールすると次のコンテンツが表示される」
といった、一見するとどうやってスクレイピングすればよいのか分からないサイトでも、Selenium
とPhantomJS
を使うとスクレイピングできることが分かりました。
参考
自分の過去記事も参考にしました。
- 「小説家になろう」をPythonでスクレイピングして本文を保存する(自然言語処理用コーパス作成) - Qiita
- YouTubeのAPIで取得した輝夜月さんの動画情報をMongoDBやpandasで触ってみた - Qiita
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スクレイピングでは著作権等で気をつけるべきことがあります。
以下記事を参考に、ルール・マナーを意識しておきましょう。
関連記事【スクレイピング・自動化】
-
基本的なスクレイピング(
BeautifulSoup4
) -
APIを利用したスクレイピング・データベースへの保存(
MongoDB
)・データ分析の一歩(pandas
) -
JavaScriptを用いたサイトのスクレイピング(
Selenium
,PhantomJS
) -
メール通知(
smtplib
)・定期的なプログラムの自動実行(タスクスケジューラ
) -
自動ログイン(
Selenium
,ChromeDriver
)