基于scrapy-redis的分布式腾讯新闻爬虫

目录

任务目标

爬取流程设计

文字描述

流程图

项目实施

硬件设施

软件设施

功能概述

结构功能图

网站分析

主页

获取其他新闻链接

内容页

获取正文及判断:

获取标题

获取评论

数据库设计

重要代码解释

tc_spider爬虫文件

middlewares中间件

setting配置文件设置

项目测试

redis去重测试

内容页测试

分布式测试

好累写了这么多谢谢收看,代码连接


任务目标

  • 对腾讯新闻进行全网站爬取
  • 提取新闻的标题、正文、评论数据

爬取流程设计

文字描述

从腾讯新闻主页开始爬取https://new.qq.com/ch/ent/,提取页面中所有的url,如果该url域名是腾讯新闻,则将此url放到待爬取队列中,否则清除,如果该url开头为https://new.qq.com/omn/,则为正文页,需要提取标题,正文,和评论。

流程图

项目实施

硬件设施

两台笔记本电脑,网卡正常。路由器等网络实施。

软件设施

操作系统:Windows10,Windows7

软件:pycharm编译器,谷歌浏览器,Chronmedriver,Redis数据库,Redis可视化工具,MySql数据库,MySql可视化工具

开发语言及主要类库:python,scrapy,scrapy-redis,seleinum

功能概述

pycharm:对python项目进行管理,启动、停止爬虫项目

谷歌浏览器+Chronmedriver:加载任务网页,获得js渲染后的页面,配合Chronmedriver可实现无头浏览器效果,加快渲染速度。

Redis:储存任务队列urlList,储存已经访问过的url指纹

MySql:持久化储存数据标题、内容、评论等数据。

结构功能图

网站分析

主页

 腾讯新闻的主页如下,该主页包含很多新闻链接,我们初次爬取就是从这个页面开始,通过这个页面来获取其他页面,其他页面页包含了很多链接,需要我们做判断是不是腾讯新闻,以便排次广告链接。

 例如这条新闻,他的链接在标题中,不过我们不需要一个一个去定位,只需要获取所有的链接标签然后判断是不是新闻即可。任意一浏览器,右键查看元素(火狐),或检查(谷歌),即可进入开发者界面,查看网页的构成。

<h3 class="">
<a href="https://new.qq.com/omn/20190218/20190218A04WZY00" target="_blank"><span class="icon-hot">&nbsp;</span><!-- react-text: 228 --><!-- /react-text --><!-- react-text: 229 -->美媒:英情报机构负责人暗示“不会施压彻底禁止华为”<!-- /react-text --></a>
</h3>

获取其他新闻链接

这里使用了scrapy内置的选择器selector,selector使用如下:

  • text属性是要进行搜索的文本
  • css方法是使用css选择器,示例为选择<a></a>标签
  • xpath是使用xpth选择器,selector特色即使可以混用xpath和css选择器,示例选择了当前结构的href属性值
  • extract方法会返回所有符合条件的结果列表
from scrapy.selector import Selector
... ...
urlList = Selector(text=response.body).css('a').xpath('@href').extract()

内容页

新闻内容页的链接是包含/omn/结构的,根据这个特性,我们可以判断当前页面是不是内容页,如果是,就获取所有新闻内容和评论。不过我使用了另一种方法,之所以放弃第一中方法是因为判断url会让scrapy框架难以维护,我的方法如下:寻找当前html是否有内容框,或者说是内容节点

老规矩,右键进入开发者模式,新闻的内容存放在这个节点中,在获取数据前判断一下,如果有这个标签,就获取标题,正文,评论信息,和其他新闻链接,如果没有,只获取其他新闻链接。

<div class="content-article">
     <!--导语-->
     <p class="one-p">  丽豪地产为何敢公然建设和售房?“丽豪房地产除了有部分土地证外,没有规划施工许可、预售许可手续”。法律明令,五证不全绝不允许开工建设,没有预售许可证绝不允许售房。丽豪地产背后是谁在撑腰?又是谁对违法违规行为熟视无睹?</p>
</div>

获取正文及判断

  • selector是scrapy自带的选择器
  • xpath获取的是div标签且class属性为content clearfix
  • extract获取所有匹配的结果。
content_div = Selector(text=response.body).xpath("//div[contains(@class,'content clearfix')]").extract()  #获取内容
if len(content_div) > 0:  #是内容页
   ''' 获取 '''

获取标题

<div class="qq_conent clearfix">
    <div class="LEFT">
       <h1>检察日报四问1800亩问题别墅:涉事地产公司背后是谁在撑腰?</h1>
       ......   
    </div>
    ... ...
