1. 程式人生 > >python scrapy 入門爬蟲 「什麼值得買」關鍵字搜尋

python scrapy 入門爬蟲 「什麼值得買」關鍵字搜尋

安裝scrapy框架

pip install Scrapy

建立一個scrapy工程,名字為smzdm

scrapy startproject smzdm

建立包含下列內容的 smzdm 目錄:

smzdm/
    scrapy.cfg
    smzdm/
        __init__.py
        items.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            ...


這些檔案分別是:
scrapy.cfg: 專案的配置檔案
smzdm/: 該專案的python模組。之後您將在此加入程式碼。
smzdm/items.py: 專案中的item檔案.
smzdm/pipelines.py: 專案中的pipelines檔案.
smzdm/settings.py: 專案的設定檔案.
smzdm/spiders/: 放置spider程式碼的目錄.

進入工程目錄,使用以下命令建立一個爬蟲(Spider)檔案

scrapy genspider concrete_search search.smzdm.com

將會spiders目錄下,生成name='concrete_search'    start_urls='search.smzdm.com'   的 concrete_search.py檔案

import scrapy


class ConcreteSearchSpider(scrapy.Spider):
    name = 'concrete_search'
    allowed_domains = ['search.smzdm.com/']
    start_urls = ['http://search.smzdm.com/']

    def parse(self, response):
        pass

檔案建立了一個ConcreteSearchSpider繼承 scrapy.Spider 類, 且定義以下三個屬性:

  • name: 用於區別Spider。 該名字必須是唯一的,您不可以為不同的Spider設定相同的名字。
  • start_urls: 包含了Spider在啟動時進行爬取的url列表。 因此,第一個被獲取到的頁面將是其中之一。 後續的URL則從初始的URL獲取到的資料中提取。
  • parse() 是spider的一個方法。 被呼叫時,每個初始URL完成下載後生成的 Response 物件將會作為唯一的引數傳遞給該函式。 該方法負責解析返回的資料(response data),提取資料(生成item)以及生成需要進一步處理的URL的 Request 物件。

在items.py檔案中,定義出需要採集的資料實體

# -*- coding: utf-8 -*-

import scrapy


class SmzdmItem(scrapy.Item):
    id = scrapy.Field()         #id
    title = scrapy.Field()      #標題
    price = scrapy.Field()      #價格
    desc = scrapy.Field()       #描述
    zhi_yes = scrapy.Field()    #點「值」的數量
    zhi_no = scrapy.Field()     #點「不值」的數量
    praise = scrapy.Field()     #文章「贊」的數量
    start = scrapy.Field()      #「收藏」的數量
    comment = scrapy.Field()    #「評論」的數量
    time = scrapy.Field()       #釋出時間
    channel = scrapy.Field()    #購買渠道
    detail_url = scrapy.Field() #詳情頁面url
    url = scrapy.Field()        #商品連結
    img = scrapy.Field()        #商品圖片

    def __str__(self):
        return 'SmzdmItem (%s) (%s) (值:%s) (%s)' % (self['title'], self['price'], self['start'], self['detail_url'])

    __repr__ = __str__

這裡我定義了一個SmzdmItem,繼承了scrapy.Item。並且重寫了__str__和__repr__,方便列印時能比較方便看到資訊。

解析網頁

在spider檔案中的parse方法裡,一般使用xpath、css來解析html,從某個具體的元素中取值。

一個例子如下

def parse(self, response):
    sites = response.css('#site-list-content > div.site-item > div.title-and-desc')
    items = []

    for site in sites:
        item = Website()
        item['name'] = site.css(
            'a > div.site-title::text').extract_first().strip()
        item['url'] = site.xpath(
            'a/@href').extract_first().strip()
        item['description'] = site.css(
            'div.site-descr::text').extract_first().strip()
        items.append(item)

在chrome裡可以這樣獲取到xpath

關閉『遵循robot協議』

