2018年10月

爱奇艺的弹幕文件形如:

https://cmts.iqiyi.com/bullet/59/00/1307555900_300_1.z?rn=0.7268306364207229&business=danmu&is_iqiyi=true&is_video_page=true&tvid=1307555900&albumid=214500601&categoryid=2&qypid=01010021010000000000

实际可简化为

https://cmts.iqiyi.com/bullet/59/00/1307555900_300_1.z

链接组成:

https://cmts.iqiyi.com/bullet/tvid倒数4位的前两位/tvid最后两位/tvid_300_x.z
x的计算方式为片子总时长除以300秒向上取整,即按每5分钟一个包。

转换方法:
二进制读取文件,转换为字节数组,用zlib库解包,以utf-8解码即可。
python实现代码:

import zlib
import requests
zread = open('1307555900_300_1.z', 'rb').read()
zarray = bytearray(zread)
xml=zlib.decompress(zarray, 15+32).decode('utf-8')
with open('qiyi.xml','w',encoding='utf-8') as f:
    f.write(xml)

如果不是保存的文件,直接读取弹幕链接的response

import zlib
import requests
zget = requests.get(url).content#url即弹幕链接 省略
zarray = bytearray(zread)
xml=zlib.decompress(zarray, 15+32).decode('utf-8')

结果:
QQ截图20181023012046.png
QQ截图20181023012131.png

http://player-pc.le.com/mms/out/video/playJson.json?platid=1&splatid=107&tss=no&id=1578861&detect=0&dvtype=1000&accessyx=1&domain=m.le.com&tkey=1331462795&devid=82b7ba07f710dc9cfa1614a7c218e596&source=1001&lang=cn&region=cn&isHttps=0

这是乐视在移动端的视频请求接口,其中tkey是主要的参数,缺少就得不到结果。

通过搜索tkey可以发现只在下面这个js出现过
http://jstatic.letvcdn.com/sdk/player.js

            var t, a = r.PROTOCAL + r.HOST_NAME.PLAYER_PC_LETV_COM + "/mms/out/video/playJson.json", i = n.getMmsKey(e);
            if (this.playerData.videoType = !this.playerData.config.supportP2P && "" != o.supportM3U8 && "1" != o.supportM3U8 || o.forceMp4 ? "no" : "ios",
            parent === window)
                try {
                    t = location.host
                } catch (l) {
                    t = "." + document.domain
                }
            var d = {
                1: "350",
                2: "1000",
                3: "1300",
                4: "720p",
                5: "1080p"
            }
              , h = {
                105: 1,
                106: 1,
                107: 1
            }
              , c = {
                platid: h[this.playerData.config.splatId] || 3,
                splatid: this.playerData.config.splatId,
                tss: this.playerData.videoType,
                id: this.playerData.vinfo.vid,
                detect: this.playerData.flashVar.mmDetect || "1",
                dvtype: d[this.playerData.config.defi] || "",
                accessyx: 1,
                domain: t,
                tkey: i,
                devid: this.manager.pingback.getLC(),
                source: "1001",
                lang: this.playerData.flashVar.lan,
                region: this.manager.listener.get(r.REGION),
                isHttps: 1 == this.playerData.flashVar.isHttps ? 1 : 0
            };

可以得知tkey是函数getMmsKey计算的,通过断点得到计算代码(VM中计算的)
代码如下

