用Python开发电影客户端

简介

本项目是用Python开发的影视客户端,具有增删改查、下载资源和数据可视化等功能。

本项目的主要技术步骤包括:

  1. 基于Python爬虫的数据获取:通过Python的Requests库和re库实现网络爬虫,获取数据。
  2. 基于Excel和Navicat的数据处理:使用Excel和Navicat对获取到的数据进行数据清洗、数据加工等数据处理。
  3. 基于PyQt的图形用户界面开发:使用PyQt库和Qt Designer进行图形用户界面开发,再用QSS对图形用户界面进行改善。
  4. 基于PyQt的数据操作与可视化:建立图形用户界面与后台数据库的连接,通过PyQt库操作数据库,实现客户端相关功能。


下面,首先展示客户端各功能的测试效果,再讲解各个技术步骤。目录如下:

  1. 各功能的测试效果
  2. 基于爬虫的数据获取
  3. 基于Excel和Navicat的数据处理
  4. 基于PyQt的图形用户界面开发
  5. 基于PyQt的数据操作与可视化

(阅读提示:若在浏览器上阅读,可使用Ctrl+F,输入相关章节标题,如“基于爬虫的数据获取”,快速定位到相应章节。)


1. 各功能的测试效果

客户端主要功能包括:浏览影库、条件查询、下载、增加影片、删除影片、可视化共6个功能。

1.1 测试环境

软件环境:Python 3.7.3,JetBrains PyCharm Edu 2019.1,Windows 10教育版

硬件环境:Intel(R) Core(TM) i5-4200H CPU @ 2.80GHz 8GB RAM


1.2 浏览影库

点击左侧菜单区的“浏览影库”按钮,可以在右下侧结果区看到数据库中所有影视数据的信息。浏览影库的效果如图所示:

浏览影库的效果

1.3 条件查询

点击“条件查询”,将弹出输入对话框,用来获取用户输入。输入影片名称的相关信息,如“中”字,点击“OK”开始查询。输入对话框如图所示:

输入对话框

可以在结果区中看到“片全名”中包含“中”字的所有数据,如图所示:

“片全名”中包含“中”字的所有数据

1.4 下载

当用户复制下载链接后,点击下载按钮,可以调用迅雷进行影片下载。在迅雷程序下载界面中,可以设置影片文件名、存储路径等信息。下载界面如图所示:

下载界面

而当用户复制的内容不是下载链接时,客户端会弹框提示用户正确复制下载链接后,再使用下载功能。下载提示如图所示:

下载提示

1.5 增加影片

当用户进行查询后,结果区会显示相应数据,此时用户可以将光标定位到结果区表格中的特定行,点击“增加影片”按钮,则会在该行前插入一行,用户可以输入符合数据表字段约束的影片数据。增加影片的效果如图所示:

增加影片的效果

1.6 删除影片

当用户进行查询后,结果区会显示相应数据,此时用户可以将光标定位到结果区表格中的特定行,点击“删除影片”按钮,则会删除该行,且操作不可撤消。被删除的行最前的列号会显示为感叹号,表示已删除影片。删除影片的效果如图 所示:

删除影片的效果

1.7 可视化

当用户点击“可视化”按钮,结果区会显示相应的图表,将数据库中的数据在某维度上的信息,通过可视化的图表形式呈现给用户。可视化的效果如图所示:

可视化的效果

2. 基于爬虫的数据获取

本项目的数据来源是通过爬虫爬取电影网站“ 电影天堂 ”的“最新影片”模块收集的。

爬取单部影视作品的发布时间、译名、片名、年代、产地、类别、语言、片长和下载地址等数据。

爬虫的实现主要有3步:确定爬虫网址、定位数据和保存数据。


2.1 确定爬虫网址

通过研究网址的组成,可发现网址规律。

每页网址组成为: www.ygdy8.net/html/gndy/dyzz/list_23_ + 页码 + .html

每页网址

单部电影网址后部分类似于:/html/gndy/dyzz/20190427/58502.html,可以通过增加“电影天堂”网址前部分“ www.ygdy8.net ”组装成完整的单部电影网址

每条电影网址

2.2 定位数据每条电影网址

通过查看网页源代码和正则表达式re库匹配相应的数据。

以定位获取网页上“片名”的信息为例。首先进入到特定“电影详情页面”网址中,查看网页源代码,找到“片名”信息在网页源代码中的位置。以下图为例,例子如图所示:

网页源代码

图中可以看到“片名”信息为“◎片  名 How To Train Your Dragon: The Hidden World/How to Train Your Dragon 3 <br />”。于是可以通过正则表达式来定位获取“片名信息”,代码为:

re.findall('片  名 (.*?) <br />', html2.text)

2.3 保存数据

