Python3.6+Scrapyでスクレイピングしてみた
はじめに
機械学習について勉強するため、機械学習を使った何かを作ろうと思っている今日このごろ
いくつかサンプルが載っているような本を買っても、サンプルを動かすのはモチベーションが上がらない
やはりモチベーションが上がるものは、自分がやりたいを作るべきだなぁ
自分が機械学習を利用してやりたいことはなんだろうなーと考えた
自分が興味あるものを学習して、コンテンツ(または元のサイトのURL)を配信するものを作ってみたい
もうすでに、公開されているサービスを利用しているけど気にしない(作ることにきっと意味がある)
そんなことで、コンテンツの内容を取得するため、Pythonでスクレイピングをやってみることにした。
昔にスクレイピングをやったことがあるけど、サーバに負荷を掛けないように配慮されたライブラリを探してみた。(昔使っていたのは、beautifulsoup4というライブラリ)
Scrapyというライブラリが、クローリングの際、時間を空けてクローリングができるみたい
なので、今回はScrapyでスクレイピングをやってみた話。
目次
参考サイト
Scrapyのインストール
まず、Scrapyをインストール
pip install scrapy
Scrapyを使うための作業
- scrapy startprojectでScrapyのプロジェクト作成
- itemsにスプレイピング後のデータ構造を定義
- scrapy genspiderでクローリング・スクレイピングをするためのクラス(spider)を作成
- settings.pyにクローニング設定を記述
- scrapy crawlでクローニングとスクレイピングを実行
チュートリアルは以下のサイト
Scrapy Tutorial — Scrapy 1.6.0 documentation
1. Scrapyのプロジェクト作成
以下のコマンドで、Scrapyのプロジェクト作成を作成する。今回のプロジェクト名はscraperにした
scrapy startproect scraper
このコマンドを実行すると、以下のようなディレクトリ・ファイルが生成される。
scraper/ ├── scraper │ ├── __init__.py │ ├── items.py │ ├── middlewares.py │ ├── pipelines.py │ ├── settings.py │ └── spiders │ └── __init__.py └── scrapy.cfg
2. スクレイピング後のデータ定義
スクレイピングしたときに取得する情報を「items.py」に定義する。
今回は、タイトル、本文、スクレイピングしたURLを取得しようと思ったので、以下のような感じになった。
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class ScraperItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() url = scrapy.Field() title = scrapy.Field() body = scrapy.Field()
3. スクレイピングをするためのクラス(spider)を作成
最初のスクレイピングを試すサイトとして、LifeHacker(日本語版)のページでやってみることにした。
作成したプロジェクトフォルダの中で、spiderを作成するコマンドを実行する
cd scraper
scrapy genspider lifehacker www.lifehacker.jp
spidersフォルダの中に新しく「lifehacker.py」が作成されているので、この中にスクレイピングのロジックを作成する
# -*- coding: utf-8 -*- import scrapy from scraper.items import ScraperItem class LifehackerSpider(scrapy.Spider): name = 'lifehacker' allowed_domains = ['www.lifehacker.jp'] start_urls = ['https://www.lifehacker.jp/'] # httpをhttpsに変更 def parse(self, response): for content_item in response.css('div.lh-summary'): item = ScraperItem() href = content_item.css('h3.lh-summary-title a::attr(href)').extract_first() title = content_item.css('h3.lh-summary-title a::text').extract_first() item['title'] = title url = response.urljoin(href) item['url'] = url yield scrapy.Request( url, callback=self.parse_detail, meta={'item': item} ) @classmethod def parse_detail(cls, response): item = response.meta['item'] str_list = response.css('#realEntryBody *::text').extract() item['body'] = ''.join(str_list) yield item
最初はLifeHacker(日本語版)のTopページにアクセスし、タイル状に並んでいるコンテンツを更にスクレイピングしている。
1つじゃ物足りなので、TechCrunch(日本語版)もスクレイピングするクラスを作成してみた
以下のコマンドで、TechCrunch用のspiderを作成
TechCrunchはRSSがあったので、RSS経由でコンテンツを取得するようにする。
scrapy genspider techcrunch https://jp.techcrunch.com/feed/
新しく作成された「techcrunch.py」にスクレイピングのロジックを作成する。
# -*- coding: utf-8 -*- import scrapy from scraper.items import ScraperItem class TechcrunchSpider(scrapy.Spider): name = 'techcrunch' allowed_domains = ['jp.techcrunch.com'] start_urls = ['https://jp.techcrunch.com/feed/'] # httpをhttpsに変更 def parse(self, response): response.selector.remove_namespaces() for content_item in response.css('item'): item = ScraperItem() title = content_item.css('title::text').extract_first() link = content_item.css('link::text').extract_first() item['title'] = title item['url'] = link yield scrapy.Request( link, callback=self.parse_detail, meta={'item': item} ) @classmethod def parse_detail(cls, response): item = response.meta['item'] str_list = response.css('div.article-entry.text :not(div):not(script):not(style):not(span)::text')\ .extract() item['body'] = ''.join(str_list).strip() yield item
4. クローニング設定を記述
スクレイピングの共通な設定は、「settings.py」にあり、以下の項目を設定した。
DOWNLOAD_DELAY = 3 FEED_EXPORT_ENCODING = 'utf-8'
DOWNLOAD_DELAYは、同じWebページ内でのダウンロード待ち時間。
FEED_EXPORT_ENCODINGは、スクレイピングの結果をファイル出力するときのエンコード設定。これを設定せずにファイル出力すると、日本語文字が「\u8a71」みたいな文字になる
5. クローニングの実行
スクレイピングを単体で実行するには、以下のコマンドを実行する。
scrapy crawl <スパイダー名> -o <出力ファイルパス>
なので今回作成した、LifeHackerをスクレイピングする場合は、以下のコマンドになる
scrapy crawl lifehacker -o result.json
スクレイピング中のログを表示したくない場合は、オプションに「--nolog」を追加する
scrapy crawl lifehacker -o result.json --nolog
クローニングの実行結果
LifeHacker(日本語版)をクローニングした結果はコチラ
全部で37のデータが取れたけど、長くなるので一部だけ
[ { "title": "朝の出勤時間を早めると得られる8つのメリット", "url": "https://www.lifehacker.jp/2019/02/if-youre-lazy-show-up-early-to-work.html", "body": "(中略)" }, { "title": "ペットに合った温度にできるホットマット?!防水で、自動電源オフ機能も搭載されてるから安心して使えるよ〜", "url": "https://www.lifehacker.jp/2019/02/amazon-pet-heater.html", "body": "(中略)" }, { "title": "ネットショッピングで衝動買いを防ぐコツ「曜日を決める」にある、2つのメリット", "url": "https://www.lifehacker.jp/2019/02/pick-a-day-of-the-week-to-do-all-of-your-online-shoppin.html", "body": "(中略)" } ]
クローニングの一括実行
コマンドライン上でscrapyのオプションを見る限り、作成した全てのspiderを起動する方法が無さそう・・・
なので、作成した全spiderを起動するスクリプトを作成した。
import subprocess import multiprocessing import datetime 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, execute_time): date_str = execute_time.strftime('%Y%m%d%H%M%S') output_file_name = '%s_%s.json' % (crawler_name, date_str) cmd = 'scrapy crawl %s -o scrape_results/%s --nolog' % (crawler_name, output_file_name) subprocess.call(cmd.split()) def main(): execute_time = datetime.datetime.now() jobs = [] for crawler_name in get_crawler_list(): job = multiprocessing.Process(target=execute_scraping, args=(crawler_name, execute_time)) jobs.append(job) job.start() [job.join() for job in jobs] print('finish !!!!') if __name__ == '__main__': main()
やっていることは
- コマンドscrapy listでspider名の一覧を取得し、spider名のリスト作成
- spiderをマルチスレッドで、それぞれ実行
- すべてのクローリングが終わるまで待機
クローニングの実行結果は、scrape_resultsというディレクトリに日時付きのファイル名で出力するようにしている。
おわりに
前にもスクレイピングをするpythonスクリプトを作成したことがあったけど、そのときには「同じドメインの場合、時間を空けてアクセス」するロジックが無かった・・・
このScrapyでは、自動でやってくれるのでとても便利
Tomcatのメモリ割り当てを自動で計算する
はじめに
AWSでTomcatサーバを作ったとき、インスタンスタイプに合わせて、毎回別のチューニングするのは面倒臭い。
なので、Tomcatに割り当てるメモリサイズを自動計算するスクリプトを作った
参考サイト
Tomcatのメモリサイズの自動計算
参考にしたサイトに以下のことが書いてあった
- パーマネント領域(-XX:MaxPermSize)搭載メモリの8分の1程度(初期値:64m)。
- メモリ最大使用量(-Xmx)搭載メモリの半分程度。
- メモリ初期使用量(-Xms)Xmxで指定した数値の半分位。
なので、書き込みを鵜呑みにして、以下のスクリプトを作成
# https://groupsession.jp/support/setup_08.html # http://yasuhiroa24.hateblo.jp/entry/2017/01/11/162101 MAX_MEM_SIZE=$(free -m | awk '/^Mem:/ {printf("%d", $2 / 2)}') START_MEM_SIZE=$(free -m | awk '/^Mem:/ {printf("%d", $2 / 4)}') PERM_MEM_SIZE=$(free -m | awk '/^Mem:/ {printf("%d", $2 / 8)}') if [ $PERM_MEM_SIZE -gt 256 ] then PERM_MEM_SIZE=256 fi CATALINA_OPTS="-Xms${START_MEM_SIZE}m -Xmx${MAX_MEM_SIZE}m -XX:MaxPermSize=${PERM_MEM_SIZE}m -server"
XX:MaxPermSizeのサイズは「256MBあれば十分」といくつかのサイトで見かけたので、256MB以上割り当てないようにした。
Tomcatをダウンロードした場合
【Tomcatの解凍ディレクトリ】/binの中にファイル「setenv.sh」(Windowsの場合は「setenv.bat」らしい)を新規作成する。
Tomcatをコマンドでインストールした場合
Amazon Linuxでは、Tomcat8がyumからインストールできる。
yumコマンドからインストールしたとき、setenv.shを作成しても設定が反映されなかった。
色々フォルダ・ファイルを漁ってみると、/etc/tomcat8/tomcat8.confというファイルがあり、このファイルで変数を突っ込む感じ
なので、ファイル/etc/tomcat8/tomcat8.confの最後に追加する
確認
以下のコマンドで、Tomcatのパラメータを確認
ps aux | grep java
/usr/lib/jvm/jre/bin/java -Xms246m -Xmx493m -XX:MaxPermSize=123m -server -classpath :/usr/share/tomcat8/bin/bootstrap.jar:/usr/share/tomcat8/bin/tomcat-juli.jar:/usr/share/java/commons-daemon.jar -Dcatalina.base=/usr/share/tomcat8 -Dcatalina.home=/usr/share/tomcat8 -Djava.endorsed.dirs= -Djava.io.tmpdir=/var/cache/tomcat8/temp -Djava.util.logging.config.file=/usr/share/tomcat8/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager org.apache.catalina.startup.Bootstrap start
ora2pgでOracleのテーブルをPostgreSQL9.6に移行した話
はじめに
とあるサービスを作ることになった。
あるパッケージに連帯するサービスで、パッケージではOracleDBを使用していた。
サービスの機能で、OracleDBの一部のテーブルのデータを取り出したかった。
しかし、サービス用のWebアプリをSpringBootで構築しようとし、OracleDBに接続できるかどうか試したところ上手くいかない・・・
運用するとなるとOracleDBのライセンス料がかかるし、どうせサービス用にパッケージとは別のDBを構築しないといけないし・・・
私はOracleDBを使ったことがなく、OracleDBのバージョンで別の部署の人がいつも揉めているイメージであまり使いたくない・・・
もうPostgreSQLを使いたい。もはやPostgreSQL信者
調べてみると、ora2pgというツールがあり、Oracle/MySQLからPostgreSQLへの移行を支援するツールがある。
パッケージで使用している一部のテーブルを、サービス用のDB(PostgreSQL)に移行・コピーしようとした
目次
環境
- Windows Server 2012(AWS)
- Oracle Database 11g Release 11.2.0.4.0 - 64bit Production
- CentOS 7.5(ローカル・Vagrant)
- PostgreSQL 9.6.9
- ora2pg 18.2
参考サイト
以下のサイトを参考にした。
ora2pgのインストール
ora2pgの動作に必要なパッケージをインストール
yum groupinstall "Development Tools" yum install libdbi-dbd-pgsql perl-ExtUtils-MakeMaker perl-DBI perl-CPAN
ora2pgに必要なパッケージをインストールするため、以下のツールをInstant Client for Linux x86-64 のダウンロードからダウンロードする。(アカウントの登録が必要)
Oracle client tool
- oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm
- oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm
- oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm
ダウンロードしたパッケージをインストール
rpm -Uvh oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm rpm -Uvh oracle-instantclient11.2-devel-11.2.0.4.0-1.x86_64.rpm rpm -Uvh oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm
ファイル「/etc/profile.d/oracle.sh」を新規作成し、Oracle用の環境変数を追加。
vim /etc/profile.d/oracle.sh #!/bin/sh export LD_LIBRARY_PATH=/usr/lib/oracle/11.2/client64/lib export ORACLE_HOME=/usr/lib/oracle/11.2/client64/
. /etc/profile
ora2pgはperlを使っていて、perlがOracleに接続できるようにDBD::Oracleのインストールする
cpan DBD::Oracle
このとき、cpanの設定(?)を聞かれたけど、全部デフォルトにした。
cd /usr/local/src/ wget https://github.com/darold/ora2pg/archive/v18.2.tar.gz -O ora2pg_v18.2.tar.gz tar xf ora2pg_v18.2.tar.gz cd ora2pg_v18.2 perl Makefile.PL make && make install
ora2pgの設定ファイルの作成
cd /etc/ora2pg
cp ora2pg.conf.dist ora2pg.conf
vim ora2pg.conf
以下の項目を修正
- ORACLE_DSN dbi:Oracle:host=<DBのホスト>;sid=<サービス名>;port=1521
- ORACLE_USER <ユーザ名>
- ORACLE_PWD <パスワード>
- SCHEMA <移行対象のスキーマ名>
移行してみる
ora2pg -c /etc/ora2pg/ora2pg.conf ↓終了するとこんな出力がある [========================>] 836/836 tables (100.0%) end of scanning. [========================>] 836/836 tables (100.0%) end of table export.
コマンドを実行したディレクトリに「output.sql」が出来ている
あとは、PostgreSQLのコマンドでSQLを実行する。
psql -U 【ユーザ名】 【DB名】 < output.sql
ちなみに、「output.sql」にはテーブルのCreate文のみだった。
エクスポートタイプが設定ファイル(/etc/ora2pg/ora2pg.conf)の「TYPE」で設定されてあり、デフォルトは「TABLE」になっている。
エクスポートタイプを変更するには、設定ファイルを修正するか、「-t」または「--type」オプションを追加してコマンドを実行する
エクスポートタイプはこんな感じ
# Type of export. Values can be the following keyword: # TABLE Export tables, constraints, indexes, ... # PACKAGE Export packages # INSERT Export data from table as INSERT statement # COPY Export data from table as COPY statement # VIEW Export views # GRANT Export grants # SEQUENCE Export sequences # TRIGGER Export triggers # FUNCTION Export functions # PROCEDURE Export procedures # TABLESPACE Export tablespace (PostgreSQL >= 8 only) # TYPE Export user defined Oracle types # PARTITION Export range or list partition (PostgreSQL >= v8.4) # FDW Export table as foreign data wrapper tables # MVIEW Export materialized view as snapshot refresh view # QUERY Convert Oracle SQL queries from a file. # KETTLE Generate XML ktr template files to be used by Kettle.
とりあえず、自分の環境ではテーブルとデータがあれば良かったので、「TABLE」と「INSERT」のエクスポートタイプを使用した。
ちなみに移行するテーブルは限られていたので、「-a/--allow」オプションで、移行テーブルを選択することができた。
※「/etc/ora2pg/ora2pg.conf」の「ALLOW」に移行対象のテーブル名を記載する方法もあり。
↓ドキュメントの該当箇所
https://ora2pg.darold.net/documentation.html#Ora2Pg-usage
おわりに
LONG RAW型を使用しているカラムがあるテーブルは変換が出来ず、レコード数が4件しかないのに処理が数時間たっても終わらなかった。
やっぱり完璧に変換できるわけではなさそうだけど、LONG RAW型カラム持ちのテーブルを変換する方法は有るらしい(試してない)