Scrapy 爬取七麥 app數據排行榜

前言

熟悉Scrapy以後,本篇文章帶你們爬取七麥數據(https://www.qimai.cn/rank )的ios appstore付費應用排行榜前100名應用。html

爬取內容包括app在列表中的下標,app圖標地址,app的名稱信息,app的類型,在分類中的排行,開發者,詳情等。node

考慮的問題:python

  • Forbidden by robots.txt的錯誤
  • 網頁返回403
  • 頁面經過動態渲染,普通的請求url,在頁面渲染以前已經返回response,解析沒有數據
  • 列表一頁20個app,想要拿到前100個須要翻頁,可是翻頁沒有更改url,而是經過js動態加載
  • ...

1563269958.jpg

建立項目

在須要放置項目的目錄下,ios

> scrapy startproject qimairank

回車便可建立默認的Scrapy項目架構。git

1563271101.jpg

建立Item

建立Item來存儲咱們爬取的app在列表中的下標,app圖標地址,app的名稱信息,app的類型,在分類中的排行,開發者,詳情。
修改items.py,在下面增長github

class RankItem(scrapy.Item):
    # 下標
    index = scrapy.Field()
    # 圖標地址
    src = scrapy.Field()
    # app標題信息
    title = scrapy.Field()
    # app類型
    type = scrapy.Field()
    # 分類中的排行
    type_rank = scrapy.Field()
    # 開發者
    company = scrapy.Field()
    # 詳情信息
    info = scrapy.Field()

建立Spider

spiders目錄下建立RankSpider.py,並建立class RankSpider,繼承於scrapy.Spider。web

import scrapy

class RankSpider(scrapy.Spider):
    name = "RankSpider"
    start_urls = ["https://www.qimai.cn/rank"]

    def parse(self, response):
       pass
  • name:用於區別Spider,該名字必須是惟一的。
  • start_urls:Spider在啓動時進行爬取的url列表,首先會爬取第一個。
  • def parse(self, response):獲得url的response信息後的解析方法。

解析付費榜

解析用的Selectors選擇器有多種方法:正則表達式

  • xpath(): 傳入xpath表達式,返回該表達式所對應的全部節點的selector list列表 。
  • css(): 傳入CSS表達式,返回該表達式所對應的全部節點的selector list列表.
  • extract(): 序列化該節點爲unicode字符串並返回list。
  • re(): 根據傳入的正則表達式對數據進行提取,返回unicode字符串list列表。

下面咱們用xpath()選擇節點,xpath的語法可參考w3c的http://www.w3school.com.cn/xp... 學習,須要熟悉語法、運算符、函數等。json

def parse(self, response):
    base = response.xpath(
        "//div[@class='ivu-row rank-all-item']/div[@class='ivu-col ivu-col-span-8'][2]//ul/li[@class='child-item']/div[@class='ivu-row']")
    for box in base:
        # 建立實例
        rankItem = RankItem()
        # 下標
        rankItem['index'] = \
            box.xpath(".//div[@class='ivu-col ivu-col-span-3 left-item']/span/text()").extract()[0]
        # 圖標地址
        rankItem['src'] = box.xpath(".//img/@src").extract()[0]
        # app名稱信息
        rankItem['title'] = box.xpath(".//div[@class='info-content']//a/text()").extract()[0]
        # app類型
        rankItem['type'] = box.xpath(".//div[@class='info-content']//p[@class='small-txt']/text()").extract()[0]
        # 分類中的排行
        rankItem['type_rank'] = box.xpath(
            ".//div[@class='info-content']//p[@class='small-txt']//span[@class='rank-item']/text()").extract()[
            0]
        # 開發者
        rankItem['company'] = box.xpath(
            ".//div[@class='info-content']//p[@class='small-txt']//span[@class='company-item']/text()").extract()[
            0]
        # 詳情頁地址
        infoUrl = "https://www.qimai.cn" + box.xpath(".//div[@class='info-content']//a/@href").extract()[0]
        yield rankItem

運行爬取初始app列表

直接運行

qimairank>scrapy crawl RankSpider -o data.json

你會發現窗口沒有item輸出,data.json中也沒有數據,是咱們寫錯了嗎?

1563358018.jpg

scrapy默認遵照robot協議的,在訪問網址前會先訪問robot.txt來查看本身是否有權限訪問。若是網站不容許被爬,就不能訪問。
怎麼樣不遵照協議呢?

settings.py

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

再次運行仍然失敗,咱們來看下具體緣由:
1563358364.jpg
由於七麥網站對請求的User-Agent作了校驗,解決辦法是在配置文件

settings.py

# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    #    'qimairank.middlewares.QimairankDownloaderMiddleware': 543,
    'qimairank.middlewares.RandomUserAgent': 1,
}

