20244323 大作业《Python程序设计》实验报告

Python大作业实验报告暨Python课程结课报告

〇、课程总结

在整个Python课程的学习中,我从一个只了解基础语句的小白变成了能够自己独立开发软件的人,是这个课程激发了我的求知欲,让我能够在老师所讲的基础上进行自己的深入学习。从第一次作业开始,我在每次分析完实验需求后,都会去思考怎样丰富程序的功能或者弥补其设计上的一些问题,这让我受益匪浅。Python的简洁让我不必费心于代码编写,把更多的时间投入到设计和总结之中,不断迭代得到自己满意的作品,最后完成了我在很久以前就希望能实现的一个软件,万分欣喜,无以言表。

王老师的课程规划和学习路线比较贴合我的学习习惯,让我可以较轻松的跟上进度。授课内容丰富,讲解生动,课堂气氛活跃,学习氛围轻松愉快,激发了同学们的学习热情。挑选的作业和实验具有很强的代表性,阶段性地总结和巩固了课程知识点,达到了非常好的复习效果。总的来说,我对整个课程非常满意。

不过还有一点遗憾是课程没有涉及到大数据处理(PandasNumpy)和神经网络(PyTorch)相关的内容,我个人还是很想去接触一下这方面的应用,因为说不定以后的项目可能会用到。

总之,作为一个入门类型的课程,王老师的Python课是非常成功的,我相信其他同学也会这样想。

感谢王志强老师,感谢Python课程,让我能够在程序设计这条路上不断前进,让我的程序设计能力渐渐增强,为我以后的项目工作打下了坚实基础。再次感谢王老师的辛勤付出。

一、需求分析和设计过程

首先,我从众多选题中,挑选了一个我认为最贴合我的生活、最能给我带来便利的一个选题——基于标签和描述的本地图片管理器。我经常在网上冲浪,所以移动设备中保存了许多从网上下载的图片,这些图片在聊天的过程中可以起到辅助语言表达的功能,所以我会经常使用它们。然而,当图片的数量越来越多时,查找一些比较久远的图片就成了一个难题,我不得不按照时间排序然后慢慢翻找,极大的降低了聊天的效率。所以我希望能用更快的方式进行图片查找,我曾经想过对每个图片的文件名进行修改,使之贴合图片内容,但是图库等软件并不具有根据文件名查找的功能,所以此路不通。然后我经过思考和统计,我在一天之内下载的图片不会很多,如果能确定我是在哪一天下载的,那么查找起来会更容易。于是我就想,我可以继续按照上面的思路走,根据名字查找到图片后,到图库的对应日期去找,就能更快找到了。

然而,在模拟实际使用中,我发现单纯的从文件名进行查找有可能出现漏查错查的情况,而且有的时候也有查找一类图片的需求,所以这种方法就稍显不便,于是我就想出了使用标签和描述一起作为关键字查找的方法。我首先设计了相关的功能,便得到了初版程序。用户可以在命令行进行操作,使用本程序进行添加图片、添加标签、添加描述、删除图片、删除标签、删除描述、查找图片、修改相关信息等操作,可以说,这个程序为我之后的二次开发奠定了基础。

初版程序形成于2024年的暑假,那会正值我放假无聊期间,所以有精力从事该工作,不过得到初版程序之后,就被其他事困扰许久,终究是忘记了这个东西。直到在Python课程上,老师说最后会有一个大作业时,我才想起了这个选题,将其和其他选题放在一起,经过多轮分析和抉择,终于是选中了它,让它能“得道飞升”,成为我大作业的底稿。

我重新设计了程序框架,并且决定增加一个图形界面。在众多GUI库中,我毅然决然选择了网页作为图形界面,原因是那些GUI库上手并不容易,而我有一些网页设计经验,使用网页作为图形界面得心应手。最开始,我想使用web框架,用Python做后端,网页做前端来处理用户操作,但是Cherrypy辜负了我的期望,它对于这种情况并不能很好的实现我的需求。于是我去网络冲浪了,在这个过程中,我发现了Eel库。这玩意可以读取本地的HTML文件,并使用浏览器进行渲染,在经过多次尝试后,我确定这个东西能够实现我的需求。

然后便是网页的设计,Python与网页的交互,以及初版程序的改写与它和图形界面的对接,这些都计划完之后就开始程序编写了,在后文进行详细叙述。

然后便是设计-代码-使用-评估的循环了,几次迭代改进之后便有了现在这个版本。

二、代码概述