var Y6K = (function () {
    var M9 = (57.9E1, 92.194E3) < 31.593E+2 ? (97.9E2, 0x50) : 40. <= (23., 0x43) ? (33E1, 0x1e0f19174415) : (99.551E2, 0.) < (0x26, 60) ? (0x56, 85.669E2) : (86, 35.95E0) > 55. ? 0x33 : 0x60,
    I9 = (function (j3, p7) {
        var V4 = "",
        R7 = 92 >= (59.172E2, 45.114E2) ? 0x6 : (0x22, 83) <= (0x53, 52.) ? (71, 81.124E3) : (4, 20.443E2) <= (0x23, 0x2c) ? 91 : (86.075E+0, 0x4f) < (2.8E3, 85) ? (0x4, false) : (30, 0x13) < (0.19E2, 21.01E1) ? (0x11, 46E1) : (87., 58.1E1);
        if (j3.length > 97. > 5.012E2 ? (0x3b, 0x1f) : (80.604E0, 52.52E2) > (0x13, 50.585E2) ? (0x3b, 0xc) : (0x4a, 0x37) < (0x33, 73.36E+0) ? (0x51, 20.8E+1) : (3, 0x3) <= (53.31E1, 18.266E+0) ? 9. : (37.2E2, 75.5E1))
            for (var G5 = 13.935E2 >= (41., 93.8E0) ? (0x49, 0xd) : (0x34, 0x21) < 0x42 ? (62, 12.09E+3) : 85. >= (0x2f, 0x1d) ? (0x58, 30.6E2) : (27.1E0, 48.) < (0x37, 87.62E3) ? (62.524E1, 0xa) : 97 <= (0x5f, 0xf) ? 49.1E1 : 5.05E2 >= (23.1E3, 80.312E+1) ? (0x36, 0x56) : (3., 0x51) < 13.9E1 ? 70.52E+2 : 72.663E3; G5 > 1; )
                V4 += (R7 = R7 ? 52. < (90.8E3, 0xa) ? (0x2c, 66) : (63.3E2, 0x55) < (0x5b, 0x0) ? (1.989E2, 15.9E1) : (28.85E1, 0x2d) >= 93.1E3 ? (26.7E0, 95.8E1) : (65.14E3, 0x34) >= (0xc, 57.8E1) ? (98.3E0, 74.8E+2) : (0x52, 0x54) > (12.35E3, 52.2E3) ? 0x49 : (37.364E2, 85.2E3) <= (36, 0x38) ? 39.7E1 : (26.176E2, 0x63) > 0x3a ? (71.858E1, false) : (2.832E2, 53E+3) : (0x27, 19.63E1) <= (0x49, 77.) ? (69.55E2, 39.85E+2) : (0x22, 0x63) <= 0x3d ? (0x38, 47.06E1) : (91., 92.87E2) <= (76.2E2, 0x50) ? (68.411E+2, 0x54) : (61.96E1, 0x3a) >= (0x9, 9.6E1) ? (0x1, 0x22) : (67.347E1, 22.699E1) < 76.574E2 ? (0xa, true) : (29., 86) >= (14, 0x1d) ? (0x52, 2.2E+0) : 0x1e) ? j3.charAt(G5) : "@%)eitg)(tDwn".charAt(G5--);
        return p7 === null ? eval(V4) : p7 ^ j3
    })("_9(mTe.)ea e(", 0x3d >= 19.2E2 ? (79, 37.1E1) : 85E3 <= 0x47 ? (33.7E1, 77) : (0x8, 14.) >= (85.778E2, 61.) ? 7 : 0x5a < (86.37E1, 3.218E2) ? null : (0xd, 0x58) >= (64., 0xc) ? (72.1E3, 16) : (1, 0x2f) > 0x54 ? (7.5E0, 0x5b) : (38.4E+2, 56.2E2));
    return {
        k0: function (S7) {
            var C7,
            R2 = (88.4E3, 90.058E3) <= (36E1, 0x1d) ? (85., 0x1) : (0x4d, 84) >= 73.041E1 ? 91 : (0xe, 0x16) >= (20., 0x5b) ? (34., 0x3e) : 10 < 0x1c ? (25.801E0, 0x0) : 92.374E3 < (56.2E+2, 20.) ? (85.08E3, 0x42) : (0x14, 24.5E2) >= (0x1a, 0x5d) ? (75, 0x44) : (0x3f, 0x2d) < (51.9E1, 0x7) ? (4, 0x1f) : (45.654E1, 0.046E+1),
            Y9 = M9 > I9,
            U2;
            for (; R2 < S7.length; ) {
                U2 = (parseInt(S7.charAt(R2), (0x2f, 0x61) >= (0x58, 61.49E2) ? (0x2b, 14.6E+0) : 80.6E1 > 1. ? (95., 0x10) : 0x59 >= 57.386E+1 ? (48., 77.) : 31.23E1 <= (94.3E2, 32.) ? (86., 60) : (7.819E+1, 0x54) < (3.9E3, 86.) ? 48.649E0 : 84. < (22, 16.6E+3) ? (0x44, 0x54) : (55, 82.9E1) <= (64.12E2, 0x57) ? (0x3a, 0xe) : (70.003E+0, 0x35))).toString(0x1f >= (67., 0x30) ? (0x62, 57.5E+1) : (61.499E1, 41.52E3) < (81.763E1, 48) ? (0x7, 68.73E2) : (12, 91.3E+2) >= (0x4b, 0xf) ? (96, 0x2) : (86., 82.84E+1) >= 0x3f ? (0x11, 35.138E1) : (0x15, 0x61) < (0x10, 0x8) ? (0x5f, 19.) : (13E+2, 0x35) > 94.2E+0 ? (0x2, 0xb) : (71.008E1, 89.65E2));
                C7 = R2++ == 0x1c >= (85., 55) ? (0xb, 0.65E0) : (0x1a, 0x26) >= (0x55, 89.657E3) ? (0x1d, 0x2) : (53.34E2, 56.9E+1) < 90. ? 6.254E1 : 12.4E2 <= (70., 63.84E3) ? 0x0 : 0x2f >= (0, 36.83E1) ? (0x61, 0x29) : (27., 67.792E3) > (67.19E1, 0x55) ? 36.979E0 : (0x2d, 28.3E1) ? U2.charAt(U2.length - (0x2e, 0x36) < (14, 0xd) ? (0x29, 7.2E+0) : (0x14, 77.) < 58. ? 16.569E2 : (0x31, 60.64E+1) > (90., 61.4E+1) ? (0x1, 81E0) : (79.43E0, 0x49) <= (20., 26.) ? (30.65E2, 55.) : (0x13, 0x1)) : C7 ^ U2.charAt(U2.length - (0.4E0, 0x22) >= 43.8E0 ? (0x38, 56.34E2) : (98, 0x16) < (0x39, 11.) ? (0x21, 13) : (0x51, 0x34) <= 97.9E2 ? (0x35, 0x1) : (28, 69.) < (0x40, 94.802E+0) ? (64.6E2, 39) : (15., 30))
            }
            return C7 ? !Y9 : Y9
        }
    }
})();
var _1 = "feda8dd6e0127da88f3487a646fe8a6b", _2 = Y6K.k0("08") ? "jjuy9567dfj6bkksomnnghwokjlu0o" : "b", _3 = Y6K.k0("68") ? "tYt2bxik" : "toString";
var I4c = {
    I: function (l, v) {
        return l != v
    },
    f: function (l, v) {
        return l << v
    },
    b: function (l, v) {
        return l & v
    },
    B: function (l, v) {
        return l < v
    },
    A: function (l, v) {
        return l !== v
    },
    G: function (l, v) {
        return l ^ v
    },
    v: function (l, v) {
        return l === v
    },
    N: function (l, v) {
        return l - v
    },
    k: function (l, v) {
        return l >> v
    },
    m: function (l, v) {
        return l | v
    },
    Y: function (l, v) {
        return l == v
    },
    j: function (l, v) {
        return l % v
    },
    h: function (l, v) {
        return l > v
    }
};
var Key = {
    liveKey: {
        "default": _1
    },
    APIKey: _2,
    secret_key: _3,
    getAPIKey: function (e) {
        var _1 = Y6K.k0("u29") ? "AES" : "rotateRight",
        _2 = "decrypt",
        _3 = Y6K.k0("63") ? "APIKey" : "decrypt",
        _4 = "toString",
        _5 = "enc",
        _6 = "Utf8",
        _7 = Y6K.k0("h93") ? "error" : "MD5";
        try {
            return Encode[_1][_2](e, this[_3])[_4](Encode[_5][_6])
        } catch (t) {
            return _7
        }
    },
    getCl: function (e) {
        var _1 = Y6K.k0("4f") ? "MD5" : "decrypt",
        _2 = Y6K.k0("471") ? "showmethemoney" : "join",
        _3 = "error";
        try {
            return Encode[_1](e + _2)
        } catch (t) {
            return _3
        }
    },
    getMmsKey: function (e) {
        var _1 = Y6K.k0("1436") ? "j" : "error",
        _2 = "rotateRight",
        _3 = "G";
        var t = ("j", 48.36E2) < 68. ? ("error", 0x61) : (85.5E2, 0x11) < 84. ? (25.045E1, 0xb074319) : 39.62E1 <= (",", 25.2E+1) ? 88. : 32.84E2 >= (81.5E2, 36.7E0) ? 0x27 : 0x55 > 0x37 ? (79.219E0, 0x1c) : ("G", 0.363E1) <= 44.834E2 ? 98 : 95.48E+3 > (0x0, 0x45) ? (1.288E3, "h") : ("b", 0x1b) >= (95.38E+0, 0x5e) ? (37.9E+0, 0x1a) : (19.1E+0, 71.6E2) > (0x30, "error") ? (0x5e, 6.62E1) : (58.58E1, 57.5E+1),
        r = I4c[_1](t, 17),
        n = e;
        n = this[_2](n, r);
        var o = I4c[_3](n, t);
        return o
    },
    rotateRight: function (e, t) {
        var _1 = Y6K.k0("572U") ? "h" : "error",
        _2 = Y6K.k0("6U8a") ? "b" : "toString";
        for (var r, n = 0; I4c[_1](t, n); n++)
            r = I4c[_2](1, e), e >>= 1, r <<= 31, e += r;
        return e
    },
    getLiveTkey: function (e, t, r) {
        var _1 = "liveKey",
        _2 = "default",
        _3 = Y6K.k0("9755") ? "MD5" : "b",
        _4 = Y6K.k0("09") ? "join" : "jjuy9567dfj6bkksomnnghwokjlu0o",
        _5 = ",",
        _6 = Y6K.k0("my") ? "toString" : "AES";
        var r = r || this[_1][_2];
        return Encode[_3]([e, t, r][_4](_5))[_6]()
    },
    getPingBackKey: function (e, t, r, n) {
        var _1 = "MD5",
        _2 = "secret_key",
        _3 = "toString";
        return Encode[_1](e + t + r + n + this[_2])[_3]()
    }
};