USER_AGENTS = [
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
    "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
]

並在middlewares.py中建立RandomUserAgent

import random

class RandomUserAgent(object):
    """
    隨機獲取settings.py中配置的USER_AGENTS設置'User-Agent'
    """

    def __init__(self, agents):
        self.agents = agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist('USER_AGENTS'))

    def process_request(self, request, spider):
        request.headers.setdefault('User-Agent', random.choice(self.agents))

再次運行,沒有報錯,可是沒有數據,是咱們的xpath寫錯啦?咱們在parse中增長輸出body的信息

1563428566.jpg

能夠看到body爲空,沒有咱們須要的列表數據,這是由於七麥數據是經過js動態渲染的,在渲染完成前,咱們的response已經返回,那麼怎麼樣才能等一等呀,等到渲染完成才返回呢?

爬取動態渲染的方式,我知道是經過Splash或者Selenium,像咱們的桌面版系統能夠選擇用Selenium,操做能夠設置可視化,全部界面操做都能看見,Splash依賴於Docker,無界面。

安裝Selenium包:

pip install selenium

使用前須要安裝驅動,配置詳情點擊

驅動安裝完成,在middlewares.py中建立 SeleniumMiddleware

class SeleniumMiddleware(object):
    def __init__(self):
        self.timeout = 50
        # 2.Firefox---------------------------------
        # 實例化參數對象
        options = webdriver.FirefoxOptions()
        # 無界面
        # options.add_argument('--headless')
        # 關閉瀏覽器彈窗
        options.set_preference('dom.webnotifications.enabled', False)
        options.set_preference('dom.push.enabled', False)
        # 打開瀏覽器
        self.browser = webdriver.Firefox(firefox_options=options)
        # 指定瀏覽器窗口大小
        self.browser.set_window_size(1400, 700)
        # 設置頁面加載超時時間
        self.browser.set_page_load_timeout(self.timeout)
        self.wait = WebDriverWait(self.browser, self.timeout)

    def process_request(self, request, spider):
        # 當請求的頁面不是當前頁面時
        if self.browser.current_url != request.url:
            # 獲取頁面
            self.browser.get(request.url)
            time.sleep(5)
        else:
            pass
        # 返回頁面的response
        return HtmlResponse(url=self.browser.current_url, body=self.browser.page_source,
                            encoding="utf-8", request=request)

    def spider_closed(self):
        # 爬蟲結束 關閉窗口
        self.browser.close()
        pass

    @classmethod
    def from_crawler(cls, crawler):
        # 設置爬蟲結束的回調監聽
        s = cls()
        crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)
        return s

在settins.py中配置

# Enable or disable downloader middlewares
DOWNLOADER_MIDDLEWARES = {
    #    'qimairank.middlewares.QimairankDownloaderMiddleware': 543,
    'qimairank.middlewares.RandomUserAgent': 1,
    'qimairank.middlewares.SeleniumMiddleware': 10,
}

再次運行scrapy crawl RankSpider -o data.json,啦啦啦~這回有數據啦。

Selenium調用JS腳本

觀察爬取出來的data.json,發現怎麼肥四,只有20條數據,並且除了前6個的app圖標都是七麥的默認圖標。

1563434696.jpg

這是由於七麥數據的列表默認每頁20條,並且默認渲染前6個的圖標,其他的頁須要觸發滑動事件加載,並且滑動到的圖標纔開始渲染。這樣怎麼辦呢?咱們只須要滑動到能夠加載的按鈕就能夠啦,檢查發如今三個列表的外層標籤有一個class爲cm-explain-bottom的標籤
1563442615.jpg
咱們用Selenium調用js腳本,滑動到這個標籤就能夠啦,在中間件process_request方法更改

