崔庆才python3爬虫-13章 Scrapy框架的使用-Spider的用法


崔庆才python3爬虫-13章 Scrapy框架的使用-Spider的用法和Downloader Middleware 的用法

1
在 Scrapy中,要抓取网站的链接配置、抓取逻辑、解析逻辑里其实都是在Spider中配置的。在前一节实例中,我们发现抓取逻辑也是在Spider中完成的。本节我们就来专门了解一下Spider的基本用法。

Spider的用法

Spider运行流程

1
2
3
4
5
6
7
8
在实现Scrapy爬虫项目时,最核心的类便是Spider类了, 它定义了如何爬取某个网站的流程和解析方式。 简单来讲, Spider要做的事就是如下两件:
□ 定义爬取网站的动作;
□ 分析爬取下来的网页。
对于Spider类来说,整个爬取循环过程如下所述。
□ 以初始的URL初始化Request,并设置回调函数。当该 Request成功请求并返回时, Response生成并作为参数传给该回调函数。
□ 在回调函数内分析返回的网页内容。返回结果有两种形式。一种是解析到的有效结果返回字典或Item对象,它们可以经过处理后(或宜接)保存。另一种是解析得到下一个(如下一页)链接,可以利用此链接构造Request并设置新的回调函数,返回Request等待后续调度。
□ 如果返回的是字典或Item对象,我们可通过Feed Exports等组件将返回结果存入到文件。如果设置了Pipeline的话,我们可以使用Pipeline处 理 (如过滤、修正等) 并保存。
□ 如果返回的是 Reqeust, 那么Request执行成功得到Response之后,Response会被传递给Request中定义的回调函数,在回调函数中我们可以再次使用选择器来分析新得到的网页内容 ,并根据分析的数据生成Item。

Spider类分析

1
2
在上一节的例子中,我们定义的Spider是继承自 scrapy.spiders.Spider, scrapy.spiders.Spider
这个类是最简单最基本的Spider类,其他Spider必须继承这个类。还有后面一些特殊Spider类也都是继承自它。
1
scrapy.spiders.Spider这个类提供了 start_requests()方法的默认实现,读取并请求start_urls属性,并根据返回的结果调用parse()方法解析结果。它还有如下一些基础属性。
1
2
3
4
5
6
7
8
name: 爬虫名称,是定义Spider名字的字符串。Spider的名字定义了 Scrapy如何定位并初始化Spider,它必须是唯一的。不过我们可以生成多个相同的Spider实例,数量没有限制。

□ name是Spider最重要的属性。如果 Spider爬取单个网站,一个常见的做法是以该网站的域名名称来命名Spider例如,Spider爬取 mywebsite.com ,该 Spider通常会被命名为mywebsite
□ allowedd_mains 允许爬取的域名,是可选配置,不在此范围的链接不会被跟进爬取。
□ start_urls它是起始URL列表,当我们没有实现start_requests()方法时,默认会从这个列表开始抓取。
□ custom _settings 它是一个字典,是专属于本Spider的配置,此设置会覆盖项目全局的设置。此设置必须在初始化前被更新,必须定义成类变量。
□ crawler。它是由from_crawler()方法设置的,代表的是本Spider类对应的Crawler对象。Crawler对象包含了很多项目组件,利用它我们可以获取项目的一些配置信息,如最常见的获取项目的设置信息,即Settings
□ settings 它是一个Settings对象,利用它我们可以直接获取项目的全局设置变量。
1
2
3
4
除了基础属性, Spider还有一些常用的方法。
□ start_requests() 此方法用于生成初始请求,它必须返回一个可迭代对象。此方法会默认使用start_urls 里面的URL来构造Request,而且 Request是GET请求方式。如果我们想在启动时以POST方式访问某个站点,可以直接重写这个方法,发送POST请求时使用FormRequest即可。
□ parse() 当Response没有指定回调函数时,该方法会默认被调用。它负责处理Response,处理返回结果,并从中提取出想要的数据和下一步的请求,然后返回。该方法需要返回一个包含Request或Item 的可迭代对象。
□ closed() 当 Spider关闭时,该方法会被调用,在这里一般会定义释放资源的一些操作或其他收尾操作。
1
以上内容可能不太好理解。不过不用担心,后面会有很多使用这些属性和方法的实例。通过这些实例,我们慢慢熟练掌握它们。

Using errbacks to catch exceptions in request processing

官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import scrapy

from scrapy.spidermiddlewares.httperror import HttpError
from twisted.internet.error import DNSLookupError
from twisted.internet.error import TimeoutError, TCPTimedOutError