</div>

先定位到最外层div,然后定位到class=LEFT的div,最后定位到h1的text值。

title = Selector(text=response.body).xpath("//div[contains(@class,'qq_conent clearfix')]").xpath("//div[contains(@class,'LEFT')]").css('h1::text').extract()[0]

获取评论

评论界面包含两个入口,一种是不断的点击查看更多评论,另一种是直接点击评论条数。我选择的是点击评论条数,因为在项目中使用了seleinum加载,第一种方法需要模拟鼠标点击事件,还需要模拟网页下拉事件,这会增加爬取时间,也会让项目更复杂。而第二种方法会获得一个新页面,只包含评论信息的,我们可以进行快速抓包来获取数据。

 

评论页的链接来自与第二种方法的标签。使用selector定位到这个标签在获取href值,在获取数字即可。此处只讲如何获取标签,正则处理及抓包在后面讲。

<a class="header-number" href="http://coral.qq.com/3730151054"> <i id="J_CommentTotal">208</i>条评论 </a>
id_href = Selector(text=response.body).xpath("//a[contains(@id,'cmtLink')]").xpath('@href').extract()[0] #获取id

数据库设计

数据库选择使用mysql,适合小项目,库和表设计如下

  • title是标题
  • content是内容
  • comment是评论信息
  • id是新闻的品论链接中提取的数字
  • cmtnum是评论数量

注意:这里可视化工具使用的是Navicat Premium11版本,其他版本会导致json无法显示。

重要代码解释

这里我讲一下前面未提到的正则过程、抓包过程,和scrapy-redis的使用,首先看一下项目结构图

tc_spider爬虫文件

代码从上到下,以函数为单位,依次注释:

def  _init_

  • 正则表达式匹配规则,此处匹配的字符串应含有https://new.qq.com/.*表示匹配任意数量的任意字段
  • item是scrapy中的item实例,类似于模型
  • r是本地的redis连接,端口6379,位置索引为0
  • hostUrl设置你从那个页面开始爬取
  • db是本地mysql连接

def  start_requests

  • 判断redis中待爬取队列urlList是否为0
  • 队列为空,应该是第一次启动,从主页开始爬取
  • 如果待爬取队列大于0
  • 弹出顶部url,进行爬取,制止队列为0
  • 关闭mysql连接

def  parse

  • 大部分代码是从页面提取数据
  • 判断是不是内容页,如果是,进一步提取。
  • 把获取到的数据提交到mysql
  • 把获取的url通过scrapy提交到redis

def getNum

  • 提取数字的增则表达式
  • 构建python正则对象
  • 对传入参数进行正则匹配
  • 迭代所有结果并返回

def getChinese

  • 提取汉字的正则表达式
  • 其他功能同上

def getJson

  • 将id转换为字符串
  • 构建匹配括号内的内容的正则表达式
  • 构建抓包请求的url
  • 匹配左括号的正则表达式
  • 匹配右括号的正则表达式
  • 将( 替换为  [
  • 将  )替换为 ]
  • 将字符串转换为json
  • 返回处理结果json

def InsertMysql

  • 获得mysql连接
  • 构建sql语句
  • 执行sql,失败则回滚事务

def CloseMysql

  • 关闭数据库连接
# -*- coding: utf-8 -*-
import re
import scrapy
from scrapy.selector import Selector
from ..items import *
import redis
import pymysql
import urllib
import json

