urllib

About

urllib是Python内置的HTTP请求库。urllib 模块提供的上层接口,使访问 www 和 ftp 上的数据就像访问本地文件一样,并且它也是requests的底层库。

其中包括4个主要模块:

  • urllib.request:请求模块。
  • urllib.error:异常处理模块。
  • urllib.parse:URL解析模块。
  • urllib.robotparser:robots.txt解析模块。

这里以Python3.6为例。

urllib、urllib2、urllib3

在Python2.x中,分为urllib和urllib2,简单来说,urllib2是urllib的增强版,但urllib中的函数又比urllib2多一些,对于简单的下载之类的,urllib绰绰有余,如果涉及到实现HTTP身份验证或者cookie或者扩展编写自定义协议,urllib2更好一些。

  • urllib支持设置编码的函数urllib.urlencode,在模拟登陆的时候经常需要传递经过post编码之后的参数,如果不想使用第三方库完成模拟登录,就必须使用到标准库中的urllib。urllib提供一些比较原始基础的方法而urllib2并没有,比如urllib中的urlencode方法用来GET查询字符串的产生。

  • urllib2比较有优势的地方在于urllib2.openurl中可以接受一个Request类的实例来设置Request参数,来修改/设置Header头从而达到控制HTTP Request的header部分的目的,也可以修改用户代理,设置cookie等,但urllib仅可以接受URL。这就意味着,如果访问一个网站想更改User Agent(可以伪装你的浏览器),你就需要使用urllib2。
  • urllib2模块没有加入urllib.urlretrieve函数以及urllib.quote等一系列quote和unquote功能,这个时候就需要urllib的辅助。

因此,在Python2.x中,这两个库可以搭配使用。
而urllib3则是增加了连接池等功能,两者互相都有补充的部分。
那么Python3.x中呢,urllib和urllib2都合并到了urllib中了,所以,Python3.x即将一统江湖,还是踏实儿的学习Python3.x下的urllib吧。

再来看看Python2.x和Python3.x的区别。
在Python2.x中,urlopen方法在urllib下:

import urllib2
response = urllib2.urlopen()

在Python3.x中,urlopen方法就移动到了urllib.request下:

import urllib.request
response = urllib.request.urlopen()

urllib.request

请求

先来看urllib怎么发送请求。

发送get请求

import urllib
response = urllib.request.urlopen('http://www.baidu.com')
# 获取bytes类型的数据
# print(response.read())
# 想要获取到字符串类型的数据,需要使用 decode 转码为字符串
print(response.read().decode('utf-8'))

发送post请求

import urllib.request
import urllib.parse
data = bytes(urllib.parse.urlencode({'world': 'hello'}), encoding='utf-8')
response = urllib.request.urlopen(url='http://httpbin.org/post', data=data)
print(response.read().decode('utf-8'))

首先,需要了解一个http://httpbin.org这个网站,HTTPBin是以Python+Flask写的一款工具,它包含了各类的HTTP场景,且每个接口一定都有返回。

data参数需要字节类型的数据,所以,首先用bytes转码,然后将字典使用urllib.parse.urlencode并指定编码方式完成编码。

另外,data参数用来控制请求方式,如果请求中有data则是post请求,否则为get请求。

timeout

import socket
import urllib.request
import urllib.error
try:
    # 没有超时,正常返回json格式的内容
    # response = urllib.request.urlopen(url='http://www.httpbin.org/get', timeout=2)
    # 超时会抛出异常,由异常语句捕获
    response = urllib.request.urlopen(url='http://www.httpbin.org/get', timeout=0.1)
    print(response.read().decode('utf-8'))
except urllib.error.URLError as e:
    # print(e)  # <urlopen error timed out>
    if isinstance(e.reason, socket.timeout):
        print(e.reason)  # timed out

e.reason为错误原因。timeout单位是秒。

响应

现在来研究urlopen返回对象提供的方法:

  • read(),readline(),readlines(),fileno(), close():这些方法与文件对象一样。
  • info():返回一个httplibHTTPMessage对象,表示远程服务器返回的头信息。
  • getcode():返回HTTP状态码。
  • geturl():返回请求的url。