在末尾添加
console.log(Key.getMmsKey('1539412506.487'));
保存为然后将代码保存为js文件,通过node xxx.js运行可以得到tkey是多少
id=1578861对应于网页链接中的数字

# -*- coding: utf-8 -*-

from selenium import webdriver

def download(driver, target_path):
    """Download the currently displayed page to target_path."""
    def execute(script, args):
        driver.execute('executePhantomScript',
                       {'script': script, 'args': args})

    # hack while the python interface lags
    driver.command_executor._commands['executePhantomScript'] = ('POST', '/session/$sessionId/phantom/execute')
    # set page format
    # inside the execution script, webpage is "this"
    page_format = 'this.paperSize = {format: "A3", orientation: "portrait" };'
    execute(page_format, [])#使用A4打印的pdf不完整,故用A3

    # render current page
    render = '''this.render("{}")'''.format(target_path)
    execute(render, [])


if __name__ == '__main__':
    """Download a webpage as a PDF."""
    driver = webdriver.PhantomJS(executable_path=r'D:\phantomjs\bin\phantomjs.exe')
    url = 'http://helloiamkitty.blog.163.com/blog/static/18967710120115306544362/'
    driver.get(url)
    driver.find_element_by_css_selector(".iknow.ztag").click()#首次访问会强制弹出公告,点击关闭
    driver.save_screenshot('test.png')
    download(driver, "save_me.pdf")