class ErrbackSpider(scrapy.Spider):
name = "errback_example"
start_urls = [
"http://www.httpbin.org/", # HTTP 200 expected
"http://www.httpbin.org/status/404", # Not found error
"http://www.httpbin.org/status/500", # server issue
"http://www.httpbin.org:12345/", # non-responding host, timeout expected
"https://example.invalid/", # DNS error expected
]

def start_requests(self):
for u in self.start_urls:
yield scrapy.Request(u, callback=self.parse_httpbin,
errback=self.errback_httpbin,
dont_filter=True)

def parse_httpbin(self, response):
self.logger.info('Got successful response from {}'.format(response.url))
# do something useful here...

def errback_httpbin(self, failure):
# log all failures
self.logger.error(repr(failure))

# in case you want to do something special for some errors,
# you may need the failure's type:

if failure.check(HttpError):
# these exceptions come from HttpError spider middleware
# you can get the non-200 response
response = failure.value.response
self.logger.error('HttpError on %s', response.url)

elif failure.check(DNSLookupError):
# this is the original request
request = failure.request
self.logger.error('DNSLookupError on %s', request.url)

elif failure.check(TimeoutError, TCPTimedOutError):
request = failure.request
self.logger.error('TimeoutError on %s', request.url)

Accessing additional data in errback functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def parse(self, response):
request = scrapy.Request('http://www.example.com/index.html',
callback=self.parse_page2,
errback=self.errback_page2,
cb_kwargs=dict(main_url=response.url))
yield request

def parse_page2(self, response, main_url):
pass

def errback_page2(self, failure):
yield dict(
main_url=failure.request.cb_kwargs['main_url'],
)

DOWNLOAD_TIMEOUT

1
2
3
4
5
6
Default: 180

The amount of time (in secs) that the downloader will wait before timing out.


This timeout can be set per spider using download_timeout spider attribute and per-request using download_timeout Request.meta key.

DUPEFILTER_CLASS

1
2
3
4
5
6
7
Default: 'scrapy.dupefilters.RFPDupeFilter'

The class used to detect and filter duplicate requests.

The default (RFPDupeFilter) filters based on request fingerprint using the scrapy.utils.request.request_fingerprint function. In order to change the way duplicates are checked you could subclass RFPDupeFilter and override its request_fingerprint method. This method should accept scrapy Request object and return its fingerprint (a string).

You can disable filtering of duplicate requests by setting DUPEFILTER_CLASS to 'scrapy.dupefilters.BaseDupeFilter'. Be very careful about this however, because you can get into crawling loops. It’s usually a better idea to set the dont_filter parameter to True on the specific Request that should not be filtered.

Settings per-spider

1
2
3
4
5
6
7
8
Spiders (See the Spiders chapter for reference) can define their own settings that will take precedence and override the project ones. They can do so by setting their custom_settings attribute:

class MySpider(scrapy.Spider):
name = 'myspider'

custom_settings = {
'SOME_SETTING': 'some value',
}

Downloader Middleware 的用法

DOWNLOAD_MIDDLEWARE官方文档

1
2
3
Downloader Middleware即下载中间件,它是处于Scrapy的Request和 Response之间的处理模块。
我们首先来看看它的架构,如图 131所示。
Scheduler从队列中拿出一个Request发送给Downloader执行下载,这个过程会经过Downloader Middleware的处理。另外,当Downloader将Request下载完成得到Response返回给Spider时会再次经过Downloader Middleware处理。
1
2
3
4
也就是说,Downloader Middleware在整个架构中起作用的位置是以下两个。
□ 在 Scheduler调度出队列的Request发 送 给 Doanloader下载之前,也就是我们可以在Request执行下载之前对其进行修改。
□ 在下载后生成的Response发送给 Spider之前,也就是我们可以在生成Resposne被 Spider解析之前对其进行修改。
Downloader Middleware的功能十分强大,修改User-Agent、 处理重定向、设置代理、失败重试、设置Cookies等功能都需要借助它来实现。下面我们来了解一下Downloader Middleware的详细用法。

使用说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
需要说明的是, Scrapy其实已经提供了许多Downloader Middleware, 比如负责失败重试、自动重定向等功能的Middleware,它们被DOWNLOADER_MIDDLEWARES_BASE变量所定义。

DOWNLOADER_MIDDLEWARES_BASE 变量的内容如下所示:
{
'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,
'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,
'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,
'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
1scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,
'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,
'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,
'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,
'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,
'scrapy.downloadermiddlewares.htt匹ache.HttpCacheMiddleware': 900,
}