响应类型

import urllib.request
response = urllib.request.urlopen(url='http://www.httpbin.org/get')
print(response)  # <http.client.HTTPResponse object at 0x07B65270>
print(type(response))  # <class 'http.client.HTTPResponse'>

状态码

import urllib.request
response = urllib.request.urlopen(url='http://www.httpbin.org/get')
print(response.status)  # 200

响应头

import urllib.request
response = urllib.request.urlopen(url='http://www.httpbin.org/get')
print(response.headers)  
print(response.getheaders())  # 列表类型的响应头
print(response.getheader('Server', None))  # 获取特定的响应头

响应体

import urllib.request
response = urllib.request.urlopen(url='http://www.httpbin.org')
# print(response.read())
print(response.read().decode('utf-8'))

之前说过,urllib获取到的内容是字节形式,通过read读出来之后需要使用decode解码。

request对象

上面发送一些简单的请求没问题,但是复杂的请求(比如带headers)就力有不逮了。这个时候,需要学习一个Request来定制复杂的请求参数了。

import urllib.request
request = urllib.request.Request(url='http://www.httpbin.org')
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))

首先,使用urllib.request.Request来构建一个request对象,然后传给urlopen即可。

再来看一个复杂点的带headers。

import urllib.request
import urllib.parse
# 指定url
url='http://www.httpbin.org/post'
# 自定义headers
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"}
# 自定义data参数并编码为bytes类型
data_dict = {"username": "zhangkai"}
data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8')
# 使用urllib.request.Request构造request对象
request = urllib.request.Request(url=url, data=data, headers=headers, method='POST')
# 将request对象交给urlopen发请求去吧
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))

再来看一个add_headers方法。

import urllib.request
import urllib.parse

url='http://www.httpbin.org/post'
# 这里跟上面的示例区别就是不在单独指定headers字典
# headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"}
data_dict = {"username": "zhangkai"}
data = bytes(urllib.parse.urlencode(data_dict), encoding='utf-8')
request = urllib.request.Request(url=url, data=data, method='POST')
# 而是使用构造好的request对象下面的add_header方法添加header,换汤不换药....
request.add_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36")
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))

add_header方法作为添加headers的补充方式,如果有多个header键值对,可以使用for循环一一添加进去.....

会了构造request对象,我们基本能应付大部分的网站了。

再来看看更复杂的一些用法,以应对更多的复杂环境,比如携带cookie什么的。

Cookie

获取cookie

import urllib.request
import http.cookiejar

# 声明 cookiejar 对象
cookie = http.cookiejar.CookieJar()
# 使用handler处理cookie对象
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
# 当opener.open发送请求
response = opener.open('http://www.baidu.com')
# 当response有了结果之后,会自动将cookie信息保存到上面声明的cookie对象中去
print(cookie)

http.cookiejar提供一个永久性的cookie对象。并且cookiejar有3个子类:

  • CookieJar:管理HTTP cookie值、存储HTTP请求生成的cookie、向传出的HTTP请求添加cookie的对象。整个cookie都存储在内存中,对CookieJar实例进行垃圾回收后cookie也将丢失。
  • FileCookieJar (filename,delayload=None,policy=None):从CookieJar派生而来,用来创建FileCookieJar实例,检索cookie信息并将cookie存储到文件中。filename是存储cookie的文件名。delayload为True时支持延迟访问访问文件,即只有在需要时才读取文件或在文件中存储数据。
  • MozillaCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与Mozilla浏览器 cookies.txt兼容的FileCookieJar实例。
  • LWPCookieJar (filename,delayload=None,policy=None):从FileCookieJar派生而来,创建与libwww-perl标准的 Set-Cookie3 文件格式兼容的FileCookieJar实例。

大多数情况下,只用CookieJar(),如果需要和本地文件交互,就用 MozillaCookjar() 或 LWPCookieJar()

补充:libwww-perl集合是一组Perl模块,它为万维网提供简单而一致的应用程序编程接口。该库的主要重点是提供允许您编写WWW客户端的类和函数。

