完成日期: 2021年6月18日
- 一、设计主题及功能介绍
- 二、采用的关键技术阐述
- 三、实现的重点与难点
- 五、总结、展望与待优化的地方
- 六、参考资料
- 七、附件:原始代码
一、设计主题及功能介绍
1.1 设计主题
早在2010年前后,全球就迎来了第三次信息化浪潮,全面进入了信息爆炸的时代。在互联网上,自媒体用户量、作品量成指数级增长。智能手机随处可见,极大多数人会用抖音、知乎、B站等手机社交应用作为娱乐放松的方式。
渐渐地,这样的生活方式悄然地改变了我们的生活,比如在平台上看到某位喜欢的关注者在进行直播带货,这时候就会有人选择支持一下,买下那个商品,尽管有时候自己并不是非常需要,又或者是看到旅游的Vlog,看到Vlog里的拍摄者非常欢快,自己也想去那个地方体验一下......这样的”从众心理”是很正常的,因为信息量过大,我们有时候无法很快做出决定,比如在即将到来的假期,我们想要去旅行,却不知道该去哪个景区,针对这种情况,本次报告就通过设计python程序,使用其中的部分官方库和部分第三方库,基于爬虫技术,数据来自携程网站的旅游信息,针对用户实现一款能够推荐旅游景区的小应用。其中囊括了对携程网的各类旅游相关信息进行数据采集、数据清洗、数据分析以及可视化等功能。
1.2 功能介绍
1.提供友好的UI界面,用户可以点击各种按钮来产生不同的效果
2.以用户为主,为用户提供评论量、评分两种指标,最终可根据这两个指标推荐景区
3.爬取携程网某个城市的所有景区信息
4.爬取携程网某个景区的所有信息
5.对某个景区的评论进行词频统计并生成词云
6.可视化生成今年第一季度国内旅游行业游客量的地图
二、采用的关键技术阐述
2.1 requests库 ——获取网页内容
Requests库是用python语言基于urllib编写的,采用的是Apache2 Licensed开源协议的HTTP库。与urllib相比,Requests更加方便,可以节约我们大量的工作。该库提供了python程序与http网络交互的接口,在整个项目中主要用该模块来爬取网站上的内容,分别通过get方式爬取单个地区的所有景区信息、景区的所有信息和post方式爬取景区的所有评论信息。
昂
2.2 BeautifulSoup4 库 ——网页内容查找[1]
BeautifulSoup是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式。BeautifulSoup对象表示的是一个文档的全部内容.大部分时候,可以把它当作Tag对象,它支持遍历文档树和搜索文档树中描述的大部分的方法.find_all()方法搜索当前tag的所有tag子节点,并判断是否符合过滤器的条件,按照CSS类名搜索tag的功能非常实用,但标识CSS类名的关键字class在Python中是保留字,使用class做参数会导致语法错误.从Beautiful Soup的4.1.1版本开始,可以通过class_参数搜索有指定CSS类名的tag,这一点非常关键,有了find_all方法就可以查找html文本中指定的类名标签,比如... ,这里根据comment关键字可以看出,该标签存储的是和评论相关的信息,这使我们爬虫时节约了大量的时间来查找需要的内容,只需要指定类名形如find_all(‘.comment’) 这样的格式就能爬取到想要的信息。
2.3 json库 ——字典和json对象转化
Json全称是JavaScript Object Notation, 称作JS 对象简谱,是一种轻量级的数据交换格式。它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言[2]。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。以下是json格式的一个范例:
{
"name": "中国",
"province": [{
"name": "黑龙江",
"cities": {
"city": ["哈尔滨", "大庆"]
}
}, {
"name": "广东",
"cities": {
"city": ["广州", "深圳", "珠海"]
}
}]
}
可以观察出和Python的字典类似,所以本次报告中主要就是拿该模块进行python字典型和json文本型的数据交换。在requests模块提交post请求时,其中header参数可以直接为python的字典对象,而传入data参数时必须为json对象,所以就需要用到该模块的dumps方法,将python对象转化为json文本,而post方式返回的json文本也可以转化成python型的字典,调用load方法即可.
2.4 tkinter库 ——GUI界面设计
Tkinter模块(Tk接口)是Python的标准Tk GUI工具包的接口.Tk 和Tkinter 可以在大多数的 Unix平台下使用,同样可以应用在Windows和Macintosh系统里。Tk8.0的后续版本可以实现本地窗口风格,并良好地运行在绝大多数平台中[3]。除了tkinter库以外,python还有其他两个常用GUI库,分别是wxPython、jython前者是一款开源软件,可设计出优美的图形界面兼容多个系统平台,后者保证程序可以和java无缝集成。
在本次报告中主要用到tkinter的组件有:
| 组件名 | 描述 | 实际运用 |
|---|---|---|
| Frame | 框架控件: 在屏幕上显示一个矩形区域,多用来作为容器 | 创建多个窗口,容纳其他的组件 |
| Label | 标签控件:可以显示文本和位图 | 显示提示的文本信息以及图片,为用户提供良好的交互体验 |
| Button | 按钮空间:在程序中显示按钮 | 方便用户根据提示进行下一步操作,按钮可以触发事件 |
| Checkbutton | 多选框空间:用在程序中提供多项选择框 | 在用户选择推荐方式时需要的组件 |
| tkMessageBox | 用于显示应用程序的消息框 | 弹窗提示 |
2.5 threading库、queue库 ——多线程爬虫[4]
多线程类似于同时执行多个不同程序,多线程运行有如下特点:
- 可把占据长时间的程序任务放到后台去处理
- 加快程序的运行速度
- 线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 每个线程都有自己的一组CPU寄存器,称为线程的上下文,它反映了线程上次运行该线程的CPU寄存器的状态。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) 即线程的退让。
Python的Queue库中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步,本次报告主要用到FIFO(先入先出)队列Queue。
基于以上的库可以将爬虫的过程细分为两个阶段:
第一阶段,爬取网页内容,第二阶段,解析网页内容,这两个阶段在一定的条件下可以进行多线程运行,比如在第一阶段有10个网页待爬取,而当爬取了第1个网页后,第二个阶段解析网页内容就可以开始,而无需等待全部网页获取完毕后才开始进行解析,基于这样的思路,可以提高爬虫以及解析的速度。
2.6 Pandas、Numpy库 —— 数据处理
Pandas库全称Python Data Analysis Library 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。Pandas 纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。pandas提供了大量能使我们快速便捷地处理数据的函数和方法[5]。可以认为Pandas库是使Python成为强大而高效的数据分析环境的重要因素之一。
在本次报告中需要实现对全国旅游信息数据的处理以及可视化、其中的处理阶段就需要用到Pandas库中的Dataframe对象,该对象提供方法read_excel(),可以在获取数据时将excel文件转化成python的Dataframe对象,从而进行数据处理,比如提取想要的一列数据,或者是对某一列数据进行排序。
2.7 random库 —— 产生随机数[6]
Random库可以返回一个随机数,这对本次报告中随机推荐景区做出了重要贡献,在一切准备就绪后,所有的景区信息都保存到了一个DataFrame对象或者一个List列表里,为了实现出现随机的景区,我们需要对下标进行随机化处理,先随机化产生一个下标,调用randInt(a, b)方法就可以产生一个[a, b) 范围的随机数,接着在根据对应的数据类型取数据即可。
2.8 wordcloud库 —— 生成词云[7]
WordClout库是一个词云展示的第三方库,它具有的特点如下:
官方
- filling all available space.
- being able to use arbitraty masks.
- having a stupid simple algorithm (with an efficient implementation) that can be easily modified.
- being in Python
自译
- 填充所有空间
- 可以使用仲裁掩码
- 非常简单的算法(伴随有效的实现)便于修改
- 基于python
另外, wordcloud 库把词云当作一个WordCloud对象,而wordcloud.WordCloud()代表一个文本对应的词云,可以根据文本中词语出现的频率等参数绘制词云,绘制词云的形状、尺寸和颜色均可设定,以某个图片作为背景进行词云展示。
在本次报告主要用于展示景区的评论,通过展示游客评论的高频词汇,用户可以非常直观地观察到一个景区的评价情况,方便用户选择。
2.9 Pyecharts库——将数据加载到地图的可视化方式
Echarts 是一个由百度开源的数据可视化,凭借着良好的交互性,精巧的图表设计,得到了众多开发者的认可。而 Python 是一门富有表达力的语言,很适合用于数据处理。当数据分析遇上数据可视化时,pyecharts 诞生了[8]。
特点:简洁的 API 设计,使用如丝滑般流畅,支持链式调用、囊括了 30+ 种常见图表,应有尽有、支持主流 Notebook 环境,Jupyter Notebook 和 JupyterLab、可轻松集成至 Flask,Django 等主流 Web 框架、高度灵活的配置项,可轻松搭配出精美的图表、详细的文档和示例,帮助开发者更快的上手项目、多达 400+ 地图文件以及原生的百度地图,为地理数据可视化提供强有力的支持。
本次报告就使用该库对2021年第一季度国内大陆各省份景区每日游客量的数据进行了一个地图可视化。
三、实现的重点与难点
3.1整体设计思路
(1)设计交互UI界面类——基于python tkinter库,它可以创建组件的有窗口,按钮、图片标签、文字标签等,实现用户和程序的友好交互,另外按钮可以添加对应的事件处理,在按钮被单击后可以执行一系列操作,比如在单击按钮后,显示出一张景区的图片,再单击一次就显示另一个景区的图片,这和web访问有什么区别?在web网站上用户可以直接看到所有的景区信息,但是因为信息过多,用户肯定会难以抉择,面对众多选择根本难以决策,而本次项目则有效地解决这个问题,基于tkinter库建立起一个较完整的景区分析可视化窗口,使得用户接受的信息量更为集中、有效。
(2)设计爬虫类——基于requests库、beautisoup库和json库实现对网页内容的提取及美化,用re库即正则表达式提取需要的内容。可以在UI界面类中创建爬虫类,这样就可以在UI界面中实现爬虫的一系列操作,将爬虫的有效信息展现在ui窗口上。
(3)数据分析及可视化——基于jieba、wordcloud库,对评论进行分词、编写代码进行词频统计后可绘制出词云图。基于pyecharts库生成可视化的中国地图
3.2 爬取某个地区所有景区信息——设计 Crawler_all_place类
该类实现对如下图网页的景区信息爬取
图. 携程温州地区的景区页面
Crawler_all_place类的主要设计构造如下:
| 方法名称 | 返回值 | 描述 |
|---|---|---|
| init(self, url, placeId) | self | 初始化,保存当前地区的id号,并调用下方的initResource方法获取bs对象 |
| initResource(self, url) | None | get获取url的网页内容, 并用bs构造方法,进行html分析 |
| getName(self) | List | 获取当前地区所有景区名称 |
| getRank(self) | List | 获取排名 |
| getAddress(self) | List | 获取地址 |
| getComment(self) | List | 获取热评 |
| getScore(self) | List | 获取评分 |
| getAllImageLink(self) | List | 获取图片链接 |
| getLink(self) | List | 获取景区对应网址 |
| getPagenum(self) | Int | 获取地区的景区页数 |
| getNum(self) | Int | 获取地区的景区个数 |
| getAllPageAll(self) | DataFrame | 获取前百个景区信息,保存数据到self.df对象 |
| getOnePageAll(self) | None | 获取一页的景区信息,保存到self.data对象,类型为List |
| sortByCommentnum(self) | None | 根据评论量对self.df进行排序 |
| sortByScore | None | 根据分数对self.df进行排序 |
| sortByCommentNumAndScore | None | 同时根据评论量和分数对self.df进行排序 |
在每个get方法中的具体实现中,主要调用了BeautifulSoup库的select方法,通过类名对html文本内容进行查找, 找到对应的html类标签后,在调用re库的findall方法,利用正则表达式提取出需要的信息,以爬取景区地址的方法为例:
Crawler_all_place.py -> def getAddress(self)
def getAddress(self): # 获取当前页面的所有景区地址
ADDRESS = \'\'.join([str(x) for x in self.soup.select(\'.rdetailbox > dl > .ellipsis\')])
self.ADDRESS = re.findall(r\'\n([^<]*)</dd>\', ADDRESS)
return self.ADDRESS
如下图其中爬取到的关键原文内容是
图. 使用浏览器调试工具查找景区名称的类标签
温州市乐清雁荡镇雁山路88号
可使用正则表达式获取到地址,关键思路:
地址名称在第2行开头,说明在它之前会有一个换行符\n ,于是先匹配换行符\n。
接着在使用>限制匹配[^<] 即除了<以外的字符都匹配,匹配的次数为 * ,即0次或者多次,这意味着要么不匹配,要么就匹配到<才结束,正好这样就可以匹配地名 在加上许多空格和 的内容,这里后面的空格和都是不需要的,为了确保最终匹配的字符串是正确的地名,于是最后加上 做限制,这里多余的空格暂时没有处理,因为空格可以直接调用字符串的replace方法除去。
以此类推即可实现其他的get方法。
3.3 爬取某个景区的所有信息——设计Crawler_a_place.py类
| 方法名称 | 返回值 | 描述 |
|---|---|---|
| init(self, url) | self | 初始化方法,通过get方式获取网页内容,并返回soup化后的对象 |
| setHtml(self, url) | None | 获取url网页内容在html解析后的bs对象,保存到self.soup对象中 |
| setData | None | 获取该景点的所有信息保存到self.data对象中,类型为字典 |
| getAll(self) | Dic | 返回setData的结果即self.data对象 |
| getAImageLink | String | 随机返回某个地区图片链接, 给用户提供参考。 |
| initImageLink | None | 调用多线程类,爬取所有地区图片链接 |
该类设计相比于Crawler_all_place.py 简化许多,但是它调用了多线程类针对所有地区进行爬虫。
其中简单的随机化返回图片链接getAImageLink方法
def getAImageLink(self, name): # 随机返回 一个图片链接
n = len(self.imagesLink[name])
i = random.randint(0, n - 1)
return self.imagesLink[name][i]
爬取单个景区所有信息的getAll方法
def getAll(self): # 获取景区信息
text = [str(x) for x in self.soup.select(\'.moduleContent\')] # 通过类名获取 景区信息
INSTRUCTION = re.findall(\'>([^<]+)<\', text[0].replace("<span>", "").replace("</span>",""))[0] # 景区介绍信息
OPENTIME = re.findall(\'>([\\-0-9:^<]+)<\', text[1])[0] # 景区开放时间信息
WAY = re.findall(\'>([^<]+)<\', text[2])[0] # 步行方式信息
TEL = re.findall(\'<p class="baseInfoText">([\\-0-9^<]+)</p>\', \'\'.join([str(x) for x in self.soup.select(\'.baseInfoText\')]))[0] # 联系电话信息
COMMENT = [str(x) for x in self.soup.select(\'.hotTag\')] # 评论 总结 所在标签
COMMENT = re.findall(\'>([^<]+)<!-- -->[(]([0-9]+)[)]</span>\', \'\'.join(COMMENT)) # 评论总结 信息
return [INSTRUCTION, OPENTIME, WAY, TEL, COMMENT]
# 返回景区信息
3.3 爬取单个景区所有评论——设计Crawler_comments.py类
之前直接通过get方式就可直接获取到需要的网页内容,只需设置header,但爬取携程网站的评论信息是通过post方式提取的,在调用requests.post方法时,需传入data参数,经测试,每一页的评论数默认有15条,不过可以更改data,修改每一页评论上限,最后测试出最大上限为每页50条评论,在切换下一页的时候,data的对应的部分表单数据、只要更新data之后再次提交post请求就可以获取到下一页的评论,以此类推可以获取到所有的评论。
| 方法名称 | 返回值 | 描述 |
|---|---|---|
| init(self) | self | 暂时不做处理 |
| get(self, url) | List[List, List, List, List] | 获取单个景区的评论信息,包括【内容、图片链接、评分、评论时间】 |
| updateRequestPayload(self, id, page) | None | 更新self.RequestPayload的值 |
| 用于设置爬取不同页面评论时所需的data | ||
| getId(self, url) | String | 爬取景区id,爬取评论该id时需要作为data提交 |
| getCommentNum(self, url) | String | 获取评论总数,为了方便输出,不进行类型转换 |
| Is_Chinese(self, ch) | Bool | 判断字符是否为中文,用于词频统计过滤非中文字符 |
| WordCount(self) | None | 实现词频统计,并生成词云 |
以下是获取评论的测试,测试url : https://you.ctrip.com/sight/yandangmountain217/135800.html
第一步,在点击下一页前先清空调试工具当前的Network列表
图. 首先清空浏览器调试工具获取到的文件
第二步,点击后 在Network界面查找带有Comment关键字的文件
图.查找评论的json文件
第三步,如下图,点击Comment文件后在右侧选择Preview,可以看到所有的评论都在该文件下,这里是以json代码的格式存储的,我们在用requests.post获取到页面内容后,需要用json库的loads方法对该格式转化,转化成python里的dict即字典型。
图.查找正确的评论文件,观察preview
查找该文件的Headers属性里的Requests URL 和 Requests Payload,后者就是post方式需要提交的data
图.复制URL
图.查看提交表单时需要修改的属性
经过分析,携程网的评论信息通过post方式提交的URL都是相同的,其中的&x-traceID=09031073114492803259-1623506818535-8121205是浏览器上一个浏览页面的标记,用于防止爬虫,这里直接先用浏览器操作,复制正确的一个ID就可以用来爬取评论信息,为了能爬取所有的评论,这里需要修改data中的arg里的3个关键属性,分别是pageIndex、页数下标、pageSize每一页评论条数、poiId景区的id,每个景区的id不同,id号则可通过景区页面进行提取。
由于post方式下每次爬取加上数据存储过程稍慢,这里为了提供用户更好的体验,将评论上限设置为1000条,以下是实现该功能的关键代码:
for i in range(1, min(20, self.PageNum + 1)): # 爬取评论
print(getTime() + \' >> 执行 爬取第 [\' + str(i) + \'] 页评论,已爬取:\' + str(num) + \'条.\')
self.updateRequestPayload(self.id, i) # 更新post信息即 RequestPayload 成员变量
text = rq.post(URL, data=json.dumps(self.RequestPayload)).text # 提交post请求,获取评论信息
text = json.loads(text) # 将提取到的json对象评论信息转化为python字典
items = text[\'result\'][\'items\'] # 评论信息在items键中
if items != None: # 防止出错,即爬取为空的情况
num += len(items) # 统计评论数
for x in items: # 保存评论信息
content.append(x[\'content\'])
images.append(x[\'images\'])
score.append(x[\'score\'])
publishTime.append(x[\'publishTime\'])
词频统计生成词云的思路是调用该类的get方法后,提取self.data[0],即所有评论的信息,将其转化为str类型后调用jieba库的lcut方法对中文进行分词,接着创建空字典,循环遍历jieba对象,统计各个评论词语出现的频率,最后调用wordcloud库的WordCloud方法创建一个词云对象,设置好各个参数后,显示出来即可。
3.4 多线程爬虫初始化所有地区信息——设计Crawler_threading类
为了给用户更好的视觉体验,我决定将各个地区的图片链接爬取下来,然后在通过tkinter库里的label组件显示到窗口上,这样用户在选择旅行地区时就有一个形象的参考。该类主要运用threading库、queue库实现运用多线程来爬取网页、解析内容[9]。
| 方法 / 类 | 返回值 | 描述 |
|---|---|---|
| Class crawler_thread(threading.Thread) | None | 爬虫类,继承于threading.Thread类,主要实现爬取各个地区网页的内容,并实现父类的run方法 |
| Class parser_thread(threading.Thread) | None | 解析类,继承于threading.Thread类,主要实现解析爬取后转化来的bs对象,并实现父类的run方法 |
| def work() | None | 实现多线程爬虫的关键方法,最后将结果保存到本地的json文件内,保证第二次打开时无需重复爬取。 |
接下来是crawler_thread(threading.Thread)类的构造
| 方法名 | 返回值 | 描述 |
|---|---|---|
| init(self, threadID, tasks) | Self | 初始化线程的Id号,和需要处理的任务队列,这里则表示所有线程会调用同一个任务队列 |
| crawl_spider(self) | None | 获取tasks队首的url,爬取网页内容,并将爬取后的结果入队到self.dataQueue中 |
| run(self) | None | 调用crawl_spider()方法 |
| Parser_thread(threading.Thread)类的构造 | ||
| 方法名 | 返回值 | 描述 |
| ----------------------------- | ---- | ---------------------------------- |
| init(self,threadID,tasks) | Self | 初始化线程的Id号和需要处理的任务队列 |
| Parse_data(self,data) | None | 根据data对象解析网页文本,使用正则表达式获取所有该地区的图片链接 |
| Run | None | 调用parse_data方法并对任务队列进行更新 |
Work方法的设计思路:
3.5 UI界面设计 —— tkinter模块
因本次报告的重点是熟悉爬虫知识且ui界面代码比较繁琐,所以ui界面设计方面阐述一些重要的实现思路,不阐述所有的结构。
3.5.1 页面布局的选择——place 绝对布局
在tkinter模块中共有三种布局方式、绝对布局place、网格布局grid、包装布局pack,因需要多个按钮组件显示景区图片,以及一些功能按钮,比如实现爬取评论的按钮、切换景区的按钮等,为了使得界面美观,整个页面我选择的都是place 绝对布局,该布局方式可以指定组件的坐标位置x、y将组件放置到窗口的固定位置,但是有一个很大的缺点就是难以调试,想让组件到合适的位置需要经过不断的测试才会达到想要的效果。
3.5.2 多窗口的动态切换思路
本次报告设计的窗口类主要有ui_Main、ui_ready、ui_choseArea、ui_choseScenic、ui_work
他们共用同一个Tk对象,故都能直接显示到Tk上,多窗口切换主要是对于其x、y坐标进行更改,基于思考,主要设计四个方法实现窗口切换。而动态效果本质上就是调用按钮组件的alter方法,每次都递归修改窗口的x、y属性,直到达到固定的位置。
def moveFrame(self, currentid = 0, toRight = True, speed = 8):
if self.tmp_x < WIDTH and toRight == True: # 右移
self.tmp_x += speed
self.getFrameByid(currentid).f.place(x=self.tmp_x, y=self.tmp_y)
self.getFrameByid(currentid - 1).f.place(x=self.tmp_x - WIDTH, y=self.tmp_y)
self.getFrameByid(currentid).f.after(10, lambda: self.moveFrame(currentid, toRight,speed))
if self.tmp_x > 0 and toRight == False: # 左移
self.tmp_x -= speed
self.getFrameByid(currentid).f.place(x = self.tmp_x - WIDTH, y = self.tmp_y)
self.getFrameByid(currentid+1).f.place(x = self.tmp_x, y = self.tmp_y)
self.getFrameByid(currentid).f.after(10, lambda:self.moveFrame(currentid, toRight,speed))
def lastFrame(self, currentid, speed): # 同时右移
# need tmp_x, tmp_y
self.tmp_x = 0
self.tmp_y = 0
self.moveFrame(currentid, True, speed)
def nextFrame(self, currentid, speed): # 同时左移动
self.tmp_x = WIDTH
self.tmp_y = 0
self.getFrameByid(currentid + 1).show()
self.moveFrame(currentid, False, speed)
def getFrameByid(self, id):
if id == 0: return self.f_ready
elif id == 1: return self.f_choseArea
elif id == 2: return self.f_choseScenic
elif id == 3: return self.f_choseGoal
3.5.3 ui窗口类和爬虫类的衔接
图. 程序运行界面
以上图为例,按钮需要实现查看评论详情,首先给按钮的command属性指定获取评论的方法,那么按钮是在ui窗口类的,所以在该ui窗口类需要创建一个爬虫类,基于这样的思路,我在每个ui窗口类中都创建了所需要的爬虫类,这样就保证了可以组件可实现对应功能
以下是图上按钮的定义代码(其他属性略),因为每个功能按钮都需要有方法进行响应,为了区分,我加了keep前缀区分,像这样就可以很清晰的区分出方法所属的类别,或者可以直接查找到该方法绑定的组件。
self.b_comment = tk.Button(command = self.keepComment)
def keepComment(self):
if self.flag == False:
self.crawler_comments.get(self.crawler_a_place.url)
self.flag = True
print(getTime() + \' >> 输出评论信息.\')
for i in range(len(self.crawler_comments.data[0])):
print(\'来自第\' + str(i+1) + \'位用户的评论:\', end = \'\')
print(\'评分: \' + str(self.crawler_comments.data[2][i]))
print(\'内容: \' + str(self.crawler_comments.data[0][i]))
print(getTime() + \' >> 输出完毕.\')
以上代码是直接调用已经完成爬虫的爬虫类数据对象,这样看来可以很方便的输出结果。
3.5.4 实现三个选择评论量、评分(即顺序推荐)与随机推荐互斥
图.需要设置的三个选择框
在进行景区推荐的时候,用户可优先选择排序的指数,评论量或者评分、又或者是两者都考虑,在这个时候景区推荐的结果就是有序的从前往后。实现一个随机推荐的功能,即当用户对这两个指标都没有固定要求的时候,可以随机地浏览每个景区信息,本质上就是在获取景区ID时进行随机化,这里我选择调用random库里的randInt方法该方法可以返回一个范围内的一个int型数据,再定义列表作为景区的访问标记,限制景区的推荐次数,防止给用户推荐到重复的景区。
三个选择框即tkinter模块里的Checkbutton组件,为了获取他们的状态,各需要三个tkinter.IntVal对象,将该对象设置到variable属性,这样在选择框状态改变时,IntVal对象的值也会对应改变。
关键代码如下:
self.c1, self.c2, self.c3 = tk.IntVar(), tk.IntVar(), tk.IntVar()
self.check1 = tk.Checkbutton(variable=self.c1, command=lambda:self.keepCheckRule(1))self.check2 = tk.Checkbutton(variable=self.c2, command=lambda:self.keepCheckRule(2))self.check3 = tk.Checkbutton(variable=self.c3, command=lambda:self.keepCheckRule(3))
def keepCheckRule(self, id):
# 1、2 和 3 互斥
if id == 3:
self.check1.deselect()
self.check2.deselect()
if (id == 1 or id == 2) and self.c1.get() == 1 or self.c2.get() == 1:
self.check3.deselect()
3.6 数据可视化之词云展示以及地图展示
3.6.1 词云展示——wordcloud库
词云展示主要用到crawler_comments类(爬取评论)、jieba库(分词)、wordcloud(生成词云),在实现好爬虫类后可以很轻易的得到数据,在这我设置了记忆化,比如在用户点击两次获取评论详情的按钮时,第二次就无需在重新爬取数据了,因为每次选取景区后,爬虫类都会重新获取评论,所以也不用考虑是不是同一个景区按下两次获取评论的按钮。
pic = plt.imread(r\'images/background.jpg\') # 加载词频统计图片
wc = wordcloud.WordCloud( # 生成图片
r\'c:\windows\fonts\simfang.ttf\',
width=600, height=480, # 图片大小
background_color=\'white\', # 背景颜色
font_step=5, # 字体间隔
mask=pic, # 背景图模板
scale=10, # 清晰程度 值越大越清晰
random_state=False # check
)
t = wc.generate_from_frequencies(dict(result)) # check
t.to_image().show() # 显示图片
print(getTime() + \' >> 评论词云展示完毕.\')
3.6.2 地图可视化——pyecharts库
该功能的实现非常轻松,只需要先读取数据文件,转化为python的DataFrame类型,然后再提取所需要的省份和对应数据,保存到两个列表中,调用Map方法在加上一些配置信息即可完成。
def keepSummary(self):
try:
print(getTime() + \' >> 正在读取excel数据文件.\')
df = pd.read_excel(r\'./data/全国2021年第一季度旅游游客量汇总.xlsx\', index_col=None).dropna()
# 数据处理
print(getTime() + \' >> 读取成功.接下来将进行数据处理.\')
provinces, data, maxi = [], [], 0
try:
# 读取省份
for p in np.array(df.iloc[1:, :1]).tolist():
for x in p:
provinces.append(x)
# 读取人数、 单位统一为 万人 每天
for d in np.array(df.iloc[1:, 3:4]).tolist():
for x in d:
x = int(int(x) / 10000) # 统一单位
if x > maxi:
maxi = x
data.append(x)
c = (
Map()
.add("", [list(z) for z in zip(provinces, data)], "china")
.set_global_opts(
title_opts=opts.TitleOpts(title="2021年国内大陆省份旅游游客量汇总图 (万人/天)"),
visualmap_opts=opts.VisualMapOpts(max_=maxi, split_number=10, is_piecewise=True),
)
)
if not os.path.exists(os.getcwd() + \'\data\'):
os.mkdir(os.getcwd() + \'\data\')
path = (os.getcwd() + r\'\data\china-2021-first-quarter-suammry.html\')
c.render(path="data/china-2021-first-quarter-summary.html")
print(getTime() + \' >> 处理完毕. 保存位置在 \' + path)
tk.messagebox.showinfo(\'提示\', \'导出成功!\n文件位置在当前运行目录下的\n\data\china-2021-first-quarter-summary\n您可以自行去查看!\')
except:
tk.messagebox.showinfo(\'提示\', \'数据处理失败,数据文件有损.\n请检查/data/全国2021年第一季度旅游游客量汇总.xlsx内容.\')
print(getTime() + \' >> 数据处理失败. 数据文件有损.\')
except:
tk.messagebox.showinfo(\'提示\', \'读取数据失败!\n请确保运行目录文件夹data内\n有[全国2021年第一季度旅游游客量汇总.xlsx]文件存在.\')
print(getTime() + " >> 操作失败.")
图.pyecharts库可视化地图的结果
四、程序实现的结果
4.1 准备界面
图. 准备出发界面
该界面只需用户在准备好后点击GO即可滑到下一个界面。
4.2 选择地区界面
图. 窗口1滑动到窗口2的过程
在上一个窗口界面点击GO后,界面会缓慢的向左滑动
图. 城市选择界面
在该界面提供了两个功能性按钮,左下角则为返回,它可以实现将窗口右移返回到上一个准备出发的窗口。另一个是刷新按钮,可以刷新当前的城市,图中的图片均是从网络上爬取下来的图片链接,然后进行压缩处理在设置到Label组件上的。
图. 点击刷新后终端更新的信息
图.刷新城市后的结果
在用户选择好要去的城市后,窗口继续右滑,进入下一个界面
4.3 选择推荐方式界面
图. 选择推荐方式的界面
该界面首先展示的爬取结果中的当前地区景区的计数,另外提供三个功能性按钮和三个选择框,其中第一个按钮返回和上个窗口的功能一致,返回到上一个窗口即选择城市的窗口,第二个是主页按钮,它可以直接返回到第一个界面。最后是继续按钮,在用户确定好以后可以点击,并且会友好的弹出提示。
图. 当前窗口继续按钮弹出的提示框
对于三个选择框,实质上是对DataFrame对象的一个排序判断,若选中评论量最后会按评论量的高低依次推荐景区。
4.4 最终显示:当前景区全功能界面
图. 最终界面
该界面提供的功能较多,接下来进行一一阐述。
4.4.1 界面切换——返回、主页按钮
这两个按钮的功能和之前的窗口功能一致,一个是左滑返回上一个窗口,另一个是直接返回第一个窗口。
4.4.2 景区切换——刷新按钮(显示爬虫结果)
该按钮实现更新当前景区的图片以及文本框里关于该景区的一些信息,包括评分、排名、简介、开放时间、通行方式。这些都是爬虫结果的展示。
4.4.3 景区汇总——导出excel按钮(导出爬虫结果)
该按钮将直接把爬虫的结果导出到excel中,用户可以在本地查看相关信息,其中包括前百个景区的信息,包括排名、名称、地址、评论量、分数、景区网页链接
图.导出景区信息的excel文件
4.4.4 获取评论、评分、排名的数据——评论、评分详情按钮
这三个按钮实现简单,在终端输出爬虫结果,使得用户可以直观的看到相关信息。
图. 正在爬取评论
图.输出爬取评论的结果
图.查看前百景区评分
图.查看景区排名情况
4.4.5 生成词频统计后的词云 —— 查看词云按钮
该按钮实现的功能就是将所有的评论进行分词、词频统计、生成词云
图.词频统计测试
图. 蜈支洲岛景区前一千评论的词频统计词云图
4.4.6 数据可视化地图——查看分析按钮
该按钮实现功能是对2021年全国大陆各省份旅游行业游客量数据的一个汇总,数据并非爬虫获取,而是来自于gov.cn网站,中国政府网站,所以保证了数据的真实有效。
图. 导出提示
图.在本地查看html文件
图.可视化地图的结果
五、总结、展望与待优化的地方
5.1 总结与展望
本次报告对Python的库tkinter、requests、BeautifulSoup、json、re、treading、pandas库进行了综合运用,采用爬虫技术对携程网上的景区信息进行了提取。并通过数据处理、分析,将数据可视化展现在UI界面,用户可以通过该程序选择偏向于自己喜好的景区。
通过本次课程设计,我体会到了python爬虫技术容易入门,但是要想快速地提取有效信息,还得进一步深入,比如用一些爬虫的框架,否则效率就会比较低。另外,正好我们所学的专业是数据科学与大数据,将来专业对口的岗位肯定会接触数据分析这一块的知识,所以学好python,运用好其中最基础的pandas库、numpy库是十分有必要的。
对于本次设计的主题,基于用户兴趣地爬取信息,实现了有效爬取,让用户做简单的抉择,当然这只对于爬虫程序,如果没有携程网这么全面的数据页面,爬虫程序也无法发挥作用,在携程网数据的基础上我们可以设计对用户更好的,旅行信息的推荐,这样可以提升用户的选择兴趣。
对于未来的展望,我的想法是除了景区信息,还可以添加对应的酒店信息以及住宿信息,当然这需要用到地理位置,在读取每个景区信息时,在其定位的一定范围内查找所有的酒店和住宿,在综合其价格,给用户提供预算的选择,根据用户的预算对三大数据规模进行综合处理,最后一定可以推导出最佳的旅游景点合集,这样的话用户也不用逐个地去比较每个景点的性价比了,节省时间。
更复杂一点的想法是加上交通信息。因为携程网站是交通、酒店、住宿、旅游集一体的旅程推荐网站,这些数据我们都可以通过爬取该网而得到,比较难的就是大量数据的综合处理了。随着现在的发展,肯定是有框架能承载这么大的数据量的,还是基于那个思想根据用户兴趣来推荐,只要先让用户选择一些选项,就能缩小推荐范围,最后逐渐缩小,在最后的范围内进行爬取数据、数据分析、处理及可视化。
那么这样的程序有什么意义吗?我认为是非常有的,特别是在疫情突发的最近时期,使得许多人压力增大,焦虑不安,合理的放松方式非常有必要,而且选择也是很重要的,在放松过程中最重要的就是体验感,如果体验感好就能提升精神气质,提升了精神气质,那么整个人的工作热情就会上升,毋庸置疑去旅游的人们肯定是来自各行各业的,如果在他们之中能提升体验感,若整体占比一直保持稳步上升的趋势,则一定能加快国家的发展,正如梁启超先生所说的:少年强,则国强。
5.2 存在的不足与待优化的地方
程序爬取地区信息的速度比较慢,因为地区信息的html文本有上千行,尽管使用了多线程进行爬虫,但每次都要进行类型转换操作以及结果存储,而且会重复提交requests请求。所以在程序中我不得不用保存数据到本地的方式来减少第二次的程序启动速度。
未用到爬虫框架,爬虫模块只是用到了requests库进行获取网页内容、BeautifulSoup库解析内容,最后又用re库对结果进行正则匹配,整个过程简单且低效,如果是想爬取所有的景区的话就肯定会需要用到爬虫框架了。
Ui界面设置简陋,对于一款从用户体验出发的应用程序,应该有比较好的UI界面,因为我是初次入门tkinter库,所以在应用上不太顺畅,所以最终的效果也不是非常的好。
元素比较单一,整个应用程序只是爬取了景区的信息,而不包含景区的门票费用,同时也缺少数据分析,因为在携程网的景区板块中数字数据是比较少的,在本次报告中我还是从国内政府网下载到的数据集,然后在利用pyecharts进行的可视化。
除了以上几点,其实还有许多不足与待优化的地方,但是我相信在将来的学习中一定有机会再学到更多的知识,到时候可以用新学的知识来解决现在的问题或者要面临的新挑战,python的用途非常强大,所以我们务必要好好学习如何运用它,包括学习使用一些数据分析相关的库。
六、参考资料
[1]BeautifulSoup官方文档https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/
[2]JSON百度百科https://baike.baidu.com/item/JSON/2462549?fr=aladdin
[3]Tkinter库 菜鸟教程https://www.runoob.com/python/python-gui-tkinter.html
[4]Python多线程 菜鸟教程https://www.runoob.com/python3/python3-multithreading.html
[5]pandas 百度百科https://baike.baidu.com/item/pandas/17209606?fr=aladdin
[6]python random 菜鸟教程https://www.runoob.com/python/func-number-random.html
[7]wordcloud官方 http://amueller.github.io/word_cloud/
[8]Pyecharts gitee https://gitee.com/mirrors/pyecharts?utm_source=alading&utm_campaign=repo
[9]Python多线程爬虫 知乎文章 https://zhuanlan.zhihu.com/p/35944711
七、附件:原始代码
附件说明:
开发环境为:Pycharm 2020.1.3 社区版 + Anaconda 3.8.0, 运行时 Run 项目中的App.py 程序即可。
所需要的第三方库有
import datetime, random, math, io, json, wordcloud, re, jieba, os, requests as rq, pandas as pd, tkinter as tk, numpy as np
from tkinter import ttk, messagebox as msgbox
from urllib.request import urlopen
from PIL import Image, ImageTk
from pyecharts.faker import Faker
from pyecharts import options as opts
from pyecharts.charts import Map
from bs4 import BeautifulSoup as bs