Python学习笔记与实战(下)

上一篇博客谈到了Python的一些基础语法,诚然,里面的内容讲得确实比较简单,很多内容都没有涉及到,但这并不影响我们完成一个对iOS工程文件扫描的脚本工具,用于扫描工程里面没有使用的类,在这之前,我们还是需要再补充一些知识,随后再来一一讲解整个扫描工具的代码逻辑和思路。

集合Set

Set与iOS中的NSet的含义是一样的,但是Python里面的Set更加灵活好用,在数学上的含义与高一所学习的集合是一样,其它包含求两个集合的交集并集差集对称差集,其基本的语法是这样的。

s = set[]   #创建一个空的集合
s.add('c') #添加元素
s.update([10,37,42])  # 在s中添加个元素
t.remove('H')  #删除某个元素,如果元素不存在,可能会crash
s.discard(x)  #安全地删除某个元素,如果元素不存在,则不会发生任何事
len(s)  #元素个数  
s.clear() #清空集合
x in s  #判断 x 存在于集合s中  
x not in s  #判断 x 不存在于集合s中 

s.issubset(t)  
s <= t   #判断集合s是集合t的子集 
s.issuperset(t)  
s >= t  #判断集合t是集合s的子集  

s.union(t)  
s | t  #求集合s和集合t的并集  
s.intersection(t)  
s & t  #求集合s与集合t的交集  

s.difference(t)  
s - t  #获取集合s与集合t的差集,即返回一个新的集合:集合s中去除集合t中的内容  

s.symmetric_difference(t)  
s ^ t  #获取集合s与集合t的对称差集,即返回一个新的集合:集合s与集合t的并集,再去两者的交集  

s.copy()  #集合的浅拷贝 

当然Set也重载了操作运算符=,例如我们可以写 s |= t,这样就可以快速获取两个集合的并集,其它运算符也可以依此类推。


文件读写

fp = open('文件路径','w')     直接打开一个文件,如果文件不存在则创建文件

关于open 模式:

模式 描述
r 以读方式打开文件,可读取文件信息。
w 以写方式打开文件,可向文件写入信息。如文件存在,则清空该文件,再写入新内容
a 以追加模式打开文件(即一打开文件,文件指针自动移到文件末尾),如果文件不存在则创建
r+ 以读写方式打开文件,可对文件进行读和写操作。
w+ 消除文件内容,然后以读写方式打开文件。
a+ 以读写方式打开文件,并把文件指针移到文件尾。
b 以二进制模式打开文件,而不是以文本模式。该模式只对Windows或Dos有效,类Unix的文件是用二进制模式进行操作的。

文件的一些其它读取技巧

fp.read([size])         #size为读取的长度,以byte为单位
fp.readline([size])    #读一行,如果定义了size,有可能返回的只是一行的一部分
fp.readlines([size])   #把文件每一行作为一个list的一个成员,并返回这个list。其实它的内部是通过循环调用readline()来实现的。如果提供size参数,size是表示读取内容的总长,也就是说可能只读到文件的一部分。
fp.write(str)        #把str写到文件中,write()并不会在str后加上一个换行符
fp.writelines(seq)     #把seq的内容全部写到文件中(多行一次性写入)。这个函数也只是忠实地写入,不会在每行后面加上任何东西。
fp.close()         #关闭文件。python会在一个文件不用后自动关闭文件,不过这一功能没有保证,最好还是养成自己关闭的习惯。  如果一个文件在关闭后还对其进行操作会产生ValueError
fp.flush()         #把缓冲区的内容写入硬盘
fp.fileno()        #返回一个长整型的文件标签
fp.isatty()        #文件是否是一个终端设备文件(unix系统中的)
fp.tell()          #返回文件操作标记的当前位置,以文件的开头为原点
fp.next()         #返回下一行,并将文件操作标记位移到下一行。把一个file用于for … in file这样的语句时,就是调用next()函数来实现遍历的。
fp.seek(offset[,whence])  #将文件打操作标记移到offset的位置。这个offset一般是相对于文件的开头来计算的,一般为正数。但如果提供了whence参数就不一定了,whence可以为0表示从头开始计算,1表示以当前位置为原点计算。2表示以文件末尾为原点进行计算。需要注意,如果文件以a或a+的模式打开,每次进行写操作时,文件操作标记会自动返回到文件末尾。
fp.truncate([size])        #把文件裁成规定的大小,默认的是裁到当前文件操作标记的位置。如果size比文件的大小还要大,依据系统的不同可能是不改变文件,也可能是用0把文件补到相应的大小,也可能是以一些随机的内容加上去。

下面开始进入正题:用Python来扫描Xcode工程中不用的类
首先我们整理一下大致的思路:

  • 扫描工程目录下的所有文件
  • 判断所有的这些文件是否被引入到工程里面了
  • 在工程文件中找出所有的类
  • 利用正则表达式来判断这些类有没有被工程中的其它文件调用

具体实现方法如下:

1、通过外部传递的参数sys.argv来获取工程路径

if len(sys.argv) == 1:
    print '请在.py文件后面输入工程路径' 
    sys.exit()

projectPath = sys.argv[1]
print '工程路径为%s' % projectPath

2、找出目录下的所有文件

把找到的.m.mm以及.h文件放入同一个List里面,把找到的工程文件,即.pbxproj文件存放在另一个List里面,我们的Xcode工程里面可能包含多个.pbxproj文件,例如我们所使用的第三方工程文件,如ZXing

resourcefile = []  #所有的类资源文件
pbxprojFile = []   #所有的工程文件