将cookie写入文件

cookie是保持登录会话信息的凭证,那么在获取到cookie后写入本地,在cookie失效前,后续访问再读取本地文件,并在请求中携带,以达到继续保持登录信息的状态。

import urllib.request
import http.cookiejar

'''
写入cookie对象到本地
'''
url = 'http://www.baidu.com'
file_name = 'cookie.txt'
# 这里将cookie信息处理成火狐浏览器cookie格式
cookie = http.cookiejar.MozillaCookieJar(file_name)
# LWPCookieJar格式cookie
# cookie = http.cookiejar.LWPCookieJar(file_name)
# 使用handler处理cookie对象
handler = urllib.request.HTTPCookieProcessor(cookie1)
opener = urllib.request.build_opener(handler)
response1 = opener.open(url)
# 将cookie信息保存到本地文件
cookie1.save(ignore_discard=True, ignore_expires=True)

'''
读取本地cookie信息
'''
# 以什么格式保存,就以什么格式读出来,这里都用MozillaCookieJar
cookie2 = http.cookiejar.MozillaCookieJar()
# 使用load方法从本地读取cookie信息
cookie2.load(file_name, ignore_discard=True, ignore_expires=True)
# 放到handle中
handler = urllib.request.HTTPCookieProcessor(cookie2)
opener = urllib.request.build_opener(handler)
response2 = opener.open(url)
print(response2.read().decode('utf-8'))

saveload中,有两个参数需要说明:

  • ignore_discard:保存cookie,即使设置为丢弃的cookie也保存。
  • ignore_expires:如果cookie已过期也要保存到文件,并且如果文件存在则覆盖。后续可以使用load或者revert方法恢复保存的文件。

代理

import urllib.request
# 调用ProxyHandler 代理ip的形式是字典
# 付费代理
# money_proxy = {"协议":"username:pwd@ip:port"}
proxy_handler = urllib.request.ProxyHandler({'sock5': 'localhost:1080'})
# 使用build_opener构建opener对象
opener = urllib.request.build_opener(proxy_handler)
# 调用opener的open方法向指定url发请求
response = opener.open('http://www.httpbin.org/get')
print(response.read().decode('utf-8'))
'''
{
  "args": {}, 
  "headers": {
    "Accept-Encoding": "identity", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Python-urllib/3.6"
  }, 
  "origin": "124.64.16.181, 124.64.16.181", 
  "url": "https://www.httpbin.org/get"
}
'''

{'sock5': 'localhost:1080'}sock5是协议,localhost:1080是走本地的1080端口。

其实urlopen在内部也是调用了opener.open方法发送请求。

另外,返回结果origin是访问IP,而这个IP正好是我们使用的代理IP。

下载

可以使用urllib下载图片,文件等到本地。

import urllib.request
url = 'http://n.sinaimg.cn/news/1_img/upload/cf3881ab/69/w1000h669/20190912/bed4-iepyyhh6925213.jpg' 
filename = 'a.jpg'
# 使用urlretrieve进行下载操作
urllib.request.urlretrieve(url=url, filename=filename)

除此之外,还可以使用urllib.urlcleanup()来清除urllib.urlretrieve所产生的缓存。

urllib.error

urllib中的异常处理有3个:

  • urllib.error.URLError:捕获关于url的错误,只有一个返回方法reason,错误原因。
  • urllib.error.HTTPError:捕获关于http的错误,有3个返回方法。code,http中定义的http状态码;reason,错误原因;headers,响应头。
  • urllib.error.ContentTooShortError:当urlretrieve检测到下载数据量小于预期时,会引发此异常。

urllib.error.URLError

import urllib.request
import urllib.error

try:
    # 这里将 cnblogs 写成 cnblogsww
    # response = urllib.request.urlopen(url='https://www.cnblogsww.com/Neeo/p/1083589221.html')
    print(response.read().decode('utf-8'))
except urllib.error.ContentTooShortError as e:
    print('ContentTooShortError: ', e.reason)