尝试使用pdfkit时,需要加载js,但是不知道怎么回事加载失败,放弃。

OSError: wkhtmltopdf reported an error:
Loading pages (1/6)
Warning: Failed to load file://www.google-analytics.com/analytics.js (ignore)
Warning: Failed to load file:///newpage/prettycode/prettify.css (ignore)
Warning: Failed to load file:///newpage/prettycode/prettify.js (ignore)
Error: Failed to load http://widget.wumii.com/ext/relatedItemsWidget.htm, with network status code 202 and http status code 403 - Error downloading http://widget.wumii.com/ext/relatedItemsWidget.htm - server replied: Forbidden
Warning: Failed to load file:///common/showhint/hintbg.png (ignore)
libpng warning: iCCP: known incorrect sRGB profile           ] 62%
libpng warning: iCCP: known incorrect sRGB profile           ] 64%
Warning: Failed to load file://www.google-analytics.com/analytics.js (ignore)
libpng warning: iCCP: known incorrect sRGB profile           ] 71%
Error: Failed to load http://rec.g.163.com/kaolaad/api/smartad/rec.s?type=240x275&location=1&site=netease&affiliate=blog&cat=detail, with network status code 301 and http status code 502 - Error downloading http://rec.g.163.com/kaolaad/api/smartad/rec.s?type=240x275&location=1&site=netease&affiliate=blog&cat=detail - server replied: Bad Gateway
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done
Exit with code 1 due to network error: ProtocolUnknownError

这样也能另存pdf
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --headless --print-to-pdf=test.pdf --disable-gpu url

参考链接:
https://stackoverflow.com/questions/23125557/how-to-run-webpage-code-with-phantomjs-via-ghostdriver-selenium/28269099#28269099
https://segmentfault.com/q/1010000012973252
https://blog.darkthread.net/blog/headless-chrome/

先上一张效果图
外星人入侵demo

再来一张动图
外星人入侵gif演示

代码如下(较多)
alien_invasion/alien_invasion.py

import pygame

from pygame.sprite import Group
from settings import Settings
from game_stats import GameStats
from scoreboard import Scoreboard
from button import Button
from ship import Ship
import game_functions as gf

