scrapy框架使用總結

scrapy總結

1 scrapy項目開發流程

1.1 創建項目命令

scrapy startproject guokespider

1.2 創建一個爬蟲

在終端中,先進入到爬蟲項目目錄下, 然後執行生成爬蟲命令,格式scrapy genspider 爬蟲名 域名

scrapy genspider guoke www.guokr.com

1.3 數據建模

items.py文件中進行建模

1.4 編寫爬蟲

  • 修改start_urls
  • 檢查修改allowed_domains
  • 編寫爬蟲解析響應方法
  • pipelines.py文件中,創建處理數據的管道,用於保存數據
  • 修改settings.py配置文件, 註冊管道

1.5 運行爬蟲

scrapy crawl guoke --nolog

下面來簡單介紹一下各個主要文件的作用:

scrapy.cfg :項目的配置文件

mySpider/ :項目的Python模塊,將會從這裏引用代碼

mySpider/items.py :項目的目標文件

mySpider/pipelines.py :項目的管道文件

mySpider/settings.py :項目的設置文件

mySpider/spiders/ :存儲爬蟲代碼目錄

2 scrapy的運行流程

  • Scrapy Engine(引擎): 負責SpiderItemPipelineDownloaderScheduler中間的通訊,信號、數據傳遞等。
  • Scheduler(調度器): 存放request對象,把request對象>引擎>下載器中間件==>下載器,並按照一定的方式進行整理排列,入隊,當引擎需要時,交還給引擎
  • Downloader(下載器):負責下載Scrapy Engine(引擎)發送的所有Requests請求,並將其獲取到的Responses交還給Scrapy Engine(引擎),由引擎交給Spider來處理,生成response對象>下載器中間件>引擎>爬蟲中間件>爬蟲
  • Spider(爬蟲):提取url和數據,提取的url會轉換成request對象>爬蟲中間件>引擎>調度器,而提取的數據通過引擎交給管道,即數據>爬蟲中間件>引擎>管道
  • Item Pipeline(管道):數據處理,保存數據。它負責處理Spider中獲取到的Item,並進行進行後期處理(詳細分析、過濾、存儲等)的地方.
  • Downloader Middlewares(下載中間件):你可以當作是一個可以自定義擴展下載功能的組件。
  • Spider Middlewares(Spider中間件):你可以理解爲是一個可以自定擴展和操作引擎Spider中間通信的功能組件(比如進入Spider的Responses;和從Spider出去的Requests)

3 代碼案例

3.1 新建scrapy項目

  • 創建爬蟲項目,命令:scrapy startproject 項目名稱
  • 創建爬蟲文件,命令:scrapy genspider 文件名稱 域名

3.2 構建項目模型

import scrapy


class BookItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    book_cate01 = scrapy.Field()
    book_cate02 = scrapy.Field()
    book_cate03 = scrapy.Field()
    book_href = scrapy.Field()
    book_name = scrapy.Field()
    book_store = scrapy.Field()
    book_num = scrapy.Field()

構建的字段模型 可以像 python字典一樣使用

在爬蟲文件中suning.py導入定義的字段項目模型:

import scrapy
from book.items import BookItem

然後再解析方法parse中實例項目模型即可當做字典使用:

def parse(self, response, **kwargs):
    """解析響應中的數據
        :param **kwargs:
        """
    # 一級分類列表
    div_list = response.xpath('//div[@class="menu-item"]')[:7]
    # 二級分類列表
    div_sub_list = response.xpath('//div[@class="menu-list"]/div[@class="menu-sub"]')
    print(div_sub_list)

    for div in div_list[:1]:
        item = BookItem()
        item['book_cate01'] = div.xpath('.//h3/a/text()').extract_first()
        pass

3.3 啓用管道