urllib.error.HTTPError

import urllib.request
import urllib.error

try:
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"}
    # 不存在的 103048922222.html
    response = urllib.request.urlopen(url='https://www.cnblogs.com/Neeo/p/103048922222.html')
    print(response.read())
except urllib.error.HTTPError as e:
    print('HTTPError: ', e.reason, e.headers, e.code)

urllib.error.ContentTooShortError

这个错误比较重要,因为在下载文件时,很可能会因为各种原因导致下载没有完成就失败了。这个时候,就可以使用ContentTooShortError来捕捉错误从新下载。如果重新下载还是下载失败,可以考虑使用递归,下载失败就重新下,直到下载成功。

import urllib.request
import urllib.error

try:
    url = 'http://n.sinaimg.cn/news/1_img/upload/cf3881ab/69/w1000h669/20190912/bed4-iepyyhh6925213.jpg'
    filename = 'a.jpg'
    urllib.request.urlretrieve(url=url, filename=filename)
except urllib.error.ContentTooShortError as e:
    print('ContentTooShortError: ', e.reason)
    # 如果下载失败,就重新下载
    urllib.request.urlretrieve(url=url, filename=filename)
    
    
def get_file(url, filename):
    """ 使用递归进行重新下载,直到下载成功为止 """
    try:
        urllib.request.urlretrieve(url=url, filename=filename)
        print('download done ......')
    except urllib.error.ContentTooShortError:
        get_file(url, filename)
url = "http://n.sinaimg.cn/news/1_img/upload/cf3881ab/69/w1000h669/20190912/bed4-iepyyhh6925213.jpg"
filename = 'a.jpg'
get_file(url, filename)

urllib.parse

urllib.parse定义了URL的标准接口,实现对于URL的各种操作,包括解析、合并、编码、解码,使用之前需导入。

urllib.parse.urlparse

urllib.parse.urlparse以元组的形式返回URL解析后的6个组件。对应的URL结构一般是这样的:

scheme://netloc/path;parameters?query#fragment

每个元组项都是一个字符串,也可能是空的。

返回的元组中:

  • scheme:协议。
  • netloc:域名。
  • path:路径。
  • params:参数。
  • query:查询条件,一般用于个get请求的URL。
  • fragment:锚点。

基本使用:

import urllib.parse
urllib.parse.urlparse(url, scheme='', allow_fragments=True)
  • url:待解析的url
  • scheme:协议,如果URL中没有协议,则使用该参数设置协议,如果有协议,该参数不生效
  • allow_fragments:是否忽略锚点,默认为True,表示表示不忽略,False表示忽略

示例:

import urllib.parse

result = urllib.parse.urlparse('https://www.cnblogs.com/Neeo/p/10864123.html?k1=v1#python')
print(result)

'''
ParseResult(scheme='https', netloc='www.cnblogs.com', path='/Neeo/p/10864123.html', params='', query='k1=v1', fragment='python')
'''

urllib.parse.urlunparse

urlparse相反,urllib.parse.urlunparse用来构造URL。

import urllib.parse

url_components = ['https', 'www.cnblogs.com', '/Neeo/p/10864123.html', '', '', 'python']
result = urllib.parse.urlunparse(url_components)
print(result)  # https://www.cnblogs.com/Neeo/p/10864123.html#python

url_components中,顺序必须遵循:

scheme, netloc, url, params, query, fragment

也就是6个选项都要有值,如果没有也要填充空字符串占位。

urllib.parse.urljoin

urllib.parse.urljoin用来拼接URL。

import urllib.parse
# 将两个url拼接成一个完整的url,base_url + sub_url
print(urllib.parse.urljoin('https://www.cnbogs.com', '/Neeo/p/10864123.html'))
# 如果每个参数中都有相应的参数的话,比如都有协议,那么将以后面的为准
print(urllib.parse.urljoin('https://www.cnbogs.com', 'http://www.baidu.com/Neeo/p/10864123.html'))

注意,urljoin方法只能接受两个url参数:

urljoin(base, url, allow_fragments=True)