首先是初版代码,相对来说十分简陋,操作也不甚方便,代码在这里

整个程序非常简单,只有一点需要注意,为了便于数据储存,我使用了XML文件充当数据库的功能,实在是没有时间去详细学习数据库相关内容,还望老师海涵。(其实这个XML文件的结构设计得很烂,但是这是历史遗留问题,我懒得改了)

当我重新拿起这份代码的时候,我想过把图形界面的部分直接写进去,但是后来在设计功能的时候,我决定让Python对本地文件的操作这部分单独放在一个类里,作为一个封装好的工具使用。所以我根据设计好的需求,首先修改了这部分代码,让它能作为接口使用,实现应有的功能。代码在

然后我为了以后可能的扩展,设计了config.json文件,虽然目前它并未发挥其计划中的功能,希望以后可以用上。

然后我开始着手设计图形界面,在草稿纸上比划了很久,才敲定了第一版界面,后面根据需求相应的也做了很多调整,最后得到了这个图形界面,我个人觉得它至少不丑,而且我也尽可能的为它做了动画,显得不那么僵硬。总体而言还是比较满意。

然后,我在网页里需要用Python插入动态代码的部分做了标记,然后慢慢用Python代码填充了这些需要根据参数做出改变的地方,让他们能实现我想要的效果。这样得到了初版的app.py,随后我在网页中各种按钮等位置绑定了函数,让他们能够调用相应的Python代码,和负责管理文件的部分对接,得到了完整的app.py

然后便是多次迭代,首先是修改了主体部分的tag显示,在右侧加上了删除键,然后调整了布局间隙,为多种搜索方式做了切换(高级搜索里的那个开关)等等,得到了现在的功能基本完善的程序。

所有的代码都在这里,里面也包含了我用来测试的图片文件,对于这些文件我没有相关版权,只用于测试程序的功能是否正常以及日常聊天的表情包,不会用于其他任何用途,如有侵权,请联系我删除。

app.py的最后,有几条标注为Todo的注释,这是我计划实现但是还没去实现或者现阶段无法实现的功能,在以后可能会考虑填坑(但是概率似乎不大)。

三、代码详解

先分段解释一下初版的代码吧

import xml.dom.minidom as dom
import os
import sys
import time

data_file="ImgData.xml"
img_path="./img/"

引入库和一些常量设置,只需注意xml.dom.minidom是我们使用 domTree 形式解析 XML 文件的库

if __name__=="__main__":
    if not os.path.exists(data_file):
        with open(data_file,"w",encoding="utf-8") as f:
            f.write("<Collection>\n</Collection>")
    im=IM()
    while(True):
        opt=int(input("""选择操作:
1.添加图片记录
2.删除图片记录(不会删除图片文件)
3.查看现有标签
4.查找记录
5.退出系统
"""))
        if opt==1:
            im.add_img()
        elif opt==2:
            im.del_img()
        elif opt==3:
            im.print_tag()
        elif opt==4:
            im.find_img()
        elif opt==5:
            exit(0)

充当我们的主函数功能,在数据文件不存在时创建并初始化,然后根据用户输入来执行不同的功能,典型的命令行管理系统

"""
xml设计:
<Collection>
    <img title="图片标题">
        <FileName>图片的完整路径,包含绝对路径,文件名,后缀名</FileName>
        <AddTime>图片的创建时间</AddTime>
        <Tags>
            <Tag>标签1</Tag>
            <Tag>标签2</Tag>
        </Tags>
    </img>
</collection>
"""