class SomethingPipeline(object):
    def __init__(self):
        # 可選實現,做參數初始化等
        # doing something

    def process_item(self, item, spider):
        # item (Item 對象) – 被爬取的item
        # spider (Spider 對象) – 爬取該item的spider
        # 這個方法必須實現,每個item pipeline組件都需要調用該方法,
        # 這個方法必須返回一個 Item 對象,被丟棄的item將不會被之後的pipeline組件所處理。
        return item

    def open_spider(self, spider):
        # 在爬蟲開始的時候僅執行一次
        # 可選實現,當spider被開啓時,這個方法被調用。

    def close_spider(self, spider):
        # 在爬蟲結束的時候僅執行一次
        # 可選實現,當spider被關閉時,這個方法被調用
  • 啓用一個Item Pipeline組件 爲了啓用Item Pipeline組件,需要在 settings.py文件中ITEM_PIPELINES 註冊:

    # 分配給每個類的整型值,確定了他們運行的順序,item按數字從低到高的順序,通過pipeline,通常將這些數字定
    ITEM_PIPELINES = {
       'book.pipelines.BookPipeline': 300,
    }
    

3.4 數據持久化

mongodb爲例,將數據存入mongodb:

import pymongo


class BookPipeline(object):
    def open_spider(self, spider):
        """
        爬蟲開始只執行一次
        :param spider: 
        :return: 
        """
        # 創建mongodb的數據庫連接
        self.client = pymongo.MongoClient(host='192.168.99.100', port=27018)
        # 選擇數據庫和數據表
        self.cursor = self.client['suning']['book']
        # 清空數據
        self.cursor.delete_many({})

    def process_item(self, item, spider):
        # 保存到數據庫中
        self.cursor.insert_one(dict(item))
        return item

    def close_spider(self, spider):
        """
        爬蟲結束時只執行一次
        :param spider: 
        :return: 
        """
        # 關閉連接
        self.client.close()
        print('爬蟲結束')

3.5 數據解析

# -*- coding: utf-8 -*-
from copy import deepcopy
from pprint import pprint

import scrapy
from book.items import BookItem


class SuningSpider(scrapy.Spider):
    name = 'suning'
    allowed_domains = ['suning.com']
    start_urls = ['https://book.suning.com/']
    # 當前頁的書籍計數
    book_num = 0

    def parse(self, response, **kwargs):
        """解析響應中的數據
        :param **kwargs:
        """
        # 一級分類列表
        div_list = response.xpath('//div[@class="menu-item"]')[:7]
        # 二級分類列表
        div_sub_list = response.xpath('//div[@class="menu-list"]/div[@class="menu-sub"]')
        print(div_sub_list)

        for div in div_list[:1]:
            item = BookItem()
            item['book_cate01'] = div.xpath('.//h3/a/text()').extract_first()
            # 一級分類下的元素列表
            sub_div = div_sub_list[div_list.index(div)]
            # 一級分類的所有二級分類和三級分類
            p_list = sub_div.xpath('./div[@class="submenu-left"]/p')

            for p in p_list[:1]:
                # 二級分類
                item['book_cate02'] = p.xpath('./a/text()').extract_first()
                # 三級分類元素列表
                li_list = p.xpath('./following-sibling::ul[1]/li')
                for li in li_list[:1]:
                    item['book_cate03'] = li.xpath('./a/text()').extract_first()
                    item['book_href'] = li.xpath('./a/@href').extract_first()

                    yield scrapy.Request(
                        item['book_href'],
                        callback=self.parse_book_list,
                        meta={'item': deepcopy(item)}
                    )

                    # 獲取當前頁的後半部分的數據
                    next_part_url = "https://list.suning.com/emall/showProductList.do?ci={}&pg=03&cp=0&il=0&iy=0&adNumber=0&n=1&ch=4&prune=0&sesab=ACBAAB&id=IDENTIFYING&paging=1&sub=0"
                    ci = item['book_href'].split('-')[1]
                    next_part_url = next_part_url.format(ci)
                    print(next_part_url)
                    yield scrapy.Request(
                        next_part_url,
                        callback=self.parse_book_list,
                        meta={'item': deepcopy(item)}
                    )
                    print('=' * 10)
                    next_part_url.format(ci)

    def parse_book_list(self, response):
        """解析response中的所有book"""
        print(1)
        item = response.meta.get('item')
        li_list = response.xpath('//li[contains(@class,"product      book")]')
        for li in li_list:
            item['book_name'] = li.xpath('.//p[@class="sell-point"]/a/text()').extract_first().strip()
            item['book_href'] = li.xpath('.//p[@class="sell-point"]/a/@href').extract_first()
            item['book_store'] = li.xpath('.//p[contains(@class, "seller oh no-more")]/a/text()').extract_first()

            yield scrapy.Request(
                response.urljoin(item['book_href']),
                callback=self.parse_book_detail,
                meta={'item': deepcopy(item)}
            )

        # TODO 分頁

    def parse_book_detail(self, response):
        """解析書籍信息"""
        # get date
        item = response.meta.get('item')
        self.book_num += 1
        item['book_num'] = self.book_num
        yield item

