Python_Scrapy

1、Scrapy框架

(1)安裝

  • 安裝Scrapy:pip install scrpay
  • Scrapy是一個適用爬取網站數據、提取結構性數據的應用程序框架
    在這裏插入圖片描述

(2)流程

  • 1、Engine首先打開一個網站,找到處理該網站的Spider,並向該Spider請求第一個要爬取的URL;
  • 2、Engine從Spider中獲取到第一個要爬取的URL,並通過Scheduler以Request的形式調度;
  • 3、Engine向Scheduler請求下一個要爬取的URL;
  • 4、Scheduler返回下一個要爬取的URL給Engine,Engine將URL通過Downloader Middlewares轉發給Downloader下載;
  • 5、一旦頁面下載完畢,Downloader生成該頁面的Response,並將其通過Downloader Middlewares發送給Engine;
  • 6、Engine從下載器中接收到Response,並將其通過Spider Middlewares發送給Spider處理;
  • 7、Spider處理Response,並返回爬取到的Item及新的Request給Scheduler;
  • 8、Spider處理Response,並返回爬取到的I及新的Request給Scheduler;
  • 9、重複2到8步,直到Scheduler中沒有更多的Request,Engine關閉該網站,爬取結束

(3)解析介紹

  • scrapy提供了兩個方法,response.xpath()和response.css()
  • extract()匹配所有元素,extract_first()匹配單個元素
  • extract_first()傳遞一個默認值參數,可以在xpath匹配不到結果的時候,返回這個參數代替;
  • 正則匹配:re()匹配所有結果,re_first()匹配第一個
  • response.xpath()使用:
response.xpath("//a/text()").extract()
response.xpath("//a/text()").extract_first()
response.xpath("//a/text()").extract_first("Default None")
response.xpath("//a/text()").re('Name:\s(.*)')
response.xpath("//a/text()").re_first('Name:\s(.*)')
  • response.css()使用:
response.css("a[href='image.html']::text").extract()
response.css("a[href='image.html']::text").extract_first()
response.css("a[href='image.html'] img::attr(src)").extract_first("Default None")

2、scrapy genspider 操作步驟scrapy.Spider

(1)創建項目和爬蟲

A:終端創建項目和爬蟲,輸入如下命令
scrapy startproject [項目名稱]
cd [項目名稱路徑]
scrapy genspider [爬蟲文件名] [爬蟲URL域名]

在這裏插入圖片描述

B:生成的項目目錄結構:

在這裏插入圖片描述
* spiders:以後所有的爬蟲,都是存放到這個裏面
* items.py:用來存放爬蟲爬取下來數據的模型;
* middlewares.py:用來存放各種中間件的文件;
* pipelines.py:定義數據管道,用來將items的模型存儲到本地磁盤中;
* settings.py:本爬蟲的一些配置信息(比如請求頭、多久發送一次請求、ip代理池等);
* scrapy.cfg:它是scrapy項目的配置文件,其內定義了項目的配置文件路徑,部署相關信息等內容

C:qsbk.py生成的內容介紹:
* name:它是每個項目唯一的名字,用來區分不同的Spider
* allowed_domains:它是允許爬取的域名,如果初始或後續的請求鏈接不是這個域名下的,則請求鏈接會被過濾掉
* start_urls:它包含了Spider在啓動時爬取的url列表,初始請求時由它定義的
* parse:該方法負責解析返回的響應、提取數據或者進一步生成處理的請求
* response是一個'scrapy.http.response.html.HtmlResponse'對象,可以執行‘xpath’和'css'語法來提取數據,提取出來的數據是一個‘Selector’或者是一個'SelectorList'對象,如果想要獲取其中的字符串,可以用getall或者get方法
# -*- coding: utf-8 -*-
import scrapy


class QsbkSpider(scrapy.Spider):
    name = 'qsbk'
    allowed_domains = ['qiushibaike.com']
    start_urls = ['https://www.qiushibaike.com/text/page/1/']

    def parse(self, response):
        pass

(2)製作爬蟲開始爬取網頁

A、settings設置HEADERS等內容
  • ROBOTSTXT_OBEY改爲False
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
  • DEFAULT_REQUEST_HEADERS修改
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537'
                  '.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'
}
B、items.py裏面編寫需要的字段模型
  • 定義字段模型,需要的字段
import scrapy


class HelloItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    author = scrapy.Field()
    content = scrapy.Field()
C、qsbk.py裏面編寫爬蟲
  • 修改start_urls,在qsbk.py裏面的parse方法裏編寫解析過程
  • 定位下一頁,回調函數,需要用到scrapy.Request,需要傳遞兩個參數:url和callback
  • dont_filter=False過濾掉已請求過的url;反之不用過濾