这是当时我对 XML 文件的设计(很丑),我们接下来的函数会围绕这个结构进行编写

    def add_img(self):
        self.domTree=dom.parse(data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        file_name=input("输入文件名:")
        title=input("输入标题:")
        addtime=time.time()
        tags=input("输入标签,以空格分隔:").split(" ")
        for i in imgs:
            if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                print("文件已存在")
                return
        img=self.domTree.createElement("img")
        img.setAttribute("title",title)
        fn=self.domTree.createElement("FileName")
        fn.setAttribute("text",file_name)
        at=self.domTree.createElement("AddTime")
        at.setAttribute("text",str(addtime))
        tgs=self.domTree.createElement("Tags")
        for i in tags:
            tg=self.domTree.createElement("Tag")
            tg.setAttribute("text",i)
            tgs.appendChild(tg)
        img.appendChild(fn)
        img.appendChild(at)
        img.appendChild(tgs)
        collection.appendChild(img)
        with open(data_file,"w",encoding="utf-8") as f:
            f.write(self.domTree.toprettyxml())
        print("添加成功")

添加一张图片,用户可以根据提示进行输入,主要需要注意的是对 domTree 的操作,如何创建节点、对参数赋值以及设置父子关系等

    def del_img(self):
        self.domTree=dom.parse(data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        file_name=input("输入文件名:")
        for i in imgs:
            if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                collection.removeChild(i)
                with open(data_file,"w",encoding="utf-8") as f:
                    f.write(self.domTree.toprettyxml())
                print("删除成功")
                return
        print("指定文件不存在!")
        return

同理,删除一张图片的记录,不会物理删除图片

    def print_tag(self):
        out=[]
        self.domTree=dom.parse(data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            tgs=i.getElementsByTagName("Tags")[0].getElementsByTagName("Tag")
            for j in tgs:
                t=j.getAttribute("text")
                if t not in out:
                    out.append(t)
        for i in out:
            print(i)
        return

获取所有的 Tag,并且输出到命令行,方便用户根据 Tag 进行搜索

    def view_img(self,imgs):
        while True:
            opt=int(input("是否预览图片,输入编号以查看对应图片,输入0退出预览:"))
            if opt==0:
                break
            os.system("\""+img_path+imgs[opt-1]+"\"")

是后面出现的查找函数的辅助功能,因为查找函数返回的列表是文件名的形式,所以用这个来帮助用户查看查找到的图片是否为用户所找的图片,使用了system()函数来打开图片,在 Windows 上会打开默认的图片查看程序显示图片。

def find_img(self):
        out=[]
        self.domTree=dom.parse(data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        opt=int(input("""选择查找方式:
1.标题(只要标题中存在输入字符就会返回结果)
2.标签(若干标签中只要有一个符合就会返回结果)
"""))
        if opt==1:
            title=input("输入标题(不确定时输入尽可能少的字符):")
            for i in imgs:
                if title in i.getAttribute("title"):
                    out.append(i.getElementsByTagName("FileName")[0].getAttribute("text"))
            t=len(out)
            if t==0:
                print("没有找到相关记录")
                return []
            for i in range(t):
                print(str(i+1)+"."+out[i])
            self.view_img(out)
            return out
        else:
            tgs=input("输入标签,以空格分隔(不确定时输入尽可能多的标签):").split(" ")
            for i in imgs:
                tags=i.getElementsByTagName("Tags")[0].getElementsByTagName("Tag")
                for j in tags:
                    if j.getAttribute("text") in tgs:
                        out.append(i.getElementsByTagName("FileName")[0].getAttribute("text"))
                        break
            t=len(out)
            if t==0:
                print("没有找到相关记录")
                return []
            for i in range(t):
                print(str(i+1)+"."+out[i])
            self.view_img(out)
            return out

使用不同的方式查找图片,然后用户可以查看图片来进行确认

以上就是初版代码的一点分析,让我们顺着这条线来看看改版后的ImgManager.py

因为我加入了一个暂时没用上的config.json,以及我发现写入 XML 文件的时候会出现大量的空行,所以新增了jsonre两个库,来处理相关需求。

下面来看分段代码解释。

class IM:
    data_file="ImgData.xml"
    config_file="config.json"
    img_path=[]
    domTree=None

还是这些基础的常量,没什么好说的,我们直接看函数

    def __init__(self):
        if not os.path.exists(self.data_file):
            with open(self.data_file,"w",encoding="utf-8") as f:
                f.write("<Collection>\n</Collection>")
        with open(self.config_file, 'r') as f:
            data=json.load(f)
            self.img_path=data['img_dir']
        self.scan_img()

初始化,对于两个文件进行读取,然后用scan_img()扫描所有的图片,更新数据

    def reset_data(self):
        with open(self.data_file,"w",encoding="utf-8") as f:
            f.write("<Collection>\n</Collection>")
    def scan_img(self):
        for i in self.img_path:
            self.scan_img_in_dir(i)
    def maintain_data(self):
        t=""
        with open(self.data_file,"r",encoding="utf-8") as f:
            t=f.read()
        t=re.sub(r'\n\s*\n','\n',t)
        t=re.sub(r'\t','    ',t)
        with open(self.data_file,"w",encoding="utf-8") as f:
            f.write(t)
    def add_img_path(self,path):
        with open(self.config_file, 'rw') as f:
            data=json.load(f)
            img_path=data['img_dir']
            img_path.append(path)
            json.dump(data, f)

scan_img()里面会按照config.json里面写的路径,分别扫描所有的图片。reset_data()是在开发阶段用的一个调试函数,用于清空数据。maintain_data()用于清除输出的 XML 文件中的空行,使用了正则表达式。add_img_path()函数暂时处于没有使用的状态,这个是留给以后对多文件夹支持用的。

    def scan_img_in_dir(self,dir):
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in os.listdir(dir):
            file_name=os.path.join(dir,i)
            title="未填写描述"
            addtime=os.path.getctime(os.path.join(dir,i))
            tags=[]
            flg=0
            for i in imgs:
                if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                    flg=1
                    break
            if flg==1:
                continue
            img=self.domTree.createElement("img")
            img.setAttribute("title",title)
            fn=self.domTree.createElement("FileName")
            fn.setAttribute("text",file_name)
            at=self.domTree.createElement("AddTime")
            at.setAttribute("text",str(addtime))
            tgs=self.domTree.createElement("Tags")
            for i in tags:
                tg=self.domTree.createElement("Tag")
                tg.setAttribute("text",i)
                tgs.appendChild(tg)
            img.appendChild(fn)
            img.appendChild(at)
            img.appendChild(tgs)
            collection.appendChild(img)
        with open(self.data_file,"w",encoding="utf-8") as f:
            f.write(self.domTree.toprettyxml())
        self.maintain_data()

和初版的添加图片比较相似,会扫描给定文件夹下的全部图片(这里没加判断是否为图片的代码,因为暂时还没考虑支持多层文件夹)

上面这些函数,是IM类的维护类型的函数,而后面的函数,就是提供给图形界面使用的接口函数了

    def get_imgs(self):
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            if not os.path.exists(i.getElementsByTagName("FileName")[0].getAttribute("text")):
                collection.removeChild(i)
        with open(self.data_file,"w",encoding="utf-8") as f:
            f.write(self.domTree.toprettyxml())
        self.maintain_data()
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        return imgs

返回所有的图片数据,顺便对于那些图片已经物理删除的数据进行清理

    def del_img(self,file_name):
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                collection.removeChild(i)
                with open(self.data_file,"w",encoding="utf-8") as f:
                    f.write(self.domTree.toprettyxml())
                self.maintain_data()
                return
        return

删除特定图片数据(并非物理删除),这个功能暂时没有在图形界面实装,因为我还没想到怎么来设计一个删除按钮,而且对于那些物理删除的文件,程序会自动清除数据,所以通过其他手段进行删除也是可以的,所以暂时不考虑添加相应的功能。

    def add_tag(self,file_name,tag):
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                tgs=i.getElementsByTagName("Tags")[0]
                tg=self.domTree.createElement("Tag")
                tg.setAttribute("text",tag)
                tgs.appendChild(tg)
                with open(self.data_file,"w",encoding="utf-8") as f:
                    f.write(self.domTree.toprettyxml())
                self.maintain_data()
                return
        return
    def del_tag(self,file_name,tag):
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                tgs=i.getElementsByTagName("Tags")[0]
                for j in tgs.getElementsByTagName("Tag"):
                    if j.getAttribute("text")==tag:
                        tgs.removeChild(j)
                        break
                with open(self.data_file,"w",encoding="utf-8") as f:
                    f.write(self.domTree.toprettyxml())
                self.maintain_data()
                return
        return
    def add_discribe(self,file_name,dis):
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            if i.getElementsByTagName("FileName")[0].getAttribute("text")==file_name:
                i.setAttribute("title",dis)
                with open(self.data_file,"w",encoding="utf-8") as f:
                    f.write(self.domTree.toprettyxml())
                self.maintain_data()
                return
        return
    def get_tags(self):
        out=[]
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            tgs=i.getElementsByTagName("Tags")[0].getElementsByTagName("Tag")
            for j in tgs:
                t=j.getAttribute("text")
                if t not in out:
                    out.append(t)
        return out

增加和删除一个 Tag,以及修改描述的函数和获取全部 Tag 的函数,没什么好说的

    def find_img_by_tags_hazily(self,tgs):
        """tgs是输入框中的文本,可能是多个标签,用空格分隔"""
        out=[]
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        tgs=tgs.split(" ")
        for i in imgs:
            tags=i.getElementsByTagName("Tags")[0].getElementsByTagName("Tag")
            for j in tags:
                for k in tgs:
                    if k in j.getAttribute("text"):
                        out.append(i)
                        break
        return out
    def find_img_by_tags_accurately(self,tgs):
        """tgs是点击的tag的文本,只有完全匹配的tag才会被选中"""
        out=[]
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            tags=i.getElementsByTagName("Tags")[0].getElementsByTagName("Tag")
            for j in tags:
                if j.getAttribute("text")==tgs:
                    out.append(i)
                    break
        return out
    def find_img_by_discribe(self,dis):
        out=[]
        self.domTree=dom.parse(self.data_file)
        collection=self.domTree.documentElement
        imgs=collection.getElementsByTagName("img")
        for i in imgs:
            if dis in i.getAttribute("title"):
                out.append(i)
        return out

三个查找函数,注意针对 Tag 的查找有两种方式,一个是精确匹配,另一个是模糊匹配,针对不同的使用场景

    def sort_img_by_discribe(self,imgs):
        return sorted(imgs,key=lambda i:i.getAttribute("title"))
    def sort_img_by_addtime(self,imgs):
        return sorted(imgs,key=lambda i:float(i.getElementsByTagName("AddTime")[0].getAttribute("text")))

两个排序函数,用于按照对应的参数对数据进行排序,我想这应该会需要这样的功能,虽然我自己可能不会用

这些就是改进之后和图形界面配合的ImgManager.py,下面我们来研究图形界面,看看app.py里面是什么样子。

首先是index()函数,由于这坨代码太多了,就不放在这里了。只需要知道这部分的功能是生成我们的网页就行。

然后是其他函数

def start():
    with open("./index.html", "w", encoding="utf-8") as f:
        f.write(index(im.get_imgs()))
    eel.init(".")
    eel.start("index.html", mode="edge", host="127.0.0.1", port=0, size=(1240, 720))
@eel.expose
def add_tag(file_name, tag):
    im.add_tag(file_name,tag)
    with open("./index.html", "w", encoding="utf-8") as f:
        f.write(index(im.get_imgs()))
@eel.expose
def add_discribe(file_name, dis):
    im.add_discribe(file_name, dis)
@eel.expose
def search_by_discribe(dis):
    imgs=im.find_img_by_discribe(dis)
    with open("./search.html", "w", encoding="utf-8") as f:
        f.write(index(imgs))
    eel.start("search.html", mode="edge", host="127.0.0.1", port=0, size=(1240, 720))
@eel.expose
def search_by_tag_hazily(tag):
    imgs=im.find_img_by_tags_hazily(tag)
    with open("./search.html", "w", encoding="utf-8") as f:
        f.write(index(imgs))
    eel.start("search.html", mode="edge", host="127.0.0.1", port=0, size=(1240, 720))
@eel.expose
def search_by_tag_accurately(tag):
    imgs=im.find_img_by_tags_accurately(tag)
    with open("./search.html", "w", encoding="utf-8") as f:
        f.write(index(imgs))
    eel.start("search.html", mode="edge", host="127.0.0.1", port=0, size=(1240, 720))
@eel.expose
def del_tag(file_name, tag):
    im.del_tag(file_name, tag)
@eel.expose
def sort_by_addtime():
    with open("./index.html", "w", encoding="utf-8") as f:
        f.write(index(im.sort_img_by_addtime(im.get_imgs())))
    eel.init(".")
    eel.start("index.html", mode="edge", host="127.0.0.1", port=0, size=(1240, 720))
@eel.expose
def sort_by_discribe():
    with open("./index.html", "w", encoding="utf-8") as f:
        f.write(index(im.sort_img_by_discribe(im.get_imgs())))
    eel.init(".")
    eel.start("index.html", mode="edge", host="127.0.0.1", port=0, size=(1240, 720))

start()函数会调用Eel库来打开index()函数创建的网页。

@eel.expose修饰器会告诉Eel库这个函数可以在网页中进行调用,这些函数的功能结合名称和代码应该能很容易地看出来,就不赘述了。

我们直接看 HTML 中和这些函数互动的部分。

<script type="text/javascript" src="/eel.js"></script>

哦对,这个东西,是使用了Eel库就需要加在网页里的,不需要在物理上存在eel.js这个文件。

<div class="search-container">
    <input type="text" id="search-input" placeholder="搜索...">
    <button id="search-btn" onclick="search_by_input()">搜索</button>
    <button id="toggle-advanced-search" class="toggle-button">
        <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-plus" viewBox="0 0 16 16">
            <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
        </svg>
    </button>
    <div id="advanced-search" class="advanced-search">
        <label>按Tag搜索</label>
        <label class="switch">
            <input type="checkbox" class="tag-switch">
            <span class="slider"></span>
        </label>
        <br>
        <label>Tags:</label>
        <!--这里存放tags-->
        <div id="tags">
            <button class="tag-btn-search" onclick='search_by_tag("test")'>test</button>
        </div>
        
    </div>
</div>
<script>
    document.getElementById('toggle-advanced-search').addEventListener('click', function() {
        const advancedSearch=document.getElementById('advanced-search');
        const toggleButton=document.getElementById('toggle-advanced-search');
        // 切换高级搜索的展开/收起状态
        advancedSearch.classList.toggle('open');
        // 切换按钮的旋转状态
        toggleButton.classList.toggle('active');
    });
    function search_by_input() {
        searchInput=document.getElementById('search-input').value;
        checkbox=document.getElementsByClassName("tag-switch")[0];
        if(checkbox.checked){
            eel.search_by_tag_hazily(searchInput);
        }
        else{
            eel.search_by_discribe(searchInput);
        }
        
    }
</script>

首先是搜索功能,这里支持按照描述搜索和按照 Tag 模糊搜索两个功能,在search_by_input()函数中可以看出。上面的那块代码,结合注释也不难理解是用来控制搜索部分的样式的,不重要。

        <!-- 这里存放tags -->
        <div id="tags">
            <button class="tag-btn-search" onclick='search_by_tag("test")'>test</button>
        </div>
        <!-- 中间省略部分代码 -->
<script>
    function search_by_tag(tag){
        eel.search_by_tag_accurately(tag);
    }
</script>
    page='''<!--这里存放tags-->
        <div id="tags">'''
    for i in tags:
        page+='''
            <button class="tag-btn-search" onclick='search_by_tag("'''+i+'''")'>'''+i+'''</button>'''
    if len(tags)==0:
        page+='''
            <button class="tag-btn-search">暂无Tag</button>'''
    page+='''
        </div>'''

上面的 HTML 部分是在搜索框中显示所有 Tag 的网页代码和给 Tag 按钮绑定的 JS 函数,下面是我们插入 Tag 的 Python 代码。可以看出我们点击 Tag 按钮会触发search_by_tag()函数,这个函数会通过 Eel 库调用图形界面中相应的相应函数,进而调用 IM 中精确匹配 Tag 的搜索函数,然后将搜索结果在新窗口中呈现出来。

<div id="img-container">
    <!--在这里是所有的图片显示-->
    <div class="img-box" id="./img/test.jpg">
        <img class="img-display" src="./img/test.jpg">
        <div class="img-info">
            <p class="img-discribe" ondblclick='add_discribe("./img/test.jpg")'>死了</p>
            <div class="img-tags">
                <button class="add-tag-btn">2025-04-17</button>
                <button class="add-tag-btn" onclick='add_tag("./img/test.jpg")'>+添加标签</button>
                <button class="tag-btn" onclick='search_by_tag("test")'>test</button><button class="del-tag-btn" onclick='del_tag("./img/test.jpg","test")'>x</button>
            </div>
        </div>
    </div>
</div>
<script type="text/javascript">
    function add_discribe(file_name){
        dis=window.prompt("请输入描述:");
        if(dis!=null){
            eel.add_discribe(file_name,dis);
            img_box=document.getElementById(file_name);
            img_dis=img_box.getElementsByClassName("img-discribe")[0];
            img_dis.innerHTML=dis;
        }
    }
    function add_tag(file_name){
        tag=window.prompt("请输入Tag:");
        if(tag!=null){
            eel.add_tag(file_name,tag);
            location.reload();
        }
    }
    function search_by_tag(tag){
        eel.search_by_tag_accurately(tag);
    }
    function del_tag(file_name,tag){
        eel.del_tag(file_name,tag);
        img_box=document.getElementById(file_name);
        img_tags=img_box.getElementsByClassName("img-tags")[0];
        tag_btns=img_tags.getElementsByClassName("tag-btn");
        for(i=0;i<tag_btns.length;i++){
            if(tag_btns[i].innerHTML==tag){
                del_btn=tag_btns[i].nextElementSibling;
                img_tags.removeChild(tag_btns[i]);
                img_tags.removeChild(del_btn);
                break;
            }
        }
    }
</script>

和上面的差不多,都是通过 JS 调用响应函数再调用接口实现的各种功能。

<div id="overlay" class="overlay" style="display: none;">
    <div class="overlay-content">
        <form>
            <label for="dropdown">选择排序方式:</label>
            <select id="dropdown" name="dropdown">
                <option value="option1">按添加时间</option>
                <option value="option2">按描述</option>
            </select>
            <button type="button" id="confirmButton">确认</button>
        </form>
    </div>
</div>
<div class="floating-button" id="floatingButton">
    <div class="menu" id="menu">
        <div class="menu-item" style="--index: 1;">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="menu-icon" viewBox="0 0 16 16">
                <path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-7.5 3.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707z"/>
            </svg>
        </div>
        <div class="menu-item" style="--index: 2;">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="menu-icon" viewBox="0 0 16 16">
                <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
            </svg>
        </div>
        <div class="menu-item" style="--index: 3;">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sort-down" viewBox="0 0 16 16">
                <path d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"/>
            </svg>
        </div>
    </div>
    <div class="button-icon" id="toggleMenu">&#43;</div>
</div>
<script>
    document.getElementById('toggleMenu').addEventListener('click', function() {
        const menu = document.getElementById('menu');
        const buttonIcon = document.getElementById('toggleMenu');
        
        if (menu.classList.contains('show-menu')) {
            menu.classList.remove('show-menu');
            buttonIcon.classList.remove('rotate-45');
        } else {
            menu.classList.add('show-menu');
            buttonIcon.classList.add('rotate-45');
        }
    });

    document.querySelector('.menu-item:first-child').addEventListener('click', function() {
        window.scrollTo({ top: 0, behavior: 'smooth' });
    });
    document.querySelector('.menu-item:nth-child(2)').addEventListener('click', function() {
        document.getElementById('toggle-advanced-search').classList.add('active');
        document.getElementsByClassName('advanced-search')[0].classList.add('open');
    });
    document.querySelector('.menu-item:nth-child(3)').addEventListener('click', function() {
        const overlay = document.getElementById('overlay');
        overlay.style.display = 'block';
    });
    document.getElementById('confirmButton').addEventListener('click', function() {
        select=document.getElementById('dropdown').value;
        overlay.style.display = 'none';
        if(select=="option1"){
            eel.sort_by_addtime();
        }
        else if(select=="option2"){
            eel.sort_by_discribe();
        }
        window.close();
    });
</script>

这部分,是右下角的一个浮动按钮,提供排序、打开高级搜索、滚回页面顶部等功能,具体的实现主要是 JS 代码,这里就不详细说明了。另外这部分的功能有可能还会增加,不过目前没有想到有什么可以加的。

上面这些就是app.py中的关键代码了,至此,整个项目的核心代码讲解完毕,相信读者已经大致理解了项目的结构和功能。

下面就是激动人心的演示时刻了!

四、演示

B站视频-Python大作业展示

很神奇,博客园现在会屏蔽iframe标签,可能是出于安全考虑(因为这玩意喜欢拿来做XSS),所以还是点上面的链接去B站看吧。

代码懒得删了,留在这吧

<div style="position: relative; padding: 30% 45%;">
<iFrAMe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;" src="//player.bilibili.com/player.html?isOutside=true&aid=114455409134357&bvid=BV1g4VzzLEc9&cid=29806563300&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></IfRaME>
</div>

五、配置华为云并部署软件

购买服务器之类的就不在这里细说了,就当我已经准备好了基础的服务器和操作系统。

因为这个程序依托浏览器运行,所以我们需要安装图形界面,这里我选了gnome图形界面。

先配置yum的软件源:

vi /etc/yum.repos.d/openEuler.repo

改为如下内容:

[openEuler-everything]
name=openEuler-everything
baseurl=http://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/everything/x86_64/
enabled=1
gpgcheck=0
gpgkey=http://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/everything/x86_64/RPM-GPG-KEY-openEuler

[openEuler-EPOL]
name=openEuler-epol
baseurl=http://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/EPOL/main/x86_64/
enabled=1
gpgcheck=0

[openEuler-update]
name=openEuler-update
baseurl=http://repo.huaweicloud.com/openeuler/openEuler-22.03-LTS/update/x86_64/
enabled=1
gpgcheck=0

yum应用更改:

yum makecache

安装gnome

yum install gnome-shell gdm gnome-session gnome-terminal -y

配置以图形界面作为默认启动选项

systemctl enable gdm.service
systemctl set-default graphical.target

重新启动,就能有图形界面了。

我们使用scp命令把相关文件上传,然后修改app.py的内容,把所有的eel.start()函数中的mode参数改为firefox,因为openEuler自带的是火狐浏览器,而且我安装chrome失败了。

scp -r diploma_project--ImgManager root@███.███.███.███:~/
cd diploma_project--ImgManager/
vi app.py

然后用VNC远程桌面进行连接(华为云自带),启动终端,安装Eel库,如果不指定国内源的话,会安装失败,因为有些文件下载不到。

pip install eel -i https://pypi.tuna.tsinghua.edu.cn/simple

赋予执行权限并运行文件

chmod +x app.py
./app.py

运行截图如下:

六、过程中遇到的问题

其实有些问题在前面有所提及,不过还是总结一下吧。

  1. 最初的设计中,图片的标签没有删除键
    解决方案:重新设计了标签样式,增加了对应的删除键
  2. 对于搜索结果的显示,如果要在原网页进行显示,则需要比较复杂的JS代码,难以实现
    解决方案:搜索结果单独启动一个页面,便于编写代码也便于回到主页面。
  3. 页面成形后才想到可能需要滚回顶部以及排序等功能,但是页面不便于再增加对应的按钮
    解决方案:在右下角增加了一个折叠菜单,将这些功能放在了那里面。
  4. 代码里有一个一键清除所有数据的函数,是调试用的,暂时不清楚用户会不会有这个功能,故没有添加相应的交互。
    解决方案:暂时不去管它
  5. 安装图形界面时,遇到问题Failed to enable unit:File /etc/systemd/system/display-manager.service already exists and is a symlink to /usr/lib/systemd/system/lightdm.service
    解决方案:这是因为display-manager.service已经存在,也就是安装过其他图形界面导致的。所以把/etc/systemd/system/display-manager.service重命名为/etc/systemd/system/display-manager.service.bak,然后重新执行systemctl enable gdm.service
  6. 安装Eel库的时候,提示fatal error: Python.h: No such file or directory
    解决方案:这是因为没有安装相应的Python开发组件导致的,使用yum install python3-devel安装就行了
  7. 安装Eel库时,在Building wheel for gevent(pyproject.toml)这里卡住了
    解决方案:把服务器从1核1G换成2核8G,这是服务器配置太烂导致的。
  8. 运行app.py出现/bin/sh: line 1: start: command not found
    解决方案:这是因为eel.start()函数的mode参数不合法导致的,原因是linux没有Edge浏览器,改成firefox就行了
  9. 如果你看到这个图形界面觉得很熟悉,好像在哪里见过,那么你不对劲
    解决方案:逮捕!

七、总结

总而言之,这个东西的设计过程也算是有很多波折了,从初版到最后成形其实不超过两周时间。那为什么现在才写报告呢?一方面不清楚要求,今天才问清楚,另一方面,我懒。这次经历让我更加熟悉了Python的使用,也重温了前端设计,受益匪浅。而且选题的也算是当代的青少年等可能会需要的功能,比较贴近生活。我个人还是非常满意这个作品,虽然Eel库的使用方法是现学的,网页设计是一边查资料一边写的,JS代码是和搜索引擎一起完成的,但是这也是学习的过程,我现在对于这一套开发流程已经比较熟悉了,就像实验三的附加部分我也使用了这个流程进行开发,效果显著。

八、杂谈

我也曾经想过,要不要做一款游戏,贪吃蛇和俄罗斯方块好像太简单了,使用RenPy引擎做Galgame好像也过于简单,而且我并没有好的剧情可以使用,音乐游戏倒是考虑过,但是实现起来也比较复杂,在没有游戏引擎的情况下我可能做不出来,所以最后选择了这个选题。至于大数据分析和神经网络方向,在一开始就因为我没有这方面的灵感而被放弃了,其实我还挺想借机学学PyTorch的。

其实还有一个遗憾,是我没有实现的。不是指Todo那部分,而是我想把这玩意移植到手机上,但是暂时还没找到方法,所以这玩意目前只能在电脑上面跑,没法完全融入到我的日常生活。首先Eel对于手机的支持好像不太好,如果要在手机软件里显示网页倒是有方法,无论是手机自带的功能还是使用Beeware的webview组件,都能实现,但是效果没有经过测试,暂时未知。而我也没那么多时间花费在这上面了,所以这份遗憾只能暂时让它继续存在了。

谨以此文纪念我和Python课程、和王老师的相遇,以上。

posted @ 2025-05-07 18:56  awcyvan  阅读(337)  评论(1)    收藏  举报