class tc_Spider(scrapy.spiders.Spider):
    name = "tcSpider"

    def __init__(self):
        self.pattern = re.compile(r'https://new.qq.com/.*')
        self.item = TengxuncrawlItem()
        self.r = redis.Redis(host='127.0.0.1', port=6379,db=0)
        self.hostUrl = 'https://new.qq.com/omn/20190218/20190218A04WZY00'
        self.db = pymysql.connect('localhost','root','password','qqnews',charset='utf8' )

    def start_requests(self):
        '''
        如果待爬队列为空那么需要启动两次爬虫程序,第一次爬取hostirl的内容,第二次爬取队列内容,并填充队列
        :return:
        '''
        if self.r.llen('tcSpider:items') == 0:
            yield scrapy.Request(url=self.hostUrl, callback=self.parse)
        while self.r.llen('tcSpider:items')> 0:
            url = eval(self.r.lpop('tcSpider:items'))['url']
            yield scrapy.Request(url=url, callback=self.parse)
        self.CloseMysql()
    def parse(self, response):
        urlList = Selector(text=response.body).css('a').xpath('@href').extract()  #获取<a>的href值
        content_div = Selector(text=response.body).xpath("//div[contains(@class,'content clearfix')]").extract()  #获取内容
        if len(content_div) > 0:  #是内容页
            title = Selector(text=response.body).xpath("//div[contains(@class,'qq_conent clearfix')]").xpath("//div[contains(@class,'LEFT')]").css('h1::text').extract()[0]
            content = self.getChinese(content_div[0]) #获取内容
            id_href = Selector(text=response.body).xpath("//a[contains(@id,'cmtLink')]").xpath('@href').extract()[0] #获取id
            cmtNum_str = Selector(text=response.body).xpath("//a[contains(@id,'cmtLink')]").css('div i::text').extract()[0]
            id = self.getNum(id_href)
            cmtNum = int(cmtNum_str)
            jsonData = self.getJson(id)
            self.InsertMysql(content,id,title,cmtNum,jsonData)  #插入mysql

        for url in urlList:
            if self.pattern.search(url):
                self.item['url'] = url
                yield self.item

    def getNum(self,str):
        pattern = "\d+"
        regex = re.compile(pattern)
        results = regex.findall(str)
        str = ''
        for r in results:
            str = str+r
        return int(str)

    def getChinese(self,str):
        pattern = "[\u4e00-\u9fa5]+"
        regex = re.compile(pattern)
        results = regex.findall(str)
        str = ''
        for r in results:
            str = str+r
        return str

    def InsertMysql(self,content,id,title,cmtNum,JsonData):
        self.db.ping(reconnect=True)
        cursor = self.db.cursor()
        sql = "INSERT INTO news(content,id,title,cmtnum,comment) VALUES ('%s','%d','%s','%d','%s')" % (content,id,title,cmtNum,pymysql.escape_string(json.dumps(JsonData)))
        try:
            cursor.execute(sql)
            self.db.commit()
            print('--------------------------------------------------------------')
        except:
            self.db.rollback()
            print('**************************************************************')

    def CloseMysql(self):
        self.db.close()

    def getJson(self,idStr):
        id = str(idStr)
        pattern = re.compile('[(](.*)[)]')
        response = urllib.request.urlopen('http://coral.qq.com/article/' + id + '/comment/v2?callback=_article' + id + 'commentv2&orinum=30')
        m = pattern.search(str(response.read().decode('utf-8')))
        p1 = re.compile(r'\(')
        p2 = re.compile(r'\)')
        s1 = p1.sub('[', m.group())
        s2 = p2.sub(']', s1)
        value = json.loads(s2)
        return value

middlewares中间件

def  _init_

  • 加载useragent轮转所需文件

def  process_request

  • 详见注释
# -*- coding: utf-8 -*-

from scrapy import signals
import scrapy
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import json
import random
import time
import random
from scrapy import signals
from TengXunCrawl.settings import IPPOOL

import sys
sys.path.append('E:\DataAnalysis\\tools\python3\project\DistributedCrawler\Setting')
from UserAgent import *

class Url_SM(object):

    def __init__(self):
        self.file1 = open('E:\DataAnalysis\\tools\python3\project\DistributedCrawler\Setting\ipGood.json', 'r')  # 打开文件
        self.jsonDict = json.load(self.file1)

    def process_request(self, request, spider):
        chrome_options = Options()
        chrome_options.add_argument('--headless')  # 使用无头谷歌浏览器模式
        chrome_options.add_argument('--disable-gpu')
        chrome_options.add_argument('--no-sandbox')
        prefs = {"profile.managed_default_content_setting.images": 2}#不加载图片
        chrome_options.add_experimental_option("prefs", prefs)
        #print('user-agent="'+random.choice(MY_USER_AGENT)+'"')
        chrome_options.add_argument('--user-agent="'+random.choice(MY_USER_AGENT)+'"')#设置请求头
        #chrome_options.add_argument("--proxy-server="+random.choice(self.jsonDict)['ipaddr'])

        # 指定谷歌浏览器路径
        self.driver = webdriver.Chrome(chrome_options=chrome_options,executable_path='E:\DataAnalysis\\tools\python3\chromedriver')
        self.driver.get(request.url)
        html = self.driver.page_source
        self.driver.quit() #释放内存
        return scrapy.http.HtmlResponse(url=request.url, body=html.encode('utf-8'), encoding='utf-8', request=request)

setting配置文件设置

# -*- coding: utf-8 -*-
import random
import sys
sys.path.append('E:\DataAnalysis\\tools\python3\project\DistributedCrawler\Setting')
from UserAgent import *

