一个用 pygame 实现的弹幕式数据可视化动画框架

一个用pygame实现的弹幕式数据可视化动画框架


需要 Python 环境+ Pygame 库,点击即可运行。效果见B站视频: 中国古代历史人物生卒时间可视化

动画代码

from msilib.schema import Font
import pygame,sys,random,datetime,os
from time import sleep,time
from random import randint
from pygame.locals import *#引用后可以直接输入按键名
from heapq import heappop,heappush,heapreplace
from math import inf,ceil,sqrt
from win32.win32api import GetSystemMetrics

soundDir="D:\\Music\\"
songs=[
    "杨洪基 - 滚滚长江东逝水.mp3",
    "朱桦 - 意中人.mp3",
]

backgroundColor=(250,250,250)
color_WHITE=(255,255,255)
color_RED=(255,0,0)
color_GRAY=(200,200,200)
color_DarkGRAY=(100,100,100)
windowSize=[int(1*GetSystemMetrics(0)),int(1*GetSystemMetrics(1))]
topMargin,bottomMargin=[min(windowSize)//18,max(windowSize)//60]
mainHeight=windowSize[1]-topMargin-bottomMargin
midXLine=windowSize[0]//2

textSize=max(windowSize)//60
titleTextSize=2*textSize
titleTopMargin=0
FPS=30#60
animationTime=FPS#创建新矩形、消除旧矩形的时间都是1秒,

FPSCLOCK=pygame.time.Clock()
BACKGROUND=pygame.display.set_mode(windowSize)
SCREEN=pygame.display.set_mode(windowSize)

def MinkowskiDis(v1:list[int],v2:list[int])->int:
    return sum(abs(v1[i]-v2[i]) for i in range(len(v1)))

def loadHistoryDatas():#peopleData=[人名,出生时间,死亡时间,是否精确]
    peopleData=[]
    with open("历史人物生卒时间.txt",'r',encoding='utf-8') as f:
        peopleData=eval(f.readline())
        print(f"从文件中加载完毕,{len(peopleData)}项")
        f.close()
    return peopleData

class HistoryVisualization:
    def __init__(self,peopleData=[]):
        self.pauseState=True
        self.timeWindowWidth=110#当前页面宽度对应的时间
        self.timeGridWidth=10
        self.curLeftTime=-2070-self.timeWindowWidth
        self.timeStep0=0.51
        self.timeStep=self.timeStep0
        if len(peopleData):
            self.peopleData=peopleData
        else:
            self.peopleData=loadHistoryDatas()#[人名,出生年份,去世年份,是否可信],及其耗时
        self.historyDataInitial(isNew=(len(peopleData)==0))
        self.curPlayState=0#共有4个状态,分别是标题、正式排序、收缩总结、结尾
        self.stateTime=[2.9*FPS,180*FPS,10*FPS,inf]
        self.marginRate=0.5 # 条目之间的排布,条目过多时要压缩
        self.defaultRowNum=20
        self.rectRowNum=self.rectRowNum0=20
        self.rowsFreeTime=[self.curLeftTime]*self.rectRowNum
        self.rowsFreeTimeHeap=[[self.curLeftTime,i] for i in range(self.rectRowNum)]
        self.rectHeight=self.targetRectHeight=mainHeight/((1+self.marginRate)*self.rectRowNum+self.marginRate)

    def historyDataInitial(self,isNew=True):
        self.startYear=min(-2070,min(i[1] for i in self.peopleData)-40)
        self.endYear=max(1910,max(i[2] for i in self.peopleData)+40)
        self.curLeftTime=min(self.curLeftTime,self.startYear)
        self.dataIdx=0#遍历到的索引
        self.hp=[]#按开始时间入堆,按结束时间出堆
        tp=[]
        if isNew:#未经处理
            self.peopleData+=tp+[["空心表示时间不确定,仅供参考",self.startYear+self.timeWindowWidth//2-20,self.startYear+self.timeWindowWidth//2+20,0],['今',2024,2025,1]]
            self.peopleData.sort(key=lambda x:(x[1],x[2]))
        n=len(self.peopleData)
        self.dataRowIdx=[0]*n#每个数据标签都在一个固定的行中
        importantPeopleDc=set(["黄帝","炎帝","大禹","老子","孔子","屈原","嬴政","秦始皇","刘彻","卫青","霍去病","诸葛亮","李世民","武则天","李白","杜甫","苏轼","辛弃疾","朱元璋","王阳明","孙中山","毛泽东",'今'])
        self.peopleDataColors=[[randint(0,255) for j in range(3)] for i in range(n)]
        for i in range(n):
            if any(name in importantPeopleDc for name in self.peopleData[i][0].split(",")):
                self.peopleDataColors[i]=color_RED
            else:
                while min(MinkowskiDis(self.peopleDataColors[i],backgroundColor),MinkowskiDis(self.peopleDataColors[i],color_RED))<60:
                    self.peopleDataColors[i]=[randint(0,255) for j in range(3)]
        
        #计算人名、称号长度
        self.dataNameLen=[0]*n#统计长度方便右对齐,可能有()、数字
        for i in range(n):
            sm=0
            for c in self.peopleData[i][0]:
                if c in "+-":
                    sm+=0.6
                elif c in "()" or c.isdigit():
                    sm+=0.5
                else:
                    sm+=1
            self.dataNameLen[i]=sm

        #时代信息
        self.eraData=[['夏朝', -2070, -1600], ['商朝', -1600, -1046], ['西周', -1046, -771],['春秋',-770,-476],['战国',-475,-221], ['秦朝', -221, -207], ['西汉', -206, 8], ['新朝', 8, 23],['玄汉',23,25], ['东汉', 25,220],['三国',220,265] ,['西晋', 265,316],['东晋',317,420],['南北朝',420,581], ['隋朝', 581,618], ['唐朝', 618,690], ['武周', 690,705], ['唐朝', 705,907],['五代十国',907,960], ['北宋', 960,1127],['南宋',1127,1279], ['元朝', 1279,1368], ['明朝', 1368,1644], ['清朝', 1644,1911]]
        self.eraLeftIdx=0
        self.eraRightIdx=0
        n=len(self.eraData)
        self.eraDataColors=[[randint(0,255) for j in range(3)] for i in range(n)]
        for i in range(n):
            while MinkowskiDis(self.eraDataColors[i],backgroundColor)<60:
                self.eraDataColors[i]=[randint(0,255) for j in range(3)]
        
        idx=self.eraData.index(['武周', 690,705])#唐朝颜色统一
        if idx!=-1:
            self.eraDataColors[idx]=self.eraDataColors[idx-1]
            self.eraDataColors[idx+1]=self.eraDataColors[idx-1]

    def plotText(self,pos,label,color=[0,0,0],textSize=30,fontName='simhei'):
        """绘制文本,不支持传入Font"""
        FONT=pygame.font.Font(pygame.font.match_font(fontName),int(textSize))
        textInfo=FONT.render(label,True,color)
        SCREEN.blit(textInfo,pos)

    def getXPosFromTime(self,t):
        return windowSize[0]*(t-self.curLeftTime)/self.timeWindowWidth

    def plotBackground(self):
        """
        静态背景,预想中只用绘制一次
        """
        SCREEN.fill(backgroundColor)
        if self.curPlayState<2:
            FONT=pygame.font.Font(pygame.font.match_font('simhei'),titleTextSize)
            textInfo=FONT.render('时代:',True,color_DarkGRAY)
            SCREEN.blit(textInfo,(0,titleTopMargin))
        #左下角
        if self.curPlayState<3:
            FONT=pygame.font.Font(pygame.font.match_font('simhei'),textSize)
            textInfo=FONT.render('公元',True,color_DarkGRAY)
            SCREEN.blit(textInfo,(0,windowSize[1]-textSize))
        
    def plotScreenBackground(self):
        """
        运动的背景,背景线等
        """
        FONT=pygame.font.Font(pygame.font.match_font('simhei'),textSize)
        t=self.curLeftTime%self.timeGridWidth
        startTime=int(self.curLeftTime+((self.timeGridWidth-t) if t else 0))#线的开始位置
        for t in range(startTime,ceil(self.curLeftTime+self.timeWindowWidth)+self.timeGridWidth,self.timeGridWidth):
            x=self.getXPosFromTime(t)
            pygame.draw.line(SCREEN,color_GRAY,[x,topMargin],[x,windowSize[1]-bottomMargin])
            text=str(t).replace('-','前')
            x-=len(text)*textSize/4
            if x>=textSize*2:
                textInfo=FONT.render(text,True,color_DarkGRAY)
            else:#颜色渐变
                newColor=self.colorDilutionByDis2Left(color_DarkGRAY,x,2*textSize)
                textInfo=FONT.render(text,True,newColor)
            SCREEN.blit(textInfo,(x,windowSize[1]-bottomMargin))

    def colorDilutionByDis2Left(self,color,x,startX):
        """
        根据到左端点的距离x将原颜色逐渐白化
        """
        x=max(x,0)
        p=x/startX
        res=[255*(1-p)+i*p for i in color]
        return res

    def 绘制人物标签圆角矩形(self,pos,recSize,color=[0,0,0],border_radius=5,labelXPosOffset=10,labelIdx=-1):
        if min(recSize)<1:
            return
        width=0 if self.peopleData[labelIdx][3] else int(recSize[1]*3/7)#不可信、有争议的时间空心表示
        pygame.draw.rect(SCREEN,color,[pos[0],pos[1],recSize[0],recSize[1]],border_radius=int(border_radius),width=width)
        
        textSize=recSize[1]
        nameHalfLen=len(self.peopleData[labelIdx][0])*textSize/2
        textSize1=min(recSize[1],recSize[0]/len(self.peopleData[labelIdx][0]))
        midx=windowSize[0]/2
        xl=self.getXPosFromTime(self.peopleData[labelIdx][1])
        xr=self.getXPosFromTime(self.peopleData[labelIdx][2])
        if self.curPlayState!=1 or textSize1<textSize:#人名居中
            x=(xl+xr-textSize1*len(self.peopleData[labelIdx][0]))/2
        elif xl+nameHalfLen+textSize/2>=midx:#右
            x=xl+textSize/2
        elif xr-1*nameHalfLen-textSize/2<=midx:#左
            x=xr-len(self.peopleData[labelIdx][0])*textSize-textSize/2
        else:#中
            x=midx-nameHalfLen
        self.plotText([x,pos[1]+(recSize[1]-textSize1)/2],self.peopleData[labelIdx][0],color=color_WHITE,textSize=textSize1,fontName='simhei')#中

    def plotExistRect(self):
        """
        包括时代标签和人物标签
        所有的人物标签等高
        """
        for i in range(self.eraLeftIdx,self.eraRightIdx+1,1):#时代
            x=self.getXPosFromTime(self.eraData[i][1])
            w=self.getXPosFromTime(self.eraData[i][2])-x
            pygame.draw.rect(SCREEN,self.eraDataColors[i],(x,topMargin,w,mainHeight),border_radius=textSize)

            eraNameLen=len(self.eraData[i][0])
            titleTextSize1=min(titleTextSize,w/eraNameLen)#宽度不够时减小字体
            FONT=pygame.font.Font(pygame.font.match_font('simhei'),int(titleTextSize1))
            p=2.5
            if self.curPlayState!=1 or titleTextSize1<titleTextSize:#阶段3居中,空间不够时居中
                eraName=FONT.render(self.eraData[i][0],True,self.eraDataColors[i])#政权
                SCREEN.blit(eraName,(x+w/2-eraNameLen*titleTextSize1/2,titleTopMargin+(titleTextSize-titleTextSize1)/2))
            elif x+w>p*titleTextSize+eraNameLen*titleTextSize1:
                eraName=FONT.render(self.eraData[i][0],True,self.eraDataColors[i])#政权
                SCREEN.blit(eraName,(max(x,p*titleTextSize),titleTopMargin))
            else:
                newColor=self.colorDilutionByDis2Left(self.eraDataColors[i],x+w-eraNameLen*titleTextSize1,titleTextSize*p)
                eraName=FONT.render(self.eraData[i][0],True,newColor)#政权
                SCREEN.blit(eraName,(x+w-eraNameLen*titleTextSize1,titleTopMargin))
        self.plotScreenBackground()
        topOffset=topMargin+self.rectHeight*self.marginRate
        border_radius=self.rectHeight//2 if self.curPlayState<2 else 0
        for et,i in self.hp:#人物
            x=self.getXPosFromTime(self.peopleData[i][1])
            width=self.getXPosFromTime(self.peopleData[i][2])-x
            top=topOffset+self.dataRowIdx[i]*self.rectHeight*(1+self.marginRate)
            self.绘制人物标签圆角矩形([x,top],[width,self.rectHeight],self.peopleDataColors[i],labelIdx=i,border_radius=border_radius)

    def playState0(self):#标题
        SCREEN.fill(backgroundColor)
        p=6
        FONT=pygame.font.Font(pygame.font.match_font('隶书'),p*textSize)
        textInfo=FONT.render('中国古代历史人物',True,color_DarkGRAY)
        SCREEN.blit(textInfo,((windowSize[0]-8*p*textSize)//2,(windowSize[1]-2*p*textSize)//2))
        textInfo=FONT.render('时间',True,[0,0,0])
        SCREEN.blit(textInfo,((windowSize[0]-0*p*textSize)//2,(windowSize[1]+1*textSize)//2))
        textInfo=FONT.render('生',True,[0,150,0])
        SCREEN.blit(textInfo,((windowSize[0]-4*p*textSize)//2,(windowSize[1]+1*textSize)//2))
        textInfo=FONT.render('卒',True,[0,0,140])
        SCREEN.blit(textInfo,((windowSize[0]-2*p*textSize)//2,(windowSize[1]+1*textSize)//2))
        if self.c:#已经绘制了至少1次
            self.curPlayStateHasEnd[self.curPlayState]=True

    def playState1(self):#滑窗
        if self.curPlayStateHasEnd[self.curPlayState]:#为滑窗阶段结束时等待时间出现的行号整体变大bug添加的补丁
            return
        self.curLeftTime+=self.timeStep

        while self.rectRowNum>self.defaultRowNum and self.rowsFreeTime[self.rectRowNum-1]<=self.curLeftTime:
            self.rectRowNum-=1#左移代替删除

        while len(self.hp) and self.hp[0][0]<=self.curLeftTime:#左边界出堆
            heappop(self.hp)
        while self.dataIdx<len(self.peopleData) and self.peopleData[self.dataIdx][1]<=self.curLeftTime+self.timeWindowWidth:#右侧入堆
            if self.dataRowIdx[self.dataIdx] or self.curPlayStateHasEnd[self.curPlayState]:#为滑窗阶段结束时等待时间出现的行号整体变大bug添加的补丁,已分配了行号或阶段结束时退出
                break
            startTime=self.peopleData[self.dataIdx][1]
            endTime=self.peopleData[self.dataIdx][2]
            heappush(self.hp,[endTime,self.dataIdx])
            #分配行号
            if self.rowsFreeTimeHeap[0][0]<startTime:#也许留点空白空间更好
                rowIdx=self.rowsFreeTimeHeap[0][1]
                heapreplace(self.rowsFreeTimeHeap,[endTime,rowIdx])
            else:
                for i in range(self.rectRowNum0,self.rectRowNum,1):
                    if self.rowsFreeTime[i]<startTime:
                        rowIdx=i
                        break
                else:
                    rowIdx=self.rectRowNum
                    self.rectRowNum+=1
                if self.rectRowNum>len(self.rowsFreeTime):
                    self.rowsFreeTime.append(endTime)
            p=0#靠后的行延迟添加新元素,来减少无效行,增大文字
            self.rowsFreeTime[rowIdx]=endTime+p*rowIdx
            self.dataRowIdx[self.dataIdx]=rowIdx
            self.dataIdx+=1
        self.refreshRectHeightAndMarginRate()
        """每个矩形占据一个位置,所有位置满了之后,需要增加行数"""

        if self.eraLeftIdx<len(self.eraData) and self.eraData[self.eraLeftIdx][2]<self.curLeftTime:#绘制的时代信息刷新
            self.eraLeftIdx+=1
        if self.eraRightIdx<len(self.eraData)-1 and self.eraData[self.eraRightIdx][1]<self.curLeftTime+self.timeWindowWidth:
            self.eraRightIdx+=1

    def refreshRectHeightAndMarginRate(self):
        self.marginRate=max(0,3/4-self.rectRowNum/80)#20->0.5,60->0
        self.targetRectHeight=mainHeight/((1+self.marginRate)*self.rectRowNum+self.marginRate)

    def playState2(self):#压缩汇总
        if self.curLeftTime<=self.startYear:
            sleep(1)#减少无效重绘,显著降低功耗
            return
        self.curLeftTime=max(self.curLeftTime-1*self.timeStep0,self.startYear)#加速
        self.timeStep0*=1.05
        self.timeWindowWidth=max(self.endYear-self.curLeftTime,self.timeWindowWidth)
        
        while self.timeWindowWidth>self.timeGridWidth*14:
            self.timeGridWidth*=2

        while self.eraLeftIdx and self.eraData[self.eraLeftIdx-1][2]>self.curLeftTime:#绘制的时代信息刷新
            self.eraLeftIdx-=1

        while self.dataIdx<len(self.peopleData)-1 and self.peopleData[self.endYearSortArr[self.dataIdx]][2]>self.curLeftTime:#按死亡时间从晚到早遍历,最后一个是注释,不显示
            idx=self.endYearSortArr[self.dataIdx]
            self.hp.append([0,idx])
            self.dataIdx+=1
            self.rectRowNum=max(self.rectRowNum,1+self.dataRowIdx[idx])
        self.refreshRectHeightAndMarginRate()

    def playState3(self):#结尾字幕
        p=2
        FONT=pygame.font.Font(pygame.font.match_font('simhei'),p*textSize)
        referenceData=[
            ['数据来源:',"百度百科,历史人物网,趣历史,全历史"],
            ['类似项目:','全历史,历史车轮网'],
            ['动画实现方式:','Python,Pygame'],
            ['代码地址:','https://blog.csdn.net/qq_21119609/'],
            [' ',        'article/details/138298765'],
            ['背景音乐:',songs[0][:-4]],
            [" ",songs[1][:-4]],
        ]
        xOffset=3*p*textSize
        yOffset=4*p*textSize
        for i in range(len(referenceData)):
            y=yOffset+(p+1)*textSize*i
            leftText=FONT.render(referenceData[i][0],True,color_DarkGRAY)
            SCREEN.blit(leftText,(xOffset+(7-len(referenceData[i][0]))*p*textSize,y))
            rightText=FONT.render(referenceData[i][1],True,[0,0,0])
            SCREEN.blit(rightText,(xOffset+7*p*textSize,y))
        sleep(1)

    def main(self,pauseState=False):
        pygame.init()
        self.pauseState=pauseState
        self.curPlayStateHasEnd=[False]*4
        self.playStateEndWaitTime=[3,1,5,inf]#每个阶段结束后的等待时间
        self.stateStartaAcelerateFPS=[1,20,20,1]
        songIdx=0
        songStep=1#正序播或倒序播
        self.c=0
        FPSCLOCK.tick(FPS)  # 设置帧率为60
        self.timeMarker=time()
        self.targetTimeStep=0#初始速度为0,有个加速过程
        pygame.mixer.music.stop()
        while True:
            if self.curPlayStateHasEnd[self.curPlayState]==True and self.rectHeight==self.targetRectHeight:# and self.timeStep==self.targetTimeStep:#动画已完成
                if self.playStateEndWaitTime[self.curPlayState]>=1:
                    self.playStateEndWaitTime[self.curPlayState]-=1
                    sleep(1)#等待后再绘制
                else:
                    if self.curPlayState==1:
                        self.hp.clear()
                        self.endYearSortArr=sorted(range(len(self.peopleData)),key=lambda x:-self.peopleData[x][2])#人物数据按结束时间排序
                        self.dataIdx=0
                    self.targetTimeStep=0
                    self.curPlayState+=1
                    self.c=0
            self.plotBackground()
            for event in pygame.event.get():
                if event.type==QUIT or (event.type==KEYUP and event.key==K_ESCAPE):
                    pygame.quit()
                    sys.exit()
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_r:
                        self.__init__(self.peopleData)
                        self.main()
                    elif event.key==pygame.K_p or event.key==pygame.K_SPACE:
                        self.pauseState^=True
                        self.timeMarker=time()-1/FPS
            
            if self.pauseState:
                sleep(1)
                continue
            
            self.c+=1
            if self.targetTimeStep<self.timeStep0:#加速和自适应调整可能有冲突?
                self.targetTimeStep+=min(self.timeStep0-self.targetTimeStep,self.timeStep0/FPS/15,(self.targetTimeStep+0.01)*0.01)
            timeCost=time()-self.timeMarker#根据绘制标签数量、预期速度、实际速度来调整时间步进,效果良好
            delayRate=timeCost*FPS
            self.timeStep=self.targetTimeStep*(delayRate**0.5)#多步调控
            self.timeMarker=time()
            if self.rectHeight!=self.targetRectHeight:
                pn=1 if (self.targetRectHeight>self.rectHeight) else -1
                self.rectHeight+=pn*min(abs(self.targetRectHeight-self.rectHeight), 0.1*delayRate)
            
            if self.curPlayState==0:
                self.playState0()
            elif self.curPlayState>=3:
                self.playState3()
            else:
                if self.curPlayState==1:
                    self.playState1()
                else:
                    self.playState2()
                self.plotExistRect()
            songsNum=len(songs)
            if songsNum and (not pygame.mixer.music.get_busy()) and self.curPlayState:#标题界面过了之后再播放
                songPath=soundDir+songs[songIdx%songsNum]
                try:
                    pygame.mixer.music.load(songPath)
                    pygame.mixer.music.play()
                    songIdx=(songIdx+songStep)%songsNum
                except:
                    songs.pop(songIdx%songsNum)
            
            pygame.display.update()#重绘变动区域

            #检测是否需要更换状态
            if self.curPlayState==1:
                if self.curLeftTime+self.timeWindowWidth>self.endYear:
                    self.curPlayStateHasEnd[self.curPlayState]=True
            elif self.curPlayState==2:
                if self.curLeftTime<=self.startYear and self.timeWindowWidth>2000:
                    self.curPlayStateHasEnd[self.curPlayState]=True

if __name__=="__main__":
    HistoryVisualization().main(0)

爬虫代码

import time,xlwings
from selenium import webdriver
from urllib import parse
from selenium.webdriver.common.by import By
from heapq import heappop,heappush,heapify
from math import inf,exp
from random import randint,uniform
from collections import defaultdict

options = webdriver.EdgeOptions()
options.add_experimental_option('excludeSwitches', ['enable-logging'])#只是关闭了提示,没解决bug
driver = webdriver.Edge( options=options)

"""
本项目从网页搜索指定信息的方式主要是By.CSS_SELECTOR,具体的路径是经常在改变的,需要重新从网页获取。
以Edge浏览器为例,获取路径的方式是在网页点击右键、进入检查,然后找到需要查的区块,点击右键,复制、点击复制selector,就可以得到类似于项目中
        path="#side > div.lemmaStatistics__38_y > div.description_EUJr_ > div:nth-child(1)"
的CSS_SELECTOR。

如果没弄懂的话,可以继续阅读其他教程blog,例如https://blog.csdn.net/qq_16519957/article/details/128740502 。
"""

def 解析百度百科时间信息(timeInfo,onlySaveYear=True):
    """
    返回解析后的时间,是否可靠,没查到年份将输出年份为-5000
    """
    isPrecise=int(not any(i in timeInfo for i in "约或??不"))
    digit=""
    year=-5000
    month=-1
    day=-1
    for idx,i in enumerate(timeInfo):
        if i.isdigit():
            digit+=i
            continue
        if i=='年' and len(digit):
            year=int(digit)
            if '前' in timeInfo[:idx]:
                year*=-1
            if onlySaveYear:
                break
        elif i=='月' and len(digit):
            month=int(digit)
        elif i=='日' and len(digit):
            day=int(digit)
        digit=""
    if year==-5000 and len(digit):
            year=int(digit)
            year=-year if ('前' in timeInfo) else year
            
    if onlySaveYear:
        return [year,isPrecise,timeInfo]
    return [[year,month,day],isPrecise,timeInfo]
      
def 百度百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name='李世民',onlySaveYear=True,relatedPersonNumLimit=5,externUrl=""):
    url = 'https://baike.baidu.com/item/'+name
    if len(externUrl):
        url=externUrl
    driver.get(url)
    try:#姓名重定向,替换掉输入的姓名
        namePath="#J-lemma-main-wrapper > div:nth-child(1) > div > div > div.contentTop_LWW5g > div.lemmaTitleWrap_pzoxh.normalText_E1tN4.normal > div.lemmaTitleBox_lWCUO > h1"
        s = driver.find_element(By.CSS_SELECTOR,namePath).text
        if len(s):
            if name!=s:
                print(f"重定向到了{s}")
            name=s
    except:
        pass
    print("当前百度查找:",name)
    相关人物=[[],[]]#亲属和同行分开存放,赋予不同的权重
    浏览次数=-1  
    def 百度百科查找生卒时间():
        birthTime=-5000
        deadTime=-5000
        findAll=0
        c=1
        for direction in ['right','left']:
            if findAll==3:break
            for c in range(1,100):
                if findAll==3:
                    break
                try:
                    path=f"#J-lemma-main-wrapper > div.contentWrapper_bjHAx > div > div.mainContent_rQP1X > div > div.basicInfo_WXCD7.J-basic-info > dl.basicInfoBlock_Svhka.{direction} > div:nth-child({c})"
                    s = driver.find_element(By.CSS_SELECTOR,path).text
                    if len(s)>4:
                        if s[:4]=='出生日期':
                            birthTime=s[s.index('\n')+1:]
                            findAll|=1
                        if s[:4]=='逝世日期':
                            deadTime=s[s.index('\n')+1:]
                            findAll|=2
                except:
                    break
        if findAll&1:
            birthTimeInfo=解析百度百科时间信息(birthTime,onlySaveYear)
        else:
            birthTimeInfo=[birthTime,0,""]
        if findAll&2:
            deadTimeInfo=解析百度百科时间信息(deadTime,onlySaveYear)
        else:
            deadTimeInfo=[deadTime,0,""]
        return birthTimeInfo,deadTimeInfo
  
    birthTime,deadTime=百度百科查找生卒时间()
    try:#查询相关人物
        c=1#从1开始
        while c<=relatedPersonNumLimit:
            path=f"#J-lemma-human-relation > div.relationContainer_qvpLm.complexContainer_lSWko.notMemorialContainer_dFBPC.specialContainer_k9FMk > div.swiper.swiper-initialized.swiper-horizontal.swiper-pointer-events > div > div:nth-child({c}) > a > div.relationDesc_S1A4J > div.relationTitle_MWRw1"
            relatedPerson=driver.find_element(By.CSS_SELECTOR,path).text
            relatedPerson=relatedPerson.replace(" ","")#部分人名前后带有空格
            c+=1
            if not len(relatedPerson):
                break
            相关人物[0].append(relatedPerson)
        ""
    except:
        # print(f"查找{name}相关人物失败")
        pass
    try:#查询相关星图人物
        c=1#从1开始
        while c<=relatedPersonNumLimit:
            path=f"#J-lemma-starmap > div.starMapContainer_Pvdj1.halfContainer_qHsks > div.swiper.swiper-initialized.swiper-horizontal.swiper-pointer-events.swiper-backface-hidden > div > div.swiper-slide.starMapSlide_Npsei.swiper-slide-active > div.starMapList_jyNmr.complexListWrap_Zsrr0 > a:nth-child({c}) > div.complexItem_JucbE > div > div.starMapMultiItemInfoTitle_eurQr"
            relatedPerson=driver.find_element(By.CSS_SELECTOR,path).text
            relatedPerson=relatedPerson.replace(" ","")
            c+=1
            if not len(relatedPerson):
                break
            相关人物[1].append(relatedPerson)
        ""
    except:
        # print(f"查找{name}相关人物失败")
        pass
    try:#查询浏览次数
        path="#side > div.lemmaStatistics__38_y > div.description_EUJr_ > div:nth-child(1)"
        浏览次数=driver.find_element(By.CSS_SELECTOR,path).text
        浏览次数=int(浏览次数[浏览次数.index(":")+1:-1])
    except:
        print(f"查找{name}词条浏览次数失败")
        if 'int' not in str(type(浏览次数)):
            浏览次数=-1
    备注=""
    try:#查询备注
        path="#lemmaDesc > div"
        备注=driver.find_element(By.CSS_SELECTOR,path).text
        备注=备注.replace(" ","")
    except:
        print(f"查找{name}词条备注失败")
    if 1 and birthTime[0]==deadTime[0]==-5000 and 浏览次数==-1:#有多个子项,要跳转到具体项目
        if 1:#循环查找
            c=1
            while True:
                try:
                    path=f"#content > div > div > a:nth-child({c})"
                    externUrl=driver.find_element(By.CSS_SELECTOR,path).get_attribute("href")
                    res=百度百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name=name,externUrl=externUrl)
                    if any(-5000<res[i][0]<1910 for i in range(1,3)) and res[4]>0:#有效时间,有浏览次数
                        return res
                except:
                    break
                c+=1
        else:#输入查找
            ipt=input(f"查询{name}信息失败,请输入义项序号(从1开始)或正确的url:")
            if ipt.isdigit()==False or int(ipt)>0:
                if ipt.isdigit():
                    cssPath=f"#content > div > div > a:nth-child({int(ipt)})"
                    externUrl=driver.find_element(By.CSS_SELECTOR,cssPath).get_attribute("href")
                else:
                    externUrl=ipt
                return 百度百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name=name,externUrl=externUrl)
    elif 0 and birthTime[0]==deadTime[0]==-5000:#没查到,手动输入
        arr=[int(i) for i in input("查询失败,请手动输入生卒时间及是否精确4个数字:").split()]
        while len(arr)!=4:
            arr=[int(i) for i in input("查询失败,请手动输入生卒时间及是否精确4个数字:").split()]
        birthTime[0:2]=arr[0:2]
        deadTime[0:2]=arr[2:4]
    print(f"{birthTime=}{deadTime=}")
    print(f'{相关人物=}')
    print(f'{浏览次数=},{备注=}')
    return name,birthTime,deadTime,相关人物,浏览次数,备注
      
def 搜狗百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name='李世民',onlySaveYear=True,relatedPersonNumLimit=5,externUrl="",canRecursion=True):#经常查询失败,仅作为备份
    url = 'https://baike.sogou.com/v29743.htm?fromTitle='+name
    if len(externUrl):
        url=externUrl
    driver.get(url)

    clearPath="#Form > a"#重新搜索
    driver.find_element(By.CSS_SELECTOR, clearPath).click()
    inputPath="#searchText"
    inputSector=driver.find_element(By.CSS_SELECTOR,inputPath).send_keys(name)
    btnPath="#enterLemma"
    searchBtn=driver.find_element(By.CSS_SELECTOR, btnPath).click()

    time.sleep(exp(randint(-5,-3)))#加载一会儿
    try:#尝试点击第一个条目
        path="#ambi_items > li:nth-child(1) > a"
        driver.find_element(By.CSS_SELECTOR,path).click()
        time.sleep(exp(randint(-5,-3)))
    except:
        print(f"点击{name}第一个条目失败")
        pass
    try:#姓名重定向,替换掉输入的姓名
        namePath="#title"
        s = driver.find_element(By.CSS_SELECTOR,namePath).text
        if len(s):
            if s=='秦始皇' and canRecursion:
                return 搜狗百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name,canRecursion=False)
            if name!=s:
                print(f"重定向到了{s}")
            name=s
    except:
        pass
    print("当前搜狗查找:",name)
    相关人物=[[],[]]#亲属和同行分开存放,赋予不同的权重
    浏览次数=-1  
    def 搜狗百科查找生卒时间():
        birthTime=-5000
        deadTime=-5000
        findAll=0
        c=1
        for direction in [1,2]:
            if findAll==3:break
            for c in range(1,100):
                if findAll==3:
                    break
                try:
                    path=f"#baseInfoCol > table > tbody > tr > td:nth-child({direction}) > table > tbody > tr:nth-child({c})"#从1开始
                    s = driver.find_element(By.CSS_SELECTOR,path).text
                    if len(s)>4:
                        if s[:4]=='出生日期':
                            birthTime=s[s.index('\n')+1:]
                            findAll|=1
                        if s[:4]=='逝世日期':
                            deadTime=s[s.index('\n')+1:]
                            findAll|=2
                except:
                    break
        if findAll&1:
            birthTimeInfo=解析百度百科时间信息(birthTime,onlySaveYear)
        else:
            birthTimeInfo=[birthTime,0,""]
        if findAll&2:
            deadTimeInfo=解析百度百科时间信息(deadTime,onlySaveYear)
        else:
            deadTimeInfo=[deadTime,0,""]
        return birthTimeInfo,deadTimeInfo
  
    birthTime,deadTime=搜狗百科查找生卒时间()
    
    #搜狗百科没有相关人物
    
    try:#查询浏览次数
        path="#side > div.lemmaStatistics__38_y > div.description_EUJr_ > div:nth-child(1)"
        path="#side_bar > div:nth-child(3) > div.lemma_info > ul > li:nth-child(1)"
        浏览次数=driver.find_element(By.CSS_SELECTOR,path).text
        浏览次数=int(浏览次数[浏览次数.index(":")+1:-1])
    except:
        print(f"查找{name}词条浏览次数失败")
        if 'int' not in str(type(浏览次数)):
            浏览次数=-1
    备注=""
    try:#查询备注
        path="#main > div.main_wrap > div.lemma_container > div.abstract_wrap > div.current_semantic_item"
        备注=driver.find_element(By.CSS_SELECTOR,path).text
        备注=备注.replace(" ","")
    except:
        print(f"查找{name}词条备注失败")
    if 0 and birthTime[0]==deadTime[0]==-5000 and 浏览次数==-1:#有多个子项,要跳转到具体项目
        ipt=input(f"查询{name}信息失败,请输入义项序号(从1开始)或正确的url:")
        if ipt.isdigit()==False or int(ipt)>0:
            if ipt.isdigit():
                cssPath=f"#content > div > div > a:nth-child({int(ipt)})"
                externUrl=driver.find_element(By.CSS_SELECTOR,cssPath).get_attribute("href")
            else:
                externUrl=ipt
            return 搜狗百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name=name,externUrl=externUrl)
    elif 0 and birthTime[0]==deadTime[0]==-5000:#没查到,手动输入
        arr=[int(i) for i in input("查询失败,请手动输入生卒时间及是否精确4个数字:").split()]
        while len(arr)!=4:
            arr=[int(i) for i in input("查询失败,请手动输入生卒时间及是否精确4个数字:").split()]
        birthTime[0:2]=arr[0:2]
        deadTime[0:2]=arr[2:4]
    print(f"{birthTime=}{deadTime=}")
    print(f'{相关人物=}')
    print(f'{浏览次数=},{备注=}')
    return name,birthTime,deadTime,相关人物,浏览次数,备注

splitKey="帝后皇汗子宗王公主" # 将"秦始皇嬴政" 类的 称号+姓名组合词拆分成两个词

def trySplitName(s):#尝试分词
    if len(s)<5:#同时写了姓名和称号
        return [s]
    if s[-1]==')':
        idx=s.find("(")
        if 1<idx<len(s)-3:
            return [s[:idx],s[idx+1:-1]]
    idx=s[2:].find(s[0])
    if idx!=-1 and 1<idx+2<len(s)-1:#属于后一个词语
        return [s[:idx+2],s[idx+2:]]
    for j in splitKey:#属于前一个词语
        idx=s.find(j)
        if 1<idx<len(s)-2:#不存在单字姓名
            return [s[:idx+1],s[idx+1:]]
    return [s]

def 百度百科加搜狗百科查找历史人物网生卒信息_边查边存(findNum=8000):#还得先读回来
    urlBaidu = 'https://baike.baidu.com/item/'
    urlSougou="https://baike.sogou.com/v7056.htm?ch=ch.bk.amb&fromTitle="
    labelArr=['姓名','百度出生时间','出生时间是否精确','原始出生信息','去世时间','去世时间是否精确','原始去世信息','浏览次数','网址','备注']+['姓名长度','年龄']+['搜狗出生时间','出生时间是否精确','原始出生信息','去世时间','去世时间是否精确','原始去世信息','浏览次数','网址','备注']
    excelFile=xlwings.Book("中国古代历史人物生卒信息汇总_百度百科+搜狗百科.xlsx")#需要手动创建同名文件
    sheet=excelFile.sheets("Sheet1")
    def findStartRow(sheet):#查找空白行
        c=2
        while sheet.range(f'A{c}').value:
            val=sheet.range(f'M{c}').value
            if 0 and (not val or int(val) in [-259]):#补充搜狗百科信息
                people=trySplitName(sheet.range(f'A{c}').value)
                print(f"\n当前行:{c},需要查找:{people}")
                people=people[0]
                people,birthTime,deadTime,相关人物,浏览次数,备注=搜狗百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(people)
                newRow=list(birthTime)+list(deadTime)+[浏览次数,f"""=HYPERLINK("{urlSougou}{people}")""",备注]
                sheet.range('M'+str(c)).value=newRow
                excelFile.save()
                
            if 0 and [int(sheet.range(f'{i}{c}').value) for i in "BCEF"]==[-5000,0,-5000,0]:#重写无效行
                print(f"当前行{c},需要重新查询,人物热度{int(sheet.range(f'H{c}').value)}")
                people=sheet.range(f'A{c}').value
                newRow=百度百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(name=people)
                people=newRow[0]
                newRow=newRow[1:]
                newRow=[people,*newRow[0],*newRow[1],newRow[3],f"""=HYPERLINK("{urlBaidu}{people}")""",newRow[4]]
                sheet.range('A'+str(c)).value=newRow
                excelFile.save()
            if 0 and [int(sheet.range(f'{i}{c}').value) for i in "BE"].count(-5000)==1:
                name=sheet.range(f'A{c}').value
                同时打开百度百科和全历史网站(name)
                print(f"{name}的生卒信息为",[int(sheet.range(f'{i}{c}').value) for i in "BCEF"],"需要补充")
                lifeTimeInfo=[int(i) for i in input("输入新的生卒信息4个量,-1将跳过").split()]
                while len(lifeTimeInfo)!=4:
                    lifeTimeInfo=[int(i) for i in input("输入新的生卒信息4个量,-1将跳过").split()]
                for i in range(4):
                    if lifeTimeInfo[i]!=-1:
                        sheet.range(f'{"BCEF"[i]}{c}').value=lifeTimeInfo[i]
                        excelFile.save()
            c+=1
        return c
    curRow=1
    originalPerson,originalHeat=[],[]
    if 0:#表格中已有信息,想继续添加,手动切换
        curRow=findStartRow(sheet)
        originalPerson=list(sheet.range(f'A2:A{curRow-1}').value)
        originalHeat=list(sheet.range(f'H2:H{curRow-1}').value)
    else:
        sheet.range('A1').value=labelArr
        curRow=2
    print(f"{originalPerson[:10]=}")

    for i in range(len(originalPerson)-1,-1,-1):#分割姓名和称号
        if len(originalPerson[i])>4:#同时写了姓名和称号
            splitName=trySplitName(originalPerson[i])
            if len(splitName)>1:
                originalPerson+=splitName
                originalHeat+=[originalHeat[i]]*len(splitName)
    origindc={originalPerson[i]:originalHeat[i] for i in range(len(originalPerson))}#存已在excel中的人名
    initialPerson=[]
    additonalPerson=['赵佗','周恩来','曾国藩','昌意','嫘祖','鲁迅']
    if 0:#用过的初始人物种子,从网上各处收集来的。更多历史人物可以去历史人物网https://www.lishirenwu.com/ 复制
        additonalPerson+=['毛泽东','孙中山','林爽文']+"岳飞,文天祥,张世杰,陆秀夫,于谦,张苍水,黄道周,孙传庭,卢象升,袁崇焕,李秀成,陈玉成".split(",")
        additonalPerson+="甄鸾 墨子 张衡 赵爽 刘徽 夏侯阳 祖冲之 祖暅 王孝通 贾宪 杨辉 秦九韶 李冶 李善兰 刘安 葛洪 陶弘景 马和 扁鹊 华佗 张仲景 孙思邈 李时珍 裴秀 郦道元 沈括 徐霞客 蔡伦 马钧 僧一行 毕昇 苏颂 杜诗 郭守敬 刘洪 宋应星 贾思勰 汜胜之 王祯 徐光启 宋慈 王景 李冰".split(" ")#科技人员
        additonalPerson+="李白 苏轼 白居易 杜甫 辛弃疾 李清照 王维 纳兰性德 李商隐 陆游 陶渊明 刘禹锡 李煜 杜牧 韩愈 欧阳修 王安石 柳宗元 屈原 柳永 孟浩然 曹操 李贺 元稹 左丘明 王勃 老子 范仲淹 孟子 温庭筠 王昌龄 谭嗣同 曹植 徐霞客 朱棣 尹志平 岑参 司马迁 秦观 晏殊 诸葛亮 杨万里 韦应物 唐寅 晏几道 贾岛 朱熹 曹雪芹 黄庭坚 刘长卿 岳飞 庄周 孟郊 方干 韦庄 高适 周邦彦 荀子 文天祥 张九龄 罗隐 马致远 陈子昂 司马相如 姜夔 张若虚 卓文君 张籍 贾谊 范成大 齐己 许浑 王建 龚自珍 鱼玄机 李耳 王之涣 贺铸 张岱 骆宾王 郑燮 贺知章 吴文英 元好问 韩非 皎然 张祜 王羲之 谢灵运 贯休 陆龟蒙 杜荀鹤 于谦 朱敦儒 卢纶 周敦颐 宋玉 袁枚 仓央嘉措 郦道元 冯延巳 白朴 蒋捷 曹丕 杨慎 曾国藩 苏洵 崔颢 韩偓 王守仁 张先 张孝祥 李峤 阮籍 刘义庆 嵇康 钱起 姚合 皮日休 苏辙 关汉卿 李斯 司马光 鲍照 朱淑真 刘向 卢照邻 刘克庄 张可久 宋濂 蒲松龄 戴叔伦 孙武 李益 张养浩 项羽 宋之问 王冕 赵嘏 李世民 郑谷 归有光 张炎 司空图 庾信 韩翃 陈与义 刘辰翁 林逋 薛涛 权德舆 皇甫冉 赵长卿 叶绍翁 李绅 列子 谢朓 吴均 黄景仁 徐再思 张说 孔丘 吕岩 杨炯 班固 王翰 顾况 刘基 李之仪 唐琬 陶弘景 黄巢 张继 王绩 李端 纪昀 程垓 吴融 陆机 张衡 马戴 张元干 常建 司空曙 左思 刘邦 李颀 曾巩 吴潜 刘过 王湾 冯梦龙 孔子 周密 汤显祖 吕蒙正 崔护 姜子牙 颜真卿 欧阳迥".split(" ")#诗人
        additonalPerson+="燧人氏,伏羲氏,神农氏,炎帝,黄帝,颛顼,帝喾,尧,舜,禹,姒禹,大禹,姒文命,启,姒启,太康,姒太康,仲康,姒仲康,姒中康,相,姒相,羿,姒羿,寒浞,姒浞,少康,姒少康,杼,姒杼,槐,姒槐,芒,姒芒,泄,姒泄,不降,姒不降,扃,姒扃,廑,姒廑,孔甲,姒孔甲,皋,姒皋,发,姒发,桀,夏桀,姒桀,汤,商汤,子履,太丁,子丁,外丙,子胜,仲壬,子庸,伊尹,篡位,太甲,子至,商太宗,沃丁,子绚,太庚,子辩,小甲,子高,雍己,子密,太戊,子伷,商中宗,仲丁,子庄,外壬,子发,河亶甲,子整,祖乙,子滕,祖辛,子旦,沃甲,子逾,祖丁,子新,南庚,子更,阳甲,子和,盘庚,子旬,小辛,子颂,小乙,子敛,武丁,子昭,商高宗,祖庚,子跃,祖甲,子载,廪辛,子先,康丁,子嚣,武乙,子瞿,文丁,子托,帝乙,子羡,帝辛,子受德,商纣王,周文王,姬昌,周武王,姬发,周成王,姬诵,周康王,姬钊,周昭王,姬瑕,周穆王,姬满,周共王,姬繄扈,周烈王,姬囏,周孝王,姬辟方,周夷王,姬燮,周厉王,姬胡,周宣王,姬静,周幽王,姬宫湦,周平王,姬宜臼,周携王,姬余臣,周桓王,姬林,周庄王,姬佗,周釐王,姬胡齐,周惠王,姬阆,周废王,姬颓,周襄王,姬郑,周废王,姬带,周顷王,姬壬臣,周匡王,姬班,周定王,姬瑜,周简王,姬夷,周灵王,姬泄心,周景王,姬贵,周悼王,姬猛,周敬王,姬匄,周废王,姬朝,周元王,姬仁,周贞定王,姬介,周哀王,姬去疾,周思王,姬叔,周考王,姬嵬,周威烈王,姬午,周安王,姬骄,周懿王,姬喜,周显王,姬扁,周慎靓王,姬定,周赧王,姬延".split(",")#先秦帝王
        additonalPerson=list(set(additonalPerson))
    initialPerson+=additonalPerson
    initialPerson=list(set(initialPerson))#-origindc不遍历已有的
    hp=[[-inf,i] for i in initialPerson]#浏览次数,人名
    hp+=[[-origindc[i],i] for i in origindc]
    heapify(hp)
    hpdc=defaultdict(int)#存堆中的人名,得更新权重,保持权重增大,存正值

    while len(hp):#curRow<=findNum and 
        print(f"\n查找数{curRow}/{findNum}________")
        prevLookTime,people=heappop(hp)#出堆->查询->入字典,代码精简,但部分人访问了多次
        if -prevLookTime<hpdc[people]:#多次出入堆,不用重复查询people in origindc or 
            continue
        """
        实际的工程项目稳定优先,不追求极致的效率,多加几个判断反而稳妥"""
        if uniform(0,1)>0.7:
            time.sleep(exp(uniform(-5,-1)))#随机睡眠,减少
        # if people in origindc:#已经在excel了
        #     continue
        people,birthTime,deadTime,相关人物,浏览次数,备注=百度百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(people)
        if birthTime[0]>1910:#现当代人物
            continue
        if birthTime[0]==deadTime[0]==-5000:#有可能不是人名
            continue

        needJoin=people not in origindc
        if needJoin:        
            newRow=[people]+list(birthTime)+list(deadTime)+[浏览次数,f"""=HYPERLINK("{urlBaidu}{people}")""",备注]
            origindc[people]=浏览次数#.add(people)
            sheet.range('A'+str(curRow)).value=newRow#百度百科信息
            excelFile.save()

        # if birthTime[1]==deadTime[1]==0:#时间信息不可靠
        #     continue
        if curRow<findNum:#人数到了一定程度后不再递归搜索
            浏览次数=浏览次数 if 浏览次数!=-1 else 4*10**6
            for people in 相关人物[0]:
                p=浏览次数/10
                if len(people) and (people not in origindc) and p>hpdc[people]:
                    hpdc[people]=p
                    heappush(hp,[-p,people])
            for people in 相关人物[1]:
                p=浏览次数
                if len(people) and (people not in origindc) and p>hpdc[people]:
                    hpdc[people]=p
                    heappush(hp,[-p,people])
        #搜狗百科信息
        if needJoin:#搜狗百科无法贡献相关人物,仅用来查新人
            people,birthTime,deadTime,相关人物,浏览次数,备注=搜狗百科查找单个人物生卒时间_相关人物_词条浏览次数_备注(people)
            newRow=list(birthTime)+list(deadTime)+[浏览次数,f"""=HYPERLINK("{urlSougou}{people}")""",备注]
            sheet.range('M'+str(curRow)).value=newRow
            excelFile.save()
        
        curRow+=int(needJoin)
        
    excelFile.close()

if __name__=="__main__":
    百度百科加搜狗百科查找历史人物网生卒信息_边查边存()

数据

如果缺数据,可以下载本文的txtexcel附件,txt可直接用 loadHistoryDatas() 函数读取(把txt文件名和函数里的文件名设置一致,txt文件和代码文件放到一个文件夹,程序运行时会自动读取),速度快,但不含百度百科浏览次数信息。
excel信息更全,读取时需要额外的xlwings等库,自己写读取函数,读取速度慢,建议读完后保存到txt文件里,下次直接读txt文件。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值