IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> ArcaeaB30录入和导出:Python简单的xlsx、json处理和图片编辑 -> 正文阅读

[Python知识库]ArcaeaB30录入和导出:Python简单的xlsx、json处理和图片编辑

一、前言?

契机是:在音游群里问了一句,群里的bot怎么查B30?然后大佬回复说最近机器人挂了,查不上了。就自己搓了一个。

虽然不能做到从服务器直接获取打歌数据,但是在手动录入痛苦一下之后还是可以实现B30查询自由的。

最终效果

特地做了个很好康的icon

二、信息摘要

  • Excel的简单信息处理
  • VBA的简单应用
  • Python的Json处理
  • Python的Excel工作表读取
  • Python的图片操作

三、正文

1、打歌成绩的录入(Excel公式+VBA)

因为Excel本身就可以很方便地存一些记录和进行简单运算,因此决定直接在Excel完成录入全部工作,后续用Python的模块读取xlsm文件。(xlsm:启用宏的Excel工作簿)

Main工作表:存储歌曲的标题和定数
Main工作表用于存储所有歌曲的标题和定数信息

?零、友好地问wiki要一份定数表

是的,目前还没实现自动/一键更新定数表。如果616更新了还得重新干一遍。

好事是,直接复制粘贴就可以了。

一、做一个录入UI

Calc工作表,用于用户填写并导入成绩

?一般录入成绩需要用户提供哪些要素?我打了什么歌?难度等级是什么?分数多少?进而再推算出本次打歌的有效ptt,再添加到成绩库之中。

直接让我输入歌名,我肯定是不干的。先不说麻不麻烦,Arcaea中有些歌名就不是人能打出来的,而且我也不想录入个成绩还胆战心惊地校对有没有哪里拼错。。因此做了一个简单的搜索宏,输入部分歌名即可返回结果,并且有一定的容错。

搜索算法大致思路:逐个比较字符,相同则继续比对并增加匹配度,不同时扣除匹配度并且判断是否连续5个字符不同若是则中断比对,跳到下一个比对目标。一次比对完成后更新匹配度top10的列表。全部比对完成后将匹配度top10返回,并自动将最匹配的歌名填入。

