Python读取iOS安装包IPA中的信息和图片

最近需要对所有的iOS和Android工程通过jenkins持续集成,软件的编译、打包、企业分发版的ipa发布都需要自动化。在做ipa自动化发布时,需要拿到一个app里面相关的信息,比如显示名称、版本号、bundle identifier等等。
然后在Jenkins构建任务中通过增加Python脚本针对每个版本生成IPA,plist和包含itms安装链接的网页。

首先需要对iOS ipa包的结构有些了解。

iOS IPA文件结构

首先,一个ipa其实就是一个zip文件改了后缀名。如果你把ipa的后缀改回.zip,那么你就能通过各种解压软件直接解压了。

解压后的目录结构是:

1
2
3
4
5
6
7
8
Payload
|-- ...
|-- ...
|-- 软件名.app
|
|-- ...
|-- ...
|-- Info.plist

其中,Info.plist就是软件信息的所在。

但是别高兴得太早,这里的Info.plist并不是纯文本文件,而是一种被称为Binary Plist的东西。之前在做iOS开发时,工程里面用的plist几乎都是XML形式的纯文本文件,所以一开始我理所当然地认为这个也是纯文本文件,结果在这里卡了半天。在命令行运行man plist就能看到下面这段话:

The property list programming interface allows you to convert hierarchically structured combinations of these basic types to and from two formats: standard XML and an optimized, opaque binary format.
所以不能看到.plist结尾的文件就当文本文件处理。

PYTHON中相应的LIBRARY,biplist和plistlib

在知道ipa的结构之后,就能知道需要用哪些Python library了,第一个是zipfile,这个库能够处理zip类型的文件,并且可以在不解压的情况下读取里面某个文件的内容。另外一个是plistlib。但是需要注意的是,在Python 3.4之前,这个库是不支持 Binary Plist 的解析的。所以如果使用 Python 2.7 的话,需要使用三方库biplist。

下面使用 Python 3.4 为例进行解析。之所以选择 Python 3.x 是因为 2.x 版本在处理 Unicode 和非 Unicode 混用时非常麻烦。而 Python 3.x 起,所有的 string 都使用 Unicode 编码了,就跟 ObjC 中一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import zipfile, plistlib, sys, re

def analyze_ipa_with_plistlib(ipa_path):
ipa_file = zipfile.ZipFile(ipa_path)
plist_path = find_plist_path(ipa_file)
plist_data = ipa_file.read(plist_path)
plist_root = plistlib.loads(plist_data)
print_ipa_info (plist_root)

def find_plist_path(zip_file):
name_list = zip_file.namelist()
pattern = re.compile(r'Payload/[^/]*.app/Info.plist')
for path in name_list:
m = pattern.match(path)
if m is not None:
return m.group()

def print_ipa_info(plist_root):
print ('Display Name: %s' % plist_root['CFBundleDisplayName'])
print ('Bundle Identifier: %s' % plist_root['CFBundleIdentifier'])
print ('Version: %s' % plist_root['CFBundleShortVersionString'])

if __name__ == '__main__':
args = sys.argv[1:]
if len(args) < 1:
print ('Usage: python3 ipaanalyze.py /path/to/ipa')

ipa_path = args[0]
analyze_ipa_with_plistlib(ipa_path)

首先使用zipfile将ipa文件打开,zipfile中,namelist()方法能够列出里面包含的所有文件路径,并返回一个list。根据ipa的结构,我们要找的Info.plist是Payload/软件名字.app/Info.plist,所以这里使用一个正则表达式找到这个文件路径。

ZipFile.read()这个方法,能够在不解压zip文件的情况下读取里面的内容,在Python 3.4中返回的是bytes类型,再使用plistlib.loads()载入。其中,loads()是 Python 3.4 才加入的新API,它接收一个bytes对象,将其解析成相应的Python对象。

这样,这个plist文件就变成了一个dict,从而可以取到里面的内容。

如果使用 Python 2.7, 则可以用biplist代替,这个库使用的是跟 Python 2.7 中plistlib相同的API,由于不想改服务器上默认的python2.7,我使用的是biplist。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

info_plist = workspace + "/build/Release-iphoneos/" + project + ".app/Info.plist"
print(info_plist)

try:
plist = readPlist(info_plist)
except (InvalidPlistException, NotBinaryPlistException), e:
print "Not a plist:", e
return -1

display_name = plist['CFBundleDisplayName']
identifier = plist['CFBundleIdentifier']
short_version = plist['CFBundleShortVersionString']

print ('Display Name: %s' % display_name)
print ('Bundle Identifier: %s' % identifier)
print ('Version: %s' % short_version)

如果不使用 Python,或者不想用这些类库,则可以通过一些外部程序来解决Binary Plist的读取问题。比如使用/usr/libexec/PlistBuddy或者是plutil,这些都是 OS X 自带的工具,通过它们,你可以读取Plist中的指定项,或者直接把plist变成JSON文件。

读取ipa .app目录下的图片并还原

由于Xcode工程中图标的文件名和位置都不固定,拿到AppIcon需要分析工程文件和AppIcon.appiconset/Contents.json文件内容,感觉分析过程太复杂了,容易出错误,因此还是想通过分析编译后的build/Release-iphoneos/myproject.app/包目录下找。app中所有的图片都在根目录下,但这些图片文件已经被xcode处理过了,在浏览器中显示为空白,因此需要想办法把图片文件还原。
在网上找到了一个python脚本,iPIn iPhone PNG Images Normalizer, 感觉这个代码时间太久了,2007年写的,测试了下不能处理正确图片。
在这篇文章的评论里面发现一个链接https://gist.github.com/urielka/3609051, 应该是个2012年的改进版本,看下面的评论说是可用,实验了下果然可以。图标问题解决了,处理后的图片可以在浏览器正常显示了。

[本文独立博客地址](http://www.offbye.com/)

Contents
  1. 1. iOS IPA文件结构
  2. 2. PYTHON中相应的LIBRARY,biplist和plistlib
  3. 3. 读取ipa .app目录下的图片并还原
,