# open-source-project **Repository Path**: RUIZRUI/open-source-project ## Basic Information - **Project Name**: open-source-project - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-04-26 - **Last Updated**: 2021-04-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [qixqiBook](http://qixqi.ourvultr.club:5911) ======= **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [1. 项目背景及意义](#1-%E9%A1%B9%E7%9B%AE%E8%83%8C%E6%99%AF%E5%8F%8A%E6%84%8F%E4%B9%89) - [1.1 网络小说行业的发展](#11-%E7%BD%91%E7%BB%9C%E5%B0%8F%E8%AF%B4%E8%A1%8C%E4%B8%9A%E7%9A%84%E5%8F%91%E5%B1%95) - [1.2 起点中文网的龙头地位](#12-%E8%B5%B7%E7%82%B9%E4%B8%AD%E6%96%87%E7%BD%91%E7%9A%84%E9%BE%99%E5%A4%B4%E5%9C%B0%E4%BD%8D) - [1.3 排行榜的意义](#13-%E6%8E%92%E8%A1%8C%E6%A6%9C%E7%9A%84%E6%84%8F%E4%B9%89) - [1.4 存在的问题](#14-%E5%AD%98%E5%9C%A8%E7%9A%84%E9%97%AE%E9%A2%98) - [2. 项目创新点](#2-%E9%A1%B9%E7%9B%AE%E5%88%9B%E6%96%B0%E7%82%B9) - [2.1基于Python的数据爬取](#21%E5%9F%BA%E4%BA%8Epython%E7%9A%84%E6%95%B0%E6%8D%AE%E7%88%AC%E5%8F%96) - [2.2 基于Echarts的数据展示](#22-%E5%9F%BA%E4%BA%8Eecharts%E7%9A%84%E6%95%B0%E6%8D%AE%E5%B1%95%E7%A4%BA) - [2.3 多维度评比](#23-%E5%A4%9A%E7%BB%B4%E5%BA%A6%E8%AF%84%E6%AF%94) - [3. 项目设计](#3-%E9%A1%B9%E7%9B%AE%E8%AE%BE%E8%AE%A1) - [4 详细设计与实现](#4-%E8%AF%A6%E7%BB%86%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0) - [4.1 数据爬取部分](#41--%E6%95%B0%E6%8D%AE%E7%88%AC%E5%8F%96%E9%83%A8%E5%88%86) - [4.1.1 获取反爬虫数据](#411-%E8%8E%B7%E5%8F%96%E5%8F%8D%E7%88%AC%E8%99%AB%E6%95%B0%E6%8D%AE) - [4.1.2 获取book完整信息](#412--%E8%8E%B7%E5%8F%96book%E5%AE%8C%E6%95%B4%E4%BF%A1%E6%81%AF) - [4.2 网页数据展示部分](#42-%E7%BD%91%E9%A1%B5%E6%95%B0%E6%8D%AE%E5%B1%95%E7%A4%BA%E9%83%A8%E5%88%86) - [4.2.1 flask框架](#421-flask%E6%A1%86%E6%9E%B6) - [4.2.2 jQuery和echarts控制页面显示和事件处理](#422-jquery%E5%92%8Cecharts%E6%8E%A7%E5%88%B6%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86) - [4.2.2.1 动态展示模块](#4221-%E5%8A%A8%E6%80%81%E5%B1%95%E7%A4%BA%E6%A8%A1%E5%9D%97) - [4.2.2.2 搜索模块](#4222-%E6%90%9C%E7%B4%A2%E6%A8%A1%E5%9D%97) - [4.2.2.3 设置模块](#4223-%E8%AE%BE%E7%BD%AE%E6%A8%A1%E5%9D%97) - [4.2.2.4 刷新模块](#4224-%E5%88%B7%E6%96%B0%E6%A8%A1%E5%9D%97) - [5 系统测试](#5-%E7%B3%BB%E7%BB%9F%E6%B5%8B%E8%AF%95) - [6 系统总结](#6-%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93) - [7 个人感悟](#7-%E4%B8%AA%E4%BA%BA%E6%84%9F%E6%82%9F) ## 1. 项目背景及意义 ### 1.1 网络小说行业的发展 * 网络小说,指新近产生的,以互联网为展示平台和传播媒介的,借助超文本连接和多媒体演绎等手段来表现的文学作品、类文学文本及含有一部分文学成分的网络艺术品。其中,以网络原创作品为主。近年来,我国数字内容版权环境持续优化,推动国内网络小说业务蓬勃发展。在2018上半年中国网络小说用户规模及使用情况中,数据显示:2018上半年中国网络小说用户规模为4.06亿人,与2017年末相比增长2821万人,在整体网民数量中网络小说用户数比例达到50.6%。 ### 1.2 起点中文网的龙头地位 * 起点中文网创建于2002年5月,是国内最大文学阅读与写作平台之一,是国内领先的[原创文学](https://baike.baidu.com/item/原创文学/3151343)门户网站,隶属于国内最大的数字内容综合平台。经过10年努力和奋斗,在众多热爱起点的作者与用户的关心下,“起点中文网”建立了完善的以创作、培养、销售为一体的电子在线出版机制,成为国内优秀的文学作品在线出版平台,树立了业内具有影响力的行业领导地位 ### 1.3 排行榜的意义 * 每一个用户在进入小说网站里面,最先吸引他(她)注意力的永远都是排行榜上名列前茅的小说。小说网站将优秀的资源摆在最上面,最显眼的位置,让用户可以轻易的从百万部小说中找到自己所需,来达到留住客户的目的。同时,排行榜还可以激起读者的攀比心理,使得他们为了自己欣赏的作者疯狂打赏,为网站赢得更高的收入。 ### 1.4 存在的问题 * 但是作为龙头老大的起点,其排行榜也存在着一定的问题,起点排行榜的排列顺序主要是以单一分项作为评判标准,如“周推荐”,“月票”,“总推荐”等,用户每一次都得更换评判标准来查看以另一种分项为标准的排行榜。这种操作不仅让用户觉得麻烦,而且也达不到综合评价一本书是否畅销。 ## 2. 项目创新点 ### 2.1基于Python的数据爬取 * 本项目采用了基于python的数据爬取技术,可以很快的将起点中文网上的数据进行抓取,迅速将更新的数据应用到本产品中。这种即时爬取的数据,能够让排行榜的评判结果更加贴近真实情况,让本项目产品更具市场竞争力。 ### 2.2 基于Echarts的数据展示 * 本项目采用了现阶段最火的Echarts开源技术。Echarts是百度的一个开源的数据可视化工具,一个纯 [Javascript](https://www.w3cschool.cn/javascript/) 的图表库,能够在 PC 端和移动设备上流畅运行,兼容当前绝大部分浏览器(IE6/7/8/9/10/11,chrome,firefox,Safari等),底层依赖轻量级的 Canvas 库 ZRender,ECharts 提供直观,生动,可交互,可高度个性化定制的数据可视化图表。创新的拖拽重计算、数据视图、值域漫游等特性大大增强了用户体验,赋予了用户对数据进行挖掘、整合的能力。 ### 2.3 多维度评比 * 本产品采用多维度、多因素来评比来评判一个作品的好坏。比起单一因素评比,本产品打破了极端因素可能带来的不利因素,让作品的评比更加公平公正。多维度评比可以最大限度的给用户推荐时下最火热的书,既给了作者展示的机会,又给了读者更优秀的资源,更大程度的吸引用户。 ## 3. 项目设计 本项目主要分为两个部分,数据爬取和数据展示部分,其总体结构如图3-1所示。  ## 4 详细设计与实现 ### 4.1 数据爬取部分 * 数据爬去部分使用fontTools、requests、re、bs4库,完成json格式的爬取。程序流程图如图4-1-1所示:  #### 4.1.1 获取反爬虫数据 * 起点网站上每本书的周推荐数默认是隐藏的,由自己的ttf文件编码,在爬去的数据中显示为乱码,所以首先要获取反爬的数字 * 关键代码如下: ```python def get_html_info(url): headers = { 'User-Agent': 'User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36' } html_data = requests.get(url, headers=headers) # 获取网页的文字ttf文件的地址 url_ttf_pattern = re.compile('',re.S) fonturl = re.findall(url_ttf_pattern,html_data.text)[0] url_ttf = re.search('woff.*?url.*?\'(.+?)\'.*?truetype', fonturl).group(1) # 获取所有反爬的数字 word_pattern = re.compile('(.*?)', re.S)#制定正则匹配规则,匹配所有标签中的内容 numberlist = re.findall(word_pattern, html_data.text) return url_ttf,numberlist ``` #### 4.1.2 获取book完整信息 * 这里开启多线程爬取数据,提高爬取数据的效率,最终爬取到name, rating, user_count, week_comment,存到ebook.json文件中,便于网页数据展示 * 爬取过程中,终端效果如图4-1-2所示:  * 爬取到的数据格式如图4-1-3所示:  * 核心代码如下: ```python def getBookId(url): result=requests.get(url) soup=BeautifulSoup(result.text,"html.parser") div_list=soup.find_all(class_='book-mid-info') for div in div_list: name_list.append(div.h4.a.text) id_list.append(div.h4.a.get("data-bid")) def replaceId(id): return 'https://book.qidian.com/ajax/comment/index?_csrfToken=MzswhxVTI5dSsUZ3aIc3g1qzH4qX8c876Sec2WeH&bookId='+str(id)+'&pageSize=15' def getXHRData(url): r=requests.get(url) dic=eval(r.text) score_list.append(dic['data']['rate']) num_list.append(dic['data']['userCount']) def getScore(): for count in range(60): url=replaceId(id_list[count]) getXHRData(url) def getRe(count): while(True): try: url='https://book.qidian.com/info/'+id_list[count] x=getRecomments(url) recomment_list[count]=x print(str(count)+"成功") break except: time.sleep(1) print(str(count)+"失败") continue for url in url_list: getBookId(url) getScore() for i in range(60): t1=threading.Thread(target=getRe,args=(i,)) threads.append(t1) t1.start() for t in threads: t.join() for i in range(60): dic.append({"name":name_list[i],"rating":score_list[i],"user_count":num_list[i],"week_recommend":recomment_list[i]}) json_str=json.dumps(dic,ensure_ascii=False,sort_keys=True, indent=4) with open('ebook.json','w') as fp: fp.write(json_str) ``` ### 4.2 网页数据展示部分 * 该模块使用flask开源网络框架组织网页,同时使用jQuery,echarts等开源技术控制网页的显示,程序后台流程图如图4-2-1所示: * 程序前端流程图如图4-2-2所示: #### 4.2.1 flask框架 * 由于flask网络框架比较轻量,所以Python编写网页比较方便快捷,app.py文件也比较简洁,共有三个页面,@app.route(‘/’)配置主页面路由,@app.route(‘getBook’, methods=[‘GET’])是js文件获取爬取使用,@app.route(‘/error’)配置出错页面的路由。 * 核心代码如下: ```python APP_ROOT = os.path.dirname(os.path.abspath(__file__)) QidianSpider_static_path = os.path.join(APP_ROOT, '../../QidianSpider') # 爬去到的json文件信息目录 @app.route('/') def index(): return render_template('index.html') @app.route('/getBook', methods=['GET']) def getBook(): # print('-'*100) # print(QidianSpider_static_path) # print(type(QidianSpider_static_path)) # print('-'*100) rst = make_response('{}') with open(os.path.join(QidianSpider_static_path, 'ebook.json'), 'r') as fp: dataDict = json.load(fp) dataStr = json.dumps(dataDict, ensure_ascii=False) print(dataStr) # return dataStr rst = make_response(dataStr) rst.headers['Access-Control-Allow-Origin'] = '*' return rst @app.route('/error') def error(): return render_template('error.html') ``` * 同时,利用Jinja2模版功能,配置网络路由是返回html格式的模版,比如@app.route(‘/’)的模版 * 网页主页面效果图如图4-2-3所示:  * 核心代码如下: ```html qixqi排行榜 欢迎来到qixqi排行榜 刷新时长(秒): 展示最大条数: default rating user_count recommend 搜索内容 ``` #### 4.2.2 jQuery和echarts控制页面显示和事件处理 * 由于页面中各个事件的处理以及css样式的控制,这部分比起flask框架比较复杂,首先页面加载完毕后,ajax访问后台http://127.0.0.1:5000/getBook获取爬到的数据 * 核心代码如下: ```javas $.ajax({ // 跨域访问 type: 'get', // 不支持post跨域访问 async: false, url: 'http://localhost:5000/getBook', dataType: 'json', success: function(data){ data.sort($.sortRule()); // 排序 names = data.map(item => item.name); // 获取属性数组 ranks = data.map(item => $.rank(item.rating, item.user_count, item.week_recommend)); $.initView(names.slice(0, maxItemNum), ranks.slice(0, maxItemNum)); counter = 0; length = names.length; console.log(length); // 此处成功后的事件处理代码 ... }, error: function(err){ window.location.href = '/error'; console.error(err.responseText); } }); ``` * ajax访问数据由两部分构成,成功时调用success函数,出现异常时调用error函数重定向到http://127.0.0.1:5000/error页面,使用templates/error.html模版显示网页 * 效果如图4-2-4所示:  * Ajax 获取获取数据成功后首先将数据进行排序,由于爬取到的数据中与排序有关的数据有rating, user_count, week_recommend,所以需要自定义排序策略将三种数据结合起来综合排名,这里规定总分值10分,rating占比5分,user_count占比2.5分,week_recommend占比2.5分,所以由于rating原来是10分值,只需要折半即可,而user_count和week_recommend则是不同的范围映射到不同的分值 * 核心代码如下: ```javascript jQuery.extend({ 'rank': function(rating, user_count, week_recommend){ var result = rating; if(user_count < 2000){ result += user_count/500 + 1; }else{ result += 5; } if(week_recommend < 4){ result += parseInt(week_recommend) + 1; }else{ result += 5; } return (result / 2).toFixed(2); // 保留两位小数 } }); // 利用jquery中sort()的自定义方法 jQuery.extend({ 'sortRule': function(){ return function(book1, book2){ rank1 = $.rank(book1.rating, book1.user_count, book1.week_recommend); rank2 = $.rank(book2.rating, book2.user_count, book2.week_recommend); if(rank1 < rank2){ // 因为靠前的在下面,所以rank小 return 1; // 交换位置 }else if(rank1 > rank2){ return -1; // 不要交换位置 }else{ return 0; // 不要交换位置 } }; } }); ``` * 数据排序后,echarts利用得到的数据初始化视图 * 如图4-2-5所示:  * 核心代码如下: ```javascript // 初始化表 jQuery.extend({ 'initView': function(names, ranks){ console.log(names); console.log(ranks); /* 柱状图 */ // var rankBar = echarts.init(document.getElementById('rank')); // rankBar.showLoading(); // 打开loading动画 rankBar.hideLoading(); // 隐藏loading动画 rankBar.setOption({ title:{ text: '风云榜' }, tooltip: {}, legend: { data: ['rank'] }, xAxis:{}, yAxis: { data: names.reverse() // 数组倒置 }, series: [{ name: 'rank', type: 'bar', smooth: true, // 数据光滑过度 itemStyle: { normal: { color: '#d11b1a' } }, data: ranks.reverse() }] }); // return rankBar; } }); ``` ##### 4.2.2.1 动态展示模块 * 如果爬取的数据比较多的话,很难在一张网页中展示,所以要进行分页处理,这时可以动态加载页面,比如每隔1s向上刷新1条数据,时间间隔和每页展示最大条目个数可以在浏览器中自定义。 * 点击图4-2-6中开始按钮:  * 图表便开始动态加载数据,由于不能保存动态图,这里使用4s后的图表展示,图表信息向上刷新了4条 * 如图4-2-7所示:  * 可以随时点击暂停按钮定制动态展示 * 该模块的核心代码如下: ```javascript // 刷新表 jQuery.extend({ 'flushView': function(names, ranks){ rankBar.setOption({ yAxis: { data: names.reverse() // 数组倒置 }, series: [{ name: 'rank', type: 'bar', smooth: true, itemStyle: { normal: { color: '#d11b1a' } }, data: ranks.reverse() // 数组倒置 }] }); } }); // 开始/播放按钮点击事件 $('.run').click(function(){ if(flushTimeout == null){ // pause --> start // alert('pause'); flushTimeout = setInterval(function(){ if(length - counter >= maxItemNum && counter > 0){ $.flushView(names.slice(counter, counter + maxItemNum), ranks.slice(counter, counter+maxItemNum)); } counter ++; if(length - counter < maxItemNum){ console.log(counter); clearInterval(flushTimeout); flushTimeout = null; $('.run').attr({'src': './static/img/start.png', 'title': '开始', 'alt': '开始'}); } }, period * 1000); console.log(flushTimeout); $('.run').attr({'src':'./static/img/pause.png', 'title': '暂停', 'alt': '暂停'}); }else{ // start --> pause // alert('start'); clearInterval(flushTimeout); console.log('flushTimeout: ' + flushTimeout); flushTimeout = null; console.log('flushTimeout: ' + flushTimeout); $('.run').attr({'src': './static/img/start.png', 'title': '开始', 'alt': '开始'}); } }); ``` ##### 4.2.2.2 搜索模块 * 由于分页显示,为了便于查找某一条目信息,所以加入搜索功能,定位到该条目信息。 * 首先点击图4-2-8中的搜索图像按钮:  * 然后网页背景会虚化,显示搜索框,输入搜索内容,这里输入“全职法师” * 如图4-2-9:  * 点击搜索按钮或者按下“Enter”键,图表便定位到“全职法师”条目 * 如图4-2-10所示:  * 搜索到的条目有青绿色着重显示 * 该模块的核心代码如下: ```javascript // 搜索效果刷新 jQuery.extend({ 'searchFlushView': function(names, ranks, index){ rankBar.setOption({ yAxis: { data: names.reverse() }, series: [{ name: 'rank', type: 'bar', smooth: true, itemStyle:{ normal:{ color: function(params){ if(params.dataIndex == index){ return '#00FF00'; // 选中颜色,绿色 }else{ return '#d11b1a'; } } } }, data: ranks.reverse() }] }); } }); // 简单搜索功能 jQuery.extend({ 'simpleSearch': function(names, content){ result = -1; $.each(names, function(index, value){ // 遍历数组 // console.log('i = ' + index + ', content = ' + content + ', value = ' + value); if(content == value){ // console.log('叮咚'); result = index; return false; // 终止循环 } }); return result; } }); // 搜索图片点击事件 $('.search').click(function(){ // alert('搜索'); // $('div:visible').css('opacity', '0.1'); $('#rank').css('opacity', '0.1'); $('#process').css('opacity', '0.1'); $('h2').css('opacity', '0.1'); if($('#setting').css('visibility') == 'visible'){ $('#setting').css('visibility', 'hidden'); } // $('.setting').off('click'); // setting点击方法禁用 // $('.setting').attr('disabled', 'disabled'); $('#search').css('visibility', 'visible'); $('#search_content').focus(); $.enterKeyDown('search'); }); // 搜索取消按钮点击事件 $('#search_cancel').click(function(){ $('#rank').css('opacity', '1.0'); $('#process').css('opacity', '1.0'); $('h2').css('opacity', '1.0'); $('#search').css('visibility', 'hidden'); $.enterKeyDown(null); // 解除键绑定 // $('.setting').on('click'); // $('.setting').attr('disabled', false); // $('div:hidden').css('opacity', '1.0'); }); // 搜索确定按钮点击事件 $('#search_confirm').click(function(){ // alert('搜索'); content = $('#search_content').val().trim(); if(content == ''){ alert('empty'); }else{ index = $.simpleSearch(names, content); // alert(index); if(index == -1){ alert('未找到该条目'); }else if(index + maxItemNum <= length){ counter = index; $.searchFlushView(names.slice(counter, counter+maxItemNum), ranks.slice(counter, counter+maxItemNum), maxItemNum-1); // 数组倒置了 }else{ counter = maxItemNum > length ? 0 : (length - maxItemNum); $.searchFlushView(names.slice(counter, length), ranks.slice(counter, length), length-index-1); // 数组倒置了 } } $('#rank').css('opacity', '1.0'); $('#process').css('opacity', '1.0'); $('h2').css('opacity', '1.0'); $('#search').css('visibility', 'hidden'); $.enterKeyDown(null); // 解除键绑定 }); ``` ##### 4.2.2.3 设置模块 * 在设置模块中,可以设置动态刷新时的时间间隔和每页展示条目个数,同时还可以设置图表类型,有默认排名推荐柱状图、rating总览折线图、user_count总览折线图、week_recommend总览折线图。 * 首先,点击图4-2-11中的设置图片按钮  * 同样背景虚化,然后设置框出现,如图4-2-12:  * 在这里设置刷新时长和展示最大条数,可以在动态展示模块立即显示效果,这里就不再赘述。 * 设置图表类型为rating,效果如图4-2-13所示:  * 设置图表类型为user_count,效果如图4-2-14所示:  * 设置图表类型为week_recommend,效果如图4-2-15所示:  * 设置模块核心代码如下: ```javascript // 小说rating总揽 jQuery.extend({ 'modifyView': function(names, numbers, title, name){ // console.log('names = ' + names); // console.log('numbers = ' + numbers); rankBar.clear(); clearFlag = true; viewType = name; if(flushTimeout != null){ clearInterval(flushTimeout); flushTimeout = null; $('.run').attr({'src': './static/img/start.png', 'title': '开始', 'alt': '开始'}); } rankBar.setOption({ title: { text: title }, tooltip: {}, legend: { data: [name] }, xAxis: { data: names }, yAxis: {}, series: [{ name: name, type: 'line', smooth: true, data: numbers }] }); } }); // 设置图片点击事件 $('.setting:first').click(function(){ // alert('设置选项'); // $('body').css('opacity', '0.1'); // 设置主题背景透明度,子元素也会被设置,不灵活 $('#rank').css('opacity', '0.1'); $('#process').css('opacity', '0.1'); $('h2').css('opacity', '0.1'); if($('#search').css('visibility') == 'visible'){ $('#search').css('visibility', 'hidden'); } $('#setting').css('visibility', 'visible'); // 设置选项可见 $('#period').focus(); // 输入框获取焦点 $('#period').val(period); $('#maxItemNum').val(maxItemNum); $.enterKeyDown('setting'); }); // 取消按钮点击事件 $('#cancel').click(function(){ // alert('取消'); $('#rank').css('opacity', '1.0'); $('#process').css('opacity', '1.0'); $('h2').css('opacity', '1.0'); $('#setting').css('visibility', 'hidden'); // 设置选项隐藏 $.enterKeyDown(null); // 解除键绑定 }); // 确定按钮点击事件,不能放到图片点击事件中,否则重复绑定事件 $('#confirm').click(function(){ // 获取单选按钮的值 // choice = $('input[type="radio"].choice').length; choice = $('input:radio:checked.choice').val(); // alert(choice); if(choice != 'default'){ real_names = data.map(item => item.name); if(choice == 'rating'){ ratings = data.map(item => item.rating); // console.log('real_names = ' + real_names); // console.log('ratings = ' + ratings); $.modifyView(real_names, ratings, '评分榜', 'rating'); }else if(choice == 'user_count'){ user_counts = data.map(item => item.user_count); $.modifyView(real_names, user_counts, '用户点击榜', 'user_count'); }else if(choice == 'week_recommend'){ week_recommends = data.map(item => item.week_recommend); $.modifyView(real_names, week_recommends, '周推荐榜', 'week_recommend'); } $('#rank').css('opacity', '1.0'); $('#process').css('opacity', '1.0'); $('h2').css('opacity', '1.0'); $('#setting').css('visibility', 'hidden'); // 设置选项隐藏 $.enterKeyDown(null); // 解除键绑定 // img标签没有disabled属性,可以CSS中的pointer-events: none代替 // $('.control:first-child').attr('disabled', true); // $('.run').attr('disabled', true); // $('.search').attr('disabled', true); // $('.control:first-child').css({'pointer-events': 'none'}); $('.run').css({'pointer-events': 'none'}); $('.search').css({'pointer-events': 'none'}); return; } if(clearFlag){ // echarts重建过 rankBar.clear(); end = (maxItemNum > length) ? length : counter + maxItemNum; $.initView(names.slice(counter, end), ranks.slice(counter, end)); clearFlag = false; viewType = 'default'; // $('.control:first-child').attr('disabled', false); // $('.run').attr('diabled', false); // $('.search').attr('disabled', false); // $('.control:first-child').css({'pointer-events': 'auto'}); $('.run').css({'pointer-events': 'auto'}); $('.search').css({'pointer-events': 'auto'}); } // alert('确定'); // alert('this: ' + $(this).attr('id')); console.log('counter: ' + counter); console.log('length: ' + length); pFlag = false; // 判断period是否改变 mFlag = false; if($('#period').val().trim() != ''){ // alert($('#period').val()); temp = parseFloat($('#period').val().trim()); if(temp != period){ pFlag = true; // period 改变 period = temp; } // alert(period); } if($('#maxItemNum').val().trim() != ''){ temp = parseInt($('#maxItemNum').val().trim()); if(temp != maxItemNum){ mFlag = true; // maxItemNum 改变 maxItemNum = temp; } } console.log('period ' + period); console.log('maxItemNum ' + maxItemNum); $('#rank').css('opacity', '1.0'); $('#process').css('opacity', '1.0'); $('h2').css('opacity', '1.0'); $('#setting').css('visibility', 'hidden'); // 设置选项隐藏 $.enterKeyDown(null); // 解除键绑定 // alert(flushTimeout); if(flushTimeout == null){ // 定时器已经关闭 if(mFlag){ // maxItemNum 改变 if(length - counter < maxItemNum){ // 回退,本来应该length - counter < maxItemNum - 1排除maxItemNum未改变情况,但由于mFlag为true,所以maxItemNum一定改变 counter = maxItemNum > length ? 0 : (length - maxItemNum); $.flushView(names.slice(counter, length), ranks.slice(counter, length)); }else if(length - counter >= maxItemNum){ // 继续刷新 flushTimeout = setInterval(function(){ if(length - counter >= maxItemNum && counter > 0){ $.flushView(names.slice(counter, counter + maxItemNum), ranks.slice(counter, counter + maxItemNum)); } counter ++; if(length - counter < maxItemNum){ clearInterval(flushTimeout); flushTimeout = null; $('.run').attr({'src': './static/img/start.png', 'title': '开始', 'alt': '开始'}); } }, period * 1000); } } }else{ // 定时器仍然开启 if(pFlag){ // period改变 clearInterval(flushTimeout); flushTimeout = null; flushTimeout = setInterval(function(){ if(length - counter >= maxItemNum && counter > 0){ $.flushView(names.slice(counter, counter + maxItemNum), ranks.slice(counter, counter + maxItemNum)); } counter ++; if(length - counter < maxItemNum){ clearInterval(flushTimeout); flushTimeout = null; $('.run').attr({'src': './static/img/start.png', 'title': '开始', 'alt': '开始'}); } }, period * 1000); } } }); // 单选按钮选中事件 var inputDisabledFlag = false; // 记录是否是disabled $('input:radio[name="choice"]').click(function(){ choice = $('input:radio:checked.choice').val(); if(choice != 'default' && !inputDisabledFlag){ $('#period').attr('disabled', true); $('#maxItemNum').attr('disabled', true); $('#period').val(period); $('#maxItemNum').val(maxItemNum); inputDisabledFlag = true; } if(choice == 'default' && inputDisabledFlag){ $('#period').attr('disabled', false); $('#maxItemNum').attr('disabled', false); inputDisabledFlag = false; } }); ``` ##### 4.2.2.4 刷新模块 * 刷新模块可以在动态展示的过程中,在不刷新页面的前提下重新渲染图表,同时在rating、user_count、week_recommend总览图中可以重新加载动画。 * 首先,点击4-2-16中的刷新图像按钮:  * 效果如图4-2-17所示:  * 这里对总览图的刷新就不再展示了。 * 刷新模块的核心代码如下: ```javascript // 刷新图片点击事件 $('.control:first').click(function(){ if(viewType != 'default'){ // 总览视图 // alert(viewType); // rankBar.refresh(); option1 = rankBar.getOption(); // 重新加载动画 rankBar.clear(); rankBar.setOption(option1); return; } // alert('点击了'); // console.log(flushTimeout); if(flushTimeout != null){ clearInterval(flushTimeout); flushTimeout = null; $('.run').attr({'src': './static/img/start.png', 'title': '开始', 'alt': '开始'}); } counter = 0; $.initView(names.slice(0, maxItemNum), ranks.slice(0, maxItemNum)); }); ``` ## 5 系统测试 | 测试序号 | 用例 | 状态 | | -------- | ------------------------------------------------------------ | ---- | | 1 | 运行spider.py,爬取数据 | 通过 | | 2 | 获取反爬虫数据week_recommmend | 通过 | | 3 | 运行flask框架主程序app.py | 通过 | | 4 | 访问http://127.0.0.1:5000 | 通过 | | 5 | 访问http://127.0.0.1:5000/getBook | 通过 | | 6 | 访问http://127.0.0.1:5000/error | 通过 | | 7 | 点击start图像,动态展示数据 | 通过 | | 8 | 动态展示数据时,点击pause图像,暂停 | 通过 | | 9 | 点击search图像,搜索“全职法师”条目 | 通过 | | 10 | 点击setting图像,设置period刷新时长 | 通过 | | 11 | 点击setting图像,设置页面展示条数 | 通过 | | 12 | 点击setting图像,单选按钮选择“rating”,显示rating总览折线图 | 通过 | | 13 | 点击setting图像,单选按钮选择“user_count”,显示user_count总览折线图 | 通过 | | 14 | 点击setting图像,单选按钮选择“ recommend”,显示week_recommend总览折线图 | 通过 | | 15 | 点击setting图像,单选按钮选择“default”,显示排名柱状图 | 通过 | | 16 | 排名柱状图下,点击refresh图像,刷新数据 | 通过 | | 17 | rating总览图下,点击refresh图像,刷新动画 | 通过 | | 18 | user_count总览图下,点击refresh图像,刷新动画 | 通过 | | 19 | week_recommend总览图下,点击refresh图像,刷新动画 | 通过 | | 20 | 搜索框或设置框显现时,按下Enter键,调用“确定”按钮的点击事件 | 通过 | | 21 | 设置框显现时,点击Tag键,调用单选按钮focus事件,可以选中单选按钮 | 通过 | ## 6 系统总结 * 该项目共分为两个大的模块:数据爬取部分和数据flask框架网页展示部分。 * 数据flask框架展示部分:主要使用到了flask、jQuery、echarts、css技术来将数据合理美观的展示在网页中,实现了项目需求分析的要求,该部分的flask框架并不难掌握,重点是使用模版来响应html格式的网页信息;对于jQuery(js)部分,比较复杂,需要ajax获取爬取到的信息以及对数据进行动态处理,还要处理页面中图像、单选按钮的点击事件以及focus事件,同时在jQuery部分还有修改html的css样式,使得页面流畅美观;这里选择echarts开源工具将数据渲染到网页中,并通过设置不同的option来设置不同效果的图表,进而可以通过柱状图详细动态的展示数据,也可以通过总览图显示所有数据的整体状况。数据展示部分除主模块外,还有四个小模块,分别是:刷新视图模块、动态展示模块、搜索模块、设置模块,将不同的功能和视图进行绑定,减弱不同模块之间的耦合性,便于调试和使用。刷新视图模块功能相对简单,就是在动态展示完毕后或正在动态展示时,想要初始化视图,这时就不要刷新页面,通过refresh图像的点击事件实现即可,同时,刷新视图模块,在总览折线图中可以重新加载数据。动态展示模块,包含两个状态:start 和 pause,可以通过点击start和pause图像来切换状态,这里,可以使用设置模块自定义的刷新时长和每页显示最大条数来动态展示数据,直到数据展示完成后停止。搜索模块,这里可以通过输入的书名查取响应的条目信息,并且定位到该条目,实现查找功能。设置模块,除了可以设置刷新时长和每页展示最大条数,还可以通过单选按钮切换图表视图,用来在排名柱状图、rating总览图、user_count总览图、week_recommend总览图之间切换。不足之处,排名策略上可以优化,在该项目中使用数据的分布特点划分了各个分值段的映射关系,但是,根据统计学和概率学的知识采用更加高效的分布函数会将数据拟合的更好,这样排名更具有代表性,这里是需要不断迭代优化的地方。 ## 7 个人感悟 * 郑翔:这次开源课程的学习,让自己受益匪浅,学习的内容非常广泛,让自己不得不感叹Python庞大的库,由Python的简单语法开始,自己刚开始是和Java对比来学习Python的语法的,发现它们在很多方面都不一样,所以最后把它当作一门全新的语言来学习总结,讲到列表、字典、元祖的时候,是自己第一次感觉到困难,那些各自的性质和函数让我大脑一片混乱,不过,通过课下多次练习后,慢慢掌握了它们。除了Python语法外,我们还学习了开源协议、turtle库、bs4库、pandas库、表格文件读写、sqlite3和monodb数据库读写、git等等。其中对我影响最大的是git控制项目版本。在这次大作业的完成过程中,自己在每次完成一个模块,便用git提交到gitlab仓库,项目结束的时候查看提交历史,发现项目编写的过程和思路一目了然,这为自己梳理项目提供了非常优秀的工具。同时,在项目的编写过程中,自己用到了大量的开源类和工具,比如jQuery, echarts, flask等,这就极大的简化了我们的工作,优化了我们的项目,所以不得不为开源的价值观念点赞。自己负责的是数据展示部分,包含后台和前端实现,由于flask框架的简洁,使得在一个app.py文件中就完成了后台主体的编写,这点是Java和PHP难以实现的,然后,就是数据具体展示到页面上,我在主模块的基础上添加了四个小模块,分别是刷新模块、动态展示模块、搜索模块、设置模块,这样功能上有了一定的补充,使用起来也比较方便流畅,自己感觉动态展示模块比较优秀,在初始化视图的基础上,设定一定的时间间隔来动态刷新图表,达成动态分页的效果,对于结果自己还是比较满意的。由于队友爬取到的数据中包含rating、user_count、week_recommend三种衡量排名的指标,所以需要自定义排名策略,共同使用这些指标,在该项目中根据数据的分布设置了各分段分值,但是自己感觉这部分应该需要统计和概率的知识来构建一个效果更好的推荐模型,这部分是自己需要不断改进的。 当然,这次大作业的编写过程中,也遇到了很多bug,页面不能展示自己想要的结果而懊恼,但当拨云散雾的那一刻,内心由衷的喜悦! * 秦天文: 在开源软件课程学习中,我收获了很多,不仅学习到了python的基础知识,而且学习到了很多开源库的使用。 和Java,C++不同的是,python是一门脚本语言,意味着python没有编译的过程,这让我在开发过程中,饱受其扰。没有编译过程,就意味着其报错机制不能和Java相提并论。当我们运行过程中出现错误时,其报错有时候会把我引向错误的方向。但是,python的强大的功能征服了我。相比于java,python实现某一个功能时有相当多的类库可以使用。 在开发过程中,我遇到了爬取数据太慢的问题,爬取几十条数据需要十几分钟。为了解决这个问题,我想到了利用多线程。多线程不仅解决了爬取速度慢的问题,同时还解决了频繁访问服务器,导致服务器拒绝服务的问题。Python强大的功能是它最强大的利器。 同时,我使用GitLab也让我明白了如何在一个团队中和队员交流。在开发过程中,如果想仅仅通过线上交流是远远不够的。因为你无法将你的意图很好的传达给对方。而且即使当时你的队友没有向你提出质疑,但是等到实际开发过程中,他依然会存在困惑。在本次大作业中,我最大的收获就是明白了团队合作的重要性。