别意淫它能跟os.path.join一样可劲儿的往后拼!

urllib.parse.urlencode

urllib.parse.urlencode将字典类型的参数序列化为url编码后的字符串,常用来构造get/post请求的参数k1=v1&k2=v2

import urllib.parse

params = {"k1": "v1", "k2": "v2"}
encode_params = urllib.parse.urlencode(params)
print('https://www.baidu.com?' + encode_params)  # https://www.baidu.com?k1=v1&k2=v2
# 这里用urljoin不好使, 它在处理base url时会将 ? 干掉
print(urllib.parse.urljoin('https://www.baidu.com', encode_params))  # https://www.baidu.com/k1=v1&k2=v2
# 或者你继续使用拼接....好蛋疼
print(urllib.parse.urljoin('https://www.baidu.com', '?' + encode_params))  # https://www.baidu.com?k1=v1&k2=v2

urllib.parse.quote系列

按照标准,URL只允许一部分ASCII字符(数字字母和部分符号),其他的字符如汉字是不符合URL标准的。所以在使用URL的时候,要进行URL编码。

urllib提供了urllib.parse.quoteurllib.parse.quote_plus进行URL编码。

urllib.parse.quote_plusurllib.parse.quote更近一步,会对/符号进行编码。

urllib.parse.unquoteurllib.parse.unquote_plus则是将编码后的URL进行还原。

import urllib.parse

url = 'https://www.baidu.com/s?&wd=张开'

result = urllib.parse.quote(url)
print(result)  # https%3A//www.baidu.com/s%3F%26wd%3D%E5%BC%A0%E5%BC%80
result_plus = urllib.parse.quote_plus(url)
print(result_plus)  # https%3A%2F%2Fwww.baidu.com%2Fs%3F%26wd%3D%E5%BC%A0%E5%BC%80

un_result = urllib.parse.unquote(result)
print(un_result)  # https://www.baidu.com/s?&wd=张开

un_result_plus = urllib.parse.unquote_plus(result_plus)
print(un_result_plus)  # https://www.baidu.com/s?&wd=张开

urllib.robotparser

可以利用urllib.robotparser对爬取网站的Robots协议进行分析。

那问题来了,什么是Robots协议?

Robots协议

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。robots.txt文件是一个文本文件,放在站点的根目录下。

当一个搜索蜘蛛访问一个站点时,它会首先检查该站点根目录下是否存在robots.txt,如果存在,搜索机器人就会按照该文件中的内容来确定访问的范围;如果该文件不存在,所有的搜索蜘蛛将能够访问网站上所有没有被口令保护的页面。

禁止所有爬虫访问任何内容

User-Agent:  *
Disallow:  /

允许所有爬虫访问任何内容

User-Agent:  *
Disallow: 

允许某些爬虫访问某些目录

User-agent:  Baiduspider
Allow:  /article
Allow:  /oshtml
Disallow:  /product/
Disallow:  /

User-Agent:  Googlebot
Allow:  /article
Allow:  /oshtml
Allow:  /product
Allow:  /spu
Allow:  /dianpu
Allow:  /oversea
Allow:  /list
Disallow:  /

User-Agent:  *
Disallow:  /

关于爬虫名称

 

爬虫名称所属公司网址
Baiduspider 百度 www.baidu.com
Googlebot 谷歌 www.google.com
Bingbot 微软必应 cn.bing.com
360Spider 360搜索 www.so.com
Yisouspider 神马搜索 http://m.sm.cn/
Sogouspider 搜狗搜索 https://www.sogou.com/
Yahoo! Slurp 雅虎 https://www.yahoo.com/

RobotFileParser

robotpaser模块提供RobotFileParser类来解析robots.txt文件,判断是否允许爬取网站的某一目录。

在实例化类时传入robots.txt。

import urllib.robotparser
response = urllib.robotparser.RobotFileParser(url='https://www.zhihu.com/robots.txt')
response.read()

或者通过实例化对象调用set_url方法传入robots.txt。

import urllib.robotparser

response = urllib.robotparser.RobotFileParser()
response.set_url('https://www.zhihu.com/robots.txt')
response.read()

