2017-04-25 7 views
1

Item Loadersを使用してScrapyスパイダーをリファクタリングしようとしています。 1つのXPath式から抽出されたデータからいくつかのフィールドにデータを取り込む部分に行きました。簡潔さのためにNested Loadersを使いたいと思います。ここでScrapyでは、さらにadd_xpathを呼び出すことなくネストされたアイテムローダーを使用する方法

は、これまでのクモです:

私はクモを実行した場合 items.py

import re 

import scrapy 
import scrapy.loader 

from scrapy.loader.processors import Compose, MapCompose, TakeFirst 

class ApkmirrorScraperItem(scrapy.Item): 
    url = scrapy.Field() 
    title = scrapy.Field() 
    developer = scrapy.Field() 
    app = scrapy.Field() 
    version_name = scrapy.Field() 
    version_code = scrapy.Field() 
    architectures = scrapy.Field() 
    package = scrapy.Field() 
    apk_file_size = scrapy.Field() 
    android_min_version = scrapy.Field() 
    android_target_version = scrapy.Field() 
    supported_dpis = scrapy.Field() 
    md5_signature = scrapy.Field() 
    time_uploaded = scrapy.Field() 
    time_scraped = scrapy.Field() 
    download_link = scrapy.Field() 


def parse_app(data_channel_name): 
    '''Parse the name of the app from the "data-channel-name" attribute of the button named "Follow [app_name] Updates".''' 
    pattern = re.compile(r'(?P<app>.+) App Updates') 
    return pattern.search(data_channel_name).groupdict().get("app") 

def get_version_line(apk_details): 
    '''Get the line containing the version from the 'APK details' section.''' 
    return next(line for line in apk_details if line.startswith("Version:")) 

def get_architectures_line(apk_details): 
    '''Get the line containing the supported architectures (e.g. "arm", "x64") from the 'APK details' section, if present.''' 
    return apk_details[1] if not apk_details[1].startswith("Package:") else None  # The line does not contain any keywords and may not be present, in which case None is returned 

def get_package_line(apk_details): 
    return next(line for line in apk_details if line.startswith("Package:"))    # The 'package line' is always present and starts with "Package:" 

def parse_version_line(version_line): 
    '''Parse the 'versionName' and 'versionCode' from the relevant line in 'APK details'.''' 
    PATTERN = r"^Version: (?P<version_name>.+) \((?P<version_code>\d+)\)\s*$"  # Note that the pattern includes the end-of-line character ($). This is necessary because some package names (e.g. Google Play) themselves contain brackets. 
    return re.match(PATTERN, version_line).groupdict() 


class ApkmirrorItemLoader(scrapy.loader.ItemLoader): 

    url_out = TakeFirst() 

    title_in = MapCompose(unicode.strip) 
    title_out = TakeFirst() 

    developer_in = MapCompose(unicode.strip) 
    developer_out = TakeFirst() 

    app_in = MapCompose(parse_app) 
    app_out = TakeFirst() 

    version_name_in = Compose(get_version_line, parse_version_line, lambda d: d.get("version_name")) 
    version_name_out = TakeFirst() 

    version_code_in = Compose(get_version_line, parse_version_line, lambda d: d.get("version_code")) 
    version_code_out = TakeFirst() 

あり、例えば、コマンドに

scrapy parse --spider=apkmirror-spider http://www.apkmirror.com/apk/google-inc/sheets/sheets-1-7-152-06-release/google-sheets-1-7-152-06-30-android-apk-download/ 

を使用してそれを抽出

from scrapy.spiders import SitemapSpider 
from apkmirror_scraper.items import ApkmirrorScraperItem, ApkmirrorItemLoader 

class ApkmirrorSitemapSpider(SitemapSpider): 
    name = 'apkmirror-spider' 
    sitemap_urls = ['http://www.apkmirror.com/sitemap_index.xml'] 
    sitemap_rules = [(r'.*-android-apk-download/$', 'parse')] 

    def parse(self, response): 
     loader = ApkmirrorItemLoader(item=ApkmirrorScraperItem(), response=response) 

     loader.add_value('url', response.url) 
     loader.add_xpath(field_name='title', xpath='//h1[@title]/text()') 
     loader.add_xpath(field_name='developer', xpath='//h3[@title]/a/text()') 
     loader.add_xpath(field_name='app', xpath='//*[contains(@data-channel-name, "App Updates")]/@data-channel-name') 

     apk_details_loader = loader.nested_xpath('//*[@title="APK details"]/following-sibling::*[@class="appspec-value"]') 

     apk_details_loader.add_xpath(field_name="version_name", xpath=".//text()") 
     apk_details_loader.add_xpath(field_name="version_code", xpath=".//text()") 

     return loader.load_item() 

期待どおりのフィールド:

# Scraped Items ------------------------------------------------------------ 
[{'app': u'Google Sheets', 
    'developer': u'Google Inc.', 
    'title': u'Google Sheets 1.7.152.06.30 (arm) (nodpi)', 
    'url': 'http://www.apkmirror.com/apk/google-inc/sheets/sheets-1-7-152-06-release/google-sheets-1-7-152-06-30-android-apk-download/', 
    'version_code': u'71520630', 
    'version_name': u'1.7.152.06.30'}] 

私はさらにapk_details_loaderをリファクタリングしたいと思います。 apk_details_load.add_xpathの2つの行が同じxpath引数、".//text()"を持っていることに注目してください。このコードの繰り返しを避けるために、私の代わりに、さらにXPath式を洗練して、呼び出した代わりに、ちょうどaddような何かを終わりにし、代わりにadd_xpath//text()含むapk_details_loader

として
apk_details_loader = loader.nested_xpath('//*[@title="APK details"]/following-sibling::*[@class="appspec-value"]//text()') 

を定義したいと思いますそれらの入力プロセッサは、単純に入力プロセッサを直接呼び出します。このようなことは可能でしょうか?

答えて

1

絶対に。

ItemLoaderクラスにこの新しいメソッドを追加する必要があります。scrapy/loader/__init__pyです。

これはそれを行う必要があります。

def add(self, field_name, *processors, **kw): values = self.selector.extract() self.add_value(field_name, values, *processors, **kw)

説明: loader.nested_xpath(xpath)は特別新しいローダーを返しません。 selectorの値をself.selector.xpath(xpath)に設定して初期化する点を除いて、これまで使用していたものと同じ種類のローダーを初期化します(ここではselfは呼び出し元オブジェクトのことを指します)apk_details_loaderには、データを抽出します。 addメソッドで参照するだけで済みます。これは、values = self.selector.extract()という行で行います。

関連する問題