Scrapy(スクリーンスクレイパー/ウェブクローラー)にいくつかの単体テストを実装したいと思います。プロジェクトは "scrapy crawl"コマンドで実行されるため、鼻のようなもので実行できます。 scrapyはツイストの上に構築されているので、その単体テストフレームワークTrialを使用できますか?もしそうなら、どうですか?それ以外の場合は、noseを機能させたいです。
更新:
私は Scrapy-Users について話していましたが、「テストコードで応答を作成し、応答を使用してメソッドを呼び出し、期待された項目を取得することを表明している」 /出力のリクエスト」。私はこれがうまくいくようには見えません。
単体テストのテストクラスとテストを作成できます。
ただし、最終的に this トレースバックが生成されます。理由に関する洞察はありますか?
私が行った方法は、偽の応答を作成することです。これにより、解析機能をオフラインでテストできます。しかし、実際のHTMLを使用することで実際の状況を把握できます。
このアプローチの問題は、ローカルHTMLファイルがオンラインの最新の状態を反映していない可能性があることです。したがって、HTMLがオンラインで変更されると、大きなバグが発生する可能性がありますが、テストケースは引き続き成功します。したがって、この方法でテストするのは最善の方法ではない可能性があります。
私の現在のワークフローは、エラーが発生したときはいつでも、URLとともに管理者にメールを送信します。次に、その特定のエラーについて、エラーの原因となっているコンテンツを含むhtmlファイルを作成します。次に、そのための単体テストを作成します。
これは、ローカルのhtmlファイルからテスト用のサンプルScrapy http応答を作成するために使用するコードです。
# scrapyproject/tests/responses/__init__.py
import os
from scrapy.http import Response, Request
def fake_response_from_file(file_name, url=None):
"""
Create a Scrapy fake HTTP response from a HTML file
@param file_name: The relative filename from the responses directory,
but absolute paths are also accepted.
@param url: The URL of the response.
returns: A scrapy HTTP response which can be used for unittesting.
"""
if not url:
url = 'http://www.example.com'
request = Request(url=url)
if not file_name[0] == '/':
responses_dir = os.path.dirname(os.path.realpath(__file__))
file_path = os.path.join(responses_dir, file_name)
else:
file_path = file_name
file_content = open(file_path, 'r').read()
response = Response(url=url,
request=request,
body=file_content)
response.encoding = 'utf-8'
return response
サンプルhtmlファイルは、scrapyproject/tests/responses/osdir/sample.htmlにあります。
その場合、テストケースは次のようになります。テストケースの場所は、scrapyproject/tests/test_osdir.pyです。
import unittest
from scrapyproject.spiders import osdir_spider
from responses import fake_response_from_file
class OsdirSpiderTest(unittest.TestCase):
def setUp(self):
self.spider = osdir_spider.DirectorySpider()
def _test_item_results(self, results, expected_length):
count = 0
permalinks = set()
for item in results:
self.assertIsNotNone(item['content'])
self.assertIsNotNone(item['title'])
self.assertEqual(count, expected_length)
def test_parse(self):
results = self.spider.parse(fake_response_from_file('osdir/sample.html'))
self._test_item_results(results, 10)
これが基本的に私の構文解析メソッドをテストする方法ですが、それは構文解析メソッドのためだけではありません。より複雑になった場合は、 Mox を参照することをお勧めします
新しく追加された Spider Contracts は試す価値があります。多くのコードを必要とせずにテストを追加する簡単な方法を提供します。
私は Betamax を使用して実際のサイトで初めてテストを実行し、http応答をローカルに保持して、次のテストが超高速で実行されるようにします。
Betamaxは、ユーザーが行うすべてのリクエストをインターセプトし、すでにインターセプトおよび記録されている一致するリクエストを見つけようとします。
サイトの最新バージョンを取得する必要がある場合は、ベータマックスが記録したものを削除して、テストを再実行してください。
例:
from scrapy import Spider, Request
from scrapy.http import HtmlResponse
class Example(Spider):
name = 'example'
url = 'http://doc.scrapy.org/en/latest/_static/selectors-sample1.html'
def start_requests(self):
yield Request(self.url, self.parse)
def parse(self, response):
for href in response.xpath('//a/@href').extract():
yield {'image_href': href}
# Test part
from betamax import Betamax
from betamax.fixtures.unittest import BetamaxTestCase
with Betamax.configure() as config:
# where betamax will store cassettes (http responses):
config.cassette_library_dir = 'cassettes'
config.preserve_exact_body_bytes = True
class TestExample(BetamaxTestCase): # superclass provides self.session
def test_parse(self):
example = Example()
# http response is recorded in a betamax cassette:
response = self.session.get(example.url)
# forge a scrapy response to test
scrapy_response = HtmlResponse(body=response.content, url=example.url)
result = example.parse(scrapy_response)
self.assertEqual({'image_href': u'image1.html'}, result.next())
self.assertEqual({'image_href': u'image2.html'}, result.next())
self.assertEqual({'image_href': u'image3.html'}, result.next())
self.assertEqual({'image_href': u'image4.html'}, result.next())
self.assertEqual({'image_href': u'image5.html'}, result.next())
with self.assertRaises(StopIteration):
result.next()
参考までに、pycon 2015で Ian Cordascoの講演 のおかげでbetamaxを発見しました。
これは非常に遅い回答ですが、私はスクレイピーテストに悩まされていたため、定義された仕様に対してスクレイピークローラーをテストするためのフレームワーク scrapy-test を書きました。
静的出力ではなくテスト仕様を定義することで機能します。たとえば、この種のアイテムをクロールする場合は、次のようになります。
{
"name": "Alex",
"age": 21,
"gender": "Female",
}
スクレイピーテストItemSpec
を定義できます:
from scrapytest.tests import Match, MoreThan, LessThan
from scrapytest.spec import ItemSpec
class MySpec(ItemSpec):
name_test = Match('{3,}') # name should be at least 3 characters long
age_test = Type(int), MoreThan(18), LessThan(99)
gender_test = Match('Female|Male')
StatsSpec
と同じように、スクレイピー統計のアイデアテストもあります。
from scrapytest.spec import StatsSpec
from scrapytest.tests import Morethan
class MyStatsSpec(StatsSpec):
validate = {
"item_scraped_count": MoreThan(0),
}
その後、ライブまたはキャッシュされた結果に対して実行できます。
$ scrapy-test
# or
$ scrapy-test --cache
私は、開発の変更のためのキャッシュされた実行と、Webサイトの変更を検出するための毎日のcronjobsを実行しています。
私はスクレイピー1.3.0と関数を使用しています:fake_response_from_file、エラーを発生させます:
response = Response(url=url, request=request, body=file_content)
私は得ます:
raise AttributeError("Response content isn't text")
解決策は、代わりにTextResponseを使用することであり、例として問題なく動作します。
response = TextResponse(url=url, request=request, body=file_content)
どうもありがとう。
選択した回答からdef fake_response_from_file
を削除することで、少し単純になります。
import unittest
from spiders.my_spider import MySpider
from scrapy.selector import Selector
class TestParsers(unittest.TestCase):
def setUp(self):
self.spider = MySpider(limit=1)
self.html = Selector(text=open("some.htm", 'r').read())
def test_some_parse(self):
expected = "some-text"
result = self.spider.some_parse(self.html)
self.assertEqual(result, expected)
if __== '__main__':
unittest.main()
スクラップサイトの this スニペットに従って、スクリプトから実行できます。次に、返されたアイテムに対して任意の種類のアサートを作成できます。
私はTwistedのtrial
を使用してテストを実行しています。これは、Scrapy自身のテストと同様です。それはすでにリアクターを開始しているので、テストでの開始と停止を心配することなくCrawlerRunner
を利用します。
check
およびparse
Scrapyコマンドからいくつかのアイデアを盗み、ライブサイトに対してアサーションを実行する次の基本TestCase
クラスを作成しました。
from twisted.trial import unittest
from scrapy.crawler import CrawlerRunner
from scrapy.http import Request
from scrapy.item import BaseItem
from scrapy.utils.spider import iterate_spider_output
class SpiderTestCase(unittest.TestCase):
def setUp(self):
self.runner = CrawlerRunner()
def make_test_class(self, cls, url):
"""
Make a class that proxies to the original class,
sets up a URL to be called, and gathers the items
and requests returned by the parse function.
"""
class TestSpider(cls):
# This is a once used class, so writing into
# the class variables is fine. The framework
# will instantiate it, not us.
items = []
requests = []
def start_requests(self):
req = super(TestSpider, self).make_requests_from_url(url)
req.meta["_callback"] = req.callback or self.parse
req.callback = self.collect_output
yield req
def collect_output(self, response):
try:
cb = response.request.meta["_callback"]
for x in iterate_spider_output(cb(response)):
if isinstance(x, (BaseItem, dict)):
self.items.append(x)
Elif isinstance(x, Request):
self.requests.append(x)
except Exception as ex:
print("ERROR", "Could not execute callback: ", ex)
raise ex
# Returning any requests here would make the crawler follow them.
return None
return TestSpider
例:
@defer.inlineCallbacks
def test_foo(self):
tester = self.make_test_class(FooSpider, 'https://foo.com')
yield self.runner.crawl(tester)
self.assertEqual(len(tester.items), 1)
self.assertEqual(len(tester.requests), 2)
または、セットアップで1つの要求を実行し、結果に対して複数のテストを実行します。
@defer.inlineCallbacks
def setUp(self):
super(FooTestCase, self).setUp()
if FooTestCase.tester is None:
FooTestCase.tester = self.make_test_class(FooSpider, 'https://foo.com')
yield self.runner.crawl(self.tester)
def test_foo(self):
self.assertEqual(len(self.tester.items), 1)