whatever,都需要使用response.read()读出来,不然后面解析不到。

常用方法:

  • set_url(url):用来设置 robots.txt 文件链接,如果在初次实例化 RobotFileParser 类的时候传入了 url 参数,那么就不需要再次调用此方法设置了。
  • read():读取 robots.txt 文件并将读取结果交给 parse() 解析器进行解析。
  • parse(lines):用来解析 robots.txt 文件内容,分析传入的某些行的协议内容。
  • can_fetch(useragent, url):需要两个参数,User-Agent、所要抓取的 URL 链接,返回此搜索引擎是否允许抓取此 URL,返回结果为 True、False。
  • mtime():返回上次抓取分析 robots.txt 文件的时间,这对于需要对 robots.txt 进行定期检查更新的长时间运行的网络爬虫非常有用 。
  • modified():同样的对于长时间分析和抓取的搜索爬虫很有帮助,将当前时间设置为上次抓取和分析 robots.txt 的时间。
  • crawl_delay(useragent):返回抓取延迟时间的值,从相应的 User-Agent 的 robots.txt 返回 Crawl-delay 参数的值。 如果没有这样的参数,或者它不适用于指定的 User-Agent,或者此参数的 robots.txt 条目语法无效,则返回 None。
  • request_rate(useragent):从robots.txt返回Request-rate参数的内容,作为命名元组RequestRate(requests,seconds)。 如果没有这样的参数,或者它不适用于指定的useragent,或者此参数的robots.txt条目语法无效,则返回None。(Python3.6新增方法)

示例:

import urllib.robotparser

response = urllib.robotparser.RobotFileParser(url='https://www.zhihu.com/robots.txt')
# 要有读操作
response.read()
# 判断某一个网址是否能爬取
print(response.can_fetch('Googlebot', 'https://www.zhihu.com/question/268464407/answer/804631692'))  # True
print(response.can_fetch('*', 'https://www.zhihu.com/question/268464407/answer/804631692'))  # False

# 返回上一次抓取分析 robots.txt 的时间
print(response.mtime())  # 1568542822.1876643

# 将当前时间设置为上次抓取和分析 robots.txt 的时间
response.modified()

# 返回 robots.txt 文件对请求频率的限制
print(response.request_rate('MSNBot').requests)  # 1
print(response.request_rate('*'))  # None

# 返回 robots.txt 文件对抓取延迟限制
print(response.crawl_delay('*'))  # None
print(response.crawl_delay('MSNBot'))  # 10

使用parser方法读取和分析robots.txt。

import urllib.robotparser
import urllib.request

# 实例化 RobotFileParser 对象
response = urllib.robotparser.RobotFileParser()
# 使用parser读取 robots.txt 文件
result = urllib.request.urlopen('https://www.zhihu.com/robots.txt').read().decode('utf-8').split('\n')
response.parse(result)

# 判断url是否可爬取
print(response.can_fetch('Googlebot', 'https://www.zhihu.com/question/268464407/answer/804631692'))  # True
print(response.can_fetch('*', 'https://www.zhihu.com/question/268464407/answer/804631692'))  # False

see also:http://www.httpbin.org/ | 使用HTTPBin测试HTTP库 | urllib — URL handling modules | http://www.66ip.cn/ | HTTP模块 | 使用ip付费的代理是遇到http.client.RemoteDisconnected: Remote end closed connection without response 解决方法 | http.cookiejar库之CookieJar |libwww-perl | 爬虫保存cookies时重要的两个参数(ignore_discard和ignore_expires)的作用 | http.cookiejar- HTTP客户端的Cookie处理 | [python]解决urllib.urlretrieve()下载不完全的问题 | urllib.parse模块使用 | 4. Urllib -- urllib.robotparser | urllib.robotparser- robots.txt的解析器 | urllib模块中的方法 | python中quote函数是什么意思,怎么用

 

 

 

 

 

 

 

 

posted @ 2019-11-28 21:40  天之坚毅  阅读(784)  评论(0编辑  收藏  举报