大部分網站都在robot協議中(比如https://www.smzdm.com/robots.txt)禁止了內容的爬去

scrapy預設遵守了網站的這個robot協議。

所以為了成功爬取內容,需要在settings.py中,把ROBOTSTXT_OBEY置為False

ROBOTSTXT_OBEY = False

使用pipelines.py進行篩選

用於去重或者篩選item

from scrapy.exceptions import DropItem


class SmzdmPipeline(object):

    # 3.過濾條件
    # 3.1表示商品的『值』數量必須 >= zhi_yes_limit
    zhi_yes_limit = -1
    # 3.2表示商品的『不值』數量必須 <= zhi_no_limit
    zhi_no_limit = -1
    # 3.3表示商品的『值』除以『不值』的比率必須 >= zhi_no_limit
    zhi_ratio_limit = -1
    # 3.4表示商品的『收藏』數量必須 >= start_limit
    start_limit = -1
    # 3.5表示商品的『評論』數量必須 >= comment_limit
    comment_limit = -1
    # 3.6需要排除的關鍵字
    exclude = []


    def __init__(self):
        self.ids_seen = set()

    def process_item(self, item, spider):
        if item['id'] in self.ids_seen:
            raise DropItem("Duplicate item found: %s" % item)
        else:
            self.ids_seen.add(item['id'])

        if SmzdmPipeline.zhi_yes_limit > -1 and item['zhi_yes'] < SmzdmPipeline.zhi_yes_limit:
            raise DropItem("zhi_yes_limit: %d, target: %s" % (SmzdmPipeline.zhi_yes_limit, str(item)))
        if SmzdmPipeline.zhi_no_limit > -1 and item['zhi_no'] > SmzdmPipeline.zhi_no_limit:
            raise DropItem("zhi_no_limit: %d, target: %s" % (SmzdmPipeline.zhi_no_limit, item))
        if SmzdmPipeline.zhi_ratio_limit > -1 and (item['zhi_yes'] / item['zhi_no']) > SmzdmPipeline.zhi_ratio_limit:
            raise DropItem("zhi_ratio_limit: %d, target: %s" % (SmzdmPipeline.zhi_ratio_limit, item))
        if SmzdmPipeline.start_limit > -1 and item['zhi_start'] < SmzdmPipeline.zhi_start_limit:
            raise DropItem("zhi_start_limit: %d, target: %s" % (SmzdmPipeline.zhi_start_limit, item))
        if SmzdmPipeline.comment_limit > -1 and item['zhi_commen'] <= SmzdmPipeline.comment_limit:
            raise DropItem("zhi_comment_limit: %d, target: %s" % (SmzdmPipeline.zhi_comment_limit, item))
        if len(SmzdmPipeline.exclude) > 0 and SmzdmPipeline.containsKeyword(item['title']):
            raise DropItem("exclude: %s, target: %s" % (str(SmzdmPipeline.exclude), item))
        print("符合條件:" + str(item))
        return item

    @classmethod
    def containsKeyword(cls, title):
        for keyword in cls.exclude:
            if keyword in title:
                return True
        return False

這裡我寫了一個,裡面有去重的程式碼,還有根據點值人數,收藏人數,點值/點不值比率篩選的邏輯

寫完後需要到settings.py開啟這個功能

ITEM_PIPELINES = {
   'smzdm.pipelines.SmzdmPipeline': 300,
}

分配給每個類的整型值,確定了他們執行的順序,item按數字從低到高的順序,通過pipeline,通常將這些數字定義在0-1000範圍內。

啟動爬蟲

進入專案的根目錄,執行下列命令啟動爬蟲:

scrapy crawl concrete_search

為了方便,我在 concrete_search.py 加入了以下程式碼

if __name__ == "__main__":
    name = 'concrete_search'
    cmd = 'scrapy crawl {0}'.format(name)
    cmdline.execute(cmd.split())

這樣直接執行該檔案就可以啟動爬蟲了。

============================================================================

以下是我寫的爬蟲的程式碼,篩選條件在__init__方法中可以修改

# -*- coding: utf-8 -*-
from urllib import parse

import scrapy
from scrapy import cmdline

from smzdm.items import SmzdmItem
from smzdm.pipelines import SmzdmPipeline


class ConcreteSearchSpider(scrapy.Spider):
    # 爬蟲的名字
    name = 'concrete_search'

    # value:SmzdmItem
    SmzdmItemList = []

    def __init__(self):
        # 1.搜尋的商品名稱
        feed = ['奶粉']
        # 2.需要搜尋的頁數,一頁為20個商品
        pages = 5
        # 3.過濾條件
        # 3.1表示商品的『值』數量必須 >= zhi_yes_limit
        zhi_yes_limit = 5
        # 3.2表示商品的『不值』數量必須 <= zhi_no_limit
        zhi_no_limit = -1
        # 3.3表示商品的『值』除以『不值』的比率必須 >= zhi_no_limit
        zhi_ratio_limit = -1
        # 3.4表示商品的『收藏』數量必須 >= start_limit
        start_limit = -1
        # 3.5表示商品的『評論』數量必須 >= comment_limit
        comment_limit = -1
        # 3.6需要排除的關鍵字
        exclude = ['嬰兒', '幼兒', '寶寶']

        SmzdmPipeline.zhi_yes_limit = zhi_yes_limit
        SmzdmPipeline.zhi_no_limit = zhi_no_limit
        SmzdmPipeline.zhi_ratio_limit = zhi_ratio_limit
        SmzdmPipeline.start_limit = start_limit
        SmzdmPipeline.comment_limit = comment_limit
        SmzdmPipeline.exclude = exclude

        for index, f in enumerate(feed):
            feed[index] = parse.quote(f)
        self.start_urls = ['http://search.smzdm.com/?c=home&s=%s&p=%d&v=a' % (y, x) for x in range(1, pages + 1) for y
                           in feed]

    def parse(self, response):
        selector_feed_main_list = response.selector.xpath('//*[@id="feed-main-list"]')[0]
        selector_list_feed_main_list = selector_feed_main_list.xpath('./li')
        for selector in selector_list_feed_main_list:
            selector_item = selector.xpath('./div/div[2]')
            # 過濾文章
            if len(selector_item.xpath('./div[2]/div[2]/div/div/a')) == 0 or len(
                    selector_item.xpath('./h5/a[2]/div/text()')) == 0:
                print(
                    "發現一篇資訊 : 獲取%s個贊,名字如下" % selector_item.xpath('./div[2]/div[1]/span[1]/span[1]/text()')[0].extract())
                print(selector_item.xpath('./h5/a[1]/text()')[0].extract())
                continue
            item = SmzdmItem()

            item['id'] = int(selector_item.xpath('./div[2]/div[1]/span[1]/span[1]/@data-article')[0].extract())
            item['title'] = selector_item.xpath('./h5/a[1]/text()')[0].extract().strip()
            if len(selector_item.xpath('./h5/a[2]/div/text()')) != 0:
                item['price'] = selector_item.xpath('./h5/a[2]/div/text()')[0].extract()
            desc_text_count = len(selector_item.xpath('./div[1]/text()').extract())
            if desc_text_count == 1 or selector_item.xpath('./div[1]/text()').extract()[0].strip() != '':
                item['desc'] = selector_item.xpath('./div[1]/text()')[0].extract().strip()
            elif desc_text_count >= 2 and selector_item.xpath('./div[1]/text()').extract()[1].strip() != '':
                item['desc'] = selector_item.xpath('./div[1]/text()')[1].extract().strip()
            if len(selector_item.xpath('./div[2]/div[1]/span[1]/span[1]/span[1]/span/text()')) != 0:
                item['zhi_yes'] = int(selector_item.xpath('./div[2]/div[1]/span[1]/span[1]/span[1]/span/text()')[
                    0].extract())
                item['zhi_no'] = int(selector_item.xpath('./div[2]/div[1]/span[1]/span[2]/span[1]/span/text()')[0].extract())
            item['start'] = int(selector_item.xpath('./div[2]/div[1]/span[2]/span/text()')[0].extract())
            # 待優化 item['comment'] = selector.xpath('./div[2]/div[1]/a/text()')[0].extract()
            item['comment'] = int(selector_item.xpath('./div[2]/div[1]/a/@title')[0].extract().split(' ')[1])
            item['time'] = selector_item.xpath('./div[2]/div[2]/span/text()')[0].extract().strip()
            item['channel'] = selector_item.xpath('./div[2]/div[2]/span/span/text()')[0].extract().strip()
            item['detail_url'] = selector.xpath('./div/div[1]/a/@href')[0].extract().strip()
            item['url'] = selector_item.xpath('./div[2]/div[2]/div/div/a/@href')[0].extract()
            item['img'] = selector.xpath('./div/div[1]/a/img/@src')[0].extract()
            # print(item)
            ConcreteSearchSpider.SmzdmItemList.append(item)
            yield item
        # print(ConcreteSearchSpider.SmzdmItemList)


if __name__ == "__main__":
    name = 'concrete_search'
    cmd = 'scrapy crawl {0}'.format(name)
    cmdline.execute(cmd.split())