Python+Scrapyでスクレイピングした結果をDB(PostgreSQL)に保存する
はじめに
前に、Scrapyでクローニング・スクレイピングするスクリプトを作成した。
このときは、とあるディレクトリにスクレイピングした結果をjsonにして出力した。
↓そのときの話
そして、出力したjsonを別のスクリプトでDBに保存しようかと考えていた。
前回全く使用していなかった「pipelines.py」に処理を追加すると、DBに追加出来ることがわかったので、処理を追加してみた。
目次
参考サイト
環境
- PostgreSQL 11
- Python 3.6.8
- Scrapy 1.5.1
- psycopg2
- Scrapyで作成したプロジェクト名:MachiMachi
DBのテーブル作成
スクレイピングした結果を保存するテーブルを作成する。
今回は、以下のデータを登録するようにした。
- URL【主キー】
- ドメイン
- 登録日時
- タイトル
- コンテンツ(本文)
SQLはこんな感じ
-- コンテンツ create table contents ( url text not null , domain_name text not null , register_date timestamp with time zone not null , title text not null , contents text not null , constraint contents_PKC primary key (url) ) ; comment on table contents is 'コンテンツ'; comment on column contents.url is 'URL'; comment on column contents.domain_name is 'ドメイン'; comment on column contents.register_date is '登録日時'; comment on column contents.title is 'タイトル'; comment on column contents.contents is 'コンテンツ';
スパイダーの作成
今回も前回と同様に、「ライフハッカー(日本語版)」と「TechCrunch(日本語版)」をクローニングする。
前に作成したスパイダーと全く同じものを使うので、今回は省略。
DB接続用の設定ファイルを作成
Scrapyで作成したプロジェクトディレクトリにDB接続用の設定ファイル「setting.ini」を作成し、pipelineクラスから使用するようにした
[default] host=192.168.xx.xx port=5432 dbname=xxxxxxx user=xxxxxxx password=xxxxxxx
パイプの作成
Scrapyのドキュメントを参考に、スクレイピングした結果をDBにインサートする処理を作成
URLを主キーにしているので、既に同じURLが存在している場合は、インサートしないようにしている。
from configparser import ConfigParser import logging import psycopg2 import datetime class MachimachiPipeline(object): def __init__(self): self.connection = None self.cursor = None self.register_datetime = datetime.datetime.now() def open_spider(self, spider): """ スパイダーが実行されたときに呼ばれる :param spider:実行中のスパイダー """ config = ConfigParser() config.read('setting.ini') host = config.get('default', 'host') port = config.get('default', 'port') db_name = config.get('default', 'dbname') user = config.get('default', 'user') password = config.get('default', 'password') logging.info('connecting to db.') self.connection = psycopg2.connect('host=%s port=%s dbname=%s user=%s password=%s' % (host, port, db_name, user, password)) self.connection.autocommit = False self.cursor = self.connection.cursor() logging.info('connected to db.') def close_spider(self, spider): """ スパイダーの処理が終わるときに呼ばれる :param spider:処理中のスパイダー """ self.connection.commit() self.cursor.close() self.connection.close() logging.info('close connection to db.') def process_item(self, item, spider): """ スクレイピングされた項目毎に呼ばれる :param item:スクレイピングで取得したデータ :param spider:処理中のスパイダー :return:dictデータ """ url = item['url'] self.cursor.execute('SELECT * FROM contents WHERE (url = %s)', (url,)) record = self.cursor.fetchone() if record is not None: logging.info('url is already registered. url:%s' % (url,)) return item else: values = ( item['url'], spider.allowed_domains[0], self.register_datetime, item['title'], item['body'] ) self.cursor.execute('INSERT INTO contents (url, domain_name, register_date , title, contents)' ' VALUES (%s, %s, %s, %s, %s)', values) logging.info('contents is registered!!. url:%s' % (url,)) return item
設定の変更
Scrapyの設定も変更する。
「settings.py」の中に、パイプラインの設定がコメントアウトされているので、そのコメントアウトを解除する。
# Configure item pipelines # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { 'MachiMachi.pipelines.MachimachiPipeline': 300, }
クローニングの実行
全スパイダーのクローニングを実行するスクリプトを作成する(前に作成したものを一部修正しただけ)
修正点としては、DBに保存するのでファイル出力をしないようにした。
import subprocess import multiprocessing def get_crawler_list(): process = subprocess.Popen('scrapy list', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout_data, stderr_data = process.communicate() if process.returncode == 0: strings = stdout_data.decode('utf-8').split('\n') return list(filter(None, strings)) else: raise RuntimeError() def execute_scraping(crawler_name): cmd = 'scrapy crawl %s --loglevel=INFO' % (crawler_name,) subprocess.call(cmd.split()) def main(): jobs = [] for crawler_name in get_crawler_list(): job = multiprocessing.Process(target=execute_scraping, args=(crawler_name,)) jobs.append(job) job.start() [job.join() for job in jobs] print('finish !!!!') if __name__ == '__main__': main()
実行結果
クローニングを実行すると、PostgreSQLのテーブルに、以下のようにデータが保存された。(見やすくするため、ドメインとタイトルのみ表示)
select domain_name, title from contents;
domain_name | title -------------------+------------------------------------------------------------------------------------------------------------- www.lifehacker.jp | 孫の世話をする祖父母は長生きする:調査結果 jp.techcrunch.com | Amazonが家庭用メッシュルーターのEeroを買収してEcho製品拡販のベースに jp.techcrunch.com | ジェフ・ベゾスのメッセージ暴露、サウジが関与を否定 jp.techcrunch.com | 米国のiPhoneユーザーが昨年アプリに使った金額は平均79ドル、前年比36%アップ jp.techcrunch.com | GoogleドキュメントのAPIでタスクの自動化が可能に jp.techcrunch.com | 無登録物件のリスト掲載でパリ市がAirbnbを告訴 jp.techcrunch.com | 小売・飲食企業のアプリ開発支援を手がけるエンターモーションが2億円調達 jp.techcrunch.com | ソフトバンク、自動運転配達のnuroに9.4億ドルを資金提供 www.lifehacker.jp | 合体と分離ができる3in1トラベルバッグ「JW Weekender」を使ってみた www.lifehacker.jp | そこまでの生産性、本当に必要ですか? www.lifehacker.jp | キッチンに貼るだけで鍋蓋もまな板もスッキリ収納。菜箸置きにも使えそう〜 www.lifehacker.jp | 「この1本」で仕事はもっと上手くいく。ビジネスコーチに教わる缶コーヒー活用術 www.lifehacker.jp | オフィスとジムで兼用できる! どんなシーンでも履ける万能シューズがコール ハーンから登場 www.lifehacker.jp | 贈った方も得をする! コスパ抜群な「バレンタインギフト」を選ぶコツとは? www.lifehacker.jp | 元Google人材育成統括部長からのメッセージ「もうがんばらないでください」 www.lifehacker.jp | 子どもに初めてのスマホ、何に注意すべき? 「2019年度版チェックリスト」 www.lifehacker.jp | 世界的メール配信サービス会社メールチンプのCEO、ベン・チェストナットさんの仕事術 www.lifehacker.jp | 働き方を変えるには「短パン」から? 全員「複業」スタートアップCEOの仕事術 www.lifehacker.jp | オフシーズンの劇場をレンタルスペース化。アーティスティック・ディレクター、エイドリアンさんの仕事術 www.lifehacker.jp | 親が子どもに言ってはいけない8つの言葉とは? www.lifehacker.jp | 自分が「敏感すぎる」「繊細すぎる」人かどうかを測る「HSPセルフチェック」
おわりに
パイプラインを使い、DBに登録する処理ができた。
ドキュメントには、パイプラインの使用用途として、以下を例に挙げていた
- HTMLのクレンジング
- データのバリデーション
- 重複データの削除
- データベースへの保存
Scrapyでクローニング・スクレイピングした結果を、他のモジュールで加工・解析・保存をやろうと考えていたけど、パイプラインの組み合わせだけで、テキストマイニングまで出来そう。