def process_request(self, request, spider):
    # 當請求的頁面不是當前頁面時
    if self.browser.current_url != request.url:
        # 獲取頁面
        self.browser.get(request.url)
        time.sleep(5)
        # 請求的url開始爲https://www.qimai.cn/rank/時,調用滑動界面,每頁20個,滑動4次
        if request.url.startswith("https://www.qimai.cn/rank"):
            try:
                for i in (0, 1, 2, 3):
                    self.browser.execute_script(
                        "document.getElementsByClassName('cm-explain-bottom')[0].scrollIntoView(true)")
                    time.sleep(4)
            except JavascriptException as e:
                pass
            except Exception as e:
                pass

再次執行scrapy crawl RankSpider -o data1.json,則可看見已經生成data1.json裏面有100個item。

獲取app詳情

詳情頁須要跟進url,咱們在RankSpider#parse方法中,不用yield Item,而是yield Request就能夠跟進。

# 詳情頁地址
infoUrl = "https://www.qimai.cn" + box.xpath(".//div[@class='info-content']//a/@href").extract()[0]
# yield rankItem
yield Request(infoUrl.replace("rank", "baseinfo"), self.parseInfo,
              meta={'rankItem': dict(rankItem).copy()}, dont_filter=True)

解析的infoUrl替換"rank"字符串爲"baseinfo"就能夠訪問app應用信息頁,用meta傳遞item到下一個解析方法中,用軟拷貝的方式,避免Item由於地址相同,內容覆蓋。

self.parseInfo爲指定此次請求的解析方法,

def parseInfo(self, response):
    print("基地址:" + response.url)
    if response.status != 200:
        return

    rankItem = response.meta['rankItem']

    info = dict()
    base = response.xpath("//div[@id='app-container']")
    if base.extract():
        # try:
        # 描述
        try:
            info['desc'] = base.xpath(
                ".//div[@class='app-header']//div[@class='app-subtitle']/text()").extract()[0]
        except Exception as e:
            print("無描述")
        # 開發商
        info['auther'] = base.xpath(
            ".//div[@class='app-header']//div[@class='auther']//div[@class='value']/text()").extract()[0]
        # 分類
        info['classify'] = base.xpath(
            ".//div[@class='app-header']//div[@class='genre']//div[@class='value']/a/text()").extract()[0]
        # appid
        info['appid'] = base.xpath(
            ".//div[@class='app-header']//div[@class='appid']//div[@class='value']/a/text()").extract()[0]
        # appstore地址
        info['appstorelink'] = base.xpath(
            ".//div[@class='app-header']//div[@class='appid']//div[@class='value']/a/@href").extract()[0]
        # 價格
        info['price'] = base.xpath(
            ".//div[@class='app-header']//div[@class='price']//div[@class='value']/text()").extract()[0]
        # 最新版本
        info['version'] = base.xpath(
            ".//div[@class='app-header']//div[@class='version']//div[@class='value']/text()").extract()[0]
        # 應用截圖
        info['screenshot'] = base.xpath(
            ".//div[@class='router-wrapper']//div[@class='app-screenshot']//div[@class='screenshot-box']//img/@src").extract()
        # 應用描述
        info['desc'] = base.xpath(
            ".//div[@class='router-wrapper']//div[@class='app-describe']//div[@class='description']").extract()[
            0]
        # 應用基本信息
        info['baseinfo'] = []
        for infoBase in base.xpath(
                ".//div[@class='router-wrapper']//div[@class='app-baseinfo']//ul[@class='baseinfo-list']/li"):
            # print(info['baseinfo'])
            try:
                info['baseinfo'].append(dict(type=infoBase.xpath(".//*[@class='type']/text()").extract()[0],
                                             info=infoBase.xpath(".//*[@class='info-txt']/text()").extract()[0]))
            except Exception as e:
                pass

        rankItem['info'] = info
        # 替換圖標 列表加載爲默認圖標
        rankItem['src'] = \
            response.xpath("//*[@id='app-side-bar']//div[@class='logo-wrap']/img/@src").extract()[
                0]
        yield rankItem

再次執行scrapy crawl RankSpider -o data1.json,則可看見已經生成data2.json,可是生成的列表不是排行的列表,甚至是亂序的,緣由是由於咱們使用了url跟進返回,每一個頁面的請求返回的速度不同,須要排序的話就寫個小腳本按照index排個序。

項目源碼

原文連接

相關文章
相關標籤/搜索