def run_game():
    # 初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode(
        (ai_settings.screen_width,ai_settings.screen_height)
    )
    # screen = pygame.display.set_mode((1200,800))
    pygame.display.set_caption("外星人入侵")

    # 创建开始游戏按钮
    play_button = Button(ai_settings, screen, "PLAY")

    # 创建一个用于存储游戏统计信息的实例,并创建记分牌
    stats = GameStats(ai_settings)
    sb = Scoreboard(ai_settings, screen, stats)
    # 设置背景色
    bg_color = (230,230,230)

    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    # 创建一个用于存储子弹的编组
    bullets = Group()

    # 创建一个外星人
    # alien = Alien(ai_settings, screen)
    # 创建一个外星人编组
    aliens = Group()

    # 创建外星人群
    gf.create_fleet(ai_settings, screen,  ship, aliens)

    # 开始游戏的主循环
    while True:

        # 监视键盘和鼠标事件
        gf.check_events(ai_settings, screen, stats, sb, play_button, ship,
                        aliens, bullets)
        if stats.game_active:
            ship.update()
            gf.update_bullets(ai_settings ,screen, stats, sb, ship, aliens,
                              bullets)
            gf.update_aliens(ai_settings, screen, stats, sb, ship, aliens, bullets)

        gf.update_screen(ai_settings, screen, stats, sb, ship, aliens,
                         bullets, play_button)

run_game()

alien_invasion/game_stats.py

class GameStats():
    """跟踪游戏的统计信息"""

    def __init__(self, ai_settings):
        """初始化统计信息"""
        self.ai_settings = ai_settings
        self.reset_stats()
        # 游戏刚启动时处于非活动状态
        self.game_active = False

        # 在任何情况下都不应该重置最高得分
        self.high_score = 0

    def reset_stats(self):
        """初始化在游戏运行期间可能变化的统计信息"""
        self.ships_left = self.ai_settings.ship_limit
        self.score = 0
        self.level = 1

alien_invasion/scoreboard.py

import pygame.ftfont
from pygame.sprite import Group

from ship import Ship

class Scoreboard():
    """显示得分信息的类"""

    def __init__(self, ai_settings, screen, stats):
        """初始化显示得分涉及的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.ai_settings = ai_settings
        self.stats = stats

        # 显示得分信息时使用的字体设置
        self.text_color = (30, 30, 30)
        self.font = pygame.ftfont.SysFont('arial', 48)

        # 准备包含最高得分和当前得分的图像
        self.prep_score()
        self.prep_high_score()
        self.prep_level()
        self.prep_ships()

    def prep_score(self):
        """将得分转换为一幅渲染的图像"""
        # rounded_score = int(round(self.stats.score, -1)) # python2
        rounded_score = round(self.stats.score, -1)
        score_str = "{:,}".format(rounded_score)
        # score_str = str(self.stats.score)
        self.score_image = self.font.render('Score ' + score_str,
            True, self.text_color, self.ai_settings.bg_color)

        # 将得分放在屏幕右上角
        self.score_rect = self.score_image.get_rect()
        self.score_rect.right = self.screen_rect.right - 20
        self.score_rect.top = 20

    def prep_high_score(self):
        """将最高得分转换为渲染的图像"""
        high_score = round(self.stats.high_score, -1)
        high_score_str = "{:,}".format(high_score)
        self.high_score_image =self.font.render('High Score ' + high_score_str,
            True, self.text_color, self.ai_settings.bg_color)

        # 将最高得分放在屏幕顶部中央
        self.high_score_rect = self.high_score_image.get_rect()
        self.high_score_rect.centerx = self.screen_rect.centerx
        self.high_score_rect.top = self.score_rect.top

    def show_score(self):
        """在屏幕上显示得分"""
        self.screen.blit(self.score_image, self.score_rect)
        self.screen.blit(self.high_score_image, self.high_score_rect)
        self.screen.blit(self.level_image, self.level_rect)
        # 绘制飞船
        self.ships.draw(self.screen)

    def prep_level(self):
        """将等级转换为渲染的图像"""
        self.level_image = self.font.render('Level ' + str(self.stats.level),
            True, self.text_color, self.ai_settings.bg_color)

        # 将等级放在得分下方
        self.level_rect = self.level_image.get_rect()
        self.level_rect.right = self.score_rect.right
        self.level_rect.top = self.score_rect.bottom + 10

    def prep_ships(self):
        """显示剩余飞船数"""
        self.ships = Group()
        for ship_number in range(self.stats.ships_left):
            ship = Ship(self.ai_settings, self.screen)
            ship.rect.x = 10 + ship_number * ship.rect.width
            ship.rect.y = 10
            self.ships.add(ship)

alien_invasion/button.py

import pygame.ftfont
import pygame.sysfont
class Button():

    def __init__(self, ai_settings, screen , msg):
        """初始化按钮的属性"""
        self.screen = screen
        self.screen_rect = screen.get_rect()

        # 设置按钮的尺寸和其他属性
        self.width, self.height = 200, 50
        self.button_color = (0, 255, 0)
        self.text_color = (255, 255, 255)
        self.font = pygame.ftfont._SysFont('arial', 48)

        # 创建按钮的rect对象,并使其居中
        self.rect = pygame.Rect(0, 0, self.width, self.height)
        self.rect.center = self.screen_rect.center

        # 按钮的标签只需要创建一次
        self.prep_msg(msg)

    def prep_msg(self, msg):
        """将msg渲染为图像,并使其在按钮上居中"""
        self.msg_image = self.font.render(msg, True, self.text_color,
                                          self.button_color)
        self.msg_image_rect = self.msg_image.get_rect()
        self.msg_image_rect.center = self.rect.center

    def draw_button(self):
        # 绘制一个用颜色填充的按钮,再绘制文本
        self.screen.fill(self.button_color, self.rect)
        self.screen.blit(self.msg_image, self.msg_image_rect)

alien_invasion/game_functions.py

import sys

import pygame

from bullet import Bullet
from alien import Alien

from time import sleep

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
        # 向右移动飞船
        # ship.rect.centerx += 1
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        fire_bullet(ai_settings, screen, ship, bullets)
    elif event.key == pygame.K_q:
        sys.exit()

def check_keyup_events(event, ship):
    """响应松开"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False