BOT_NAME = 'TengXunCrawl'
SPIDER_MODULES = ['TengXunCrawl.spiders']
NEWSPIDER_MODULE = 'TengXunCrawl.spiders'

ROBOTSTXT_OBEY = False

ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline' : 400, #连接redis的pipline
}

DOWNLOADER_MIDDLEWARES = {
    #seleium+chrom无头浏览器
    'TengXunCrawl.middlewares.Url_SM': 543,  #下载中间件设置

    #scrapy ip 代理
    #'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware':123,
    #'DC.middlewares.IPPOOlS' : 125

}
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" #对request请求进行查重
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

SCHEDULER_PERSIST = True

USER_AGENT = random.choice(MY_USER_AGENT)

REDIS_HOST = '127.0.0.1'   #redis设置
REDIS_PORT = 6379

REDIS_PARAMS = {'db':0}

IPPOOL = [
{"ipaddr": "119.101.116.6:9999"},
{"ipaddr": "119.101.117.76:9999"},
{"ipaddr": "119.101.116.127:9999"},
{"ipaddr": "110.52.235.54:9999"},
{"ipaddr": "119.101.119.22:9999"},
{"ipaddr": "115.151.2.73:808"},
{"ipaddr": "119.101.113.121:9999"},
{"ipaddr": "121.61.0.197:9999"},
{"ipaddr": "119.101.118.94:9999"},
{"ipaddr": "119.101.115.141:9999"},
{"ipaddr": "119.101.117.202:9999"},
{"ipaddr": "119.101.113.58:9999"},
{"ipaddr": "119.101.112.35:9999"},
{"ipaddr": "119.101.116.146:9999"},
{"ipaddr": "119.101.113.245:9999"},
{"ipaddr": "121.61.1.239:9999"},
{"ipaddr": "119.101.114.182:9999"},
{"ipaddr": "119.101.113.238:9999"},
{"ipaddr": "119.101.114.97:9999"},
{"ipaddr": "119.101.115.2:9999"},
{"ipaddr": "119.101.118.142:9999"},
{"ipaddr": "119.101.113.57:9999"},
{"ipaddr": "110.52.235.96:9999"},
{"ipaddr": "119.101.116.177:9999"},
{"ipaddr": "119.101.116.227:9999"},
{"ipaddr": "119.101.118.5:9999"},
{"ipaddr": "119.101.116.131:9999"},
{"ipaddr": "119.101.117.56:9999"},
{"ipaddr": "119.101.117.15:9999"},
{"ipaddr": "119.101.112.180:9999"},
{"ipaddr": "119.101.114.4:9999"},
{"ipaddr": "119.101.116.205:9999"},
{"ipaddr": "119.101.113.70:9999"},
{"ipaddr": "119.101.112.4:9999"},
{"ipaddr": "221.206.100.133:34073"},
{"ipaddr": "119.101.115.28:9999"},
{"ipaddr": "119.101.112.64:9999"},
{"ipaddr": "119.101.113.202:9999"},
{"ipaddr": "119.101.117.203:9999"},
{"ipaddr": "140.207.25.114:41471"},
{"ipaddr": "119.101.115.70:9999"},
{"ipaddr": "110.52.235.35:9999"},
{"ipaddr": "119.101.115.237:9999"},
{"ipaddr": "119.101.113.56:9999"},
{"ipaddr": "58.50.3.174:9999"},
{"ipaddr": "119.101.115.196:9999"},
{"ipaddr": "119.101.118.120:9999"},
{"ipaddr": "119.101.115.114:9999"},
{"ipaddr": "119.101.112.12:9999"},
{"ipaddr": "119.101.118.147:9999"},
{"ipaddr": "119.101.118.151:9999"},
{"ipaddr": "125.109.195.174:33883"},
{"ipaddr": "180.118.77.29:9999"},
{"ipaddr": "119.101.117.93:9999"},
{"ipaddr": "119.101.113.97:9999"},
{"ipaddr": "180.118.77.11:9999"},
{"ipaddr": "119.101.112.43:9999"},
{"ipaddr": "110.52.235.165:9999"},
{"ipaddr": "119.101.113.11:9999"},
{"ipaddr": "119.101.112.152:9999"},
{"ipaddr": "115.46.74.18:8123"},
{"ipaddr": "119.101.116.140:9999"},
{"ipaddr": "119.101.112.109:9999"},
{"ipaddr": "119.101.115.87:9999"},
]

# 配置Scrapy执行的最大并发请求(默认值:16)
CONCURRENT_REQUESTS = 2

项目测试

redis去重测试

