学爬虫?一篇文章就够了!

前言:
——想入门爬虫?想要爬一批数据毕业?想要爬一整个网站做数据分析?只要你有python基本功,看完这篇文章,就可以达到上述目的。
——想要进阶学习,做爬虫工程师?大可以看完文章,再有指向性的,针对薄弱处深入学习,实现月薪X万的目标。
5年前写过一篇博客 《python Scrapy 框架做爬虫 ——入门地图》 ,现在看来已经比较局限。所以,接着最近做的事情,重新总结爬虫中的林林总总,作为比较完整的新地图,给后来的爬虫学习者一些便利。

本文的使用方法是,第一遍通读全文了解概况,建立整体认知框架。第二遍逐个链接进入,细致学习和实践。

Happy Hacking!

1,认识爬虫

网络是一个比喻。分散在地球不同角落的机器中存储的文档、音乐、视频、图片等资源,可以在初始地被拆散分解成线缆或空气中的能量波动,再在目的地重新组装成原来的形式。这种无处不达的资源联通系统,被我们称为互联网。
作为网上冲浪的弄潮儿,通常只关心一部电影、一首音乐或一部小说。但如果我们关注某一类信息,比如全部的科幻电影、全部的言情小说、全部的摇滚音乐、甚至是关注全世界所有的信息(搜索引擎),我们就需要一个自动化的程序,在互联网上不停歇的来回移动,这便是“爬虫”。

友情提示,以下内容,需要python才能上车。

一个比较完善的爬虫

# An Crawler
start_url = 'http://link.to.resource/first.html';
tobe_visited_list = [start_url]
have_visited_list = []
while(tobe_visited_list):
	url = tobe_visited_list.pop()
	if url not in have_visited_list:
		have_visited_list.append(url)
		page = webtool.get(url)
		for new_url in page:
			tobe_visited_list.append(new_url)
		for resource in page:
			save(resource)

上面就是一个完整的爬虫(python伪代码):从待访问列表中取出链接,获取链接中的页面,将想要的数据存储下来,再将页面中的新链接放进待访问列表中,循环往复。
有了框架,下面我们介绍几个工具,用于替代掉伪代码中的伪函数,让爬虫真正可以运行。

网络交互工具:requests

requests
据说如果用Python代码访问网络,只能学一个库,就学这个。打开上面的超链接,你会发现安装和使用的方法;

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
u'{"type":"User"...'
>>> r.json()
{u'private_gists': 419, u'total_private_repos': 77, ...}

用requests.get()替代伪代码中的WEBTOOL.get(),就可以真正获取到链接中的内容!

内容提取工具:正则表达式

正则表达式
获取到页面的内容,就要从里面提取我们想要的resource了。我们想要的数据,通常会存在固定的格式,比如手机号码通常是11个数字组成的字符串;
正则表达式就是描述这种格式的语言,点上面的链接,可以看一下正则表达式的语法,比如手机号的11个数字可以表示成:[0-9]{11}

爬虫小结

通过理清爬虫的思路,学习用requests工具获取网络上的资源,正则表达式提取网页中的元素,你已经掌握了爬虫,恭喜!
下面让我们来用爬虫爬取一个页面中的网址,让爬虫跑起来吧:

# An Crawler
import requests
import re

start_url = 'http://www.cma.gov.cn/';
tobe_visited_list = [start_url]
have_visited_list = []
while(tobe_visited_list):
	url = tobe_visited_list.pop()
	if url not in have_visited_list:
		have_visited_list.append(url)
		page = requests.get(url)
		pattern = re.compile(r'\"[a-zA-Z]+://[^\s]*[.com|.cn]\"')
        for new_url in pattern.findall(page.text):
			tobe_visited_list.append(new_url.split("\"")[1])
			print(new_url)
#		for resource in page:
#			save(resource)

爬虫输出一系列网址

"http://rays.cma.cn"
"http://m.weather.com.cn/m/cmapn/weather.htm"
"http://www.nmc.cn/publish/satellite/FY4A-true-color.htm"
...

Tips:你想要的正则表达式可能有很多人都分享过,搜索一下可以在起步阶段省下不少时间。

有了上面的工具基础,幸运的话,爬几万条数据毕业的任务就可以顺利完成了。但如同爬上景山和爬上珠穆朗玛峰都是“爬山”,爬虫之间的差距也可以大到令人咋舌。

2,网页分析:让抓取内容丰富起来

上面我们介绍的爬虫,抓取到的内容是一个html页面,用html详细的描述了资源的排布。对于浏览器来说,html是他的母语,但是对于想要爬数据的我们,获取到的html是非常杂乱的。我们需要一些好工具,把html安排妥当,方便我们上下其手随心所欲。