这是一个字典格式,字典的键名是Scrapy内置的Downloader Middleware的名称,键值代表了调用的优先级,优先级是一个数字,(数字越小代表越靠近Scrapy引擎),数字越大代表越靠近Downloader,数字小的Downloader Middleware会被优先调用。
如果自己定义的Downloader Middleware要添加到项目里, DOWNLOADE R_MIDDLEWARES_BASE变量不能直接修改。 Scrapy提供了另外一个设置变量DOWNLOADER_MIDDLEWARES,我们直接修改这个变量就可以添加自己定义的Downloader Middleware, 以及禁用DOWN LOADER_MIDDLEWARES_BASE里面定义的Downloader Middleware

下面我们具体来看看Downloader Middleware的使用方法。
1
2
3
4
5
6
7
8
DOWNLOADER_MIDDLEWARE_BASE和DOWNLOADER_MIDDLEWARE列表里的会最终一起排序

如果想禁用内置的在DOWNLOADER_MIDDLEWARE_BASE里且启用的middleware,那么需要在DOWNLOADER_MIDDLEWARE设置它的值为None(You should never modify this setting in your project, modify DOWNLOADER_MIDDLEWARES instead.)


(DOWNLOADER_MIDDLEWARE_BASE默认值:https://docs.scrapy.org/en/latest/topics/settings.html#std-setting-DOWNLOADER_MIDDLEWARES_BASE)

Finally, keep in mind that some middlewares may need to be enabled through a particular setting. See each middleware documentation for more info.

核心方法

1
2
3
4
5
6
7
8
9
  Scrapy内置的Downloader Middleware为Scrapy提供了基础的功能,但在项目实战中我们往往需要单独定义Downloader Middleware
不用担心,这个过程非常简单,我们只需要实现某几个方法即可。
每个Downloader Middleware都定义了一个或多个方法的类,核心的方法有如下三个。
□ process_request(request, spider)
□ process_response(request, response, spider)
□ process_exception(request, exception, spider)

我们只需要实现至少一个方法,就可以定义一个Downloader Middleware
下面我们来看看这三个方法的详细用法。

process_request(request, spider)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Request被 Scrapy引擎调度给Downloader之前,process_request()方法就会被调用,也就是在Request从队列里调度出来到Downloader下载执行之前 ,我们都可以用 process_request()方法对Request进行处理。方法的返回值必须为None,Response对象,Request对象之一,或者抛出IgnoreRequest
异常。

process_request()方法的参数有如下两个。
□ request,是 Request对象,即被处理的 Request。
□ spider,是 Spdier对象,即此 Request对应的Spider

返回类型不同,产生的效果也不同。下面归纳一下不同的返回情况。
□ 当返回是None时,Scrapy将继续处理该Request, 接着执行其他 Downloader Middleware的process_request()方法,一直到 Downloader把 Request执行后得到Response才结束。这个过程其实就是修改Request的过程,不同的Downloader Middleware按照设置的优先级顺序依次
对Request进行修改,最后送至Downloader执行。

□ 当返回为 Response对象时,更低优先级的Downloader Middleware的 process_request()和process_exception()方法就不会被继续调用,每个 Downloader Middleware的process_response()方法转而被依次调用。调用完毕之后,直接将 Response对象发送给Spider来处理。

□ 当返回为Request对象时,更低优先级的Downloader Middleware的 process_request()方法会停止执行。这个Request会重新放到调度队列里,其实它就是一个全新的Request,等待被调度。如果被Scheduler调度了,那么所有的 Downloader Middleware 的 process_request()方法会被重新按照顺序执行。

□ 如果IgnoreRequest异常抛出,则所有的Downloader Middleware的 process_exception()方法会依次执行。如果没有一个方法处理这个异常,那 么 Request的 errorback()方法就会回调。如果该异常还没有被处理,那么它便会被忽略。

process_response(request, response, spider)

1
2
3
4
5
6
7
8
9
10
11
Downloader执行Request下载之后,会得到对应的Response。Scrapy引擎便会将Response发送给Spider进行解析。在发送之前,我们都可以用process_response()方法来对Response进行处理。方法的返回值必须为Request对象、Response对象之一,或者抛出IgnoreRequest异常。

process_response()方法的参数有如下三个。
□ request,是 Request对象,即此Response对应的 Request
□ response,是 Response对象,即此被处理的Response
□ spider,是Spider对象,即此Response对应的 Spider

下面归纳一下不同的返回情况。
□ 当返回为Request对象时,更低优先级的Downloader Middleware的 process_response()方法不会继续调用。该Request对象会重新放到调度队列里等待被调度,它相当于一个全新的Request。然后,该Request会被process_request()方法顺次处理。
□ 当返回为Response对象时,更低优先级的Downloader Middleware的 process_response()方法会继续调用, 继续对该Response对象进行处理。
□如果IgnoreRequest异常抛出,则Request的errorback()方法会回调。如果该异常还没有被处理 ,那么它便会被忽略。

process_exception(request, exception, spider)

参考案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
当Downloader或process_request()方法抛出异常时,例如抛出 IgnoreRequest异常,process_exception()方法就会被调用。方法的返回值必须为None、Response对象、Request对象之一。

process_exception()方法的参数有如下三个。
□ request,是Request对象,即产生异常的Request
□ exception,是Exception对象,即抛出的异常。
□ spdier,是Spider对象,即Request对应的Spider。

下面归纳一下不同的返回值。
□ 当返回为None时,更低优先级的Downloader Middleware的 process_exception()会被继续顺次调用,直到所有的方法都被调度完毕。
□ 当返回为 Response对象时,更低优先级的Downloader Middleware的process_exception()方法不再被继续调用,每个Downloader Middleware的 process_response()方法转而被依次调用。
□ 当返回为Request对象时,更低优先级的Downloader Middleware的process_exception()也不再被继续调用,该Request对象会重新放到调度队列里面等待被调度,它相当于一个全新的Request。然后,该 Request又会被process_request()方法顺次处理。

以上内容便是这三个方法的详细使用逻辑。在使用它们之前,请先对这三个方法的返回值的处理情况有一个清晰的认识。在自定义Downloader Middleware的时候,也一定要注意每个方法的返回类型。

下面我们用一个案例实战来加深一下对Downloader Middleware用法的理解

项目实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
新建一个项目,命令如下所示:
scrapy startproject scrapydownloadertest
新建了一个Scrapy项目,名为scrapydownloadertest 进入项目,新建一个

Spider,命令如下所示:
scrapy genspider httpbin httpbin.org
新建了一个Spider,名为httpbin ,源代码如下所示:
import scrapy
class HttpbinSpider(scrapy.Spider):
name = 'httpbin'
allowed_domains = ['httpbin.org']
start_urls = ['http://httpbin.org/']
def parse(self, response):
pass
1
2
3
4
5
6
7
8
9
10
11
接下来我们修改 start_urls为 :[http://httpbin.org/get]
随后将 parse()方法添加一行日志输出,将 response变量的text属性输出出来,这样我们便可以看到Scrapy发送的Request信息了。

修改Spider内容如下所示:
import scrapy
class HttpbinSpider(scrapy.Spider):
name = 'httpbin'
allowed_domains = ['httpbin.org']
start_urls = ['http://httpbin.org/get']
def parse(self, response):
self.logger.debug(response.text)

(使用process_request)设置USER_AGENT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接下来运行此Spider,执行如下命令:
scrapy crawl httpbin
Scrapy运行结果包含Scrapy发送的Request信息, 内容如下所示:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*; q=0.8",
"Accept-Encoding": "gzip,deflate,br",
"Accept-Language": "en",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "Scrapy/1.4.0 (+http://scrapy.org)"
},
"origin": "60.207.237.85",
"url": "http://httpbin.org/get"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我们观察一下Headers, Scrapy发送的Request使用的User-Agent是(Scrapy/lda+httpW/scrapy.org),

这其实是由Scrapy内置的UserAgentMiddleware设置的, UserAgentMiddleware的源码如下所示:
from scrapy import signals
class UserAgentMiddleware(object):
def _init_(self, user_agent='Scrapy'):
self.user_agent = user_agent
@classmethod
def from_crawler(cls, crawler):
o = cls(crawler.settings['USER_AGENT'])
crawler.signals.connect(o.spider_opened, signal=signals.spider_opened)
return o
def spider_opened(self, spider):
self.user_agent = getatti(spider,'user_agent', self.user_agent)
def process_request(self, request, spider):
if self.user_agent:
request.headers.setdefault(b'User-Agent', self.user_agent)
1
2
3
在from_crawler()方法中,首先尝试获取settings里面USER_AGENT,然后把USER_AGENT传递给_init_()方法进行初始化,其参数就是user_agent。如果没有传递USER_AGENT参数就默认设置为Scrapy字符串。我们新建的项目没有设置USER_AGENT,所 这里的user_agent变量就是Scrapy接下来,在 process_request()方法中,将user-agent变量设置为headers变量的一个属性,这样就成功设置了User-Agent 因此,User-Agent 就是通过此 Downloader Middleware 的 process_request()方法设置的。

修改请求时的User-Agent可以有两种方式:一是修改settings里面的 USER_AGENT变量;二是通过Downloader Middleware的process_request()方法来修改。
1
2
3
4
5
第一种方法非常简单,我们只需要在setting.py里面加一行USER_AGENT的定义即可:
USER_AGENT = 'Moz订la/5.0 (Macintosh; Intel Mac OS X 1O_12_6) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/59.0.3071.115 Safari/537.36'

一般推荐使用此方法来设置。但是如果想设置得更灵活,比如设置随机的User-Agent,那就需要借助Downloader Middleware了。所以接下来我们用 Downloader Middleware实现一个随机 User-Agent的设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
在 middlewares.py 里面添加一个RandomUserAgentMiddleware 的类,如下所示:
import random
class RandomUserAgentMiddleware():
def _ init_ (self):
self.user_agents = [
'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)',
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0
Safari/537.2','Mozilla/5.0 (Xll; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1'
]
def process_request(self, request, spider):
request.headers['User-Agent'] = random.choice(self.user_agents)

我们首先在类的 _init_方法中定义了三个不同的 User-Agent,并用一个列表来表示。接下来实现了process_request()方法,它有一个参数request,我们直接修改request的属性即可。在这里我们直接设置了request变量的 headers属性的User-Agent,设置内容是随机选择的User-Agent,这样一个 Downloader Middleware就写好了。
1
2
3
4
不过,要使之生效我们还需要再去调用这个Downloader Middleware 在 settings.py中,将DOWNLOADER_MIDDLEWARES取消注释,并设置成如下内容:
DOWNLOADER_MIDDLEWARES = {
'scrapydownloadertest.middlewares.RandomUserAgentMiddleware': 543,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接下来我们重新运行Spider,就可以看到User-Agent被成功修改为列表中所定义的随机的一个User-Agent了 :
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip,deflate,br",
"Accept-Language": "en",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9 0 en-US)"
},
"origin": "60.207.237.85",
"url": "http://httpbin.org/get"
}