url去重是整个项目较为重要的功能,因为在这个项目中是进行全站爬取,提取页面的所有url,仅仅主页就包含了所有分类,而每个新闻页还会对应的推送新闻,所以平爬取量还是很大的,这是去重显得尤为重要。下面开始测试

1.启动mysql数据库

cmd 以管理员身份运行控制台,net start mysql

2.启动redis数据库

cmd 运行控制台 redis-server

3.清空数据库内容

4.启动项目

cd /d TengXunCrawl 定位到项目目录

将爬取主页设置为腾讯主页

scrapy crawl tcSpider 启动scrapy爬虫

redis-url指纹

redis-url待爬取列表 获取到了115条链接

mysql无记录

清空待爬取队列,因为队列为空时会再次爬取主页

再次启动爬虫程序,日志如下,没有爬取该页

E:\DataAnalysis\tools\python3\project\TengXunNews\TengXunCrawl>scrapy crawl tcSpider
2019-02-19 16:20:06 [scrapy.utils.log] INFO: Scrapy 1.5.1 started (bot: TengXunCrawl)
2019-02-19 16:20:06 [scrapy.utils.log] INFO: Versions: lxml 4.3.1.0, libxml2 2.9.5, cssselect 1.0.3, parsel 1.5.1, w3lib 1.20.0, Twisted 18.9.0, Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)], pyOpenSSL
 18.0.0 (OpenSSL 1.1.0j  20 Nov 2018), cryptography 2.4.2, Platform Windows-10-10.0.17134-SP0
2019-02-19 16:20:06 [scrapy.crawler] INFO: Overridden settings: {'BOT_NAME': 'TengXunCrawl', 'CONCURRENT_REQUESTS': 2, 'DUPEFILTER_CLASS': 'scrapy_redis.dupefilter.RFPDupeFilter', 'NEWSPIDER_MODULE': 'TengXunCrawl.spiders', 'SCHEDULER': 'scrapy
_redis.scheduler.Scheduler', 'SPIDER_MODULES': ['TengXunCrawl.spiders'], 'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)'}
2019-02-19 16:20:06 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.logstats.LogStats']
2019-02-19 16:20:07 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
 'TengXunCrawl.middlewares.Url_SM',
 'scrapy.downloadermiddlewares.retry.RetryMiddleware',
 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2019-02-19 16:20:07 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
 'scrapy.spidermiddlewares.referer.RefererMiddleware',
 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2019-02-19 16:20:07 [scrapy.middleware] INFO: Enabled item pipelines:
['scrapy_redis.pipelines.RedisPipeline']
2019-02-19 16:20:07 [scrapy.core.engine] INFO: Spider opened
2019-02-19 16:20:07 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2019-02-19 16:20:07 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2019-02-19 16:20:07 [scrapy_redis.dupefilter] DEBUG: Filtered duplicate request <GET https://news.qq.com/> - no more duplicates will be shown (see DUPEFILTER_DEBUG to show all duplicates)
2019-02-19 16:20:07 [scrapy.core.engine] INFO: Closing spider (finished)
2019-02-19 16:20:07 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'finish_reason': 'finished',
 'finish_time': datetime.datetime(2019, 2, 19, 8, 20, 7, 27028),
 'log_count/DEBUG': 2,
 'log_count/INFO': 7,
 'start_time': datetime.datetime(2019, 2, 19, 8, 20, 7, 15620)}
2019-02-19 16:20:07 [scrapy.core.engine] INFO: Spider closed (finished)

数据库内容不变

 

 测试成功,清空数据库。

内容页测试

爬取内容时报了错误,解决路径如下https://blog.csdn.net/okm6666/article/details/80618767,

谢谢这位仁兄

host替换为任意内容主页,然后启动爬虫项目,如下

 redis-url指纹

redis-url队列

mysql记录

测试成功,清空数据库

分布式测试

在路由器下进行分布式测试,需要准备改变几个设置。

第一,要能ping通另一台电脑,具体百度修改防护墙

第二,编辑mysql用 户,将主机改为任意

第三,修改redis配置文件如下。第一个是取消绑定主机,第二个是取消保护。

第四,需要修改另一台笔记本的部分代码,比如引入文件的绝对路径,连接数据库主机的ip,可以在主机ipcongfig可查看

开始爬取,主机先启动一次爬虫,以便队列中有url

 依次启动Master和矿机,可以看到待爬取队列在一直变化,访问过的url指纹也在增加,mysql也有更新

矿机

Master

好累写了这么多谢谢收看,代码连接

https://github.com/GuoHongYuan/TengXunNews

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海人001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值