xpath

通过xpath提供的初始化方法,把html变成一个可以操作的selector对象,再用该对象提供的多种多样的内建函数取出我们想要取出的东西。
学习xpath语法
python使用xpath的实例

beautiful soup

一个强大的网页分析工具,xpath不好用,正则不好拼凑,试试这个。
比较细致的教程

进阶NLP

通用爬虫中,要匹配的信息形式千奇百怪,成千上万的网站又是由不同的人打造,有没有那种不用手工做选择的工具,直接拿到想要的内容呢?
有。目前还不成熟,基于NLP的自动内容提取,但未来可期。所以,现在,还是去研究页面吧。

3,数据库:给抓取内容一个家

如果我们想要抓取的内容,不是一个txt就可以存下来,不是一个文件夹就可以保存好:比如我想抓知乎上的用户和问题,但是用户和问题又有交叉,怎么存好?用MySQL。
如果我不在乎字段间的关系,但是获取的信息特别多,我想要把这大量的信息快速的存起来,又怎么办?用MongoDB。

MySQL

学习 MySQL

MongoDB

学习 MongoDB

4,异步:让速度飞起来

如果我们要爬取的数据量巨大,比如1亿,再如果每次通过url获取信息需要1s,那么爬完全部信息,需要的时间就是1亿秒,即3年左右。有没有什么办法,可以加速获取信息呢?

有。请求传送到远程服务器、资源从服务器传回本地都需要时间,在这漫长的网络传输时间里,cpu一直在等待资源收齐,再进行解析资源、存储资源动作。如果我们可以让cpu发送完一条请求,继续发送下一条请求,而等资源传输完成,信息收集好时,再提醒cpu进行处理的话,cpu是没有等待时间的。

上述的这种把「发送请求」和「处理资源」拆分的爬虫,就是异步爬虫。异步爬虫将网络IO的时间排除,可以榨干cpu的效率,大大加速爬虫爬取速度!

Tips:异步是一个相当通用的概念,要深刻理解异步,需要比较全面的了解计算机工作的原理。对于Python来说,线程和进程、阻塞和非阻塞交相辉映,事件驱动的异步编程写法比较复杂,容易出错。从目的考量选择出发的道路,站在巨人的肩膀上非常明智。
理解Python异步

5,使用框架:站在巨人的肩膀上

前面几节,针对基础爬虫的拓展方向,我们给出了工具说明和学习链接。网页析取工具Xpath、beautiful soup可以帮助我们从获取到的页面中收集想要的数据,MySQL、MongoDB可以帮我们把数据很妥善的保存。但是,如果我们在开始的小爬虫的基础上,添加页面分析和数据库读写,一个超大的ugly函数就要出现,难读、难维护、难扩展,难上加难。

此外,对于加速,我们知道有一种叫做异步爬虫的机制可以让速度起飞,但是学会编写这种异步爬虫好像很费时(学不会)。

因为已经21世纪了,爬虫作为几十年前伴随互联网发展的技术,已经有了许多积累了前人心血的成果可以复用。目标比较出色的,就是Scrapy框架。

Scrapy

关于scrapy,这里有一个稍显过时,但依然很有借鉴意义的doc
我们这里仅对scrapy为什么好用(架构),能够怎么用(插件),做比较宏观的描述,并给出具体使用可以参考的链接。

scrapy架构

对scrapy的初步使用,就是了解架构,写自己的spider和item_pipeline。
scrapy架构图
scrapy梳理了爬虫中的关键流程,将爬虫分为几个组件,用引擎engine串联起整个流程:
1,spider组件:用来分析网页,将其中的新链接作为request对象输出给engine,将其中的数据作为item输出给engine;
2,downloader组件:用engine给予的请求对外获取信息,包装成response对象传递回来作为输出还给engine;
3,scheduler组件:持续从engine收集request对象放入队列,同时engine不断的从中按需拿取request对象;
4,item pipeline组件:当engine收到item对象就传递给该组件,该组件进行处理;

在实际的应用中,通常我们只要用前面学过的xpath、beautiful soup定义好自己的spider,然后写一个将数据存入数据库的pipeline,框架就可以飞快的跑起来,你就拥有了一个单机最强异步爬虫。

一个使用案例

scrapy拓展

机构虽定,功能却依然可以拓展。scrapy中,spider和engine质检的交互要通过spiker_middleware,downloader和engine之间的交互要通过downloader_middleware。通过启用框架提供或自己写的spiker_middleware、downloader_middleware,我们可以控制request的细致参数和下载的细致动作。