4 CrawlSpider的高階使用

通過命令創建 CrawlSpider爬蟲的代碼:

scrapy genspider -t crawl tencent tencent.com

它是Spider的派生類,Spider類的設計原則是隻爬取start_url列表中的網頁,而CrawlSpider類定義了一些規則(rule)來提供跟進link的方便的機制,從爬取的網頁中獲取link並繼續爬取的工作更適合。

CrawlSpider繼承於Spider類,除了繼承過來的屬性外(name、allow_domains),還提供了新的屬性和方法

4.1 rules

CrawlSpider使用rules來決定爬蟲的爬取規則,並將匹配後的url請求提交給引擎。所以在正常情況下,CrawlSpider不需要單獨手動返回請求了。

在rules中包含一個或多個Rule對象,每個Rule對爬取網站的動作定義了某種特定操作,比如提取當前相應內容裏的特定鏈接,是否對提取的鏈接跟進爬取,對提交的請求設置回調函數等。

如果多個rule匹配了相同的鏈接,則根據規則在本集合中被定義的順序,第一個會被使用。

class scrapy.spiders.Rule(
        link_extractor,
        callback = None,
        cb_kwargs = None,
        follow = None,
        process_links = None,
        process_request = None
)
  • link_extractor:是一個Link Extractor對象,用於定義需要提取的鏈接。

  • callback: 從link_extractor中每獲取到鏈接時,參數所指定的值作爲回調函數,該回調函數接受一個response作爲其第一個參數。

    注意:當編寫爬蟲規則時,避免使用parse作爲回調函數。由於CrawlSpider使用parse方法來實現其邏輯,如果覆蓋了 parse方法,crawl spider將會運行失敗。

  • follow:是一個布爾(boolean)值,指定了根據該規則從response提取的鏈接是否需要跟進。 如果callback爲None,follow 默認設置爲True ,否則默認爲False。

  • process_links:指定該spider中哪個的函數將會被調用,從link_extractor中獲取到鏈接列表時將會調用該函數。該方法主要用來過濾。

  • process_request:指定該spider中哪個的函數將會被調用, 該規則提取到每個request時都會調用該函數。 (用來過濾request)

4.2 LinkExtractors

class scrapy.linkextractors.LinkExtractor

Link Extractors 的目的很簡單: 提取鏈接。

每個LinkExtractor有唯一的公共方法是 extract_links(),它接收一個 Response 對象,並返回一個 scrapy.link.Link 對象。

Link Extractors要實例化一次,並且 extract_links 方法會根據不同的 response 調用多次提取鏈接。

class scrapy.linkextractors.LinkExtractor(
    allow = (),
    deny = (),
    allow_domains = (),
    deny_domains = (),
    deny_extensions = None,
    restrict_xpaths = (),
    tags = ('a','area'),
    attrs = ('href'),
    canonicalize = True,
    unique = True,
    process_value = None
)

