こちらの続きです。
今回は、以前のチュートリアルを基にして、コマンドによるspiderの生成、取得した情報のmysqlへの保存を行います。
以下を参考にしています。
PythonのScrapyでHTML、XML、CSV用のクローラーを作ってみる
プロジェクトとspiderの生成
まずはプロジェクトを生成します。
$ scrapy startproject tutorial2
続けてspiderを生成します。
# プロジェクトフォルダへの移動 $ cd tutorial2 # 扱えるtemplateの確認 $ scrapy genspider -l Available templates: basic crawl csvfeed xmlfeed # scrapy genspider [-t template] ‹name› ‹domain› $ scrapy genspider -t crawl quotes2 quotes.toscrape.com
templateは、scrapyをインストールしたpythonフォルダの\Lib\site-packages\scrapy\templates\spiders
に入っています。また、githubではこちらにあります。
今回はページをクロールするbotを作るので、crawlを選んでいます。
生成されるフォルダ、ファイルは以下のようになります。
C:\USERS\USER\TUTORIAL2 │ scrapy.cfg │ └─tutorial2 │ middlewares.py │ pipelines.py │ settings.py │ __init__.py │ ├─spiders │ │ quotes2.py │ │ __init__.py │ │ │ └─__pycache__ │ __init__.cpython-37.pyc │ └─__pycache__ settings.cpython-37.pyc __init__.cpython-37.pyc
setting.pyの設定
setting.pyの設定を行い、サーバーへの負荷を軽減します。
ダウンロードの間隔
DOWNLOAD_DELAY のコメントを外して、ダウンロードの間隔を空けます。
# Configure a delay for requests for the same website (default: 0) # See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs DOWNLOAD_DELAY = 3
キャッシュの有効化
HTTPのキャッシュを有効にして、同じコンテンツのダウンロードを避けます。
# Enable and configure HTTP caching (disabled by default) # See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings HTTPCACHE_ENABLED = True HTTPCACHE_EXPIRATION_SECS = 3600 HTTPCACHE_DIR = 'httpcache' HTTPCACHE_IGNORE_HTTP_CODES = [] HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
items.pyの設定
ittems.pyの設定を行います。
前回見たように、特に設定を行わなくともスクレイピングは行えますが、items.pyに設定を行うことで、取得したデータをscrapy内で構造化して扱うことができるようになります。
前回と同じ情報+情報を取得したurlを収集します。
import scrapy class Tutorial2Item(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() text = scrapy.Field() author = scrapy.Field() tag = scrapy.Field() url = scrapy.Field()
spiderの設定
今回は、下記のように既に雛形が出来上がっているので、こちらを改変します。
# -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class Quotes2Spider(CrawlSpider): name = 'quotes2' allowed_domains = ['quotes.toscrape.com'] start_urls = ['http://quotes.toscrape.com/'] rules = ( Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True), ) def parse_item(self, response): item = {} #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get() #item['name'] = response.xpath('//div[@id="name"]').get() #item['description'] = response.xpath('//div[@id="description"]').get() return item
rulesとdef parse_item(self, response)が前回と異なっています。
Crawling rules
今回の設定の場合、rulesの中のLinkExtractorにより、spiderは自動的にページ内のリンクを探して見つけます。
このように、rulesの中に色々と記述することで、今いるページの情報を収集するかどうか、見つけたリンク先に進むかどうかという、spiderの動きをコントロールします。
以下の公式の例を眺めると、使い方は何となく理解できます。
rules = ( # Extract links matching 'category.php' (but not matching 'subsection.php') # 'category.php'にマッチするページのリンクを抜き出し、'subsection\.php'にマッチするページのリンクは抜き出さずに、 # and follow links from them (since no callback means follow=True by default). # 抜き出したリンクをたどる。(callbackを指定しないとfollow=Trueと解釈される) Rule(LinkExtractor(allow=('category\.php', ), deny=('subsection\.php', ))), # Extract links matching 'item.php' and parse them with the spider's method parse_item # item.phpにマッチするリンクを抜き出し、parse_itemメソッドで指定したように内容をパースする。 Rule(LinkExtractor(allow=('item\.php', )), callback='parse_item'), )
def parse_item(self, response)
前回はyieldでパースした内容を返していましたが、今回はitems.pyを設定したので、そちらにパースした内容を返すようコールバック関数を指定します。
quote2.py
前回と同じ内容+urlを取得するspiderです。
# -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class Quotes2Spider(CrawlSpider): name = 'quotes2' allowed_domains = ['quotes.toscrape.com'] start_urls = ['http://quotes.toscrape.com/'] rules = ( Rule(LinkExtractor(allow=r'quotes.toscrape.com/page/\d*/'), callback='parse_item', follow=True), ) def parse_item(self, response): for quote in response.css('div.quote'): item = {} item['text'] = quote.css('span.text::text').get() item['author'] = quote.css('small.author::text').get() item['tag'] = quote.css('div.tags a.tag::text').getall() item['url'] = response.url yield item
retrun itemだと関数が終了してしまうので、yieldに変更しておきます。
spiderを走らせて、csvに取得した情報を保存してみると、多分収集できているようです。
pipelines.py
pipelins.pyは、spiderが収集してくれたitemへの処理を記述しておくと、spiderがitemを収集した時にその処理を実行してくれます。
settings.py
pipelinesを有効にするために、settings.pyの以下の部分のコメントアウトを外しておきます。
# Configure item pipelines # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html ITEM_PIPELINES = { 'tutorial2.pipelines.Tutorial2Pipeline': 300, }
雛形ファイル
コマンドが作成してくれる雛形は以下のようになっています。
class Tutorial2Pipeline(object): def process_item(self, item, spider): return item
itemを変更、削除
まず、authorは全て大文字にしてみます。
class Tutorial2Pipeline(object): def process_item(self, item, spider): item['author'] = item['author'].upper() return item
次に、http://quotes.toscrape.com/page/2/ の結果は削除してみます。
class Tutorial2Pipeline(object): def process_item(self, item, spider): item['author'] = item['author'].upper() if item['url'] == 'http://quotes.toscrape.com/page/2/': return return item
csvに保存するよう実行してみます。
$ scrapy crawl quotes2 -o quote.csv
とりあえず出来ているようです。
mysqlへの保存
sqliteの方が楽で良いのですが、ここでは、mysqlへsqlalchemyを使ってitemを保存する処理を記述してみようと思います。
以下を参照しています。
Scrapy Tutorial #9: How To Use Scrapy Item
データベースの作成
mysqlでデータベースを作成します。一応接続用のユーザーを設定します。
$ mysql -h localhost -u root -p # 省略 mysql> create database quote_scrapy character set utf8 collate utf8_general_ci; Query OK, 1 row affected, 2 warnings (0.11 sec) mysql> show create database quote_scrapy; +--------------+----------------------------------------------------------------------------------------------------------+ | Database | Create Database | +--------------+----------------------------------------------------------------------------------------------------------+| quote_scrapy | CREATE DATABASE `quote_scrapy` /*!40100 DEFAULT CHARACTER SET utf8 */ /*!80016 DEFAULT ENCRYPTION='N' */ | +--------------+----------------------------------------------------------------------------------------------------------+1 row in set (0.00 sec) mysql> create user quote_scrapy@localhost identified by 'quote_scrapy'; Query OK, 0 rows affected (0.05 sec) mysql> grant all privileges on quote_scrapy.* to quote_scrapy@localhost; Query OK, 0 rows affected (0.08 sec) mysql> exit Bye
sqlalchemy
sqlalchemyを使って、データベースのモデルを作成します。
models.pyという名前で、pipelines.pyと同じフォルダに保存します。
from sqlalchemy import create_engine, Column, Table, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import ( Integer, SmallInteger, String, Date, DateTime, Float, Boolean, Text, LargeBinary) CONNECTON_STRING = '{drivername}://{user}:{password}@{host}:{port}/{db_name}?charset=utf8'.format( drivername = 'mysql+pymysql', user = 'quote_scrapy', password = 'quote_scrapy', host = 'localhost', port = '3306', db_name = 'quote_scrapy' ) DeclarativeBase = declarative_base() def db_connect(): return create_engine(CONNECTON_STRING, echo=True) def create_table(engine): DeclarativeBase.metadata.create_all(engine) class QuoteDatabase(DeclarativeBase): __tablename__ = 'quote_table' id = Column(Integer, primary_key=True) text = Column('text', Text()) author = Column('author', String(255)) tag = Column('tag', String(255)) url = Column('url', String(255))
上のコードが動くかテストします。
test_models.pyという名前で、models.pyと同じフォルダに以下を作成して、実行してみます。
from sqlalchemy.orm import sessionmaker import models engine = models.db_connect() models.create_table(engine) Session = sessionmaker(bind=engine) session = Session() quotedb = models.QuoteDatabase() quotedb.text = "The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking." quotedb.author = "ALBERT EINSTEIN" quotedb.tag = "change,deep-thoughts,thinking,world" quotedb.url = "http://quotes.toscrape.com/page/1/" try: session.add(quotedb) session.commit() # データが挿入されているか確認。 obj = session.query(models.QuoteDatabase).first() print(obj.text) except: session.rollback() raise finally: session.close()
挿入できているようなので、sqlalchemyのモデルは動くようです。
pipelines.pyに、mysqlへデータを保存するコードを記述します。
from sqlalchemy.orm import sessionmaker from tutorial2.models import QuoteDatabase, db_connect, create_table class Tutorial2Pipeline(object): def __init__(self): engine = db_connect() create_table(engine) self.Session = sessionmaker(bind=engine) def process_item(self, item, spider): # 前処理 item['author'] = item['author'].upper() if item['url'] == 'http://quotes.toscrape.com/page/2/': return # dbへの登録 session = self.Session() quotedb = QuoteDatabase() quotedb.text = item['text'] quotedb.author = item['author'] quotedb.tag = item['tag'] quotedb.url = item['url'] try: session.add(quotedb) session.commit() except: session.rollback() raise finally: session.close() return item
クロールを実行してみます。
$ scrapy crawl quotes2
ログを確認するとエラーが出たり出なかったりしています。
デバッグ リストのstringへの変換
エラーメッセージが長いですが、端的に言えば以下のエラーのようです。
sqlalchemy.exc.InternalError: (pymysql.err.InternalError) (1241, 'Operand should contain 1 column(s)')
エラーを出す入力でtest_models.pyをいじってみると、リストをデータベースに入力しようとしてエラーが出ているようなので、リストをjoinでstirngに変更するようにします。
from sqlalchemy.orm import sessionmaker import models engine = models.db_connect() models.create_table(engine) Session = sessionmaker(bind=engine) session = Session() quotedb = models.QuoteDatabase() data = {'text': '“... a mind needs books as a sword needs a whetstone, if it is to keep its edge.”', 'author': 'GEORGE R.R. MARTIN', 'tag': ['books', 'mind'], 'url': 'http://quotes.toscrape.com/page/10/'} quotedb.text = data['text'] quotedb.author = data['author'] quotedb.tag = ';'.join(data['tag']) quotedb.url = data['url'] try: session.add(quotedb) session.commit() #query again obj = session.query(models.QuoteDatabase).first() print(obj.text) except: session.rollback() raise finally: session.close()
これでエラーが出なくなったようなので、piplines.pyを以下のように書き直します。
from sqlalchemy.orm import sessionmaker from tutorial2.models import QuoteDatabase, db_connect, create_table class Tutorial2Pipeline(object): def __init__(self): engine = db_connect() create_table(engine) self.Session = sessionmaker(bind=engine) def process_item(self, item, spider): # 前処理 item['author'] = item['author'].upper() # listをstirngに変換 item['tag'] = ';'.join(item['tag']) if item['url'] == 'http://quotes.toscrape.com/page/2/': return # dbへの登録 session = self.Session() quotedb = QuoteDatabase() quotedb.text = item['text'] quotedb.author = item['author'] quotedb.tag = item['tag'] quotedb.url = item['url'] try: session.add(quotedb) session.commit() except: session.rollback() raise finally: session.close() return item
再びクロールを実行します。
$ scrapy crawl quotes2
エラーが出ませんでした。
mysqlで確認すると、csvと同じ件数分のデータが保存できているようです。
$ mysql -h localhost -u root -p # 省略 mysql> use quote_scrapy; Database changed mysql> select * from quote_table limit 1; +----+-----------------------------------------------------------------------------------------------------------------------+-----------------+-------------------------------------+------------------------------------+ | id | text | author | tag | url | +----+-----------------------------------------------------------------------------------------------------------------------+-----------------+-------------------------------------+------------------------------------+ | 1 | “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.” | ALBERT EINSTEIN | change;deep-thoughts;thinking;world | http://quotes.toscrape.com/page/1/ | +----+-----------------------------------------------------------------------------------------------------------------------+-----------------+-------------------------------------+------------------------------------+ 1 row in set (0.00 sec) mysql> select count(*) from quote_table; +----------+ | count(*) | +----------+ | 90 | +----------+ 1 row in set (0.07 sec)