使用代理

代理是一个ip:port对,类似123.345.456.789:1234,代理的作用是把你的请求发送到代理服务器上,代理服务器获取到信息后,再反向传输给你。所以代理服务是一种计算和网络资源,因此通常需要去网上付费购买。(免费的通常非常慢)
购买到代理服务后,可以将获取代理IP的接口(例如5秒钟取到5个ip:port对)进一步封装。如果每个代理地址的有效时间1分钟,那么1分钟内每隔5秒我们能够获取到5个代理ip,存储积累起来我们就有了规模为60个代理ip的代理池。成熟的代理池需要:不断从接口获取代理ip并存储,不间断测试删除过期代理ip,提供对外接口返回代理池中的一个可用代理。(工具见后续工具链接)
有了自己的代理池服务,我们就可以随时通过接口获取到一个代理ip了,那么,如何在scrapy中使用呢?
可以在middlewares.py中实现一个自己的中间件:

class ProxyMiddleware():
    def __init__(self, proxy_url):
        self.logger = logging.getLogger(__name__)
        self.proxy_url = proxy_url
    
    def process_request(self, request, spider):
        request.meta['proxy'] = self.proxy_url
        
    # 使用classmethod,通过crawler初始化,方便调用settings    
    @classmethod 
    def from_crawler(cls, crawler):
        settings = crawler.settings
        return cls(
        	# 获取在settings中设置的key:val
            proxy_url=settings.get('PROXY_URL')
        )

同时在setting.py中添加一行:

# 启用代理中间件
DOWNLOADER_MIDDLEWARES = {
    'YOUR-PROJECT-NAME.middlewares.ProxyMiddleware': 555,
}
# 你自己的代理服务地址
PROXY_URL = 'http://127.0.0.1:5000'
更换UA

user-agent是request中的一个字段,用来标识请求,告知服务器你的操作系统类型,浏览器版本等等信息。通过更换user-agent,每一个请求都告诉服务器不同的信息,可以降低请求被禁止的概率。
再实现一个middleware:

class RotateUserAgentMiddleware(UserAgentMiddleware):
    """
        a useragent middleware which rotate the user agent when crawl websites

        if you set the USER_AGENT_LIST in settings,the rotate with it,if not,then use the default user_agent_list attribute instead.
    """

    #the default user_agent_list composes chrome,I E,firefox,Mozilla,opera,netscape
    #for more user agent strings,you can find it in http://www.useragentstring.com/pages/useragentstring.php
    user_agent_list = [
        'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31',
        'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17',
        'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)',
        'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)',
        'Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)',
        'Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1',
        'Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1',
        'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20120910144328 Firefox/15.0.2',
        'Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201',
        'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a3pre) Gecko/20070330',
        'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13; ) Gecko/20101203',
        'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14',
        'Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50',
        'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52',
        'Mozilla/5.0 (Windows; U; Win 9x 4.90; SG; rv:1.9.2.4) Gecko/20101104 Netscape/9.1.0285',
        'Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1.7pre) Gecko/20070815 Firefox/2.0.0.6 Navigator/9.0b3',
        'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080219 Firefox/2.0.0.12 Navigator/9.0.0.6',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36',
        ]

    def __init__(self, user_agent=''):
        self.user_agent = user_agent

    def _user_agent(self, spider):
        if hasattr(spider, 'user_agent'):
            return spider.user_agent
        return random.choice(self.user_agent_list)

    def process_request(self, request, spider):
        ua = self._user_agent(spider)
        if ua:
            request.headers.setdefault('User-Agent', ua)

在settings.py中启用这个中间件:

DOWNLOADER_MIDDLEWARES = {
    'YOUR-PROJECT-NAME.middlewares.RotateUserAgentMiddleware': 400,
}
提供Cookie

登录网站之后,我们是不是很久都不用重新登录?这种记忆机制就是cookie和服务器“会话”的交互完成的。

cookie是request的另外一种特殊字段,用来标识request是否来自某个特定会话,服务器会根据这个字段选择会话再进行应答。如果没有cookie,就不能找到特定会话,服务器就会不知所措。

因此如果我们爬取的内容需要登录之后,由特定建立的会话来提供,那么可以先登录一批账号,把登录之后的cookie保存下来,存到一个数据库里,对外提供一个访问接口返回随机cookie,就形成了自己的cookie池服务。

有了自己的cookie池服务,类似接入代理池服务,再写一个中间件,给request添加cookie:

class CookiesMiddleware():
    def __init__(self, cookies_url):
        self.logger = logging.getLogger(__name__)
        self.cookies_url = cookies_url

    def get_random_cookies(self):
        try:
            response = requests.get(self.cookies_url)
            if response.status_code == 200:
                cookies = json.loads(response.text)
                return cookies
        except requests.ConnectionError:
            return False

    def process_request(self, request, spider):
        self.logger.debug('正在获取Cookies')
        cookies = self.get_random_cookies()
        if cookies:
            request.cookies = cookies
            self.logger.debug('使用Cookies ' + json.dumps(cookies))

    @classmethod
    def from_crawler(cls, crawler):
        settings = crawler.settings
        return cls(
            cookies_url=settings.get('COOKIES_URL')
        )

同样在settings.py中添加:

# 启用cookie中间件
DOWNLOADER_MIDDLEWARES = {
    'YOUR-PROJECT-NAME.middlewares.CookiesMiddleware': 555,
}
# cookie池地址
COOKIES_URL = 'http://localhost:5000/random'
控制下载错误重试次数

response 返回之后,不已定总是好消息,网络抖动或者是代理过期都会造成访问失败。失败了怎么办?当然是继续干了!Retry!
下面的retry中间件,对418错误进行无限retry。你可以根据自己的需求,更改重试行为。

from scrapy.downloadermiddlewares.retry import *

class MyRetry(RetryMiddleware):

    def process_response(self, request, response, spider):
        if request.meta.get('dont_retry', False):
            return response
        if response.status in self.retry_http_codes:
            reason = response_status_message(response.status)
            return self._retry(request, reason, spider, response.status) or response
        return response

    def _retry(self, request, reason, spider, response_status = 0):
        retries = request.meta.get('retry_times', 0) + 1

        retry_times = self.max_retry_times

        if 'max_retry_times' in request.meta:
            retry_times = request.meta['max_retry_times']

        stats = spider.crawler.stats
        if response_status == 418 or retries <= retry_times:
            logger.debug("Retrying %(request)s (failed %(retries)d times): %(reason)s",
                         {'request': request, 'retries': retries, 'reason': reason},
                         extra={'spider': spider})
            retryreq = request.copy()
            retryreq.meta['retry_times'] = retries
            retryreq.dont_filter = True
            retryreq.priority = request.priority + self.priority_adjust

            if isinstance(reason, Exception):
                reason = global_object_name(reason.__class__)

            stats.inc_value('retry/count')
            stats.inc_value('retry/reason_count/%s' % reason)
            return retryreq
        else:
            stats.inc_value('retry/max_reached')
            logger.debug("Gave up retrying %(request)s (failed %(retries)d times): %(reason)s",
                         {'request': request, 'retries': retries, 'reason': reason},
                         extra={'spider': spider})

同样对settings.py更改,让修改生效:

DOWNLOADER_MIDDLEWARES = {
	#禁用原来的retry中间件
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': None,
    #启用自己的retry中间件
    'YOUR-PROJECT-NAME.middlewares.MyRetry': 301,
}

6,分布式:让巨人飞起来

通过上面介绍中的案例,我们已经可以熟练的利用scrapy框架,定制爬虫,安排数据处理和存储,更换代理、UA、Cookie来隐匿我们的行踪,也可以通过控制下载错误代码控制下载行为。另外作为scrapy的优势,我们已经可以做到非常高的并发量,把单机的资源榨干。

但是回到开始的大数据问题,如果我们要获取1亿数据需要同步爬虫运行3年,就算异步爬虫可以加速10倍,依然需要4个月的时间,还是错过了毕业季!

如果我们能够拥有4台机器一起运行,是不是可以进一步把时间降低到1个月,保证顺利毕业呢?前人又已经搞定了相关的方案,可以让多台机器上的scrapy一起配合完成同一个任务。

Scrapy-Redis

我们把视线回到架构图上,engine/downloader没有什么值得改的地方,spider是我们自己写的,spider middleware可以自定义,pipline是我们自己写的,downloader middleware可以自定义。所以我们把焦点放到scheduler上。
这个scheduler可以接受engine给出的request对象,判重后,放到队列中备用,等待engine获取。如果我们在同一个机器上启动很多个scrapy进程,这很多个进程能够共享一个scheduler,就可以单机做并行了。进一步如果在很多机器上运行的多个scrapy进程,都用一个scheduler来缓存request对象,那么分布式就实现了。

思路明白了之后,我们就可以安装Scrapy-Redis到本地了。
第一步,scrapy-redis 依赖redis,所以需要安装和启动redis;
安装和启动redis
第二步,scrapy-redis 重写了spider,我们只要在定义爬虫时把自建爬虫的父类改成"RedisSpider";
第三步,scrapy-redis启动后,会等待redis库中的start_url,所以需要手工push一条url进redis;
下面是具体攻略:scrapy-redis安装和使用的攻略