def check_events(ai_settings, screen, stats, sb, play_button, ship, aliens,
                 bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ai_settings, screen, ship, bullets)
            # check_keydown_events(event, ship)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_x, mouse_y = pygame.mouse.get_pos()
            check_play_button(ai_settings, screen, stats, sb, play_button, ship,
                              aliens, bullets, mouse_x, mouse_y)

def update_screen(ai_settings, screen, stats, sb, ship, aliens,
                  bullets, play_button):
    """更新屏幕上的图像,并切换到新屏幕"""
    # 每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    # screen.fill(bg_color)

    # 在飞船和外星人后面重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()

    ship.blitme()
    aliens.draw(screen)
    # alien.blitme()

    # 显示得分
    sb.show_score()

    # 如果游戏处于非活动状态,就绘制开始游戏按钮
    if not stats.game_active:
        play_button.draw_button()

    # 让最近的绘制屏幕可见
    pygame.display.flip()

def update_bullets(ai_settings ,screen, stats, sb, ship, aliens, bullets):
    """更新子弹的位置 并删除已消失的子弹"""
    # 更新子弹的位置
    bullets.update()

    # 删除已消失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship,
                                  aliens, bullets)

def fire_bullet(ai_settings, screen, ship, bullets):
    """如果还没有达到限制,就发射一颗子弹"""
    # 创建一颗子弹,并将其加入到编组bullets中
    if len(bullets) < ai_settings.bullets_allowed:
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)

def create_fleet(ai_settings ,screen, ship, aliens):
    """创建外星人群"""
    # 创建一个外星人,并计算一行可容纳多少个外星人
    # 外星人间距为外星人宽度
    alien = Alien(ai_settings, screen)
    number_space_x = get_number_aliens_x(ai_settings, alien.rect.width)
    number_rows = get_number_rows(ai_settings, ship.rect.height,
                                  alien.rect.height)
    for row_nunber in range(number_rows):
        # 创建第一行外星人
        for alien_number in range(number_space_x):
            # 创建一个外星人并将其加入当前行
            create_alien(ai_settings, screen, aliens ,alien_number,
                         row_nunber)
            # create_alien(ai_settings, screen, aliens ,alien_number)

def get_number_aliens_x(ai_settings ,alien_width):
    """计算每行可容纳多少外星人"""
    avaliable_sqpce_x = ai_settings.screen_width - 2 * alien_width
    number_space_x = int(avaliable_sqpce_x / (2 * alien_width))
    return number_space_x

def create_alien(ai_settings, screen, aliens ,alien_number, row_number):
    """创建一个外星人并将其放在当前行"""
    alien = Alien(ai_settings, screen)
    alien_width = alien.rect.width
    alien.x = alien_width + 2 * alien_width * alien_number
    alien.rect.x = alien.x
    alien.rect.y = alien.rect.height + 2 * alien.rect.height * row_number
    aliens.add(alien)

def get_number_rows(ai_settings, ship_height, alien_height):
    """计算屏幕可容纳多少行外星人"""
    available_space_y = (ai_settings.screen_height -
                         (3 * alien_height) - ship_height)
    number_rows = int(available_space_y / (2 * alien_height))
    return number_rows

def update_aliens(ai_settings, screen, stats, sb, ship, aliens, bullets):
    """检查是否有外星人位于屏幕边缘,更新外星人群中所有外星人的位置"""
    check_fleet_edges(ai_settings, aliens)
    aliens.update()

    # 检测外星人和飞船相撞
    if pygame.sprite.spritecollideany(ship, aliens):
        ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)
        # print("飞船遭遇撞击!!!")
    # 检查是否有外星人到达屏幕底端
    check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens, bullets)

