V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
xiaoxiaohaoa
V2EX  ›  Python

关于非程序员尝试写的脚本虽然能跑但内存爆了这件事

  •  
  •   xiaoxiaohaoa · 250 天前 · 1159 次点击
    这是一个创建于 250 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码部分可能存在大量令人高血压的语句,如果使您感到不适,非常抱歉

    省流版:请各位帮忙看看下面一段代码里哪里存在可能导致内存溢出/泄漏的问题。如果可以的话,请指点一下如何优化,万分感谢。

    以下是完整代码内容(已隐去部分隐私信息):

    import sys
    f = open('a.log', 'a')
    sys.stdout = f
    sys.stderr = f # redirect std err, if necessary
    
    import xlrd3
    games = xlrd3.open_workbook(filename=r'games.xlsx')
    gamelist = games.sheet_names()
    
    sgames = xlrd3.open_workbook(filename=r'sgames.xlsx')
    sgamelist = sgames.sheet_names()
    
    import asyncio
    from bilibili_api import Credential, sync, user
    from bilibili_api.session import Session, Event, send_msg
    from bilibili_api.user import User, RelationType
    from bilibili_api.utils.picture import Picture
    
    #登录凭据,记得改成自己的
    SESSDATA = "※"
    BILI_JCT = "※"
    BUVID3 = "※"
    
    credential = Credential(sessdata=SESSDATA, bili_jct=BILI_JCT, buvid3=BUVID3)
    session = Session(credential)
    
    list1 = [] #用于记录收到私信的粉丝的 uid
    list2 = [] #用于记录关注时间,作为判定取关并重新关注操作的凭据
    list3 = [] #用于记录无视开关状态的白名单 uid
    
    with open('list1.txt',"r") as f: #读取已记录列表,该列表存储在本地,位置为指令执行时所处的目录
        for line in f:
            list1.append(line.strip('\n')) 
    
    with open('list2.txt',"r") as f: #同上
        for line in f:
            list2.append(line.strip('\n'))
    
    with open('list3.txt',"r") as f: #同上
        for line in f:
            list3.append(line.strip('\n'))
    
    status = []
    
    @session.on(Event.TEXT) #轮询,检测收到文字私信时触发
    async def reply(event: Event):
    
        token = "1"
    
        code = event.content[-5:]
        name = event.content[:-5]
    
        uid = event.sender_uid #查询发信者的 uid
        talker = User(uid, credential)
        rela = await talker.get_relation(uid)
        follow = rela['be_relation']['attribute'] #查询关注关系,此处描述的是对方对你的关注状态:0=未关注,2=已关注,6=互关,128=黑名单
        mtime = rela['be_relation']['mtime'] #关注时间,以时间戳的形式记录,未关注时为 0 ,取关后重新关注会刷新,以此为凭据进行拉黑;需注意关系变为互相关注时也会刷新,后文单独拎出来讨论
        fid = str(uid) #转换为字符串
        ftime = fid + str(mtime) #同上
    
    
        if fid == "※":
    
            if event.content == "/终止": #杀死进程
                await session.reply(event, "脚本已停运")
                session.close()
    
            elif event.content == "/开始": #用于开始自动回复的口令,触发后将开始自动回复私信
                await session.reply(event, "脚本已开启")
                status.append(token)
    
            elif event.content == "/结束": #用于暂停自动回复的口令,触发后将不再自动回复私信
                await session.reply(event, "脚本已暂停")
                status.clear()
    
            elif event.content.isdigit():
    
                if event.content in list1:
    
                    list1.remove(event.content)
                    with open('list1.txt', "w") as f:
                        for element in list1:
                            f.write(element + "\n")
    
                    await send_msg(credential=credential, msg_type=Event.TEXT, content="用户已移出黑名单", receiver_id=uid)
    
                else:
    
                    await send_msg(credential=credential, msg_type=Event.TEXT, content="用户当前不在黑名单中,操作失败", receiver_id=uid)
    
    
    
        if token in status or fid in list3: #开关开启 [或] 发信人在白名单内
    
            if follow > 0 and follow < 128: #筛选:已关注用户
    
                if fid not in list1: #筛选:未通过关键词获取过回复的粉丝
    
                    if event.content == "彩蛋": 
                        await session.reply(event, "恭喜你找到了一颗彩蛋!")
    
    
                    elif name in gamelist: #正确的游戏关键词,接下来将进一步检测口令中的数字
    
                        gamex = games.sheet_by_name(sheet_name=name)
                        codelist = gamex.col_values(0)
    
                        if code in codelist: #口令正确,标志着对方成功获取回复;触发时发信人的 uid 和关注时间会随之被记录
    
                            def search(gamecode):
    
                                num_rows = gamex.nrows
    
                                for row in range(num_rows):
                                    if gamex.cell_value(row, 0) == code:
                                        return gamex.cell_value(row, 1)
                                return None
    
                            password = search(code)
    
    
                            await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n 您的密码是:\n" + password) #正确关键词对应的回复
    
                            list1.extend([fid]) #记录发信粉丝的 uid
                            with open("list1.txt","a+") as f: 
                                f.write(fid + "\n")
                            
                            list2.extend([ftime]) #记录发信粉丝的 uid 和关注时间
                            with open("list2.txt","a+") as f: 
                                f.write(ftime + "\n")
    
    
    
                else: #曾通过关键词获取过回复的粉丝
    
                    if ftime in list2 or follow == 6: #对方两次私信的关注时间一致(或关系为互相关注),说明对方自上次获取回复后没有取消关注过(或对方是你的好友),此时执行正常流程,跟上文一致,但不重复记录
    
                        if event.content == "彩蛋": 
                            await session.reply(event, "恭喜你找到了一颗彩蛋!")
    
    
                         #我需要治疗 1.1
    
                        elif name in gamelist: #正确的游戏关键词,接下来将进一步检测口令中的数字
    
                            gamex = games.sheet_by_name(sheet_name=name)
                            codelist = gamex.col_values(0)
    
                            if code in codelist: #口令正确,标志着对方成功获取回复
    
                                def search(gamecode):
    
                                    num_rows = gamex.nrows
    
                                    for row in range(num_rows):
                                        if gamex.cell_value(row, 0) == code:
                                            return gamex.cell_value(row, 1)
                                    return None
    
                                password = search(code)
    
    
                                await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n 您的密码是:\n" + password) #正确关键词对应的回复
    
    
    
                    else: #对方两次私信的关注时间不一致且不是你的互关好友,说明对方自上次获取回复后曾取消关注
                        await session.reply(event, "获取密码功能已失效。")
    
                        #如果想要拉黑,复制粘贴这一句: [ await talker.modify_relation(relation=RelationType.BLOCK)] 
    
    
         #即使开关关闭、发送人也不在白名单时也能获取的游戏
    
        elif follow > 0 and follow < 128: #筛选:已关注用户
    
            if fid not in list1: #筛选:未通过关键词获取过回复的粉丝
    
                if event.content == "彩蛋α": 
                    await session.reply(event, "恭喜你找到了一颗高级彩蛋!")
    
    
                elif name in sgamelist: #正确的关键词,接下来将进一步检测口令中的数字
    
                    gamex = sgames.sheet_by_name(sheet_name=name)
                    codelist = gamex.col_values(0)
    
                    if code in codelist: #口令正确,标志着对方成功获取回复;触发时发信人的 uid 和关注时间会随之被记录
    
                        def search(gamecode):
    
                            num_rows = gamex.nrows
    
                            for row in range(num_rows):
                                if gamex.cell_value(row, 0) == code:
                                    return gamex.cell_value(row, 1)
                            return None
    
                        password = search(code)
    
    
                        await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n 您的密码是:\n" + password) #正确关键词对应的回复
              
                        list1.extend([fid]) #记录发信粉丝的 uid
                        with open("list1.txt","a+") as f: 
                            f.write(fid + "\n")
                        
                        list2.extend([ftime]) #记录发信粉丝的 uid 和关注时间
                        with open("list2.txt","a+") as f: 
                            f.write(ftime + "\n")
    
    
    
            else: #曾通过关键词获取过回复的粉丝
    
                if ftime in list2 or follow == 6: #对方两次私信的关注时间一致(或关系为互相关注),说明对方自上次获取回复后没有取消关注过(或对方是你的好友),此时执行正常流程,跟上文一致,但不重复记录
    
                    if event.content == "彩蛋α": 
                        await session.reply(event, "恭喜你找到了一颗高级彩蛋!")
    
    
                    elif name in sgamelist: #正确的关键词,接下来将进一步检测口令中的数字
    
                        gamex = sgames.sheet_by_name(sheet_name=name)
                        codelist = gamex.col_values(0)
    
                        if code in codelist: #口令正确,标志着对方成功获取回复;触发时发信人的 uid 和关注时间会随之被记录
    
                            def search(gamecode):
    
                                num_rows = gamex.nrows
    
                                for row in range(num_rows):
                                    if gamex.cell_value(row, 0) == code:
                                        return gamex.cell_value(row, 1)
                                return None
    
                            password = str(search(code))
    
    
                            await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n 您的密码是:\n" + password) #正确关键词对应的回复
          
    
                else: #对方两次私信的关注时间不一致且不是你的互关好友,说明对方自上次获取回复后曾取消关注
                        await session.reply(event, "获取密码功能已失效。") #可删除
    
    
    sync(session.start())
    

    关于文件:脚本内涉及到的几个.txt 和.xlsx 文件大小均不超过 20kb ,这些文件本身的大小应该不是问题。日志输出文件.log 这段时间的运行下来,大小已经达到 20Mb+了,但由于是只写不读,应该也不是这个的问题?

    背景:如您所见,这段脚本的作用是实现“bilibili 根据私信收到的关键词触发自动回复”的功能,目的是为下载本人汉化的游戏作品的用户分发启动密码。

    本人此前没有编程经验,某天想到设置自动回复的点子后误打误撞发现了Bilibili API的库,于是动手实践,一点点试错码出来了一个姑且能满足需要的脚本。后来在使用过程中慢慢改进,断断续续花了四个多月的时间变成了现在看到的这个样子。

    现在这个脚本挂在 Oracle Cloud 的服务器上长期运行。最初一两个月还算顺利,但从两个多月前开始偶尔会出现脚本没有响应的状况,b 站给自己发送的私信不会触发回复,Xftp 和 Xshell 也无法连接到远程主机,但每次等一会儿就会自行恢复。而最近半个月这种断线变得越来越频繁,每次断线的时间也在延长。查看了 Oracle 提供的统计数据发现了端倪:

    pkpdL0e.md.png

    每次断线都跟内存占用率暴增的时间点吻合,图表上缺失的时间段也正是断线的持续时间。我意识到可能是内存溢出导致的服务器宕机,但在正常运行期间查看进程的内存占用率也看不出所以然。目前只能推测是这段脚本里什么地方写得不好,引入了大量临时变量之类的导致内存爆了。由于缺乏相关的知识,我对于该怎么解决也没有头绪,四处搜寻下找到了本站,斗胆提问,希望能得到各位的指点。

    有任何建议或看法都非常欢迎。感谢!

    题外话,这些汉化作品是免费发布的,不会向用户收取费用。设置启动密码的初衷是避免盗卖,此前的资源因为无需验证,时常会被人二次上传并牟利;而在采用这种方式之后用户必须通过私信我来获取密码,盗卖的情况就没再发生过了。不用 b 站自带的自动回复功能是因为支持的关键词数量太少了。

    9 条回复
    dode
        1
    dode  
       250 天前
    list1 = [] #用于记录收到私信的粉丝的 uid
    list2 = [] #用于记录关注时间,作为判定取关并重新关注操作的凭据
    list3 = [] #用于记录无视开关状态的白名单 uid

    这几个列表运行中做了增加,是否进行了去重?
    shurimasoul
        2
    shurimasoul  
       250 天前
    有没有可能是出现异常时资源没释放导致的?尝试加入一些异常处理,然后在捕获到异常时释放资源,看看还有没有这种问题出现
    Van426326
        3
    Van426326  
       250 天前
    其实这种问题可以先问问 ai




    ## 内存占用高的可能原因:

    **1. Excel 文件过大:**

    * 代码使用了 `xlrd3` 库读取 Excel 文件,如果 Excel 文件本身很大,包含大量数据,那么读取文件时会占用大量内存。
    * 建议检查 Excel 文件的大小,如果文件很大,可以考虑优化文件结构或者使用其他方式存储数据。

    **2. 数据结构不合理:**

    * 代码使用了列表 `list1`、`list2`、`list3` 存储粉丝信息,如果粉丝数量很多,这些列表会占用大量内存。
    * 可以考虑使用更节省内存的数据结构,例如集合( set )或者字典( dict )。

    **3. 循环处理逻辑:**

    * 代码中有一些循环处理逻辑,例如读取 Excel 文件、遍历粉丝列表等,如果循环次数很多,也会占用大量内存。
    * 可以考虑优化循环逻辑,例如减少循环次数、使用生成器等。

    **4. bilibili_api 库的使用:**

    * `bilibili_api` 库本身可能存在内存泄漏问题,导致内存占用过高。
    * 建议检查 `bilibili_api` 库的版本和相关 issue ,或者尝试使用其他 Bilibili API 库。

    **5. 其他原因:**

    * 操作系统、Python 版本、其他运行的程序等因素也可能影响内存占用。

    ## 建议:

    * **使用内存分析工具:** 可以使用 Python 内置的 `memory_profiler` 库或者其他内存分析工具,分析代码中哪些部分占用了大量内存。
    * **优化数据结构:** 使用更节省内存的数据结构,例如集合或字典。
    * **优化循环逻辑:** 减少循环次数,使用生成器等。
    * **检查第三方库:** 检查 `bilibili_api` 库的版本和相关 issue ,或者尝试使用其他 Bilibili API 库。
    * **监控内存使用情况:** 定期监控程序的内存使用情况,及时发现内存泄漏问题。


    希望以上分析能帮助你找到内存占用高的原因并进行优化。
    harmless
        4
    harmless  
       250 天前 via iPhone
    感觉脚本没啥问题,是不是机器上有其他服务导致内存周期性暴增
    xiaoxiaohaoa
        5
    xiaoxiaohaoa  
    OP
       250 天前
    @dode 在增加前已使用 if 语句对 uid 进行了筛选,已存在的 uid 不会被重复记录。每次新增记录后会将列表同步到本地位置,能够证实确实没有重复。谢谢解答!
    xiaoxiaohaoa
        6
    xiaoxiaohaoa  
    OP
       250 天前
    @shurimasoul 确实,运行日志记录了一些报错信息,不过没有什么严重的影响所以一直放着没管……我去搜一下异常处理要怎么实现。感谢解答!
    xiaoxiaohaoa
        7
    xiaoxiaohaoa  
    OP
       250 天前
    @Van426326 谢谢指点,但实际看下来,ai 的回答中有效的结果似乎并不是很多?

    1.已检查过涉及到写入的文件,大小均不超过 20kb ,应该可以认为不是这个问题;

    2.列表改为字典/集合的思路有道理,但有资料说 dict 的内存占用比 list 更大?还是说这里的“内存”概念我理解得不对?
    暂时无法发布链接,先引用一段原文:
    -和 list 比较,dict 有以下几个特点:
    -a.查找和插入的速度极快,不会随着 key 的增加而变慢;
    -b.需要占用大量的内存,内存浪费多。
    -而 list 相反:
    -a.查找和插入的时间随着元素的增加而增加;
    -b.占用空间小,浪费内存很少。

    3.反复遍历 list 似乎是一个问题,我会试着搜索一下生成器的用法;

    4.使用 bilibili_api 库的其他人没有报告过内存泄漏问题,并且不在本人的能力范围内,暂不考虑;

    内存监控我也有考虑过,但具体怎么实现还暂时没想好。先试试用其他办法把问题解决了,之后再考虑监控吧。谢谢解答!
    @Van426326
    xiaoxiaohaoa
        8
    xiaoxiaohaoa  
    OP
       250 天前
    @harmless 确实有这个可能,用 ps -aux 命令会发现大量不认识的进程,但我个人使用的云实例上除了这个脚本没有主动运行过其他服务,所以一直认为是系统进程。可能有必要实时监控各进程的内存占用并输出日志了。感谢解答!
    xiaoxiaohaoa
        9
    xiaoxiaohaoa  
    OP
       236 天前
    虽然时隔两周了还是来更新一下后续吧,结论是确实不是脚本本身的问题,是 dnf 进程的问题。

    定位问题进程:dmesg | grep -i memory 输出中包含大量 Out of memory: Killed process ***** (dnf) 的 OOM 报错信息,得以确定是 dnf 相关进程的问题;进一步搜索后确认是由于 dnf 软件包信息更新导致的系统 OOM 崩溃。
    解决办法:sudo systemctl disable dnf-makecache.timer

    参考:
    Linux dmesg 命令介绍 ( https://www.jianshu.com/p/4a029091b705)
    解决 centos dnf 自动更新异常问题 ( https://thisblog.cn/2023/05/10/centos-dnf-makecache/)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2411 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 02:09 · PVG 10:09 · LAX 18:09 · JFK 21:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.