(由于加粗部分的算法设置,绝大多数情况下top10列表里只有一个结果

12.16更新:逐个比对字符并按照一定规则计算匹配度:

完全相同,相似度增加并重置惩罚

大小写模糊后相同,相似度增加

希腊字母模糊后相同,惩罚增加,相似度不变。为了防止搜什么都是那几首霸屏。

模糊后的结果

?

均不满足,惩罚增加,扣除惩罚值相同的相似度。惩罚>7时额外扣除大量相似度。

至于难度等级和分数只能由用户手动输入了。在完成后,Excel通过公式计算单曲ptt。具体的计算在wiki上公开。公式如下:

=MAX(IF(F5>=10000000,HLOOKUP(E5,F2:I3,2,FALSE)+2,IF(F5>=9800000,HLOOKUP(E5,F2:I3,2,FALSE)+1+(F5-9800000)/200000,HLOOKUP(E5,F2:I3,2,FALSE)+(F5-9500000)/300000)),0)

至于搜索按钮和导入按钮,只是两个分配了宏的圆角矩形罢了。因为宏的存在,文档只能保存为xlsm格式。

一共使用了4个宏,除搜索宏外,还做了导入宏和两个便于维护的一键排序+索引生成。搜索时可根据索引提高效率。

B30工作表,稍后Python会读取这一工作表的数据进行图像生成

?2、获取对应歌曲的曲绘(Python+apk解包)

解包的直接坏处是:会占用几十MB的硬盘,并且更新了之后需要手动加新的曲绘。但是我不想不会做等它慢慢去网上爬回来我要的曲绘,这一条好处就足够了。

下载Arcaea的apk安装包,后缀改.zip,在songs文件夹下找到了我们需要的所有东西:按照歌曲id整齐存放的曲绘,和songlist文件。

这里的问题是:歌名不同于歌曲id,需要一番转换。

简单分析,songlist文件是用Json写的,因此可以直接利用。并且 ,它存储了各个歌曲的所有信息,包括id和歌名(en翻译就足够了,因为wiki提供的是英语;有些其他语种的歌名用不上)。

因此,在Python中导入json模块并处理之。

这里的file_path变量即songlist的路径,涉及到获取当前运行路径,稍后再说。songlist是无后缀名的,不要把它同文件夹混淆。顺便,末尾别忘了随手关门。

filejson = open(file_path,'r',encoding='utf-8')
songdic=json.load(filejson)

filejson.close()

并且导入openxlrd模块,打开我需要的工作簿(filename变量指向)和工作表B30。用for循环遍历cell(i,1)即第1列的单元格,append到一个List上。如法炮制多定义几个List之后,工作表中我们需要的所有数据就可以在Python中调用了。

book = xlsx.load_workbook(filename)
sheet = book['B30']

Songs=[]
for i in range(2,32):
    Songs.append(sheet.cell(i,1).value)

之后,在Json(此处及下文均会使用Json指代前文已经处理好的由songlist生成的字典songdic)中顺序暴力查找第i个歌名对应的歌曲id并如法炮制地append到另一个List。

如此就完成了歌曲id的获取。稍后要获取曲绘的时候只要稍加修改就可以了,图片的打开需要导入PIL.Image模块。代码预览:

for i in song_index:
    if(song_dl[i]):
        dlstr='dl_'
    else:
        dlstr=''
	#处理需要下载的歌曲的特殊文件夹名。解包时发现的,616你做得好,做得好啊
    if(diff[i].lower()=='byd'):
        flstr='3.jpg'
    else:
        flstr='base.jpg'
	#处理byd难度的特殊曲绘
    im=Image.open(os.path.join(root_path,'src/songs',dlstr+song_id[i],flstr))#打开对应曲绘

注1:os.path.join(path1,path2,...)可以帮助拼接并生成一条合法路径。

注2:root_path即当前所在路径。

3、开始处理图片

这里我选择了PIL,没有用cv2的原因是,打包出来exe之后cv2模块会报错。。只能忍痛删除,并且把所有背景图都预先处理成2560*1440+高斯模糊。

????????一、先在背景上贴曲绘

大致设想:在一张2560*1440的背景上,每首歌曲占用256*356(纵向多留100px给文字),其中曲绘240*240(留16px的空白),并且稍作修饰以表现难度等级和是否PM的信息。(需要用到的模块:PIL.Image,PIL.ImageFont,PIL.ImageDraw)

其中,背景从某个文件夹(/src/bgs)随机选取一张图片(os模块获取其列表,random模块生成随机数)。至于那么多背景哪里来,我选择解包拿几张CG图。

因此,直接把背景打开到output上,之后的操作全部对output进行。os.listdir(path)可以生成path下所有内容的List,Image.open(path)则打开path指向的图片文件,random.choice(List)可以返回List中的随机一个元素。

bg=os.listdir(os.path.join(root_path,'src/bgs'))
output=Image.open(os.path.join(root_path,'src/bgs',random.choice(bg)))

然后就可以一张张贴曲绘了。由于上文是单变量遍历,因此需要用到简单的取余(%)、整除(//)来算坐标。

for i in song_index:
    if(song_dl[i]):
        dlstr='dl_'
    else:
        dlstr=''
	#处理需要下载的歌曲的特殊文件夹名。解包时发现的,616你做得好,做得好啊
    if(diff[i].lower()=='byd'):
        flstr='3.jpg'
    else:
        flstr='base.jpg'
	#处理byd难度的特殊曲绘
    im=Image.open(os.path.join(root_path,'src/songs',dlstr+song_id[i],flstr))#打开对应曲绘
    im=im.resize((240,240))#调整大小,一个图总共占256*356空间,多出来的留作空白和放数据文字
    output.paste(im,(256*(i%10)+8,350+356*(i//10)+8,256*(i%10+1)-8,350+356*(i//10+1)-108))

注1:image.resize((x,y)[,Image.ANTIALIAS])传递的两个参数中,第二个是可选的,第一个是一个二元组表示缩放结果大小。因此在不强行指定缩放方式的情况下,应当打两层括号,例如img.resize((256,256))。否则报错。

注2:image.paste(icon,box[,mask])中的box可以为只指定左上角的二元组坐标,也可以为同时指定左上、右下角的四元组坐标。在使用四元组坐标的情况下,如果icon的大小不符合box划出来的大小则会直接报错。另外在粘贴PNG图片时,需要传第三个参数来传递mask的透明度通道,例如img.paste(myicon,(0,0),myicon)。也可以使用另一个图片文件的透明度通道。

? ? ? ? 二、对每个曲绘稍作修饰

我希望在曲绘的右上角直接显示对应歌曲的难度。并且在曲绘的右侧绘制8px宽的竖条矩形,使用某一个难度的对应色来辅助传递这一信息。另外,已经PM的歌曲就没什么继续推分的必要了我太菜了完全没法理论,所以我希望特别标注出来。

解包时,我翻到了我要的素材,全部拷到/src/tags下面。另外对各个难度进行了取色(QQ截图就能直接复制色号),直接写到Python里。因此定义如下:

clr={'byd':(125,21,43),'ftr':(112,47,99),'prs':(139,163,96),'pst':(62,159,183)}
badge={}

for dif in ['pst','prs','ftr','byd']:
    tim=Image.open(os.path.join(root_path,'src/tags',dif+'.png'))
    tim=tim.resize((73,22))
    badge[dif]=tim

两个我都定义成了字典,因为Excel中的数据本来就是如此标级的,一会方便直接用。字典的“append”只要对一个不存在值的key赋值即可。

另一个PM标记也是直接Image.open即可,不再赘述。

因为显示顺序的关系,涂8px的矩形肯定是要先于贴难度tag的。此处用的是image.putpixel(pos,rgb),其中pos是二元组(x,y),rgb是三元组(r,g,b)。

直接进行一个双层循环。因为最外面有个for i in index的大循环,因此直接通过之前存好的List获取某个歌曲的难度。

for x in range(256*(i%10+1)-16,256*(i%10+1)-8):
        for y in range(350+356*(i//10)+8,350+356*(i//10+1)-108):
            output.putpixel((x,y),clr[diff[i]])

再就是贴tag,仍然是image.paste(),不再赘述

再就是贴PM(如果你PM了,就奖你一朵小P花罢!),判断如果这首歌得分超过10,000,000则image.paste(),PM标记是有透明度通道的,所以得传三个参。也是很简单的代码,不赘述了。

? ? ? ? 三、文字信息的添加

再是绘制文字。主要用了三大句子:

font=ImageFont.truetype(font_file, size)
draw = ImageDraw.Draw(img)
draw.text((x,y),text,(r,g,b),font)

提前定义一些字体,包括字体文件(*.ttc,*.ttf)和字号

定义一个ImageDraw对象,告诉它你得在img这个图上涂画

调用ImageDraw的text方法,这个的参数很好懂

其中,字体文件就用解包出来的结算分数同款字体(谢谢光光),复制到/src/下

至于text这边的字符串控制可以稍微提一下:如果是熟悉c而不熟悉py的同学,可以像这样格式化输出字符串:

"%s, %.1f?is your score."%(player_name, score)

家的味道。

这样就可以完成所有文字的绘制工作了。

最终使用image.save()来保存图片,详情直接参考其他大佬的博客就行(我参不透)

4、一些小细节

使用tkinter.filedialog的对话框来打开文件、指定路径时,会蹦出来一个空的Tk图形窗口。因此加入如下代码:

root = tkinter.Tk()
root.withdraw()#隐藏tk产生的窗口

#主程序

root.destroy()#销毁tk留下的窗口

tk这波是纯对话框工具人。

image.putpixel()在进行很大范围的像素操作时效率很低。(而我偏偏又得对几乎整张图进行暗化

四、后记

整个工程从开工到现在不过三天,且我在开始前对这一块内容一无所知,甚至对Python只有自学得来的一些很浅薄的认知。回过头一看,很多地方都是毛毛糙糙。

1、目前仍然只能待在“实验室”里

因为从头到尾,测试者只有我一个人,而我又规规矩矩地输入着各项数据,所以很多漏洞都不得以出场。例如,难度我如果不是全小写,输入一个“FTR”会怎么样呢?或者干脆拼错?

一方面缺乏对于输入的自动调整来保证程序最大限度地正常运行,另一方面没有报错。假如哪天崩溃了,我连它为什么崩溃可能都无法得知。

至少先去把报错做上吧。

2、优化

因为Arcaea整个曲库也不过几百首,很多算法的性能问题都没有什么表现的机会,全程暴力也能完成任务。另外,也因此我可以放心大胆地定义一堆玩应,可能造成了很大的浪费。

bot快点恢复查ptt啊我就算能算B30难度还能算R10吗录入那么多我要死了

感谢lowiro。

这个工程仅供个人学习交流之用。

五、Python源码

import tkinter.filedialog
import openpyxl as xlsx
import json
from PIL import Image, ImageFont, ImageDraw
import os
import sys
import random
gen_path = os.path.dirname(os.path.realpath(sys.argv[0]))#打包成exe之后,直接使用os.getcwd()或者abspath()都无法正确获取,解决方案如左照抄
clr={'byd':(125,21,43),'ftr':(112,47,99),'prs':(139,163,96),'pst':(62,159,183)}#四个难度的颜色定义,一会方便直接用

#Main()
root = tkinter.Tk()
root.withdraw()#隐藏tk产生的窗口
player=input('Enter your Player Name:')
print('Select a B30 file:')
filename=tkinter.filedialog.askopenfilename(title='选择B30表', filetypes=[('所有文件','.*'),('Excel文档','.xls'),('Excel2003文档','.xls'),('Excel启用宏的工作簿','.xlsm')])
print('filename=%s'%(filename))
print('Determine Output Directory:')
savepath = tkinter.filedialog.askdirectory(title='保存路径选择')
print('savepath=%s'%(savepath))
#获取用户定义的玩家名,Excel工作簿和导出路径并echo
badge={}
for dif in ['pst','prs','ftr','byd']:
    tim=Image.open(os.path.join(gen_path,'src/tags',dif+'.png'))
    tim=tim.resize((73,22))
    badge[dif]=tim
#打开各个难度名对应的图片文件备用,用字典存储
pm=Image.open(os.path.join(gen_path,'src/tags/pm.png'))
#打开PM标记的图片备用
book = xlsx.load_workbook(filename)
sheet = book['B30']
#openpyxl模块开文件,给B30工作表一个变量存着
filejson = open(os.path.join(gen_path,'src/songlist'),'r',encoding='utf-8')
songdic=json.load(filejson)
#打开songlist文件,json模块转字典songdic备用
filejson.close()
#随手关门
font=ImageFont.truetype(os.path.join(gen_path,'src/font.ttf'), 42)
titlefont=ImageFont.truetype(os.path.join(gen_path,'src/font.ttf'), 102)
#字体文件定义,一会在图片上写文字直接调用
bg=os.listdir(os.path.join(gen_path,'src/bgs'))
#背景文件列表获取
Songs=[]
diff=[]
ptt=[]
Sc=[]
#定义了一万个list。留着等后面慢慢append
song_index=[]
song_id=[]#存储各个歌曲对应文件夹位置
song_dl=[]#各个歌曲是否需要下载
avg=0.0
for i in range(2,32):
    Songs.append(sheet.cell(i,1).value)
    diff.append(sheet.cell(i,2).value)
    ptt.append(sheet.cell(i,3).value)
    avg=avg+sheet.cell(i,3).value
    Sc.append(sheet.cell(i,4).value)
#读取B30工作表,存入list
cnt=0
for songtitle in Songs:
    found=False
    for i in songdic['songs']:
        if(i['title_localized']['en']==songtitle):#暴力在songdic里搜索我要的歌。如果找不到那就崩tm的溃(划掉)
            song_id.append(i['id'])
            song_dl.append(i.get('remote_dl',False))
            song_index.append(cnt)
            found=True
            break
    if( not found):
        print('ERROR:cannot find   ',songtitle)
        break
    cnt=cnt+1
avg=avg/(cnt)#B30平均PTT计算
output=Image.open(os.path.join(gen_path,'src/bgs',random.choice(bg)))
#随机选取src/bgs/中的一张图片作背景。大小2560*1440.
print('Background image fetched. Processing...')
for x in range(1,2560):
    for y in range(1,1440):
        rgb=output.getpixel((x,y))
        output.putpixel((x,y),(int(rgb[0]/1.5),int(rgb[1]/1.5),int(rgb[2]/1.5)))
#整体暗化,不然字看不清。但是效率巨低
print('Process complete. Stitching song & data...')
draw = ImageDraw.Draw(output)#之后所有绘制操作都经由这一个draw
for i in song_index:
    if(song_dl[i]):
        dlstr='dl_'
    else:
        dlstr=''
	#处理需要下载的歌曲的特殊文件夹名。不解包还不知道。616你做得好,做得好啊
    if(diff[i].lower()=='byd'):
        flstr='3.jpg'
    else:
        flstr='base.jpg'
	#处理byd难度的特殊曲绘
    im=Image.open(os.path.join(gen_path,'src/songs',dlstr+song_id[i],flstr))#打开对应曲绘
    im=im.resize((240,240))#调整大小,一个图总共占256*356空间,多出来的留作空白和放数据文字
    output.paste(im,(256*(i%10)+8,350+356*(i//10)+8,256*(i%10+1)-8,350+356*(i//10+1)-108))#基础坐标计算。image.paste(src,(x1,y1,x2,y2))
    for x in range(256*(i%10+1)-16,256*(i%10+1)-8):
        for y in range(350+356*(i//10)+8,350+356*(i//10+1)-108):
            output.putpixel((x,y),clr[diff[i]])#在曲绘右边绘制一个8px的竖直长条矩形,颜色用对应难度颜色填充
    output.paste(badge[diff[i]],(256*(i%10+1)-8-60,350+356*(i//10),256*(i%10+1)-8-60+73,350+356*(i//10)+22))#贴上对应难度的tag。同样是基础坐标计算。
    if(Sc[i]>=10000000):
        output.paste(pm,(256*(i%10+1)-8-97,350+356*(i//10+1)-108-73,256*(i%10+1)-8+10,350+356*(i//10+1)-108+10),pm)#如果你pm了,奖励你一朵小P花
    draw.text((256*(i%10)+8,350+356*(i//10+1)-105),'%s%.6f'%('ptt:',ptt[i]),(255,230,255),font)#绘制单曲ptt
    draw.text((256*(i%10)+8,350+356*(i//10+1)-55),'%d%s'%(Sc[i],'pts'),(255,230,255),font)#绘制单曲得分
draw.text((80,40),'%s%s'%('Player:',player),(255,230,255),titlefont)#绘制标题:玩家名
draw.text((80,180),'%s%.6f'%('B30 Average:',avg),(220,200,220),titlefont)#绘制标题:均PTT
output.save(os.path.join(savepath,"output.png"),"PNG")#导出到目标路径
print("Complete!")

#End Main
root.destroy()#销毁开局tk留下的窗口。tk这波纯对话框工具人

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2021-12-18 15:55:49  更:2021-12-18 15:58:04 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 5:33:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码