def check_fleet_edges(ai_settings, aliens):
    """有外星人到达边缘时采取相应的措施"""
    for alien in aliens.sprites():
        if alien.check_edges():
            change_fleet_direction(ai_settings, aliens)
            break

def change_fleet_direction(ai_settings, aliens):
    """将外星人下移,并改变他们的方向"""
    for alien in aliens.sprites():
        alien.rect.y += ai_settings.fleet_drop_speed
    ai_settings.fleet_direction *= -1

def check_bullet_alien_collisions(ai_settings, screen, stats, sb, ship, aliens, bullets):
    """响应子弹和外星人的碰撞"""
    # 删除发生碰撞的子弹和外星人

    # 检查是否有子弹击中了外星人
    # 如果是这样,就删除相应的子弹和外星人
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)

    if collisions:
        for aliens in collisions.values():
            stats.score += ai_settings.alien_points
            sb.prep_score()
        check_high_score(stats, sb)

    if len(aliens) == 0:
        # 如果整群外星人被消灭,就提高一个等级
        # 删除现有的子弹并创建一群新外星人
        bullets.empty()
        ai_settings.increase_speed()

        # 提高等级
        stats.level += 1
        sb.prep_level()

        create_fleet(ai_settings, screen, ship, aliens)

def ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets):
    """响应被撞到的飞船"""
    if stats.ships_left > 0:
        # 将ships_left减1
        stats.ships_left -= 1

        # 更新记分牌
        sb.prep_ships()

        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()

        # 创建一群新的外星人,并将飞船放到屏幕底中央
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

        # 暂停
        sleep(0.5)
    else:
        stats.game_active = False
        pygame.mouse.set_visible(True)

def check_aliens_bottom(ai_settings, screen, stats, sb, ship, aliens, bullets):
    """检查是否有外星人到达了底端"""
    screen_rect = screen.get_rect()
    for alien in aliens.sprites():
        if alien.rect.bottom >= screen_rect.bottom:
            # 像飞船被撞倒一样处理
            ship_hit(ai_settings, screen, stats, sb, ship, aliens, bullets)
            break

def check_play_button(ai_settings, screen, stats, sb, play_button, ship, aliens,
                      bullets, mouse_x, mouse_y):
    """在玩家单击开始游戏按钮时开始游戏"""
    button_clicked = play_button.rect.collidepoint(mouse_x, mouse_y)
    if button_clicked and not stats.game_active:
        # 重置游戏设置
        ai_settings.initialize_dynamic_settings()

        # 隐藏光标
        pygame.mouse.set_visible(False)
        # 重置游戏统计信息
        stats.reset_stats()
        stats.game_active = True

        # 重置记分牌图像
        sb.prep_score()
        sb.prep_high_score()
        sb.prep_level()
        sb.prep_ships()

        # 清空外星人列表和子弹列表
        aliens.empty()
        bullets.empty()

        # 创建一群外星人,并让飞船居中
        create_fleet(ai_settings, screen, ship, aliens)
        ship.center_ship()

def check_high_score(stats, sb):
    """检查是否诞生了新的最高得分"""
    if stats.score > stats.high_score:
        stats.high_score = stats.score
        sb.prep_high_score()

alien_invasion/alien.py

import sys
import os.path
import pygame
from pygame.sprite import Sprite

def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

class Alien(Sprite):
    """表示单个外星人的类"""

    def __init__(self, ai_settings, screen):
        """初始化外星人并设置其起始位置"""
        super().__init__()
        self.screen = screen
        self.ai_settings = ai_settings

        # 加载外星人图像,并设置其rect性
        self.image = pygame.image.load(resource_path('alien.bmp'))
        self.rect = self.image.get_rect()

        # 每个外星人最初都在屏幕左上角附近
        self.rect.x = self.rect.width
        self.rect.y = self.rect.height

        # 存储外星人的准确位置
        self.x = float(self.rect.x)

    def blitme(self):
        """在指定位置绘制外星人"""
        self.screen.blit(self.image, self.rect)

    def update(self):
        """向右移动外星人"""
        self.x += (self.ai_settings.alien_speed_factor *
                   self.ai_settings.fleet_direction)
        # self.x += self.ai_settings.alien_speed_factor
        self.rect.x = self.x

    def check_edges(self):
        """如果外星人位于屏幕边缘,就返回True"""
        screen_rect = self.screen.get_rect()
        if self.rect.right >= screen_rect.right:
            return True
        elif self.rect.left <= 0:
            return True

alien_invasion/ship.py

import sys
import os.path
import pygame
from pygame.sprite import Sprite

def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

