本文为译文,原文见地址:https://docs.scrapy.org/en/latest/topics/selectors.html#module-scrapy.selector
选择器
当你爬取网页时,通常需要你去执行的任务是从HTML源中提取数据。这里有一些可用的库能够帮你实现这个操作:
- BeautifulSoup是一个在Python程序员中非常流行的web爬取库,它根据HTML代码的结构构造出Python对象,并且合理处理不好的标记,但是它有一个缺点:速度慢。
- lxml是一个基于ElementTree的Python API接口的XML解析库(也可以解析HTML)。lxml目前不是Python标准库的一部分。
Scrapy本身自带一套提取数据的机制,这套机制被称作选择器(Selector),因为这套机制“选择”由XPath或CSS表达式指定的HTML文档的某些部分。
在XML文档中,XPath是一个用于选择节点的语言,同时也能运用于HTML。CSS是一种用于向HTML文档应用样式的语言。它定义了选择器,将这些样式与特定的HTML元素相关联。
Scrapy选择器都构建在了lxml库中,这意味着它们在速度和解析精度方面都非常相似。
这个页面解释了选择器的工作方式,并描述了它们的API,这个API非常小而且简单,不像lxml API那么大,因为lxml库可以用于选择标记文档之外的许多其他任务。
有关选择器API的完整引用,请参见选择器引用。
使用选择器
构造选择器
Scrapy选择器是Selector类对象,它可以传递文本或者TextResponse对象来进行构造。Selector类对象会根据输入参数的类型,自动选择最佳解析规则(XML vs HTML):
>>> from scrapy.selector import Selector
>>> from scrapy.http import HtmlResponse
从文本构造:
>>> body = '<html><body><span>good</span></body></html>'
>>> Selector(text=body).xpath('//span/text()').extract()
[u'good']
为了方便,响应对象在.selector属性上公开了一个Selector对象,如果可能的话,使用这个快捷方式是完全可以的:
>>> response.selector.xpath('//span/text()').extract()
[u'good']
使用选择器
为了解释如何使用选择器,我们将使用Scrapy shell(提供了交互式测试),以及一个Scrapy文档的示例页面:https://doc.scrapy.org/en/latest/_static/selectors-sample1.html
HTML代码如下:
<html>
<head>
<base href='http://example.com/' />
<title>Example website</title>
</head>
<body>
<div id='images'>
<a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
<a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
<a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
<a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
<a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
</div>
</body>
</html>
首先,我们将打开shell:
scrapy shell https://doc.scrapy.org/en/latest/_static/selectors-sample1.html
然后,在shell加载完成之后,你可以访问shell变量respnose作为一个可用的响应对象,并且这个变量的附加选择器存储在response.selector属性中。
由于我们正在处理HTML,因此选择器将自动使用HTML解析器。
所以,让我们看看这里的HTML代码,我们先构造一个XPath,用于选择title标签里面的文本:
>>> response.selector.xpath('//title/text()')
[<Selector xpath='//title/text()' data='Example website'>]
使用XPath和CSS来查询响应是很通常的方式,因此响应对象包括两个快捷方式:response.xpath()和response.css():
>>> response.xpath('//title/text()')
[<Selector xpath='//title/text()' data='Example website'>]
>>> response.css('title::text')
[<Selector xpath='descendant-or-self::title/text()' data='Example website'>]
正如你所看到的,.xpath()和.css()函数返回了一个SelectorList实例,表示一组新的选择器。这个API可以被用来快速选择嵌套的数据:
>>> response.css('img').xpath('@src').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
为了提取实际的文本数据,你必须调用选择器的.extract()函数,如下所示:
>>> response.xpath('//title/text()').extract()
['Example website']
如果你只希望提取出第一个匹配的元素,你可以调用选择器的.extract_first()函数:
>>> response.xpath('//div[@id="images"]/a/text()').extract_first()
'Name: My image 1 '
如果没有找到匹配的元素则返回None:
>>> response.xpath('//div[@id="not-exists"]/text()').extract_first()
# Python3.7这里没有输出
可以提供一个默认返回值,作为没有找到匹配元素时候的输出:
>>> response.xpath('//div[@id="not-exists"]/text()').extract_first(default='not-found')
'not-found'
注意CSS选择器可以使用CSS3伪元素选择文本或者属性节点:
>>> response.css('title::text').extract()
['Example website']
下面我们将获取base的URL,以及一些image的链接:
>>> response.xpath('//base/@href').extract()
['http://example.com/']
>>> response.css('base::attr(href)').extract()
['http://example.com/']
>>> response.xpath('//a[contains(@href, "image")]/@href').extract()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
>>> response.css('a[href*=image]::attr(href)').extract()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
>>> response.xpath('//a[contains(@href, "image")]/img/@src').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
>>> response.css('a[href*=image] img::attr(src)').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
嵌套选择器
上述的选择函数(.xpath()或者.css())返回的是一组相同类型的选择器,因此你也可以调用这些选择器的选择函数。示例如下:
>>> links = response.xpath('//a[contains(@href, "image")]')
>>> links.extract()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>', '<a href="image2.html">Name: My image 2 <br><im
g src="image2_thumb.jpg"></a>', '<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg"></a>', '<a href="image4
.html">Name: My image 4 <br><img src="image4_thumb.jpg"></a>', '<a href="image5.html">Name: My image 5 <br><img src="image5_thu
mb.jpg"></a>']
>>> for index, link in enumerate(links):
... args = (index, link.xpath('@href').extract(), link.xpath('img/@src').extract())
... print('Link number %d points to url %s and image %s' % args)
...
Link number 0 points to url ['image1.html'] and image ['image1_thumb.jpg']
Link number 1 points to url ['image2.html'] and image ['image2_thumb.jpg']
Link number 2 points to url ['image3.html'] and image ['image3_thumb.jpg']
Link number 3 points to url ['image4.html'] and image ['image4_thumb.jpg']
Link number 4 points to url ['image5.html'] and image ['image5_thumb.jpg']
使用带有正则表达式的选择器
Selector也含有.re()函数,可以使用正则表达式提取数据。然而,比使用.xpath()和.css()函数不同的是,.re()函数返回了一组unicode字符串,因此你不能构建嵌套的.re()调用。
这里有一个提取image名称的示例:
>>> response.xpath('//a[contains(@href, "image")]/text()').extract()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']
>>> response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')
['My image 1 ', 'My image 2 ', 'My image 3 ', 'My image 4 ', 'My image 5 ']
对于.re()函数来说,还有一个与.extract_first()一样的辅助方法,叫做.re_first()。使用这个方法将提取第一个匹配的字符串:
>>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)')
'My image 1 '
使用相对的XPath
请记住,如果你正在使用嵌套选择器,并且使用一个以/开头的XPath,那么XPath相对于文档是绝对的,而不是相对于你正在调用的Selector。
举个栗子,假设你希望提取在<div>元素内的所有<p>元素。首先,你应该获取所有<div>元素:
>>> divs = response.xpath('//div')
首先,你可能试图使用下面的方法,这是错误的,因为它实际上从文档中提取了所有p元素,而不仅仅是从div元素中提取:
>>> for p in divs.xpath('//p'): # 这是错误的 - 试图从文档中获取所有的<p>元素
... print(p.extract())
这是正确的做法(注意最前面的点.//p):
>>> for p in divs.xpath('.//p'): # 提取所有内部的<p>元素
... print(p.extract())
另一种常见的情况是提取所有直接的<p>子元素:
>>> for p in divs.xpath('p'):
... print(p.extract())
有关相对XPath的详细信息,请参阅XPath规范中的Location Paths部分。
XPath表达式中的变量
XPath允许在XPath表达式中引用变量,使用@somevariable语法。这有点类似于SQL世界中的参数化查询或准备语句,在SQL世界中,你将查询中的一些参数替换为占位符,比如?,然后用查询传递的值替换这些占位符。
下面是一个根据元素的“id”属性值匹配元素的例子,无需对其进行硬编码(如前所述):
>>> # 表达式中使用'$val','val'参数需要被解析
>>> response.xpath('//div[@id=$val]/a/text()', val='images').extract_first()
'Name: My image 1 '
这里有另一个示例,为了找到<div>标签“id”属性,且这个标签包含五个<a>子元素(这里我们传递整数5):
>>> response.xpath('//div[count(a)=$cnt]/@id', cnt=5).extract_first()
'images'
当调用.xpath()时,所有参数引用必须有一个对应的绑定值(否则你将得到一个ValueError: XPath error: 异常)。这是通过传递尽可能多的命名参数来完成的。
支持Scrapy选择器的库parsel,提供了关于XPath变量的更多细节和示例。
使用EXSLT扩展
Scrapy选择器还支持一些建立在lxml之上的EXSLT扩展,并附带这些预先注册的命名空间用于XPath表达式:
| prefix | namespace | usage |
|---|---|---|
| re | http://exslt.org/regular-expressions | 正则表达式 |
| set | http://exslt.org/sets | 集合处理 |
正则表达式
举个栗子,在XPath的starts-with()或者contains()功能不满足我们的需求时,test()函数是非常有用的。
示例中,在列表项选择以数字结尾的“class”属性链接:
>>> from scrapy import Selector
>>> doc = """
... <div>
... <ul>
... <li class="item-0"><a href="link1.html">first item</a></li>
... <li class="item-1"><a href="link2.html">second item</a></li>
... <li class="item-inactive"><a href="link3.html">third item</a></li>
... <li class="item-1"><a href="link4.html">fourth item</a></li>
... <li class="item-0"><a href="link5.html">fifth item</a></li>
... </ul>
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> sel.xpath('//li//@href').extract()
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').extract()
['link1.html', 'link2.html', 'link4.html', 'link5.html']
警告:C库的libxslt本身不支持EXSLT正则表达式,因此lxml的实现使用钩子来连接Python的re模块。因此,在XPath表达式中使用regexp函数可能会带来很小的性能损失。
集合运算
例如,在提取文本元素之前,可以方便地排除文档树的某些部分。
下面示例中,提取了微数据(样本内容取自http://scehma.org/Product),包含一组itemscopes和相应的itemprops:
>>> doc = """
... <div itemscope itemtype="http://schema.org/Product">
... <span itemprop="name">Kenmore White 17" Microwave</span>
... <img src="kenmore-microwave-17in.jpg" alt='Kenmore 17" Microwave' />
... <div itemprop="aggregateRating"
... itemscope itemtype="http://schema.org/AggregateRating">
... Rated <span itemprop="ratingValue">3.5</span>/5
... based on <span itemprop="reviewCount">11</span> customer reviews
... </div>
...
... <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
... <span itemprop="price">$55.00</span>
... <link itemprop="availability" href="http://schema.org/InStock" />In stock
... </div>
...
... Product description:
... <span itemprop="description">0.7 cubic feet countertop microwave.
... Has six preset cooking categories and convenience features like
... Add-A-Minute and Child Lock.</span>
...
... Customer reviews:
...
... <div itemprop="review" itemscope itemtype="http://schema.org/Review">
... <span itemprop="name">Not a happy camper</span> -
... by <span itemprop="author">Ellie</span>,
... <meta itemprop="datePublished" content="2011-04-01">April 1, 2011
... <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
... <meta itemprop="worstRating" content = "1">
... <span itemprop="ratingValue">1</span>/
... <span itemprop="bestRating">5</span>stars
... </div>
... <span itemprop="description">The lamp burned out and now I have to replace
... it. </span>
... </div>
...
... <div itemprop="review" itemscope itemtype="http://schema.org/Review">
... <span itemprop="name">Value purchase</span> -
... by <span itemprop="author">Lucas</span>,
... <meta itemprop="datePublished" content="2011-03-25">March 25, 2011
... <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
... <meta itemprop="worstRating" content = "1"/>
... <span itemprop="ratingValue">4</span>/
... <span itemprop="bestRating">5</span>stars
... </div>
... <span itemprop="description">Great microwave for the price. It is small and
... fits in my apartment.</span>
... </div>
... ...
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> for scope in sel.xpath('//div[@itemscope]'):
... print('current scope: %s' % scope.xpath('@itemtype').extract())
... props = scope.xpath('set:difference(./descendant::*/@itemprop, .//*[@itemscope]/*/@itemprop)')
... print(' properties: %s' % props.extract())
... print()
...
current scope: ['http://schema.org/Product']
properties: ['name', 'aggregateRating', 'offers', 'description', 'review', 'review']
current scope: ['http://schema.org/AggregateRating']
properties: ['ratingValue', 'reviewCount']
current scope: ['http://schema.org/Offer']
properties: ['price', 'availability']
current scope: ['http://schema.org/Review']
properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']
current scope: ['http://schema.org/Rating']
properties: ['worstRating', 'ratingValue', 'bestRating']
current scope: ['http://schema.org/Review']
properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']
current scope: ['http://schema.org/Rating']
properties: ['worstRating', 'ratingValue', 'bestRating']
>>>
这里我们首先迭代itemscope元素,对于每个元素,我们查找所有的itemprops元素,并排除位于另一个itemscope内的它们本身。
一些XPath的小技巧
在使用带有Scrapy选择器的XPath时,我们根据ScrapingHub博客上的这篇文章可以找到一些有用的技巧。如果你对于XPath还不太熟悉,那么你可能需要先看一下这个XPath教程。
在一个条件中使用text节点
当你需要在XPath字符串函数中使用文本内容作为参数时,应该避免使用.//text(),而使用.代替。
这是因为表达式.//text()生成一个文本元素集合——一个节点集合。当节点集合被转换为字符串时(当它作为参数传递给contains()或者start-with()之类的字符串函数时发生这种情况),它只生成第一个元素的文本。
示例:
>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')
将节点集合转换为字符串:
>>> sel.xpath('//a//text()').extract() # peek节点集合的顶层元素
['Click here to go to the ', 'Next Page']
>>> sel.xpath("string(//a[1]//text())").extract() # 转换为字符串
['Click here to go to the ']
转换为字符串的节点将其本身的文本及其所有子节点的文本放在一起:
>>> sel.xpath('//a[1]').extract() # 选择第一个节点
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath('string(//a[1])').extract() # 转换为字符串
['Click here to go to the Next Page']
在这个场景中,使用.//text()节点集不会选择出任何内容:
>>> sel.xpath("//a[contains(.//text(), 'Next Page')]").extract()
[]
但是使用.意味着当前这个节点,这将会正常工作:
>>> sel.xpath("//a[contains(., 'Next Page')]").extract()
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
注意//node[1]和(//node)[1]的区别
//node[1]选择首先出现在各自父节点下的所有节点。
(//node)[1]选择文档中的所有节点,然后只获取第一个节点。
示例:
>>> from scrapy import Selector
>>> sel = Selector(text="""
....: <ul class="list">
....: <li>1</li>
....: <li>2</li>
....: <li>3</li>
....: </ul>
....: <ul class="list">
....: <li>4</li>
....: <li>5</li>
....: <li>6</li>
....: </ul>""")
>>> xp = lambda x: sel.xpath(x).extract()
获取所有第一个<li>元素,不论这个元素的父节点是什么:
>>> xp("//li[1]")
['<li>1</li>', '<li>4</li>']
获取整个文档的第一个<li>元素:
>>> xp("(//li)[1]")
['<li>1</li>']
获取父节点<ul>下的所有第一个<li>元素:
>>> xp("//ul/li[1]")
['<li>1</li>', '<li>4</li>']
获取整个文档中,父节点<ul>下的第一个<li>元素:
>>> xp("(//ul/li)[1]")
['<li>1</li>']
通过class查询时,考虑使用CSS
因为一个元素不能包含多个CSS类,使用XPath方式通过CSS类去选择元素比较冗长:
*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]
如果你使用@class=‘someclass’,那么你可能会丢失具有其他类的元素,如果你只是使用contains(@class, ‘someclass’)来弥补这一点,那么你可能会得到更多你想要的元素,如果它们有一个不同的类名来共享字符串someclass。
事实证明,Scrapy选择器允许链接选择器,因此大多数情况下,你只需要使用CSS通过类进行选择,然后在需要时切换到XPath:
>>> from scrapy import Selector
>>> sel = Selector(text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special date</time></div>')
>>> sel.css('.shout').xpath('./time/@datetime').extract()
['2014-07-23 19:00']
这比使用上面所示的冗长的XPath技巧更简洁。只要记住在接下来的XPath表达式中使用.。
内置选择器引用
Selector对象
class scrapy.selector.Selector(response=None, text=None, type=None)
一个被响应包裹的Selector实例,用来选择响应的特定内容。
response是一个HtmlResponse或者一个XmlResponse对象,这个对象可以用来选择和提取数据。
text是一个unicode字符串或者utf-8编码的文本,当response无效时可以使用这个字段来赋值需要解析的数据源。同时使用text和response是一种未定义的行为。
type定义了选择器的类型,可以使"html","xml"或者None(使用默认值)。
如果type是None,那么选择器将基于response的类型自动选择最合适的类型,或者在没有response而使用text时,type默认为"html"。
如果type是None并且传递了一个response对象,选择器的类型变化如下:
- response为HtmlResponse类型时,type为"html"
- response为XmlResponse类型时,type为"xml"
- response为其他类型时,type为"html"
另外,如果type被设置了值,选择器的类型将会被强制设置,而不会自动检测。
xpath(query)
找到与xpath中query匹配的节点,并且以SelectorList实例的形式返回结果,其中所有元素都是扁平的。列表元素也可以执行Selector实例的接口。
query是一个包含了要应用的XPATH查询的字符串。
注意:为了方便,这个函数可以这样调用:response.xpath()
css(query)
应用给定的CSS选择器,并返回SelectorList实例。
query是一个包含了要应用的CSS选择器的字符串。
在后台代码中,CSS查询会通过cssselect库转换为XPath查询,并运行.xpath()函数。
注意:为了方便,这个函数可以这样调用:response.css()
extract()
序列化并将匹配的节点作为一组unicode字符串返回。编码内容的百分号并未被引用。
re(regex)
应用给定的正则表达式,并返回带有匹配项的unicode字符串列表。
regex可以是一个编译了的正则表达式,也可以是一个能够被re.compile(regex)编译成正则表达式的字符串。
注意:re()和re_first()都能解码HTML的实体类(除了<和&)。
register_namespace(prefix, uri)
注册在Selector中使用的给定命名空间。如果不注册命名空间,则不能从非标准命名空间中选择或提取数据。详情见下面的示例。
remove_namespaces()
移除所有的命名空间,允许使用无命名空间的xpath遍历文档。详情见下面的示例。
nonzero()
如果选择了任何真实内容则返回True,否则返回False。换句话说就是,Selector的布尔值是由它选择的内容给出的。
SelectorList对象
SelectorList类是内置的list类的一个子类,SelectorList类可以提供一些额外的函数。
xpath(query)
为这个列表中的所有元素调用.xpath()函数,并且以SelectorList的形式返回它们的扁平化结果。
query参数与Selector.xpath()的参数一致。
css(query)
为这个列表中的所有元素调用.css()函数,并且以SelectorList的形式返回它们的扁平化结果。
query参数与Selector.css()的参数一致。
extract()
为这个列表的所有元素调用.extract()函数,并且以unicode字符串列表的形式返回它们的扁平化结果。
re()
为这个列表的所有元素调用.re()函数,并且以unicode字符串列表的形式返回它们的扁平化结果。
HTML响应的Selector示例
这里有几个Selector示例来说明几个概念。在所有情况下,我们假设已经有一个接收了HtmlResponse对象作为参数的Selector实例:
sel = Selector(html_response)
- 从HTML响应的body中选择所有的<h1>元素,返回一个Selector对象的列表(比如一个SelectorList对象):
sel.xpath('//h1')
- 从HTML响应的body中提取所有<h1>元素的文本,返回一个unicode字符串的列表:
sel.xpath('//h1').extract() # 这里包括了h1标签
sel.xpath('//h1/text()').extract() # 这里不包括h1标签
- 遍历所有的<p>标签,并打印它们的class属性:
for node in sel.xpath('//p'):
print(node.xpath('@class').extract())
XML响应的Selector示例
这里有几个示例来说明几个概念。在所有情况下,我们假设已经有一个接收了HtmlResponse对象作为参数的Selector实例:
sel = Selector(xml_response)
- 从XML响应的body中选择所有的<product>元素,返回一个Selector对象的列表(比如一个SelectorList对象):
sel.xpath('//product')
- 从Google Base XML feed提取所有价格,这里是需要注册命名空间的:
sel.register_namespace('g', 'http://base.google.com/ns/1.0')
sel.xpath('//g:price').extract()
移除命名空间
当处理爬取项目时,为了方便我们常常移除命名空间,然后只需要在元素名称上工作即可,这样可以编写更简单/方便的XPath。你可以使用Selector.remove_namespaces()函数来实现这个功能。下面这个示例演示了这一点。
首先,我们打开shell,并携带我们想要爬取的url:
$ scrapy shell https://github.com/blog.atom
一旦我们尝试选择所有的<link>对象,我们可以发现其并没有工作(因为Atom XML的命名空间混淆了这些节点):
>>> reponse.xpath('//link')
[]
但是一旦我们调用了Selector.remove_namespaces()函数,所有节点都能通过它们的名称直接被访问:
>>> response.selector.remove_namespaces()
>>> response.xpath("//link")
[<Selector xpath='//link' data=u'<link xmlns="http://www.w3.org/2005/Atom'>,
<Selector xpath='//link' data=u'<link xmlns="http://www.w3.org/2005/Atom'>,
...
如果你想知道为什么默认情况下不是自动调用命名空间移除过程,而我们必须手动调用它,这是因为两个原因,按照相关性的顺序,它们是:
- 删除命名空间需要迭代和修改文档中的所有节点,这对于由Scrapy爬行的所有文档来说是一个相当昂贵的操作
- 在某些情况下,实际需要使用命名空间,以防命名空间之间的某些元素名称发生冲突。但这种情况非常罕见。
本文详细介绍了Scrapy选择器的使用,包括构造选择器、XPath和CSS选择器的运用、嵌套选择器、正则表达式选择、相对XPath、XPath中的变量以及一些实用技巧。Scrapy选择器基于lxml库,提供了高效且精确的数据提取能力。文章通过实例展示了如何在Scrapy shell中使用选择器提取HTML和XML文档中的数据。
——选择器(Selector)&spm=1001.2101.3001.5002&articleId=85778457&d=1&t=3&u=ea28de5f6a8b41fd94737b3f108c20df)
1万+

被折叠的 条评论
为什么被折叠?



