もなかアイスの試食品

「とりあえずやってみたい」そんな気持ちが先走りすぎて挫折が多い私のメモ書きみたいなものです.

Python3.6+Scrapyでスクレイピングしてみた

はじめに

機械学習について勉強するため、機械学習を使った何かを作ろうと思っている今日このごろ

いくつかサンプルが載っているような本を買っても、サンプルを動かすのはモチベーションが上がらない

やはりモチベーションが上がるものは、自分がやりたいを作るべきだなぁ

自分が機械学習を利用してやりたいことはなんだろうなーと考えた

自分が興味あるものを学習して、コンテンツ(または元のサイトのURL)を配信するものを作ってみたい

もうすでに、公開されているサービスを利用しているけど気にしない(作ることにきっと意味がある)

そんなことで、コンテンツの内容を取得するため、Pythonスクレイピングをやってみることにした。

昔にスクレイピングをやったことがあるけど、サーバに負荷を掛けないように配慮されたライブラリを探してみた。(昔使っていたのは、beautifulsoup4というライブラリ)

Scrapyというライブラリが、クローリングの際、時間を空けてクローリングができるみたい

なので、今回はScrapyでスクレイピングをやってみた話。

目次

参考サイト

note.nkmk.me

dragstar.hatenablog.com

Scrapyのインストール

まず、Scrapyをインストール

pip install scrapy

Scrapyを使うための作業

  1. scrapy startprojectでScrapyのプロジェクト作成
  2. itemsにスプレイピング後のデータ構造を定義
  3. scrapy genspiderでクローリング・スクレイピングをするためのクラス(spider)を作成
  4. settings.pyにクローニング設定を記述
  5. 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()

やっていることは

  1. コマンドscrapy listでspider名の一覧を取得し、spider名のリスト作成
  2. spiderをマルチスレッドで、それぞれ実行
  3. すべてのクローリングが終わるまで待機

クローニングの実行結果は、scrape_resultsというディレクトリに日時付きのファイル名で出力するようにしている。

おわりに

前にもスクレイピングをするpythonスクリプトを作成したことがあったけど、そのときには「同じドメインの場合、時間を空けてアクセス」するロジックが無かった・・・

このScrapyでは、自動でやってくれるのでとても便利

Tomcatのメモリ割り当てを自動で計算する

はじめに

AWSTomcatサーバを作ったとき、インスタンスタイプに合わせて、毎回別のチューニングするのは面倒臭い。

なので、Tomcatに割り当てるメモリサイズを自動計算するスクリプトを作った

参考サイト

groupsession.jp

yasuhiroa24.hateblo.jp

secureassist.jp

park1.wakwak.com

d.hatena.ne.jp

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)に移行・コピーしようとした


目次


環境


参考サイト

以下のサイトを参考にした。

kkida-galaxy.blogspot.com


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

ダウンロードしたパッケージをインストール

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/

設定したOracle環境変数を反映

. /etc/profile

ora2pgはperlを使っていて、perlOracleに接続できるようにDBD::Oracleのインストールする

cpan DBD::Oracle

このとき、cpanの設定(?)を聞かれたけど、全部デフォルトにした。

ora2pgのソースコードをダウンロード・コンパイルする

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

以下の項目を修正

移行してみる

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型カラム持ちのテーブルを変換する方法は有るらしい(試してない)