爬取的数据会按序存放在movie_data列表中,再由writer对象的writerow方法,一行行地将movie_data列表中的元素写入csv(逗号分隔符)文件中,待后续进行数据处理,存入数据库中。具体代码如下:

writer.writerow(movie_data)

2.4 爬虫的完整代码

import requests
import re
import csv
import datetime
# 计算程序运行所需时间
starttime = datetime.datetime.now()
# 'w' 表示只能写入(已存在的同名文件会被删除),utf-8解析电影数据的编码,newline=''避免写入时多余的空行
f = open('./data/movie_dytt5.csv', 'w', encoding='utf-8', newline='')
writer = csv.writer(f)
# 写入字段名:片名、译名、链接、类别、产地、发布时间、年代、语言、片长
movie_colum = ['origin_name', 'tran_name', 'link', 'category', 'place', 'link_time', 'year', 'language', 'length']
writer.writerow(movie_colum)
# 电影页数为193,时间截止为2019/4/15 15:17
for i in range(1, 194):
    # 每页电影网址组成:http://www.ygdy8.net/html/gndy/dyzz/list_23_ + 1/2/3/... + .html
    url = f"http://www.ygdy8.net/html/gndy/dyzz/list_23_{i}.html"
    # requests.get() 返回 Response 对象
    html = requests.get(url)
    # gb2312网页编码,正确地获取网页数据
    html.encoding = 'gb2312'
    # findall方法用于匹配获取每页电影的后部分网址
    # 每页电影的后部分网址格式形如:/html/gndy/dyzz/20190328/58369.html
    data = re.findall('<a href="(.*?)" class="ulink">', html.text)
    # 提示网络爬虫进度
    print(f"正在获取第{i}页电影数据...")
    for j in data:
        # 构造每页电影的网址,访问相应的电影详情页面
        url2 = f"http://www.ygdy8.net{j}"
        html2 = requests.get(url2)
        html2.encoding = 'gb2312'
        # 获取相关电影字段数据,存入列表中
        movie_data = re.findall('片  名 (.*?) <br />', html2.text)
        movie_data.extend(re.findall('译  名 (.*?) <br />', html2.text))
        movie_data.extend(re.findall('<a href="(.*?)">.*?</a></td>', html2.text))
        movie_data.extend(re.findall('类  别 (.*?) <br />', html2.text))
        movie_data.extend(re.findall('产  地 (.*?) <br />', html2.text))
        movie_data.extend(re.findall('发布时间:(.*?) ', html2.text))
        movie_data.extend(re.findall('年  代 (.*?) <br />', html2.text))
        movie_data.extend(re.findall('语  言 (.*?) <br />', html2.text))
        movie_data.extend(re.findall('片  长 (.*?) <br />', html2.text))
        # 将电影数据写入csv文件中
        writer.writerow(movie_data)
# 关闭文件,确保安全
f.close()
print(f"完成数据获取!")
# 程序运行结束计时
endtime = datetime.datetime.now()
print(f"程序运行时间为:{(endtime - starttime)}")

3. 基于Excel和Navicat的数据处理

由于爬虫目标网页的不规范和爬虫策略的不足,导致获取的数据质量不高,存在诸如乱码、值错位、值缺失等问题。

未处理的数据


可用Excel进行数据清洗,删除空值行、错误值、重复值等,再用Navicat进行数据加工,导入数据库、合并字段、新增字段等。完成数据处理后的数据效果如下。

处理后的数据


4. 基于PyQt的图形用户界面开发

Qt是一套跨平台的C ++库,提供大量访问桌面系统和移动系统的高级API。涉及到多个方面,如UI开发等。而PyQt是Qt的一套全面的Python绑定,能够用于图形用户界面的开发。

通过QtWidgets.QSplashScreen设置程序启动界面

程序启动界面


通过布局和组件设置,得到基本的程序主页界面

程序主页界面


再通过类似CSS语言的QSS语言改善界面

QSS改善界面


5. 基于PyQt的数据操作与可视化

通过PyQt的QtSql模块,配置和连接本地数据库,再根据其MVC(Model–View–Controller)模式,实现增删改查功能。接着调用迅雷实现下载功能。最后用PyQt的QtChart模块来生成图表,展示数据。

5.1 配置和连接数据库

本项目使用PyQt5的QtSql来操作数据库,而这需要相应的数据库驱动,才能连接相应的数据库上,从而进行相关的数据库操作。由于本项目使用的数据库为MySQL,所以先将通过文件拷贝,将MySQL安装路径下相关文件配置到PyQt5相关安装路径。配置好后,便可以开始连接MySQL。确定数据库驱动名称,设置主机名称、数据库名称、用户名和密码:

def database(self):
    db = QtSql.QSqlDatabase.addDatabase('QMYSQL')
    db.setHostName('localhost')
    db.setDatabaseName('moviedb')
    db.setUserName('root')
    db.setPassword('password')


5.2 增删改查功能

根据PyQt的MVC模式,可通过模型来操作数据,实现数据的增删改查功能。

创建Model连接表
model = QtSql.QSqlTableModel()
table_widget.setModel(self.model)
model.setTable('movie_copy2')
model.insertRow(self.table_widget.currentIndex().row())
model.removeRow(self.table_widget.currentIndex().row())
model.setEditStrategy(QtSql.QSqlTableModel.OnFieldChange)
model.select()
model.setFilter(f"all_name Like '%{cond}%'")


5.3 下载功能

下载功能是通过监测剪切板的文本,当其满足一定条件后,可以调用迅雷程序进行下载。

监测剪切板文本
import win32clipboard as w 
import win32con  
def getText():
    w.OpenClipboard()
    d = w.GetClipboardData(win32con.CF_TEXT)
    w.CloseClipboard()
    return (d).decode('GBK')
调用迅雷下载
from win32com.client import Dispatch  
def download(self):
    url = getText()
    # 若文本内容为下载链接则进行下载
    if url and ('ftp:' in url) or ('magnet:' in url):
        print('复制内容正确,可以开始下载')
        # 调用迅雷,新建任务,提交执行任务
        filename = 'inputFilename'
        thunder = Dispatch('ThunderAgent.Agent64.1')
        thunder.AddTask(url, filename)
        thunder.CommitTasks()


5.4 数据可视化

数据可视化是指通过可视化的方式,如图表等,展示数据库中数据的某些维度的信息。通过PyQt5的QtSql模块来读取数据,再用QtChart模块来生成图表,展示数据。

首先通过QtSql.QSqlQuery 装载SQL语句,进行查询。再通过遍历查询结果,将数据存在列表,作为图表的数据源。

然后通过QtChart.QBarSet和QtChart.QBarSeries设置图表的数据源,再用QtChart.QChart制作图表,再通过QChartView显示在图形用户界面上。

数据库中各影片的“年份”分布情况如图:

数据库中各影片的“年份”分布情况


def visualization(self):
    # 查询部分
    query = QtSql.QSqlQuery()
    col_name = 'year'
    # 调用exec()函数后QSqlQuery的内部指针位于第一个记录的前一个位置
    # 查询得到year字段值:有多少种、每种多少个
    query.exec(f'Select {col_name}, Count(id) As count From movie_copy2 Group By {col_name} Order By {col_name}')
    kind_list = []
    num_list = []
    # 在访问第一条记录之前必须要调用QSqlQuery::next()
    while query.next():
        v1 = query.value(0)  # int print(type(v1))
        v2 = query.value(1)  # int
        # print(f'{v1}    {v2}')  # sql语句结果
        # kind_list 用作坐标轴的分组标签名,故为str类型
        kind_list.append(str(v1))
        # numList 用作QBarSet数据集,要求为float类型(float高于int)
        num_list.append(v2)
    # 打印列表情况,后续用于组装数据集
    print(kind_list)
    print(num_list)
    # 图表部分
    # 查询列名(面向用户)
    setName = '年份'
    # 数据系列:数据集赋值
    # QBarSet对象类型为float
    set0 = QBarSet(setName)
    set0.append(num_list)
    # 用系列来控制数据集,可将数据集分配到不同的组(另一个类别)
    # The series groups the data from sets to categories.
    series = QBarSeries()
    series.append(set0)
    # 图表区域
    self.chart = QChart()
    self.chart.addSeries(series)
    self.chart.setTitle(f'影库统计({setName})')
    # 启动时动画
    self.chart.setAnimationOptions(QChart.SeriesAnimations)
    # 数据系列:类别赋值
    categories = kind_list
    # 横坐标轴:categories数据
    axisX = QBarCategoryAxis()
    axisX.append(categories)
    # 添加横坐标轴到图表区域,设置居底对齐
    self.chart.addAxis(axisX, Qt.AlignBottom)
    # 添加坐标轴到系列中
    series.attachAxis(axisX)
    # 纵坐标轴
    axisY = QValueAxis()
    # 纵坐标轴取值范围
    axisY.setRange(0, 300)
    self.chart.addAxis(axisY, Qt.AlignLeft)
    series.attachAxis(axisY)
    # 显示图例
    self.chart.legend().setVisible(True)
    self.chart.legend().setAlignment(Qt.AlignBottom)
    # 将图表添加到视图
    self.chartView = QChartView(self.chart)
    # 视图的抗锯齿效果
    self.chartView.setRenderHint(QPainter.Antialiasing)