class Ship(Sprite):

    def __init__(self,ai_settings, screen):
        """初始化飞船并获取其初始位置"""
        super().__init__()
        self.screen = screen
        self.ai_settings = ai_settings

        # 加载飞船图像并获取其外接矩形
        self.image = pygame.image.load(resource_path('ship.bmp'))
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()

        # 将每艘飞船放在屏幕底中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

        # 在飞船的属性center中存储小数值
        self.center = float(self.rect.centerx)

        # 移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船的位置"""
        # 更新飞船的center值,而不是rect
        if self.moving_right and self.rect.right < self.screen_rect.right:
        # if self.moving_right:
            self.center += self.ai_settings.ship_speed_factor
            # self.rect.centerx += 1
        if self.moving_left and self.rect.left > 0:
        # if self.moving_left:
            self.center -= self.ai_settings.ship_speed_factor
            # self.rect.centerx -= 1

        # 根据self.center更新rect对象
        self.rect.centerx = self.center

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

    def center_ship(self):
        """让飞船在屏幕上居中"""
        self.center = self.screen_rect.centerx

alien_invasion/settings.py

class Settings():
    """存储《外星人入侵》的所有设置的类"""

    def __init__(self):
        """初始化游戏的静态设置"""
        # 屏幕设置
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)
        self.ship_limit = 3

        # 子弹设置
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60,60,60
        self.bullets_allowed = 3
        # 外星人设置
        self.fleet_drop_speed = 10

        # 以什么样的速度加快游戏节奏
        self.speedup_scale = 1.2
        # 外星人点数的提高速度
        self.score_scale = 1.5

        self.initialize_dynamic_settings()

    def initialize_dynamic_settings(self):
        # 初始化随游戏进行而变化的设置
        self.ship_speed_factor = 1.5
        self.bullet_speed_factor = 3
        self.alien_speed_factor = 1
        self.alien_points = 50

        # fleet_direction 为1表示右移,-1表示左移
        self.fleet_direction = 1

    def increase_speed(self):
        """提高速度设置"""
        self.ship_speed_factor *= self.speedup_scale
        self.bullet_speed_factor *= self.speedup_scale
        self.alien_speed_factor *= self.speedup_scale

        # 提高点数外星人
        self.alien_points = int(self.alien_points * self.score_scale)

alien_invasion/bullet.py

import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
    """一个对飞船发射的子弹进行管理的类"""

    def __init__(self, ai_settings, screen, ship):
        """在飞船所处的位置创建一个子弹对象"""
        super().__init__() # python3
        # super(Bullet, self).__init__() # python2
        self.screen = screen

        # 在(0, 0)处创建一个表示子弹的矩形,再设置正确的位置
        self.rect = pygame.Rect(0, 0, ai_settings.bullet_width,
                                ai_settings.bullet_height)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top

        # 存储用小数表示的子弹位置
        self.y = float(self.rect.y)

        self.color = ai_settings.bullet_color
        self.speed_factor = ai_settings.bullet_speed_factor

    def update(self):
        """向上移动子弹"""
        # 更新表示子弹位置的小数值
        self.y -= self.speed_factor
        # 更新表示子弹的rect的位置
        self.rect.y = self.y

    def draw_bullet(self):
        """在屏幕上绘制子弹"""
        pygame.draw.rect(self.screen, self.color, self.rect)

今天跟着书写完整了一个小游戏demo,然后想做个单独的exe,不过问题是py文件中加载本地图片但pyinstaller是不打包的。

准确说是要手动添加下才行。

pyinstaller -F -i=my.ico -w yourmain.py

先通过此步生成spec文件,然后做出下面的修改:

# -*- mode: python -*-

block_cipher = None

added_files = [
('images/alien.bmp','.'),
('images/ship.bmp','.')
]

a = Analysis(['alien_invasion.py'],
             pathex=['C:\\xxx\\alien_invasion'],
             binaries=[],
             datas = added_files,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
extra_tree = Tree('./images', prefix = '.')
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='alien_invasion',
          debug=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=False )

其中

added_files = [
('images/alien.bmp','.'),
('images/ship.bmp','.')
]

这里是添加资源文件,前面是相对路径,当然也可以写绝对路径,后面是在打包好的程序中的“路径”,一个点即根目录。

datas = added_files

这里就不用多说了,这样写比较直观,你要想合并在一起也是可以的。

extra_tree = Tree('./images', prefix = '.')

这里前者是打包前的资源文件路径,后面的点和上面一致(不是很确定)

在重新打包前,如果代码中有对路径文件的操作,需要通过一个函数确定其在程序包中的“路径”
函数如下(记得import os和sys两个包):

def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

最后再次执行

pyinstaller -F -i=my.ico yourmain.spec

就OK了,这个时候exe就能完整独立运行了

参考链接:
https://loserfer.blogspot.com/2018/04/pyinstaller.html