def getAllfile(rootDir): 
    for lists in os.listdir(rootDir): 
        path = os.path.join(rootDir, lists) 
        if os.path.isdir(path): 
            getAllfile(path) 
        else:
            ex = os.path.splitext(path)[1]  
            if ex == '.m' or ex == '.mm' or ex == '.h':
                resourcefile.append(path)
            elif ex == '.pbxproj':
                pbxprojFile.append(path)

getAllfile(projectPath)

print '工程中下的所有类文件:'
for f in resourcefile:
    print f

备注:这里我们还可以使用os.walk()函数来获取所有文件,但是这两者是有一些区别的,后者读取到的文件是按文件夹的层级排的,而上面的例子是文件的深度进行排的。所以上面的文件会把一些功能相似的代码放在同相邻目录。这对于后面进行正则表达式的查找是十分有利的。

大家注意到,这里并没有对C和C++文件的.cpp,.c文件做检测,大家如果愿意的话,可以再自行扩展。

3、接下来找出工程文件所引用到的所有文件

totalClass = set([]) #工程中引用的所有类文件
for e in pbxprojFile:
    f = open(e, 'r')
    content = f.read()
    array = re.findall(r'\s+([\w,\+]+\.[h,m]{1,2})\s+',content)
    see = set(array)
    totalClass = totalClass|see
    f.close()

print '工程中所引用的.h与.m及.mm文件'
for x in totalClass:
    print x

备注:这里的正则表达式为'\s+([\w,\+]+\.[h,m]{1,2})\s+',这里的意思是前面有若干个空格,中间是一些字母以及+组成,以.h,.m,.mm结尾,后面又是跟着若干个空格。
另外我们在工程文件. pbxproj文件中发现有很多重复的类文件,所以这里我们使用了Set来过滤掉重复的元素。

4、把引入的文件与目录下的全部文件进行匹配,找出没被引入的文件

unusedFile = [] #未引入到工程文件中的类文件

for x in resourcefile:
    ex = os.path.splitext(x)[1]
    if ex == '.h': #.h头文件可以不用检查
        continue
    fileName = os.path.split(x)[1]
    print fileName
    if fileName not in totalClass:
        unusedFile.append(x)

for x in unusedFile:
    resourcefile.remove(x)      #把没有引用到工程的文件从所有的类资源文件中移除

print '未引用到工程的文件列表为:'

writeFile = []     #把不使用的类文件记录下来,待会会写入文件当中
for unImport in unusedFile:
    ss = '未引用到工程的文件:%s\n' % unImport
    writeFile.append(ss)
    print unImport

5、从所有的类资源文件中找出所有的类

allClassDic = {}  #记录所有的类,以及这些类所在的文件路径,这里是一个Dictionary

for x in resourcefile:
    f = open(x,'r')
    content = f.read()
    array = re.findall(r'@interface\s+([\w,\+]+)\s+:',content)
    for xx in array:
        allClassDic[xx] = x
    f.close()

print '所有类及其路径:'
for x in allClassDic.keys():
    print x,':',allClassDic[x]

备注:其中正则表达式'@interface\s+([\w,\+]+)\s+:'是指以@interface开头,中间有若干个空格,中间的文字为 字母或者+号,后面以若干个空格结尾。
找出这些类以后,我们把类作为key,路径作为value存入一个Dictionary里面。

6、循环遍历所有类,以及类资源文件,找出没有使用的类

#在path文件里面查找className这个类的使用情况,如果有使用返回True,否则返回False。
def checkClass(path,className): 
    f = open(path,'r')
    content = f.read()
    if os.path.splitext(path)[1] == '.h':
        match = re.search(r':\s+(%s)\s+' % className,content) #这里用于查找子类的调用
    else:
        match = re.search(r'(%s)\s+\w+' % className,content) #.m文件中类的使用
    f.close()
    if match:
        return True

ivanyuan = 0
totalIvanyuan = len(allClassDic.keys())    #用于打印扫描的进度

for key in allClassDic.keys():
    path = allClassDic[key]

    index = resourcefile.index(path)
    count = len(resourcefile)

    used = False

    offset = 1
    ivanyuan += 1
    print '完成',ivanyuan,'共:',totalIvanyuan,'path:%s'%path #打印扫描的进度

    while index+offset < count or index-offset > 0:
        if index+offset < count:
            subPath = resourcefile[index+offset]
            if checkClass(subPath,key):
                used = True
                break
        if index - offset > 0:
            subPath = resourcefile[index-offset]
            if checkClass(subPath,key):
                used = True
                break
        offset += 1

    if not used:
        str = '未使用的类:%s 文件路径:%s\n' %(key,path)
        unusedFile.append(str)
        writeFile.append(str)

for p in unusedFile:
    print '未使用的类:%s' % p

备注:offset的作用是从class文件的位置,往两边进行查找。这样做的好处是,我们代码中有关联的类通常在放在相近的目录下面的,这样的话可以快速地命中,加快查询。

7、Finally:我们把查找出来的结果写入文件

filePath = os.path.split(projectPath)[0]
writePath = '%s/未使用的类.txt' % filePath
f = open(writePath,'w+')
f.writelines(writeFile)
f.close()

备注:写入的文件与工程目录处于同一级目录,命名为未使用的类.txt,当然,我们也可以随意的命名为其它名称。

总结

关于这个脚本工具,我已经放到github上面了,大家可以自取。
如何使用呢?请在终端上输入python py文件目录 Xcode工程文件目录。它就可以自动运行起来了。

另外要强调的是,由于工程中存在.aFrameWork的存在,以及一些注释等客观因素的存在,扫描出来的结果主要还是作为一个参考,仍然需要大家仔细的辨别。

2015-09-03 16:49410