import scrapy
from Hello.items import HelloItem


class QsbkSpider(scrapy.Spider):
    name = 'qsbk'
    allowed_domains = ['qiushibaike.com']
    start_urls = ['https://www.qiushibaike.com/text/page/1/']
    base_domain = "https://www.qiushibaike.com"

    def parse(self, response):
        divs = response.xpath('//div[@class="col1 old-style-col1"]/div')
        for div in divs:
            author = div.xpath(".//h2/text()").get().strip()
            # 選擇器的前方加.,代表提取元素內部的數據,如果沒有加.則代表從根節點開始提取
            content = div.xpath('.//div[@class="content"]//text()').getall()
            content = ''.join(content).strip()
            # items.py裏面定義好模型後,替換下面兩行
            # duanzi = {'author': author, 'content': content}
            # yield duanzi
            item = HelloItem(author=author, content=content)
            yield item
            
        # 處理下一頁的請求,並settings設置DOWNLOAD_DELAY = 1
        next_page_url = response.xpath(
            '//span[@class="next"]/ancestor::a/@href').get()
        if not next_page_url:
            return
        else:
            yield scrapy.Request(
                f"{self.base_domain}{next_page_url}", callback=self.parse, dont_filter=False)

(3)存儲內容 (pipelines.py):設計管道存儲爬取內容

A、settings設置ITEM_PIPELINES
  • ITEM_PIPELINES設置,其中值越小優先級越高
ITEM_PIPELINES = {
   'Hello.pipelines.HelloPipeline': 300,
}
B、編寫pipelines.py
  • open_spider()方法,當爬蟲被打開的時候執行
  • process_item()方法,當爬蟲有item傳過來的時候會被調用
  • close_spider()方法,當爬蟲關閉時會被調用
import json


class HelloPipeline(object):

    def open_spider(self, spdier):
        print("爬蟲開始了……")

    def process_item(self, item, spider):
        with open('duanzi.json', "a+", encoding="utf-8") as fp:
            item_json = json.dumps(dict(item), ensure_ascii=False)
            fp.write(item_json+'\n')
            return item

    def close_spider(self, spdier):
        print("爬蟲結束了……")
  • 上述代碼的第二種寫法:使用JsonLinesItemExporter:這個每次調用export_item的時候就把這個item存儲到硬盤中,輸出也是字典
from scrapy.exporters import JsonLinesItemExporter


class HelloPipeline(object):

    def __init__(self):
        self.fp = open('duanzi.json', 'wb')
        self.exporter = JsonLinesItemExporter(
            self.fp, ensure_ascii=False, encoding='utf-8')

    def open_spider(self, spdier):
        print("爬蟲開始了……")

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

    def close_spider(self, spdier):
        self.fp.close()
        print("爬蟲結束了……")
C、不通過pipelines,直接命令保存到文件
命令 解釋
scrapy crawl quotes -o quotes.json 保存成JSON文件
scrapy crawl quotes -o quotes.jl 每一個Item輸出一行JSON
scrapy crawl quotes -o quotes.csv 保存成csv文件
scrapy crawl quotes -o quotes.xml 保存成xml文件
scrapy crawl quotes -o quotes.pickle 保存成pickle文件
scrapy crawl quotes -o quotes.marshal 保存成marshal文件
scrapy crawl quotes -o quotes.ftp://user:[email protected]/path/to/quotes.csv ftp遠程輸出
D、MongoDB存儲
import pymongo


class MongoPipeline(object):

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db
	
	@classmethod
	def from_crawler(cls, crawler):
		return cls(
			mongo_uri = crawler.settings.get('MONGO_URI')
			mongo_db = crawler.settings.get('MONGO_DB')
		)
		
    def open_spider(self, spdier):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def process_item(self, item, spider):
        self.db[item.collection].insert(dict(item))
        return item

    def close_spider(self, spdier):
        self.client.close()
E、MySQL存儲
import pymysql


class MysqlPipeline(object):

    def __init__(self, host, database, user, password, port):
        self.host = host
        self.database = database
        self.password = password
        self.port = port
	
	@classmethod
	def from_crawler(cls, crawler):
		return cls(
			host = crawler.settings.get('MYSQL_HOST')
			database = crawler.settings.get('MYSQL_DATABASE')
			user = crawler.settings.get('MYSQL_USER')
			password = crawler.settings.get('MYSQL_PASSWORD')
			port = crawler.settings.get('MYSQL_PORT')
		)
		
    def open_spider(self, spdier):
        self.db = pymysql.connect(self.host, self.user, self.password, self.database, charset="utf-8", port=self.port)

    def process_item(self, item, spider):
        data = dict(item)
        keys = ','.join(data.keys())
        values = ','.join(['%s'] * len(data))
		sql = 'insert into %s (%s) values (%s)' % (item.table, keys, values)
		self.cursor.execute(sql, tuple(data.values()))
		self.db.commit()
        return item

    def close_spider(self, spdier):
        self.db.close()

