基于Python+爬虫+Echarts的起点小说数据分析可视化系统设计与实现
今天带来的是起点小说数据分析与可视化平台,一个专为小说爱好者和数据分析师设计的综合性工具。该平台采用了java语言的springboot框架,数据采用MySQL数据库进行存储。结合B/S结构进行开发设计,功能强大,界面化操作便于上手。还利用Echarts的强大数据可视化功能,使得用户能够直观地分析和探索小说的多种维度数据。通过系统首页,用户可以迅速了解到当前热门的小说排行、最新更新的作品,以及平台
💗博主介绍:✌全网粉丝10W+,CSDN全栈领域优质创作者,博客之星、掘金/知乎/b站/华为云/阿里云等平台优质作者、专注于Java、小程序/APP、python、大数据等技术领域和毕业项目实战,以及程序定制化开发、文档编写、答疑辅导等。
👇🏻 精彩专栏 推荐订阅👇🏻
计算机毕业设计精品项目案例(持续更新)
🌟文末获取源码+数据库+文档🌟
感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以和学长沟通,希望帮助更多的人
一、前言

起点小说数据分析与可视化平台是一个专为小说爱好者和数据分析师设计的综合性工具。该平台采用了java语言的springboot框架,数据采用MySQL数据库进行存储。结合B/S结构进行开发设计,功能强大,界面化操作便于上手。还利用Echarts的强大数据可视化功能,使得用户能够直观地分析和探索小说的多种维度数据。通过系统首页,用户可以迅速了解到当前热门的小说排行、最新更新的作品,以及平台的最新公告资讯。个人中心则为用户提供了一个便捷的个人信息管理界面,包括修改密码、管理收藏等功能。对于管理员而言,平台提供了一套完整的用户管理系统,可以方便地进行用户信息的查看和管理。同时,网络小说部分允许管理员对小说进行上传、编辑、分类等操作,确保内容的丰富性和多样性。
本文拟采用Idea开发工具,Java语言SpringBoot框架、Echarts可视化技术开发,使用Scrapy-spiders进行数据爬虫,后台使用MySQL数据库进行信息管理,对于各个模块设计制作有一定的安全性。把获取到的数据进行清洗、整合,储存数据到MySQL,然后进行数据可视化的呈现,简单对呈现的图进行数据分析。除此之外,系统的数据分析功能是其核心亮点之一。借助Echarts,平台能够生成各种图表,如柱状图、折线图、饼图等,展示小说的阅读量、评分、评论等数据。这不仅有助于用户发现新的有趣作品,还能帮助分析师洞察小说市场的动态和趋势。
二、功能设计
起点小说数据分析与可视化平台综合网络空间开发设计要求。目的是将传统管理方式转换为在网上管理,完成起点小说数据分析与可视化管理的方便快捷、安全性高、交易规范做了保障,目标明确。起点小说数据分析与可视化平台可以将功能划分为管理员功能和用户功能。
(1)管理员关键功能包含用户、网络小说、系统管理、用户信息等进行管理。管理员用例如下:
(2)用户登录系统首页可以查看网络小说、公告资讯、个人中心,点击个人中心可以对个人中心、修改密码、我的收藏等进行管理。用户用例如下:
系统功能结构图如下所示:

系统B/S架构原理图:
三、数据设计
概念模型是对现实中的问题出现的事物的进行描述,ER图是由实体及其关系构成的图,通过E-R图可以清楚地描述系统涉及到的实体之间的相互关系。本文将“用户、网络小说、系统简介、公告资讯”等作为实体,它们的局部E-R如图所示:
四、部分效果展示
4.1系统前台功能实现效果
当人们打开系统的网址后,首先看到的就是首页界面。在这里,人们能够看到系统的导航条,通过导航条导航进入各功能展示页面进行操作。系统首页界面如图所示:

在注册流程中,用户在Vue前端填写必要信息(如用户名、密码等)并提交。前端将这些信息通过HTTP请求发送到Java后端。后端处理这些信息,检查用户名是否唯一,并将新用户数据存入MySQL数据库。完成后,后端向前端发送注册成功的确认,前端随后通知用户完成注册。这个过程实现了新用户的数据收集、验证和存储,系统注册页面如图所示:
网络小说,在网络小说页面的输入栏中输入小说名称进行查询,可以查看到网络小说详细信息,并进行评论或收藏操作;网络小说页面如图所示:

个人中心,在个人中心页面输入个人信息可以进行更新操作,还可以对修改密码,我的收藏进行操作;如图所示:
4.2系统后台管理功能实现效果
在登录流程中,用户首先在Vue前端界面输入用户名和密码。这些信息通过HTTP请求发送到Java后端。后端接收请求,通过与MySQL数据库交互验证用户凭证。如果认证成功,后端会返回给前端,允许用户访问系统。这个过程涵盖了从用户输入到系统验证和响应的全过程。如图所示。

管理员进入主页面,主要功能包括对用户、网络小说、系统管理、用户信息等进行操作。
用户功能在视图层(view层)进行交互,比如点击“查询、添加或删除”按钮或填写用户表单。这些用户表单动作被视图层捕获并作为请求发送给相应的控制器层(controller层)。控制器接收到这些请求后,调用服务层(service层)以执行相关的业务逻辑,例如验证输入数据的有效性和与数据库的交互。服务层处理完这些逻辑后,进一步与数据访问对象层(DAO层)交互,后者负责具体的数据操作如查看、修改或删除用户信息,并将操作结果返回给控制器。最终,控制器根据这些结果更新视图层,以便用户功能可以看到最新的信息或相应的操作反馈。如图所示:
网络小说功能在视图层(view层)进行交互,比如点击“查询、删除或爬取数据”按钮或填写网络小说信息表单。这些网络小说表单动作被视图层捕获并作为请求发送给相应的控制器层(controller层)。控制器接收到这些请求后,调用服务层(service层)以执行相关的业务逻辑,例如验证输入数据的有效性和与数据库的交互。服务层处理完这些逻辑后,进一步与数据访问对象层(DAO层)交互,后者负责具体的数据操作如查看、修改、查看评论或删除网络小说信息,并将操作结果返回给控制器。最终,控制器根据这些结果更新视图层,以网络小说功能可以看到最新的信息或相应的操作反馈。如图所示:
系统管理功能在视图层(view层)进行交互,比如点击“查询、添加或删除”按钮或填写公告资讯信息表单。这些公告资讯表单动作被视图层捕获并作为请求发送给相应的控制器层(controller层)。控制器接收到这些请求后,调用服务层(service层)以执行相关的业务逻辑,例如验证输入数据的有效性和与数据库的交互。服务层处理完这些逻辑后,进一步与数据访问对象层(DAO层)交互,后者负责具体的数据操作如查看、修改或删除公告资讯信息,并将操作结果返回给控制器。最终,控制器根据这些结果更新视图层,以便公告资讯功能可以看到最新的信息或相应的操作反馈。还可以对公告资讯分类、关于我们、系统简介、轮播图管理进行详细操作。如图所示:
4.3系统数据可视化分析大屏实现效果
管理员进行爬取数据后可以在看板页面查看到系统简介、周推荐、作品总数、分类占比、小说词云、网络小说总数、网络小说详情等实时的分析图进行可视化管理;看板大屏选择了Echart作为数据可视化工具,它是一个使用JavaScript实现的开源可视化库,能够无缝集成到Java Web应用中。Echart的强大之处在于其丰富的图表类型和高度的定制化能力,使得管理人员可以通过直观的图表清晰地把握网络小说的各项运营数据。
为了实现对网络小说信息的自动化收集和更新,我们采用了Apache Spark作为爬虫技术的基础。Spark的分布式计算能力使得系统能够高效地处理大规模数据,无论是从互联网上抓取最新的网络小说信息,还是对内部数据进行ETL(提取、转换、加载)操作,都能够保证数据的实时性和准确性。
在大数据分析方面,系统采用了Hadoop框架。Hadoop是一个能够处理大数据集的分布式存储和计算平台,它的核心是HDFS(Hadoop Distributed File System)和MapReduce计算模型。通过Hadoop,我们可以对收集到的大量数据进行存储和分析。看板页面如图所示:
部分功能代码
# 配置文件
# # -*- coding: utf-8 -*-
# 数据爬取文件
import scrapy
import pymysql
import pymssql
from ..items import WangluoxiaoshuoItem
import time
from datetime import datetime,timedelta
import datetime as formattime
import re
import random
import platform
import json
import os
import urllib
from urllib.parse import urlparse
import requests
import emoji
import numpy as np
import pandas as pd
from sqlalchemy import create_engine
from selenium.webdriver import ChromeOptions, ActionChains
from scrapy.http import TextResponse
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
# 网络小说
class WangluoxiaoshuoSpider(scrapy.Spider):
name = 'wangluoxiaoshuoSpider'
spiderUrl = 'https://www.qidian.com/finish/chanId21-page{}/'
start_urls = spiderUrl.split(";")
protocol = ''
hostname = ''
realtime = False
def __init__(self,realtime=False,*args, **kwargs):
super().__init__(*args, **kwargs)
self.realtime = realtime=='true'
def start_requests(self):
plat = platform.system().lower()
if not self.realtime and (plat == 'linux' or plat == 'windows'):
connect = self.db_connect()
cursor = connect.cursor()
if self.table_exists(cursor, 'di2zvh33_wangluoxiaoshuo') == 1:
cursor.close()
connect.close()
self.temp_data()
return
pageNum = 1 + 1
for url in self.start_urls:
if '{}' in url:
for page in range(1, pageNum):
next_link = url.format(page)
yield scrapy.Request(
url=next_link,
callback=self.parse
)
else:
yield scrapy.Request(
url=url,
callback=self.parse
)
# 列表解析
def parse(self, response):
_url = urlparse(self.spiderUrl)
self.protocol = _url.scheme
self.hostname = _url.netloc
plat = platform.system().lower()
if not self.realtime and (plat == 'linux' or plat == 'windows'):
connect = self.db_connect()
cursor = connect.cursor()
if self.table_exists(cursor, 'di2zvh33_wangluoxiaoshuo') == 1:
cursor.close()
connect.close()
self.temp_data()
return
list = response.css('ul[class="all-img-list cf"] li')
for item in list:
fields = WangluoxiaoshuoItem()
if '(.*?)' in '''div.book-mid-info h2 a::text''':
try:
fields["name"] = str( re.findall(r'''div.book-mid-info h2 a::text''', item.extract(), re.DOTALL)[0].strip())
except:
pass
else:
try:
fields["name"] = str( self.remove_html(item.css('''div.book-mid-info h2 a::text''').extract_first()))
except:
pass
if '(.*?)' in '''div.book-img-box a img::attr(src)''':
try:
fields["picture"] = str('https:'+ re.findall(r'''div.book-img-box a img::attr(src)''', item.extract(), re.DOTALL)[0].strip())
except:
pass
else:
try:
fields["picture"] = str('https:'+ self.remove_html(item.css('''div.book-img-box a img::attr(src)''').extract_first()))
except:
pass
if '(.*?)' in '''a.go-sub-type::text''':
try:
fields["fenlei"] = str( re.findall(r'''a.go-sub-type::text''', item.extract(), re.DOTALL)[0].strip())
except:
pass
else:
try:
fields["fenlei"] = str( self.remove_html(item.css('''a.go-sub-type::text''').extract_first()))
except:
pass
if '(.*?)' in '''p.intro::text''':
try:
fields["miaoshu"] = str( re.findall(r'''p.intro::text''', item.extract(), re.DOTALL)[0].strip())
except:
pass
else:
try:
fields["miaoshu"] = str( self.remove_html(item.css('''p.intro::text''').extract_first()))
except:
pass
if '(.*?)' in '''div.book-img-box a::attr(href)''':
try:
fields["xqdz"] = str('https:'+ re.findall(r'''div.book-img-box a::attr(href)''', item.extract(), re.DOTALL)[0].strip())
except:
pass
else:
try:
fields["xqdz"] = str('https:'+ self.remove_html(item.css('''div.book-img-box a::attr(href)''').extract_first()))
except:
pass
detailUrlRule = item.css('div.book-img-box a::attr(href)').extract_first()
if self.protocol in detailUrlRule or detailUrlRule.startswith('http'):
pass
elif detailUrlRule.startswith('//'):
detailUrlRule = self.protocol + ':' + detailUrlRule
elif detailUrlRule.startswith('/'):
detailUrlRule = self.protocol + '://' + self.hostname + detailUrlRule
fields["laiyuan"] = detailUrlRule
else:
detailUrlRule = self.protocol + '://' + self.hostname + '/' + detailUrlRule
detailUrlRule ='https:'+ detailUrlRule
yield scrapy.Request(url=detailUrlRule, meta={'fields': fields}, callback=self.detail_parse, dont_filter=True)
# 详情解析
def detail_parse(self, response):
fields = response.meta['fields']
try:
if '(.*?)' in '''span.author::text''':
fields["author"] = str( re.findall(r'''span.author::text''', response.text, re.S)[0].strip().replace('作者:',''))
else:
if 'author' != 'xiangqing' and 'author' != 'detail' and 'author' != 'pinglun' and 'author' != 'zuofa':
fields["author"] = str( self.remove_html(response.css('''span.author::text''').extract_first()).replace('作者:',''))
else:
try:
fields["author"] = str( emoji.demojize(response.css('''span.author::text''').extract_first()).replace('作者:',''))
except:
pass
except:
pass
try:
if '(.*?)' in '''p.count em::text''':
fields["zishu"] = str( re.findall(r'''p.count em::text''', response.text, re.S)[0].strip())
else:
if 'zishu' != 'xiangqing' and 'zishu' != 'detail' and 'zishu' != 'pinglun' and 'zishu' != 'zuofa':
fields["zishu"] = str( self.remove_html(response.css('''p.count em::text''').extract_first()))
else:
try:
fields["zishu"] = str( emoji.demojize(response.css('''p.count em::text''').extract_first()))
except:
pass
except:
pass
try:
if '(.*?)' in '''p.count em:nth-child(3)::text''':
fields["zongtuijian"] = str( re.findall(r'''p.count em:nth-child(3)::text''', response.text, re.S)[0].strip())
else:
if 'zongtuijian' != 'xiangqing' and 'zongtuijian' != 'detail' and 'zongtuijian' != 'pinglun' and 'zongtuijian' != 'zuofa':
fields["zongtuijian"] = str( self.remove_html(response.css('''p.count em:nth-child(3)::text''').extract_first()))
else:
try:
fields["zongtuijian"] = str( emoji.demojize(response.css('''p.count em:nth-child(3)::text''').extract_first()))
except:
pass
except:
pass
try:
if '(.*?)' in '''p.count em:nth-child(5)::text''':
fields["zhoutuijian"] = int( re.findall(r'''p.count em:nth-child(5)::text''', response.text, re.S)[0].strip())
else:
if 'zhoutuijian' != 'xiangqing' and 'zhoutuijian' != 'detail' and 'zhoutuijian' != 'pinglun' and 'zhoutuijian' != 'zuofa':
fields["zhoutuijian"] = int( self.remove_html(response.css('''p.count em:nth-child(5)::text''').extract_first()))
else:
try:
fields["zhoutuijian"] = int( emoji.demojize(response.css('''p.count em:nth-child(5)::text''').extract_first()))
except:
pass
except:
pass
try:
if '(.*?)' in '''div.work-number em.color-font-card::text''':
fields["worknum"] = int( re.findall(r'''div.work-number em.color-font-card::text''', response.text, re.S)[0].strip())
else:
if 'worknum' != 'xiangqing' and 'worknum' != 'detail' and 'worknum' != 'pinglun' and 'worknum' != 'zuofa':
fields["worknum"] = int( self.remove_html(response.css('''div.work-number em.color-font-card::text''').extract_first()))
else:
try:
fields["worknum"] = int( emoji.demojize(response.css('''div.work-number em.color-font-card::text''').extract_first()))
except:
pass
except:
pass
try:
if '(.*?)' in '''div.write em.color-font-card::text''':
fields["writenum"] = str( re.findall(r'''div.write em.color-font-card::text''', response.text, re.S)[0].strip())
else:
if 'writenum' != 'xiangqing' and 'writenum' != 'detail' and 'writenum' != 'pinglun' and 'writenum' != 'zuofa':
fields["writenum"] = str( self.remove_html(response.css('''div.write em.color-font-card::text''').extract_first()))
else:
try:
fields["writenum"] = str( emoji.demojize(response.css('''div.write em.color-font-card::text''').extract_first()))
except:
pass
except:
pass
try:
if '(.*?)' in '''div.days em.color-font-card::text''':
fields["days"] = int( re.findall(r'''div.days em.color-font-card::text''', response.text, re.S)[0].strip())
else:
if 'days' != 'xiangqing' and 'days' != 'detail' and 'days' != 'pinglun' and 'days' != 'zuofa':
fields["days"] = int( self.remove_html(response.css('''div.days em.color-font-card::text''').extract_first()))
else:
try:
fields["days"] = int( emoji.demojize(response.css('''div.days em.color-font-card::text''').extract_first()))
except:
pass
except:
pass
return fields
# 数据清洗
def pandas_filter(self):
engine = create_engine('mysql+pymysql://root:123456@localhost/spiderdi2zvh33?charset=UTF8MB4')
df = pd.read_sql('select * from wangluoxiaoshuo limit 50', con = engine)
# 重复数据过滤
df.duplicated()
df.drop_duplicates()
#空数据过滤
df.isnull()
df.dropna()
# 填充空数据
df.fillna(value = '暂无')
# 异常值过滤
# 滤出 大于800 和 小于 100 的
a = np.random.randint(0, 1000, size = 200)
cond = (a<=800) & (a>=100)
a[cond]
# 过滤正态分布的异常值
b = np.random.randn(100000)
# 3σ过滤异常值,σ即是标准差
cond = np.abs(b) > 3 * 1
b[cond]
# 正态分布数据
df2 = pd.DataFrame(data = np.random.randn(10000,3))
# 3σ过滤异常值,σ即是标准差
cond = (df2 > 3*df2.std()).any(axis = 1)
# 不满⾜条件的⾏索引
index = df2[cond].index
# 根据⾏索引,进⾏数据删除
df2.drop(labels=index,axis = 0)
# 去除多余html标签
def remove_html(self, html):
if html == None:
return ''
pattern = re.compile(r'<[^>]+>', re.S)
return pattern.sub('', html).strip()
# 数据库连接
def db_connect(self):
type = self.settings.get('TYPE', 'mysql')
host = self.settings.get('HOST', 'localhost')
port = int(self.settings.get('PORT', 3306))
user = self.settings.get('USER', 'root')
password = self.settings.get('PASSWORD', '123456')
try:
database = self.databaseName
except:
database = self.settings.get('DATABASE', '')
if type == 'mysql':
connect = pymysql.connect(host=host, port=port, db=database, user=user, passwd=password, charset='utf8')
else:
connect = pymssql.connect(host=host, user=user, password=password, database=database)
return connect
# 断表是否存在
def table_exists(self, cursor, table_name):
cursor.execute("show tables;")
tables = [cursor.fetchall()]
table_list = re.findall('(\'.*?\')',str(tables))
table_list = [re.sub("'",'',each) for each in table_list]
if table_name in table_list:
return 1
else:
return 0
# 数据缓存源
def temp_data(self):
connect = self.db_connect()
cursor = connect.cursor()
sql = '''
insert into `wangluoxiaoshuo`(
id
,name
,picture
,author
,fenlei
,miaoshu
,zishu
,zongtuijian
,zhoutuijian
,worknum
,writenum
,days
,xqdz
)
select
id
,name
,picture
,author
,fenlei
,miaoshu
,zishu
,zongtuijian
,zhoutuijian
,worknum
,writenum
,days
,xqdz
from `di2zvh33_wangluoxiaoshuo`
where(not exists (select
id
,name
,picture
,author
,fenlei
,miaoshu
,zishu
,zongtuijian
,zhoutuijian
,worknum
,writenum
,days
,xqdz
from `wangluoxiaoshuo` where
`wangluoxiaoshuo`.id=`di2zvh33_wangluoxiaoshuo`.id
))
order by rand()
limit 50;
'''
cursor.execute(sql)
connect.commit()
connect.close()
源码及文档获取
文章下方名片联系我即可~
大家点赞、收藏、关注、评论啦 、查看👇🏻获取联系方式👇🏻
精彩专栏推荐订阅:在下方专栏👇🏻
最新计算机毕业设计选题篇-选题推荐
小程序毕业设计精品项目案例-200套
Java毕业设计精品项目案例-200套
Python毕业设计精品项目案例-200套
大数据毕业设计精品项目案例-200套
💟💟如果大家有任何疑虑,欢迎在下方位置详细交流。
更多推荐


所有评论(0)