1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Scrapyを1ファイルで動かすサンプル

Last updated at Posted at 2022-06-23

概要

Scrapyをstartprojectを使わず1ファイルで記述し、pythonコマンドで実行するサンプルです。

対象サイト( https://blog.qiita.com/ )から各記事のデータを収集してJSON Lines形式でファイル出力します。

実行コマンド 終了はCtrl+Cを2回

$ python main.py

サンプルコード

main.py
# 要インストール
# $ pip install scrapy
import scrapy
from scrapy.crawler import CrawlerProcess
from scrapy.extensions.httpcache import DummyPolicy


class MySpider(scrapy.Spider):
    """スパイダー https://doc-ja-scrapy.readthedocs.io/ja/latest/topics/spiders.html"""

    name = "my_spider"

    # クロールを開始するURL このURLが最初にリクエストされレスポンスはparseメソッドで処理される
    start_urls = ["https://blog.qiita.com/"]

    def parse(self, response):
        """一覧ページ処理"""
        # 詳細ページへのリンクを選択
        for detail_href in response.css('h2.indexArticle_title a::attr("href")'):
            # hrefが相対パスの場合は絶対パスに直す
            detail_url = response.urljoin(detail_href.get())
            # 詳細ページをリクエスト
            yield scrapy.Request(
                # リクエストするURL
                detail_url,
                # レスポンスを処理するコールバック
                self.parse_detail,
            )
        # 次ページがあればリクエストする
        if next_href := response.css('a.nextpostslink::attr("href")').get():
            # Requestオブジェクトを作成するショートカットとしてresponse.followを使用できる
            yield response.follow(
                # リクエストするURL(相対パスでOK)
                next_href,
                # レスポンスを処理するコールバック
                self.parse,
            )

    def parse_detail(self, response):
        """詳細ページ処理"""
        # アイテムクラスの代わりにdictでもOK
        item = dict()
        item["title"] = response.css("h1.article_title::text").get()
        item["date"] = response.css("time.entry-date::text").get()
        item["content"] = response.css("div.article_body").xpath("string()").get()
        item["url"] = response.url

        # これが1レコード分のデータになる
        yield item


class MyItemPipeline:
    """アイテム・パイプライン https://doc-ja-scrapy.readthedocs.io/ja/latest/topics/item-pipeline.html"""

    def process_item(self, item, spider):
        # contentを省略する
        item["content"] = item["content"][:50] + "..."
        return item


# 200ステータスのみキャッシュするポリシー
class CachePolicy(DummyPolicy):
    def should_cache_response(self, response, request):
        return response.status == 200


def get_settings():
    # 設定 https://doc-ja-scrapy.readthedocs.io/ja/latest/topics/settings.html
    settings = {
        # robots.txtポリシーを尊重する
        "ROBOTSTXT_OBEY": True,
        # ダウンロード間隔 デフォルト0
        "DOWNLOAD_DELAY": 1,
        # リクエストヘッダーの言語をjaに設定
        "DEFAULT_REQUEST_HEADERS": {
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "ja",
        },
        # キャッシュを有効にする
        "HTTPCACHE_ENABLED": True,
        # キャッシュの有効期限 0は期限なし
        "HTTPCACHE_EXPIRATION_SECS": 60 * 60 * 24,  # 24h
        # 200ステータスのみキャッシュする
        "HTTPCACHE_POLICY": "__main__.CachePolicy",
        # アイテム・パイプラインを設定する
        "ITEM_PIPELINES": {"__main__.MyItemPipeline": 500},
        # 出力ファイルの設定 https://docs.scrapy.org/en/latest/topics/feed-exports.html#std-setting-FEEDS
        "FEEDS": {
            "output.jsonl": {
                "format": "jsonlines",
                "encoding": "utf8",
                "overwrite": True,
            }
        },
        # ログレベル デフォルト"DEBUG"
        # "LOG_LEVEL": "INFO",
    }
    return settings


if __name__ == "__main__":
    # Scrapy実行
    process = CrawlerProcess(settings=get_settings())
    process.crawl(MySpider)
    process.start()

実行結果

output.jsonl
{"title": "募集ページをリリースします", "date": "2022/6/7", "content": "こんにちは、Qiita運営です。今回は「募集ページ」のリリースについてお知らせします。 募集ページに...", "url": "https://blog.qiita.com/opportunity-release/"}
{"title": "Qiitadonのサービス提供を終了します", "date": "2022/2/1", "content": "こんにちは、Qiita運営です。今日は「Qiitadon」の提供終了についてお知らせします。2017...", "url": "https://blog.qiita.com/terminate-provision-of-qiitadon/"}
{"title": "QiitaのMarkdownパーサーの変更をベータ版として公開します", "date": "2022/2/8", "content": " こんにちは、Qiita運営です。 今日はQiitaのMarkdownパーサーの変更をベータ版として...", "url": "https://blog.qiita.com/replace-markdown-parser-beta/"}
{"title": "QiitaのMarkdownパーサーの変更を正式にリリースします", "date": "2022/2/21", "content": " こんにちは、Qiita運営です。今日はベータ版として公開していたMarkdownパーサーの変更を正...", "url": "https://blog.qiita.com/replace-markdown-parser-general/"}
{"title": "Alexaスキル – Qiita 人気の投稿 の提供を終了します", "date": "2022/3/1", "content": "こんにちは、Qiita運営です。今日は「Alexaスキル – Qiita 人気の投稿」の提供終了につ...", "url": "https://blog.qiita.com/terminate-provision-of-qiita-trend-available-in-alexa-skill/"}
{"title": "記事カードのUI変更をベータ版として公開します", "date": "2022/3/18", "content": "こんにちは、Qiita運営です。今日は、Qiitaの記事カードのUI変更についてお知らせいたします。...", "url": "https://blog.qiita.com/update-article-card-beta/"}
{"title": "期間限定の記事投稿キャンペーン「Qiita Engineer Festa 2022」を6〜7月に開催決定!", "date": "2022/3/25", "content": "こんにちは、Qiita運営スタッフです! 2022年6月1日(水)より「Qiita Engineer...", "url": "https://blog.qiita.com/qiita-engineer-festa-2022-sponsor/"}
...

.scrapyディレクトリ以下にページのキャッシュが保存されます。

$ tree .scrapy 
.scrapy
└── httpcache
    └── my_spider
        ├── 02
        ...
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?