(4)運行爬蟲的兩種方法

  • 第一種:切換到Hello文件夾路徑下,然後直接終端scrapy crawl qsbk
E:\Test_com\Hello>scrapy crawl qsbk
  • 第二種:在Hello文件夾下新建一個start.py文件,然後寫命令,然後運行start.py文件即可
from scrapy import cmdline

cmdline.execute(['scrapy', 'crawl', 'qsbk'])

在這裏插入圖片描述

3、scrapy Shell操作

  • 可以方便我們做一些數據提取的測試代碼;
  • 如果想要執行scrapy命令,首先進入到scrapy所在的環境中;
scrapy shell url

4、scrapy Request對象/Respose對象

(1)Request對象

  • callback:在下載其下載完響應的數據後執行的回調函數;
  • headers:請求頭,對於一些固定的設置,放在settings.py中指定就可以了,對於那些非固定的,可以在發送請求時指定
  • meta:共享數據,用於在不同的請求之間傳遞數據的
  • dot_filter:表示不由調度器過濾,在執行多次重複的請求的時候用的比較多
  • errback:在發生錯誤的時候執行的函數
import scrapy
scrapy.Request(url, callback=None, method='GET', headers=None, body=None,cookies=None, meta=None, 
encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None, cb_kwargs=None)
  • 發送POST請求:有時候我們想要在請求數據的時候發送post請求;那麼這時候需要使用Request的子類FormRequest來實;如果想要在爬蟲一開始的時候就發送POST請求,那麼需要在爬蟲類中重寫start_requests(self)方法,並且不再調用start_urls裏的url,在這個方法中發送post請求
# 模擬登錄人人網, 只是案例,網站已改,不一定能實現
import scrapy


class RenrenSpiderSpider(scrapy.Spider):
   name = 'renren_spider'
   allowed_domains = ['renren.com']
   start_urls = ['http://renren.com/']

   # def parse(self, response):
   #     pass
   def start_requests(self):
       url = "http://www.renren.com/PLogin.do"
       data = {"email": "[email protected]", "password": "pythonspider"}
       request = scrapy.FormRequest(
           url, formdata=data, callback=self.parse_page)
       yield request

   def parse_page(self, response):
       # with open("renren.html", "w", encoding="utf-8") as f:
       #     f.write(response.text)
       request = scrapy.Request(
           url="http://www.renren.com/880151247/profile",
           callback=self.parse_profile)
       yield request

   def parse_profile(self, response):
       with open("renren.html", "w", encoding="utf-8") as f:
           f.write(response.text)
  • 第二個post請求案例
# 模擬登錄豆瓣網,可能已失效,驗證碼已改,只是一個思路案例
import scrapy
from urllib import request
from PIL import Image


class DoubanSpider(scrapy.Spider):
    name = 'douban'
    allowed_domains = ['douban.com']
    start_urls = ['http://douban.com/']


    def parse(self, response):
        form_data = {
            "source": "None",
            "redir": "https://www.douban.com/",
            "form_email": "[email protected]",
            "form_password": "pythonspider",
            "login": "登錄"
        }
        captcha_url = response.css("img#captcha_image::attr(src)").get()
        if captcha_url:
            captcha = self.regonize_captcha(captcha_url)
            form_data['captcha-solution'] = captcha
            captcha_id = response.xpath("//input[@name='captcha-id']/@value").get()
            form_data['captcha-id'] = captcha_id
        yield scrapy.FormRequest(url=self.login_url, formdata=form_data,
                                 callback=self.parse_after_login)

    def parse_after_login(self, response):
        if response.url == "https://www.douban.com/":
            print("登錄成功")
        else:
            print("登錄失敗")

    @staticmethod
    def regonize_captcha(image_url):
        """肉眼識別驗證碼,以前網站是驗證碼,現在是滑動,只提供簡短思路"""
        request.urlretrieve(image_url, 'captcha.png')
        image = Image.open('captcha.png')
        image.show()
        captcha = input("請輸入驗證碼:")
        return captcha

