公司企业APP描述文件过期,没有提醒,导致当天出现不可用的问题。
为了避免再次发生类似的问题,笔者想要写一个Python
脚本,读取描述文件,获取有效期,设置提醒,且自动运行。
<!--More-->
首先再来理一下思路,所有的描述文件都在~/Library/MobileDevice/Provisioning Profiles/
目录下,但是里面的内容通常不会自动删除,过期的或者重复的都在这个目录中,而且这个目录下的文件名是uuid
命名的和Xcode中的文件名字也不能直接对应,所以一眼看去,只能用一个字形容:乱。
如果账号是管理员,直接登录在电脑上,项目中用的自动管理描述文件的,还好一些,现在会自动续期。但是如果账号是开发者,发布的描述文件没有权限用自动管理的,就需要注意这个描述文件有效期的问题。
再来理一下思路,想要的是一个读取描述文件夹下所有描述文件,获取描述文件中的内容,根据有效期,设置提醒,且自动运行的脚本。
那这里面最重要的是什么?是获取描述文件的内容,这关系到这个思路是否可行。
首先,来看下,描述文件的格式是uuid.mobileprovision
,而这个.mobileprovision
格式默认是直接安装到 Xcode 的,通过预览可以看到里面的内容。但是用脚本如何读取里面的内容呢?
首先用VSCode
打开一个这样的描述文件,提示如下:
点击Open Anyway
,然后选择用Text Editor
的方式打开,可以看到,文件的开始和结束都是一堆乱码,中间却是一段plist
格式的内容,如下:
所以猜想可以通过读取文件内容,截取开始和结束字符串,生成Plist文件。然后再通过读取 Plist 文件并解析获取对应的属性的内容。
下面来一步步尝试实现:
首先是读取文件内容,出师不利,在这一步就遇到了困难,设置encoding
为utf-8
,通过 open 读取到的文件内容一直为空,排查了好久。一开始以为是encoding
指定的不对,调试后发现,设置errors='ignore'
即可。
# read mobileprovision file content
def readMobileProvisionContent(fileName):
fileFullName = fileName + ".mobileprovision"
with open(fileFullName, "r", encoding="utf-8", errors="ignore") as file:
fileContent = file.read()
return fileContent
Plist
格式的文件获取到文件内容之后,下一步是截取指定字符串之间的内容,生成新的Plist
格式的文件。根据上面的用文本格式查看.mobileprovision
内容的分析,需要截取的内容是<?xml
和</plist>
之间的内容,然后生成新的文件。这里需要注意的是,查找到结束位置时,获取到的Location
,需要加上</plist>
的长度才是完整的内容,详细代码如下:
Ps:这里走了一部分弯路,一开始转为XML
格式的文件,生成后内容的读取并不方便,后来发现直接转为Plist
格式的读取内容更为快捷。
# 获取指定字符串之间的内容
def getSubContentBetween(startStr, endStr, sourceStr):
startLoc = sourceStr.find(startStr)
if startLoc >= 0:
endLoc = sourceStr.find(endStr, startLoc)
if endLoc >= 0:
# 这里需要注意,获取到的 endLoc 需要加上 endStr 的长度才是完整的内容
endLoc += len(endStr)
return sourceStr[startLoc:endLoc].strip()
接下来是用获取到的内容,生成Plist
文件。在这里需要注意写入的方式,要用覆盖写入的方式,而不是拼接写入,防止多次执行出现问题。具体代码如下:
# 根据字符串内容生成 plist 文件
def generatePlistFile(fileName, fileContent):
fileFullName = fileName + '.plist'
# 需要注意,mode为打开文件的方式,a 为追加,r 为只读,w 为覆盖
file = open(fileFullName, mode='w', encoding='utf-8')
for i in range(len(fileContent)):
text = fileContent[i]
file.write(text)
file.close()
生成 Plist
文件后,接下来是解析 Plist
文件内容,获取到描述文件名字、有效期、UUID 等信息,下面具体来看看:
Plist
文件在解析Plist
之前,需要思考一下,具体需要获取哪些字段,最终目的是提醒,所以过期日期字段是一定要解析的。然后需要考虑提醒的问题,是添加日历提醒,还是通过生成一个Excel 或者 html 的表格文件,用不同颜色区分不同有效期。这里用第二种生成 Excel 或者 html 的方式。
接下来需要考虑的就是显示哪些字段:
由于描述文件的名字中显示的是 UUID.mobileprovision
,和 Xcode 中配置的不同,Xcode 中显示的是名字,所以名字和UUID都要显示出来,用于一一对应。
然后是描述文件对应的bundleID,用于确认具体的APP。
再然后是有效期相关信信息,CreationDate
和ExpirationDate
,以及计算出来的剩余天数。
Name | UUID | bundleID | CreationDate | ExpirationDate | 剩余过期天数 |
---|---|---|---|---|---|
单元格 | 单元格 | 单元格 | 单元格 | 单元格 | 单元格 |
解析Plist
使用Python
的plistlib
库,日期计算使用datetime
库,都不需要额外安装,直接导入使用,具体代码如下:
Ps:
CreationDate
和ExpirationDate
都是 date 类型,而不是 string 类型。rb
,如果指定为r
,则会提示TypeError: startswith first arg must be str or a tuple of str, not bytes
# 解析Plist文件内容
import plistlib # 解析Plist需要的库
import datetime # 日期计算需要的库
def parsePlistInfo(fileName):
fileFullName = fileName + ".plist"
# 注意下面的mode需要为`rb`,读取 plist 内容
with open(fileFullName, mode='rb') as plist:
plistDic = plistlib.load(plist)
name = plistDic["Name"]
uuid = plistDic["UUID"]
creationDate = plistDic["CreationDate"]
expirationDate = plistDic["ExpirationDate"]
entitlements = plistDic["Entitlements"]
applicationIdentifier = entitlements["application-identifier"]
teamIdentifier = entitlements["com.apple.developer.team-identifier"]
bundleIDLoc = applicationIdentifier.find(teamIdentifier) + len(teamIdentifier) + 1
bundleID = applicationIdentifier[bundleIDLoc:].strip()
currentDate = datetime.datetime.now()
dateDelta = expirationDate - currentDate
leftDays = dateDelta.days
dateformatterStr = "%Y %m %d %H:%M:%S"
creationDateStr = creationDate.strftime(dateformatterStr)
expirationDateStr = expirationDate.strftime(dateformatterStr)
return (name, uuid, bundleID, creationDateStr, expirationDateStr, leftDays)
到这一步说明之前的思路是可行的,即读取描述文件xxx.mobileprovision
的内容,生成新的plist
格式的文件,然后再通过读取plist
的content获取对应属性的值,并计算到期日期。
最后需要考虑的是设置提醒的逻辑,起初打算直接写入 Mac 日历,调研后发现能做到的是生成日历格式的文件,然后手动导入。所以改为生成一个 html
或Excel
文件,对快过期和已过期的标红显示,然后自动发送到邮箱(在这里实现为直接打开)。下面来看一下生成html
或Excel
的逻辑。
html
或Excel
文件在生成之前需要考虑哪些状态是需要标红显示的:如果剩余天数小于 0,说明已过期;如果剩余天数小于 30,说明一个月内过期,这两种可以高亮显示;如果大于 30,则说明有效期大于 1 个月,只需要正常显示即可。
html
文件先来看下生成 html 的代码:
def writeToHtml(infoTurple, filepath):
htmlPath = filepath + "iOS描述文件统计.html"
columnTitles = ['name', 'uuid', 'bundleID', '创建日期', '过期日期', '有效期']
fileout = open(htmlPath, 'w')
table = "<table border='1' width='70%'>\n"
# create table column headers
table += " <tr>\n"
for title in columnTitles:
table += " <th>{0}</th>\n".format(title.strip())
table += " </tr>\n"
# Create the table's row data
columnCount = len(infoTurple)
table += " <tr>\n"
for i, x in enumerate(infoTurple):
if i == columnCount - 1:
color = '#FFFFFF'
valueStr = str(x) + "天内过期"
if x < 0:
valueStr = "已过期"
color = '#FF0000'
elif x < 30:
valueStr = str(x) + "天内过期"
color = '#FFF000'
else:
valueStr = "还有" + str(x) + "天过期"
table += " <td bgcolor='{bgcolor}'>{value}</td>\n".format(bgcolor=color, value=valueStr)
else:
table += " <td>{0}</td>\n".format(x)
table += " </tr>\n"
table += "</table>"
fileout.writelines(table)
fileout.close()
运行后显示效果,如下图所示:
Excel
文件再来看一下,如何生成 Excel 格式的文件,毕竟如果要发送给他人,Excel
格式的比html
的更正式点。
生成Excel
格式的文件,需要安装三方库,这里使用的是openpyxl
,首先用如下命令安装:
pip3 install openpyxl
然后生成Excel
的代码如下:
from openpyxl import Workbook
from openpyxl.styles import PatternFill
from openpyxl.styles.colors import Color
# 生成 Excel 文件
def writeToExcel(infoTurple, filepath):
excelPath = filepath + "iOS描述文件统计.xlsx"
wb = Workbook()
ws = wb.active
ws.title = '描述文件信息'
columnTitles = ['name', 'uuid', 'bundleID', '创建日期', '过期日期', '有效期']
for i, x in enumerate(columnTitles):
c1 = ws.cell(row = 1, column = i + 1)
c1.value = x
count = len(infoTurple)
for i, x in enumerate(infoTurple):
columnIndex = i + 1
c2 = ws.cell(2, column = columnIndex)
if columnIndex == count:
color = '00FFFFFF'
if x < 0:
c2.value = "已过期"
color = '00FF0000'
elif x < 30:
color = '00FFF000'
c2.value = str(x) + "天内过期"
else:
c2.value = "还有" + str(x) + "天过期"
color = '00FFFFFF'
c2.fill = PatternFill(patternType='solid',fgColor=color)
else:
c2.value = x
wb.save(excelPath)
运行后效果如下:
最终生成的文件,可以通过脚本发送给相关人,或者直接打开以达到提醒的效果。这里用的是直接打开。具体代码如下:
import platform
import subprocess
def openFile(fullFilePath):
systemType = platform.platform() # 获取系统类型
if 'mac' in systemType:
fullFilePath = fullFilePath.replace('\\', '/') # mac 下,遇到"\\"的路径打不开
subprocess.call(["open", fullFilePath])
else:
fullFilePath = fullFilePath.replace("/", "\\") # win 下,遇到"/"的路径打不开
os.startfile(fullFilePath)
截止到这一步,针对单个描述文件的处理已经完成,即对单个描述文件,解析内容并生成可视化提醒的一整套逻辑都已经实现。下面需要考虑的是另外三个方面:
~/Library/MobileDevice/Provisioning Profiles/
中,里面存放了许多描述文件,所以下一步首先要考虑的是批量扫描处理的逻辑。feature
,因为~/Library/MobileDevice/Provisioning Profiles/
这个目录下,如果没有清理过,可能存在很多已过期的文件,所以既然能获取到这个文件是否已过期,那么就能实现已过期的文件直接删除,但是这一步是可选,取决于自己是否需要。批量处理需要注意的是,由于描述文件所在目录~/Library/MobileDevice/Provisioning Profiles/
是相对路径,需要转为绝对路径再打开。脚本所在目录就没有限制,不需要和描述文件放在同一个文件夹也可。
再来思考一下整体处理的思路:
Excel
或html
文件整体处理的完整代码如下:
import plistlib
import datetime
import os
from openpyxl import Workbook
from openpyxl.styles import PatternFill
from openpyxl.styles.colors import Color
import shutil
import platform
import subprocess
# read mobileprovision file content
def readMobileProvisionContent(fileName):
fileFullName = fileName + ".mobileprovision"
with open(fileFullName, "r", encoding="utf-8", errors="ignore") as file:
fileContent = file.read()
return fileContent
# 获取指定字符串之间的内容
def getSubContentBetween(startStr, endStr, sourceStr):
startLoc = sourceStr.find(startStr)
if startLoc >= 0:
endLoc = sourceStr.find(endStr, startLoc)
if endLoc >= 0:
# 这里需要注意,获取到的 endLoc 需要加上 endStr 的长度才是完整的内容
endLoc += len(endStr)
return sourceStr[startLoc:endLoc].strip()
# 根据字符串内容生成 plist 文件
def generatePlistFile(fileName, fileContent):
fileFullName = fileName + '.plist'
# 需要注意,mode为打开文件的方式,a 为追加,r 为只读,w 为覆盖
file = open(fileFullName, mode='w', encoding='utf-8')
for i in range(len(fileContent)):
text = fileContent[i]
file.write(text)
file.close()
def parsePlistInfo(fileName):
fileFullName = fileName + ".plist"
# 注意下面的mode需要为`rb`,读取 plist 内容
with open(fileFullName, mode='rb') as plist:
plistDic = plistlib.load(plist)
name = plistDic["Name"]
uuid = plistDic["UUID"]
creationDate = plistDic["CreationDate"]
expirationDate = plistDic["ExpirationDate"]
entitlements = plistDic["Entitlements"]
applicationIdentifier = entitlements["application-identifier"]
teamIdentifier = entitlements["com.apple.developer.team-identifier"]
bundleIDLoc = applicationIdentifier.find(teamIdentifier) + len(teamIdentifier) + 1
bundleID = applicationIdentifier[bundleIDLoc:].strip()
currentDate = datetime.datetime.now()
dateDelta = expirationDate - currentDate
leftDays = dateDelta.days
dateformatterStr = "%Y-%m-%d %H:%M:%S"
creationDateStr = creationDate.strftime(dateformatterStr)
expirationDateStr = expirationDate.strftime(dateformatterStr)
return (name, uuid, bundleID, creationDateStr, expirationDateStr, leftDays)
def writeToHtml(infoTurpleList, repeatNameList, filepath):
htmlPath = filepath + "iOS描述文件统计.html"
columnTitles = ['name', 'uuid', 'bundleID', '创建日期', '过期日期', '有效期']
fileout = open(htmlPath, 'w')
table = "<table border='1' width='70%'>\n"
# create table column headers
table += " <tr>\n"
for title in columnTitles:
table += " <th>{0}</th>\n".format(title.strip())
table += " </tr>\n"
# Create the table's row data
for row, infoTurple in enumerate(infoTurpleList):
columnCount = len(infoTurple)
table += " <tr>\n"
for i, x in enumerate(infoTurple):
color = '#FFFFFF'
valueStr = x
if i == 0:
# 说明是名字
if x in repeatNameList:
color = '#FFF000'
table += " <td bgcolor='{bgcolor}'>{value}</td>\n".format(bgcolor=color, value=valueStr)
elif i == columnCount - 1:
if x < 0:
valueStr = "已过期"
color = '#FF0000'
elif x < 30:
valueStr = str(x) + "天内过期"
color = '#FFF000'
else:
valueStr = "还有" + str(x) + "天过期"
table += " <td bgcolor='{bgcolor}'>{value}</td>\n".format(bgcolor=color, value=valueStr)
else:
table += " <td>{0}</td>\n".format(x)
table += " </tr>\n"
table += "</table>"
fileout.writelines(table)
fileout.close()
# 生成 Excel 文件
def writeToExcel(infoTurpleList, repeatNameList, filepath):
excelPath = filepath + "iOS描述文件统计.xlsx"
wb = Workbook()
ws = wb.active
ws.title = '描述文件信息'
columnTitles = ['name', 'uuid', 'bundleID', '创建日期', '过期日期', '有效期']
for i, x in enumerate(columnTitles):
c1 = ws.cell(row = 1, column = i + 1)
c1.value = x
for row, infoTurple in enumerate(infoTurpleList):
count = len(infoTurple)
for i, x in enumerate(infoTurple):
columnIndex = i + 1
cellColumn = ws.cell(row = row + 2, column = columnIndex)
if i == 0:
# 说明是名字
cellColumn.value = x
if x in repeatNameList:
cellColumn.fill = PatternFill(patternType='solid',fgColor='00FFF000')
elif columnIndex == count:
color = '00FFFFFF'
if x < 0:
cellColumn.value = "已过期"
color = '00FF0000'
elif x < 30:
color = '00FFF000'
cellColumn.value = str(x) + "天内过期"
else:
cellColumn.value = "还有" + str(x) + "天过期"
color = '00FFFFFF'
cellColumn.fill = PatternFill(patternType='solid',fgColor=color)
else:
cellColumn.value = x
wb.save(excelPath)
# 创建目录
def createDir(fileDir):
if not os.path.exists(fileDir):
os.mkdir(fileDir)
# 打开文件
def openFile(fullFilePath):
systemType = platform.platform() # 获取系统类型
if 'mac' in systemType:
fullFilePath = fullFilePath.replace('\\', '/') # mac 下,遇到"\\"的路径打不开
subprocess.call(["open", fullFilePath])
else:
fullFilePath = fullFilePath.replace("/", "\\") # win 下,遇到"/"的路径打不开
os.startfile(fullFilePath)
# 获取指定目录下,所有描述文件的名字
def findAllMobileprovision(filePath):
resultList = []
for root, ds, fs in os.walk(filePath):
targetFileExt = '.mobileprovision'
for f in fs:
if f.endswith(targetFileExt):
fullProfilePath = os.path.join(root, f).replace(targetFileExt, '')
resultList.append(fullProfilePath)
return resultList
# 删除
def delExpiredMobileProvision(fileDir):
for idx, file in enumerate(fileDir):
targetFullPath = file + '.mobileprovision'
if os.path.exists(targetFullPath):
os.remove(targetFullPath)
print("已删除: " + targetFullPath)
else:
print('文件不存在: ' + targetFullPath)
def main():
# 最终生成文件的目录
resultFilePath = '/Users/xxx/Desktop/TempProfilePath/'
# 描述文件的目录
mobileProvisionPath = '/Users/xxx/Library/MobileDevice/Provisioning Profiles/'
# 过程中生成plist文件的目录
tempPlistDir = resultFilePath + 'Plist/'
# 创建最终文件的文件夹
createDir(resultFilePath)
# 创建暂存 Plist 的文件夹
createDir(tempPlistDir)
# 获取所有的描述文件名字
mobileProfisionList = findAllMobileprovision(mobileProvisionPath)
# 存储解析出Plist信息的数组
plistInfoList = []
# 存储过期文件名的数组
expiredList = []
# PlistName数组
plistInfoNameList = []
# 存储重复文件名的数组
repeatNameList = []
for idx, fileName in enumerate(mobileProfisionList):
# 查找内容开始标志
startTag = '<?xml '
# 查找内容结束标志
endTag = '</plist>'
# 读取文件内容
contentStr = readMobileProvisionContent(fileName)
# 获取开始标志和结束标志之间的字符串
plistContent = getSubContentBetween(startTag, endTag, contentStr)
# 获取要写入的Plist路径
plistPath = fileName.replace(mobileProvisionPath, tempPlistDir)
# 生成Plist文件
generatePlistFile(plistPath, plistContent)
# 解析plist文件
plistInfo = parsePlistInfo(plistPath)
# 存储到 Plist 信息数组
plistInfoList.append(plistInfo)
# 判断是否重复
plistName = plistInfo[0]
if plistName in plistInfoNameList:
repeatNameList.append(plistName)
else:
plistInfoNameList.append(plistName)
# 判断是否过期
if plistInfo[5] < 0:
expiredList.append(fileName)
# 生成html文件
writeToHtml(plistInfoList, repeatNameList, resultFilePath)
# 生成Excel文件
writeToExcel(plistInfoList, repeatNameList, resultFilePath)
# # 删除所有过期文件
# delExpiredMobileProvision(expiredList)
# 删除生成的Plist文件夹
shutil.rmtree(tempPlistDir)
# 打开生成的文件
openFile(resultFilePath + 'iOS描述文件统计.xlsx')
openFile(resultFilePath + 'iOS描述文件统计.html')
if __name__ == '__main__':
main()
还差最后一步,设置脚本自动运行,参考mac 自动执行python项目,根据需求设置脚本每隔多久自动运行即可。
再来回顾一下整体的处理逻辑,由于原有的描述文件分析查看不方便,所以想要通过脚本读取描述文件内容,生成一种便于阅读的格式,并用于提醒。
首先做的是针对单个描述文件验证,这种思路是否可行,通过读取文件、截取文件内容、生成新的便于处理格式,获取想要的信息,最终生成便于阅读的格式。
单个文件的处理通过验证,发现可行后,再来做针对整个描述文件夹的处理:通过扫描文件夹,然后针对文件夹中的每个文件都做如上处理,并添加过期和重复的处理逻辑,把最终的信息拼接到一起,即是对所有文件的处理逻辑。
最后再通过设置定时运行,来达到提醒的目的,还可以通过发送邮件,定时提醒相关人员,感兴趣的可以自己实现。
整体的流程大致如上,流程不太复杂,但处理稍微有点绕,网上并没有类似的处理方案,所以这里记录分享出来,供大家参考。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。