主要參數:

  • allow:滿足括號中“正則表達式”的URL會被提取,如果爲空,則全部匹配。
  • deny:滿足括號中“正則表達式”的URL一定不提取(優先級高於allow)。
  • allow_domains:會被提取的鏈接的domains。
  • deny_domains:一定不會被提取鏈接的domains。
  • restrict_xpaths:使用xpath表達式,和allow共同作用過濾鏈接。

4.3 爬取規則(Crawling rules)

以騰訊招聘爲例,給出配合rule使用CrawlSpider的例子:

  1. 首先運行

     scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
    
  2. 導入LinkExtractor,創建LinkExtractor實例對象。:

     from scrapy.linkextractors import LinkExtractor
    
     page_lx = LinkExtractor(allow=('position.php?&start=\d+'))
    

    allow : LinkExtractor對象最重要的參數之一,這是一個正則表達式,必須要匹配這個正則表達式(或正則表達式列表)的URL纔會被提取,如果沒有給出(或爲空), 它會匹配所有的鏈接。

    deny : 用法同allow,只不過與這個正則表達式匹配的URL不會被提取)。它的優先級高於 allow 的參數,如果沒有給出(或None), 將不排除任何鏈接。

  3. 調用LinkExtractor實例的extract_links()方法查詢匹配結果:

     page_lx.extract_links(response)
    
  4. 沒有查到:

     []
    
  5. 注意轉義字符的問題,繼續重新匹配:

     page_lx = LinkExtractor(allow=('position\.php\?&start=\d+'))
     # page_lx = LinkExtractor(allow = ('start=\d+'))
    
     page_lx.extract_links(response)
    

4.4 CrawlSpider 版本

那麼,scrapy shell測試完成之後,修改以下代碼

#提取匹配 'http://hr.tencent.com/position.php?&start=\d+'的鏈接
page_lx = LinkExtractor(allow = ('start=\d+'))

rules = [
    #提取匹配,並使用spider的parse方法進行分析;並跟進鏈接(沒有callback意味着follow默認爲True)
    Rule(page_lx, callback = 'parse', follow = True)
]

這麼寫對嗎?

