文件和格式

当你使用电脑(Windows 或者 macOS 操作系统)或手机(Android 或者 iOS 操作系统)点击打开一个文件的时候,有没有想过,这中间发生了什么呢?

你有没有想过,.jpg.png 文件到底是什么,有什么区别?

为了处理数据,我们可能得先知道一下这些原理。

程序的组成

首先,请回顾一下我们在基本概念中提到的,计算机只能接受 10 (二进制)组成的指令(机器码)。任何语言书写的程序,都会被先翻译成 10 组成的指令,然后才能执行。

但是,程序不只包含指令,还包含数据。让我们拿王者荣耀(一个超级复杂的程序)作为例子:

当你被打了一下,你会损失生命值,我们来猜测一下它的实现方式。

注意以下几点:
- 这可以看作是不能运行的 伪代码,或者代码框架,用来给人读,而不能直接运行;
- 程序里有很多我们并没有定义的函数,大家可以根据函数名猜测一下它大概是做什么的。这是很重要的抽象思想,在之后我们会讲解到;
- 类似于 victim.getArmor() 这样的语法,可以类比一下字符串的 someString.split()someString.split() 会把 someString 拿过来作为参数,然后输出相关的东西(分割好的字符串列表);同理,victim.getArmor() 会把 victim 变量拿来作为参数,输出 victim 的当前护甲;
- 我们只考虑物理伤害;

def calculateRealDamage(baseDamage, armorPenFlat, armorPenPercent, armor):
    """
    根据伤害和护甲计算真实伤害
    baseDamage: 技能描述上写的基础伤害
    armorPenFlat: armor penetration flat,攻击者的固定护甲穿透
    armorPenPercent: armor penetration percent,攻击者的百分比护甲穿透
    armor: 被攻击者的护甲
    """ 
    realArmor = (armor - armorPenFlat) - armor * armorPenPercent # 经过穿透后的剩余护甲
    damageReduction = realArmor / (realArmor + 600) # 伤害减免的比例

    realDamage = damage * (1 - damageReduction)
    return realDamage

def attack(attacker, ability, victim):
    """
    attacker: 攻击的人
    ability: 攻击的人使用的技能
    victim: 被攻击的人
    """
    baseDamage = ability.getDamage(attacker) # 根据攻击者的属性计算技能基础伤害
    realDamage = calculateRealDamage(baseDamage, attacker.getArmorPenFlat() ,attacker.getArmorPenPercent(), victim.getArmor()) # 调用 calculateRealDamage 计算真实的伤害

    life = victim.getLife() # 获取被攻击者生命值
    newLife = life - realDamage
    if newLife <= 0:  # 如果被攻击者生命值降到 0 以下
        victim.setLife(0)
        victim.die()       
    else:
        victim.setLife(newLife) # 玩家的生命值变为一个新的值(被扣除了一些)
    return 

之后,这段代码会被翻译成 10 组成的一连串指令,储存在你的手机上。
每当你玩游戏,有攻击事件发生的时候,这段指令就会被执行。

但是,除了指令之外,你的手机还有别的东西要储存:这就是数据

让我们来看一下 victim.die() 这个函数。有一些面向对象的方法(比如 class 等),我们暂时不需要掌握。

import soundLibrary
class Player: # victim 就是一个 Player

    def die(self):
        """
        英雄阵亡时播放英雄专属的阵亡音效
        """
        with open(self.getSoundFilePath(), 'rb') as f: 
        # self.getSoundFilePath() 可能返回类似于 'Libai-die-2.wav' 这样的字符串
            soundWave = soundLibrary.process(f) # 把声音处理成可以播放的
            soundLibrary.play(soundWave) # 播放英雄专属的阵亡音效

    #####################
    ## 可能还有很多其它函数...
    #####################



这里,f 就是二进制的声音文件,它是和整个游戏一起被下载到你的手机里,并储存起来的。我们管这样的东西叫做 数据

事实上,好几个 GB 大小的王者荣耀游戏文件中,指令 可能只占了不到百分之一,剩下的全部都是图片、音频、文字、模型等等数据,这些 数据 需要在运行的时候被读取、处理、展示。
(当然,die 函数的逻辑肯定不是上面这样,因为每次有英雄阵亡都重新读文件非常非常耗时(读取文件是相当慢的操作),一定是加载游戏的时候读出来,然后反复使用的)

数据的本质

那么,这些数据是怎么保存在电脑上的呢?

和指令一样,它也是以二进制的形式保存的。对电脑来说,硬盘里的存储介质被磁化就代表 1,否则就是 0

我们要存储的信息(比如一个图片)先是以一定规则被 编码 成二进制(比如,规定好,文件的每 24 位二进制数代表一个像素的 RGB 值,用这种方式保存图片),存储下来,用到的时候,再用对应的 解码 方式还原出来。我们可以用任何方法 解码 任何数据,只不过解出来的东西会让人不知所云。比如,对一个 .doc 文件(word 文档),你可以选择使用记事本打开,然后会看到一堆乱码,这就是所谓的 打开方式不对

.doc.docx.jpg 等拓展名,只是用来标明文件的类型,告诉人们需要用什么方式来 解码的,并没有什么实际的作用。当然,现在的 Windows 系统会根据对应的拓展名自动选择解码方式(指定默认打开方式),所以会出现更改拓展名之后无法打开的情况。当然,你可以强行把一个 .jpg 文件的拓展名改成 .doc,然后手动选择用图片查看器打开,这完全没有问题。

Python 中的实现

Python 中的 open 函数,会返回一个变量。这个变量本身并不包含任何内容,它只是一个读取文件用的“窗口”,我们可以通过这个变量读取文件内容。

通常情况下,对于文本文件(就是使用记事本打开,能直接看到文字内容,而不是乱码的东西),我们使用 open(filename),对于非文本(图片、音频等),我们使用 open(filename, 'rb')

open 打开一个 txt 文件,并且调用 readlines,你可以看到它的内容。

f = open("txtFile.txt")
print(f.readlines())

你可以试试用它来打开其它格式的文件。你会发现,大多数情况下会得到乱码。这是因为,readlines() 会从字面意思上读取文件(得到二进制码),而 print 会尝试把它 解码 成英文字符。然而,由于它实际上是其它东西,所以 解码 出来的东西会不知所云。

想要读取特定类型的文件,我们需要与文件匹配的 解码 方法。在 Python 中,我们可以使用第三方库来解决。
比如,如果你想读取一个图片并展示出来,你可以使用 Pillow 这个库。

import PIL
from PIL import Image
image = Image.open("yazi.jpg") # 这个函数其实先使用了 Python 的 open 函数,然后进行解码。
# 希望深究的同学可以看一下它的源码 https://pillow.readthedocs.io/en/stable/_modules/PIL/Image.html#open
image.show()    # 这句段代码可以打开一个新的窗口来展示图片。
                # 你也可以直接通过 image 变量读取或修改每一个像素点。