Tips:因为python存在GIS,所以根据自己机器CPU的个数多跑几个scrapy进程,可以有加速效果!

7,初遇大数据:用布隆过滤器判重

使用scrapy会遇到时间瓶颈,所以我们用了分布式的scrapy-redis,解决了规模问题,用机器资源换取时间。但是我们的redis是中心化的,虽然request对象来来回回可能可以保持平衡,但是去重队列却会一直增长,那么数据量大了之后,redis一定会爆掉。此时我们的老朋友登场:哈希!
有一种重复散列的高效查重方法叫做bloomfiter,很多人做了实现,但是这个叫崔庆才的小哥做了讲解、实现之后,还做成了安装包。大家可以一行命令安装好,更改settings即可生效,非常方便。
bloomfilter加强的scrapy-redis

BloomFilter

8,登录:向反爬虫工程师亮剑

众所周知,大清已经亡了。21世纪的今天,想通过requests获取网页的text抓取信息,有可能根本看不到信息,或者看到一堆乱码,或者不是乱码,但是被层层加密。

动态网页

最现实的问题是,随着软件技术的发展,静态网页越来越少,动态渲染的东西越来越多。如果我们需要抓取的东西是Ajax动态获取的,我们可以通过浏览器自带的工具进行网络分析,构造请求直接获取数据。
学习分析ajax构造爬虫请求

如果是需要和网页进行进一步的交互,那么仅仅通过ajax请求可能比较复杂,此时需要模拟浏览器来解救你。模拟浏览器就是一个工具库,它向浏览器一样解析渲染网页,又对编程非常友好——你可以用代码实现点击、拖拽等等动作,和网页进行交互。模拟浏览器对验证码破解也有重要意义,因为现在的验证码很多都在分析你的鼠标移动轨迹。
关于模拟浏览器,只给一个关键词,需求很多样,有了入口就不迷茫:selenium

验证码破解

如果没登录,获取到的信息很有可能是这样的:

<option value="http://www.zgqxb.com.cn/">中国气象新闻网</option>
<option value="http://www.cma-lpinfo.com">中国防雷信息网</option>
<option value="http://www.wmc-bj.net/">世界气象中心(北京)</option>
<option value="http://www.jianzai.gov.cn/">国家减灾网</option>

很感人的画面,催人泪下。但是也很容易理解,人家做一个网站要买服务器和带宽,自然不愿意被非用户占用了计算和网络资源,大家爬取时,也要注意限速。

通常情况下,除了对被爬取的网站心存感激之外,我们需要把我们的需求做比较好的封装,让拒绝爬虫访问的服务器认为你就是用户,这就需要登录。

登录最大的拦路虎就是验证码,在爬和反爬不断升级的道路上,机器学习也参与进来,鉴别爬虫流量,也鉴别验证码字符。

验证码破解的第一招,就是不要碰到验证码。
1,找防护力量比较薄弱的站点,比如爬微博,爬移动站可能就比PC站点容易一些,也许都用不到Cookie;
2,如果需要登录,多换马甲,不要让人家看出来你是在用多个账户登录获取Cookie。
破解验证码的第二招,就是见招拆招。此处不设讲解,不给链接,因为世界变化太快。

进阶网页加解密

就算登录,网站依然可以有方法让你获取不到信息,这就是网页加密。如果你喜爱前端技术和福尔摩斯,你就可以成为此间大神。
Tips:此为进阶方向,可顺势成为前端工程师。

9,爬虫工程师必备百宝箱

前面已经学会如何在request中变换UA,添加、更改Cookie和代理了,那么如何能够拥有UA、Cookie、代理的强大后援呢?

User-agent池

https://github.com/selwin/python-user-agents

Cookie池

https://github.com/dhfjcuff/Cookie-Pool
https://github.com/Python3WebSpider/CookiesPool

代理池

https://github.com/jhao104/proxy_pool

崔庆才

在爬虫领域非常活跃,知识全面,讲解清晰。但因为提供的内容太全,容易让新人眼花缭乱,所以不适合快速入门,可进阶使用。
博客

站在爬虫的山顶上:进阶指南

我们在文中已经提到了可能的进阶方向,当然事实上大家很可能跟我一样本来是那几个方向的人,只是偶尔客串一下,养个虫虫。

爬虫专家

养虫养成专家,例如崔庆才;

数据科学家

分析爬取来的数据,形成决策和报告;

机器学习专家

机器学习分析爬虫流量,NLP分析爬取字段,CV分析验证码;

前端工程师

网页加解密;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值