我们就通过实现Downloader Middleware并利用pmcess_request()方法成功设置了随机的User-Agent。

使用process_response()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  另外,Downloader Middleware还有process_response()方法。 Downloader对Request执行下载之后会得到Response,随后Scrapy引擎会将Response发送回Spider进行处理。但是在Response被发送给Spider之前,我们同样可以使用process_response()方法对Response进行处理。
比如这里修改一下Response 的状态码,在 RandomUserAgentMiddleware 添加如下代码:
def process_response(self, request, response, spider):
response.status = 201
return response
我们将response变量的status属性修改为201 ,随后将 response返回 ,这个被修改后的Response就会被发送到Spider

我们再在Spider里面输出修改后的状态码,在 parse()方法中添加如下的输出语句:
self.logger.debug('Status Code: ' + str(response.status))

重新运行之后,控制台输出了如下内容:
[httpbin] DEBUG: Status Code: 201

可以发现, Response的状态码成功修改了。

process_exception

1
2
因此要想对Response进行后处理,就可以借助于process_response()方法。
另外还有一个process_exception()方法,它是用来处理异常的方法。如果需要异常处理的话,我们可以调用此方法。不过这个方法的使用频率相对低一些,在此不用实例演示。
1
本节讲解了Downloader Middleware的基本用法。此组件非常重要,是做异常处理和反爬处理的核心。后面我们会在实战中应用此组件来处理代理、 Cookies等内容。

没有伞的孩子,必须努力奔跑!

Typewriter Mode** 已开启。

可以在视图菜单中关闭

不再显示关闭

本文标题:崔庆才python3爬虫-13章 Scrapy框架的使用-Spider的用法

文章作者:TTYONG

发布时间:2020年03月22日 - 17:03

最后更新:2022年03月28日 - 17:03

原始链接:http://tianyong.fun/%E5%B4%94%E5%BA%86%E6%89%8Dpython3%E7%88%AC%E8%99%AB-13%E7%AB%A0(13.4%2013.5)%20%20Scrapy%E6%A1%86%E6%9E%B6%E7%9A%84%E4%BD%BF%E7%94%A8-%20Spider%E7%9A%84%E7%94%A8%E6%B3%95%E5%92%8CDownloader%20Middleware%20%E7%9A%84%E7%94%A8%E6%B3%95.html

许可协议: 转载请保留原文链接及作者。

多少都是爱
0%