(2)Respose對象

  • meta:從其他請求傳過來的meta屬性,可以用來保持多個請求之間的數據連接;
  • encoding:返回當前字符串編碼和解碼的格式;
  • text:將返回來的數據作爲unicode字符串返回;
  • body:將返回來的數據作爲bytes字符串返回;
  • xpath:xpath選擇器;
  • css:css選擇器

5、scrapy內置下載圖片

(1)介紹

  • scrapy爲下載item中包含的文件(比如在爬取到產品時,同時也想保存對應的圖片提供了一個可重用的items pipelines。這寫pipelines有些共同的方法和結構我們稱之爲media pipeline),一般來說會使用Files Pipeline或者Images Pipeline;
  • 使用scrapy內置下載文件的優點:避免重複下載,方便指定路徑,方便轉換爲通用格式,方便生成縮略圖,方便檢測圖片寬和高,異步下載,效率非常高
  • 步驟:
    • 定義好一個item,然後item中定義兩個屬性,分別爲image_urls以及images;image_urls是用來存儲需要下載的文件的url鏈接,需要給一個列表;
    • 當文件下載完成後,會把文件下載的相關信息存儲到item的images屬性中,比如下載路徑、下載的url和文件的校驗碼等;
    • 在配置文件settings.py中配置IMAGES_STORE,這個配置是用來設置文件下載下來的路徑;
    • 啓動pipeline:在ITEM_PIPELINES中設置’scrapy.pipelines.images.ImagesPipelin’: 1

(2)設置

  • items.py設置
import scrapy


class ImageItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    category = scrapy.Field()
    image_urls = scrapy.Field()
    images = scrapy.Field()
  • settings設置
import os
# 圖片pipelines設置,將原有的pipeline註釋掉
ITEM_PIPELINES = {
   # 'bmw.pipelines.BmwPipeline': 300,
    'scrapy.pipelines.images.ImagesPipelin': 1
}
# 圖片下載路徑, 供images pipelines使用
IMAGES_STORE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images')

6、scrapy下載器中間件Downloader Middlewares

  • 下載器中間件是引擎和下載器之間通信的中間件。在這個中間件中我們可以設置代理、更換請求頭等來達到反反爬蟲的目的;要寫下載器中間件,可以在啊下載器中實現兩個方法。
  • 一個是process_request(self, request, spider),這個方法是在請求發送之前會執行,
  • 還有一個是process_response(self,request,response,spider),這個方法是數據下載到引擎之前執行;

(1)下載請求頭中間件

DOWNLOADER_MIDDLEWARES = {
   'Hello.middlewares.UserAgentDownloadMiddleware': 543,
}
  • middlewares.py
import random


class UserAgentDownloadMiddleware:
    USER_AGENT = [
        "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
    ]
    
    def process_request(self, request, spider):
        user_agent = random.choice(self.USER_AGENT)
        request.headers['User-Agent'] = user_agent

(2)ip代理中間件

DOWNLOADER_MIDDLEWARES = {
    'Hello.middlewares.UserAgentDownloadMiddleware': 543,
    'Hello.middlewares.IPProxyDownloadMiddleware': 100,
}
class IPProxyDownloadMiddleware:
    PROXIES = ["http://101.132.190.101:80", "http://175.148.71.139:1133", "http://222.95.144.68:3000"]

    def process_request(self, request, spider):
        proxy = random.choice(self.PROXIES)
        request.meta['proxy'] = proxy

(3)selenium中間件

  • 當process_request()方法返回Response對象的時候,更低優先級的Downloader Middleware的process_request()和process_exception()方法就不會被繼續調用了, 轉而開始執行每個Downloader Middleware的process_response()方法,調用完畢之後直接將Response對象發送給Spider處理
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from scrapy.http import HtmlResponse
from logging import getLogger


class SeleniumMiddleware():
    def __init__(self, timeout=None, service_args=[]):
        self.logger = getLogger(__name__)
        self.timeout = timeout
        self.browser = webdriver.PhantomJS(service_args=service_args)
        self.browser.set_window_size(1400, 700)
        self.browser.set_page_load_timeout(self.timeout)
        self.wait = WebDriverWait(self.browser, self.timeout)
    
    def __del__(self):
        self.browser.close()
    
    def process_request(self, request, spider):
        """
        用PhantomJS抓取頁面
        :param request: Request對象
        :param spider: Spider對象
        :return: HtmlResponse
        """
        self.logger.debug('PhantomJS is Starting')
        page = request.meta.get('page', 1)
        try:
            self.browser.get(request.url)
            if page > 1:
                input = self.wait.until(
                    EC.presence_of_element_located((By.CSS_SELECTOR, '#mainsrp-pager div.form > input')))
                submit = self.wait.until(
                    EC.element_to_be_clickable((By.CSS_SELECTOR, '#mainsrp-pager div.form > span.btn.J_Submit')))
                input.clear()
                input.send_keys(page)
                submit.click()
            self.wait.until(
                EC.text_to_be_present_in_element((By.CSS_SELECTOR, '#mainsrp-pager li.item.active > span'), str(page)))
            self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.m-itemlist .items .item')))
            return HtmlResponse(url=request.url, body=self.browser.page_source, request=request, encoding='utf-8',
                                status=200)
        except TimeoutException:
            return HtmlResponse(url=request.url, status=500, request=request)
    
    @classmethod
    def from_crawler(cls, crawler):
        return cls(timeout=crawler.settings.get('SELENIUM_TIMEOUT'),
                   service_args=crawler.settings.get('PHANTOMJS_SERVICE_ARGS'))

