关于非程序员尝试写的脚本虽然能跑但内存爆了这件事 - V2EX
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 2024-04-22 15:33:40 +08:00 1586 次点击
    这是一个创建于 536 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

    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 站自带的自动回复功能是因为支持的关键词数量太少了。

    dode
        1
    dode  
       2024-04-22 17:43:23 +08:00
    list1 = [] #用于记录收到私信的粉丝的 uid
    list2 = [] #用于记录关注时间,作为判定取关并重新关注操作的凭据
    list3 = [] #用于记录无视开关状态的白名单 uid

    这几个列表运行中做了增加,是否进行了去重?
    shurimasoul
        2
    shurimasoul  
       2024-04-22 18:08:06 +08:00
    有没有可能是出现异常时资源没释放导致的?尝试加入一些异常处理,然后在捕获到异常时释放资源,看看还有没有这种问题出现
    Van426326
        3
    Van426326  
       2024-04-22 18:27:59 +08:00
    其实这种问题可以先问问 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  
       2024-04-22 19:05:36 +08:00 via iPhone
    感觉脚本没啥问题,是不是机器上有其他服务导致内存周期性暴增
    xiaoxiaohaoa
        5
    xiaoxiaohaoa  
    OP
       2024-04-23 02:50:35 +08:00
    @dode 在增加前已使用 if 语句对 uid 进行了筛选,已存在的 uid 不会被重复记录。每次新增记录后会将列表同步到本地位置,能够证实确实没有重复。谢谢解答!
    xiaoxiaohaoa
        6
    xiaoxiaohaoa  
    OP
       2024-04-23 02:53:47 +08:00
    @shurimasoul 确实,运行日志记录了一些报错信息,不过没有什么严重的影响所以一直放着没管……我去搜一下异常处理要怎么实现。感谢解答!
    xiaoxiaohaoa
        7
    xiaoxiaohaoa  
    OP
       2024-04-23 03:17:29 +08:00
    @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
       2024-04-23 03:24:25 +08:00
    @harmless 确实有这个可能,用 ps -aux 命令会发现大量不认识的进程,但我个人使用的云实例上除了这个脚本没有主动运行过其他服务,所以一直认为是系统进程。可能有必要实时监控各进程的内存占用并输出日志了。感谢解答!
    xiaoxiaohaoa
        9
    xiaoxiaohaoa  
    OP
       2024-05-07 07:47:07 +08:00
    虽然时隔两周了还是来更新一下后续吧,结论是确实不是脚本本身的问题,是 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     Solana     2739 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 14:56 PVG 22:56 LAX 07:56 JFK 10:56
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86