不對!千萬記住 callback 千萬不能寫 parse,再次強調:由於CrawlSpider使用parse方法來實現其邏輯,如果覆蓋了 parse方法,crawl spider將會運行失敗。

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class TecentSpider(CrawlSpider):
    name = 'tecent'
    allowed_domains = ['hr.tencent.com']
    start_urls = ['http://hr.tencent.com/position.php?&start=0']
    page_lx = LinkExtractor(allow=r'start=\d+')
    #position.php?&start=10#a
    rules = (
        Rule(page_lx, callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
        for item in items:
            temp = dict(
                position=item.xpath("./td[1]/a/text()").extract()[0],
                detailLink="http://hr.tencent.com/" + item.xpath("./td[1]/a/@href").extract()[0],
                type=item.xpath('./td[2]/text()').extract()[0] if len(
                    item.xpath('./td[2]/text()').extract()) > 0 else None,
                need_num=item.xpath('./td[3]/text()').extract()[0],
                location=item.xpath('./td[4]/text()').extract()[0],
                publish_time=item.xpath('./td[5]/text()').extract()[0]
            )
            print(temp)
            yield temp

    # parse() 方法不需要重寫     
    # def parse(self, response):                                              
    #     pass

運行: scrapy crawl tencent

5 Logging

Scrapy提供了log功能,可以通過 logging 模塊使用。

可以修改配置文件settings.py,任意位置添加下面兩行,效果會清爽很多。

LOG_FILE = "TencentSpider.log"
LOG_LEVEL = "INFO"

5.1 Log levels

  • Scrapy提供5層logging級別:
    • CRITICAL- 嚴重錯誤(critical)
    • ERROR - 一般錯誤(regular errors)
    • WARNING - 警告信息(warning messages)
    • INFO - 一般信息(informational messages)
    • DEBUG - 調試信息(debugging messages)

5.2 logging設置

通過在setting.py中進行以下設置可以被用來配置logging:

  1. LOG_ENABLED 默認: True,啓用logging
  2. LOG_ENCODING 默認: 'utf-8',logging使用的編碼
  3. LOG_FILE 默認: None,在當前目錄裏創建logging輸出文件的文件名
  4. LOG_LEVEL 默認: 'DEBUG',log的最低級別
  5. LOG_STDOUT 默認: False 如果爲 True,進程所有的標準輸出(及錯誤)將會被重定向到log中。例如,執行 print "hello" ,其將會在Scrapy log中顯示

6 中間件

6.1 分類和作用

  • 分類

    • 下載中間件
    • 爬蟲中間件
  • 作用-預處理requestreponse對象

    • headercookie進行更換處理
    • 使用代理ip
    • 對請求進行定製化操作

兩種中間件都在middlewares.py文件中

6.2 下載中間件(Downloader Middlewares)

下載中間件是處於引擎(crawler.engine)和下載器(crawler.engine.download())之間的一層組件,可以有多個下載中間件被加載運行。

  1. 當引擎傳遞請求給下載器的過程中,下載中間件可以對請求進行處理 (例如增加http header信息,增加proxy信息等);
  2. 在下載器完成http請求,傳遞響應給引擎的過程中, 下載中間件可以對響應進行處理(例如進行gzip的解壓等)

要激活下載器中間件組件,將其加入到 DOWNLOADER_MIDDLEWARES 設置中。 該設置是一個字典(dict),鍵爲中間件類的路徑,值爲其中間件的順序(order)。

這裏是一個例子:

DOWNLOADER_MIDDLEWARES = {
    'mySpider.middlewares.MyDownloaderMiddleware': 543,
}

編寫下載器中間件十分簡單。每個中間件組件是一個定義了以下一個或多個方法的Python類:

class scrapy.contrib.downloadermiddleware.DownloaderMiddleware

6.2.1 process_request

process_request(self, request, spider)

當每個request通過下載中間件時,該方法被調用。

  • process_request() 必須返回以下其中之一:一個 None 、一個 Response 對象、一個 Request 對象或 raise IgnoreRequest:

    • 返回 None值 :

      Scrapy將繼續處理該request,執行其他的中間件的相應方法,直到合適的下載器(download handler)被調用, 該request被執行(其response被下載。

    • 返回 Response 對象:

      Scrapy將不會調用 任何 其他的 process_request() 或 process_exception() 方法,或相應地下載函數; 其將返回該response給引擎。

    • 返回 Request 對象:

      Scrapy則停止調用 process_request方法並通過引擎重新返回給調度器。當新返回的request被執行後, 相應地中間件鏈將會根據下載的response被調用。

    • 如果其raise一個 IgnoreRequest 異常,則安裝的下載中間件的 process_exception() 方法會被調用。如果沒有任何一個方法處理該異常, 則request的errback(Request.errback)方法會被調用。如果沒有代碼處理拋出的異常, 則該異常被忽略且不記錄(不同於其他異常那樣)。

  • 參數:

    • request (Request 對象) – 處理的request
    • spider (Spider 對象) – 該request對應的spider

6.2.2 process_response

process_response(self, request, response, spider)

當下載器完成http請求,傳遞響應給引擎的時候調用

  • process_response() 必須返回以下其中之一: 返回一個 Response 對象、 返回一個 Request 對象或raise一個 IgnoreRequest 異常。

    • 返回 Response 對象:

      可以與傳入的response相同,也可以是全新的對象, 該response會被在鏈中的其他中間件的 process_response() 方法處理。

    • 返回 Request 對象:

      中間件停止處理, 返回的request會通過引擎交給調度器,等待重新請求。處理類似於 process_request() 返回request所做的那樣。

    • 如果其拋出一個 IgnoreRequest 異常,則調用request的errback(Request.errback)。 如果沒有代碼處理拋出的異常,則該異常被忽略且不記錄(不同於其他異常那樣)。

  • 參數:

    • request (Request 對象) – response所對應的request
    • response (Response 對象) – 被處理的response
    • spider (Spider 對象) – response所對應的spider

6.3 使用案例

1. 創建middlewares.py文件。

Scrapy代理IP、Uesr-Agent的切換都是通過DOWNLOADER_MIDDLEWARES進行控制,我們在settings.py同級目錄下創建middlewares.py文件,包裝所有請求。

# middlewares.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import random
import base64

from settings import USER_AGENTS
from settings import PROXIES

# 隨機的User-Agent
class RandomUserAgent(object):
    def process_request(self, request, spider):
        useragent = random.choice(USER_AGENTS)

        request.headers.setdefault("User-Agent", useragent)

class RandomProxy(object):
    def process_request(self, request, spider):
        proxy = random.choice(PROXIES)

        if proxy['user_passwd'] is None:
            # 沒有代理賬戶驗證的代理使用方式
            request.meta['proxy'] = "http://" + proxy['ip_port']
        else:
            # 對賬戶密碼進行base64編碼轉換
            base64_userpasswd = base64.b64encode(proxy['user_passwd'])
            # 對應到代理服務器的信令格式裏
            request.headers['Proxy-Authorization'] = 'Basic ' + base64_userpasswd
            request.meta['proxy'] = "http://" + proxy['ip_port']

爲什麼HTTP代理要使用base64編碼:

HTTP代理的原理很簡單,就是通過HTTP協議與代理服務器建立連接,協議信令中包含要連接到的遠程主機的IP和端口號,如果有需要身份驗證的話還需要加上授權信息,服務器收到信令後首先進行身份驗證,通過後便與遠程主機建立連接,連接成功之後會返回給客戶端200,表示驗證通過,就這麼簡單,下面是具體的信令格式:

CONNECT 59.64.128.198:21 HTTP/1.1
Host: 59.64.128.198:21
Proxy-Authorization: Basic bGV2I1TU5OTIz
User-Agent: OpenFetion

其中Proxy-Authorization是身份驗證信息,Basic後面的字符串是用戶名和密碼組合後進行base64編碼的結果,也就是對username:password進行base64編碼。

HTTP/1.0 200 Connection established

OK,客戶端收到收面的信令後表示成功建立連接,接下來要發送給遠程主機的數據就可以發送給代理服務器了,代理服務器建立連接後會在根據IP地址和端口號對應的連接放入緩存,收到信令後再根據IP地址和端口號從緩存中找到對應的連接,將數據通過該連接轉發出去。

2. 修改settings.py配置USER_AGENTS和PROXIES

  • 添加USER_AGENTS:
  USER_AGENTS = [
    "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"
    ]
  • 添加代理IP設置PROXIES:

    免費代理IP可以網上搜索,或者付費購買一批可用的私密代理IP:

PROXIES = [
    {'ip_port': '111.8.60.9:8123', 'user_passwd': 'user1:pass1'},
    {'ip_port': '101.71.27.120:80', 'user_passwd': 'user2:pass2'},
    {'ip_port': '122.96.59.104:80', 'user_passwd': 'user3:pass3'},
    {'ip_port': '122.224.249.122:8088', 'user_passwd': 'user4:pass4'},
]
  • 除非特殊需要,禁用cookies,防止某些網站根據Cookie來封鎖爬蟲。
COOKIES_ENABLED = False
  • 設置下載延遲
DOWNLOAD_DELAY = 3
  • 最後設置setting.py裏的DOWNLOADER_MIDDLEWARES,添加自己編寫的下載中間件類。
DOWNLOADER_MIDDLEWARES = {
    #'mySpider.middlewares.MyCustomDownloaderMiddleware': 543,
    'mySpider.middlewares.RandomUserAgent': 1,
    'mySpider.middlewares.ProxyMiddleware': 100
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章