7、scrapy genspider -t 操作步驟CrawlSpider,適用於數據量大的網站

(1)創建項目和爬蟲

A:終端創建項目和爬蟲,輸入如下命令
scrapy startproject [項目名稱]
cd [項目名稱路徑]
scrapy genspider -t crawl [爬蟲文件名] [爬蟲URL域名]

在這裏插入圖片描述

B:生成的項目目錄結構:

在這裏插入圖片描述

(2)製作爬蟲開始爬取網頁

A、settings設置HEADERS等內容
  • ROBOTSTXT_OBEY改爲False
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
  • DEFAULT_REQUEST_HEADERS修改
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537'
                  '.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'
}
B、items.py裏面編寫需要的字段模型
  • 定義字段模型,需要的字段
import scrapy


class WxappItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field()
    author = scrapy.Field()
    pub_time = scrapy.Field()
C、wxap_spider.py裏面編寫爬蟲
  • 修改start_urls,在wxap_spider.py裏面的parse_detail方法裏編寫解析過程;
  • 需要使用"LinuxExtractor和Rule",這兩個東西決定爬蟲的具體走向;
  • allow設置的規則的方法,要能夠限制在我們想要的url上面,不要和其他的url產生相同的正則表達式即可;
  • 上面情況下使用follow:如果在爬取頁面的時候,需要將滿足當前條件的url再進行跟進,那麼設置爲True,否則設置爲False;
  • 什麼情況下該指定callback:如果這個url對應的頁面,只是爲了獲取更多的url,並不需要裏面的數據,可以不指定callback;如果想要獲取url對應頁面中的數據,那麼就需要指定一個callback
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from wxapp.items import WxappItem


class WxapSpiderSpider(CrawlSpider):
    name = 'wxap_spider'
    allowed_domains = ['www.wxapp-union.com']
    start_urls = ['http://www.wxapp-union.com/portal.php?mod=list&catid=1']

    rules = (
    # 列表頁
        Rule(LinkExtractor(
            allow=r'.+mod=list&catid=2&page=\d'),
            follow=True),
   # 詳情頁
        Rule(LinkExtractor(
            allow=r'.+article.+\.html'),
            callback="parse_detail",
            follow=False),
    )

    def parse_detail(self, response):
        title = response.xpath('//h1[@class="ph"]/text()').get()
        author_p = response.xpath('//p[@class="authors"]')
        author = author_p.xpath(".//a/text()").get()
        pub_time = author_p.xpath(".//span/text()").get()
        print(f"title:{title},author:{author},pub_time:{pub_time}")
        item = WxappItem(title=title, author=author, pub_time=pub_time)
        yield item

(3)存儲內容 (pipelines.py):設計管道存儲爬取內容

A、settings設置ITEM_PIPELINES
  • ITEM_PIPELINES設置,其中值越小優先級越高
ITEM_PIPELINES = {
   'Hello.pipelines.HelloPipeline': 300,
}
B、編寫pipelines.py
  • 使用JsonLinesItemExporter:這個每次調用export_item的時候就把這個item存儲到硬盤中,輸出也是字典
from scrapy.exporters import JsonLinesItemExporter


class WxappPipeline(object):
    def __init__(self):
        self.fp = open('wxjc.json', 'wb')
        self.exporter = JsonLinesItemExporter(
            self.fp, ensure_ascii=False, encoding='utf-8')

    def open_spider(self, spdier):
        print("爬蟲開始了……")

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

    def close_spider(self, spdier):
        self.fp.close()
        print("爬蟲結束了……")

(4)運行爬蟲

  • 在wxapp文件夾下新建一個start.py文件,然後寫命令,然後運行start.py文件即可
from scrapy import cmdline

cmdline.execute(['scrapy', 'crawl', 'wxap_spider'.split()])

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章