目录
项目简介
一、项目名称:
智能出行规划网站
二、项目背景:
每当计划外出旅游时,我们最关心的应该就是目的地天气情况和两地间的交通情况。但遗憾的是,天气平台往往不提供交通信息,而出行平台往往又不提供天气信息,这样对于选择合适的出行目的地是很不方便的。
三、项目核心:
后台定时爬取所有城市的天气数据并保存在数据库中,当用户在地图上点击对应城市名后,能在图表内动态更新城市天气信息;并根据用户提交的起止地区,即时爬取航班信息后展示在表格中。
四、项目要求:
1)展示当前所选城市的一周天气信息(日期、温度、天气状况、风级);
2)展示当前所选城市从当前时间开始,24小时内气象数据(温度变化、相对湿度变化、风力等级变化);
3)在给出起点城市和终点城市后,爬取并展示两城市(必须有机场)间的航班信息(航班号、起点机场与时间、飞行时长、终点机场与时间、餐食供应情况、票价)。
技术说明
1)爬虫(lxml、selenium):由于天气网公开天气数据,所以可以直接使用xpath直接获取其响应中携带的数据;而航班信息网站反爬措施较多,需要使用selenium模拟浏览器操作来获取信息;
2)flask:搭建后端服务器,用于连接python代码与前端代码;
3)echarts:用于生成图表,实现数据的直观展示与中国地图(地理图表)的显示;
4)前端代码:网站的外观渲染与事件绑定;
代码展示
一、前端部分:
- 网站只包含一个主页面,内容通过ajax动态更新,html文件如下:
<!DOCTYPE html>
<html lang="ch">
<head>
<meta charset="UTF-8">
<!--viewport就是设备的屏幕上能用来显示我们的网页的那一块区域;
viewport标记,用于指定用户是否可以缩放Web页面,并对相关的选项进行设定;
如设置了固定的显示区域宽度,那么移动设备端就会出现横向拖动条;
下述content设定方式是为了配合flexible.js使用:
width=device-width是使页面适配设备显示宽度,禁止用户自行缩放;
initial-scale=1.0设置初始缩放值为1,即不缩放-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>智能出行规划</title>
<link rel="stylesheet" href="{{ url_for('static',filename='css/index.css') }}"/>
</head>
<body>
<!-- 页面顶端部分 -->
<header>
<h1>智能出行规划平台</h1>
<div class="showtime"></div>
<script>
var div = document.querySelector('.showtime');
getDate();
setInterval(getDate, 1000);
function getDate() {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
month = month < 10 ? '0' + month : month;
var dates = date.getDate();
dates = dates < 10 ? '0' + dates : dates;
var arr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
var day = date.getDay();
var hour = date.getHours();
var half = hour >= 12 ? '下午' : '上午';
hour = hour <= 12 ? hour : (hour - 12);
hour = hour < 10 ? '0' + hour : hour;
var min = date.getMinutes();
min = min < 10 ? '0' + min : min;
var sed = date.getSeconds();
sed = sed < 10 ? '0' + sed : sed;
div.innerHTML = (year + '年' + month + '月' + dates + '日 ' + arr[day] + ' ' + half + hour + '点' + min + '分' + sed + '秒');
}
</script>
</header>
<!-- 页面主体部分 -->
<section class="mainbox">
<!-- 左侧 -->
<div class="column">
<!-- 定义搭载图表的盒子,必须设置大小 -->
<div class="panel info">
<h2>一周内天气预览</h2>
<div class="box">
<table>
<tr>
<td>18日<br>(今天)</td>
<td>19日<br>(明天)</td>
<td>20日<br>(后天)</td>
<td>21日<br>(周日)</td>
<td>22日<br>(周一)</td>
<td>23日<br>(周二)</td>
<td>24日<br>(周三)</td>
</tr>
<tr>
<td>多云</td>
<td>多云</td>
<td>多云转晴</td>
<td>晴</td>
<td>晴</td>
<td>晴</td>
<td>晴</td>
</tr>
<tr>
<td>?/2℃</td>
<td>9℃/2℃</td>
<td>10℃/1℃</td>
<td>8℃/-1℃</td>
<td>4℃/-4℃</td>
<td>6℃/-4℃</td>
<td>7℃/-3℃</td>
</tr>
<tr>
<td>3-4级</td>
<td><3级</td>
<td><3级</td>
<td>4-5级</td>
<td>4-5级<br>转<3级</td>
<td><3级</td>
<td><3级</td>
</tr>
</table>
</div>
<div class="panel-footer" id="aii"></div>
</div>
<div class="panel bar">
<h2>24小时气候变化</h2>
<div class="chart"></div>
<div class="panel-footer"></div>
</div>
</div>
<!-- 中间 -->
<div class="column">
<!-- 数字部分 -->
<div class="ar">
<div class="ar-hd">
<form action="" method="post" target="son-window">
<input id="from_city" name="from_city" type="text" placeholder='----' onclick="input_click(this)">
<input id="to_city" name="to_city" type="text" placeholder='----' onclick="input_click(this)">
<input id="submit">
<button type="button" onclick="postData()"></button>
</form>
</div>
<div class="ar-bd">
<ul>
<li>始发地区</li>
<li>终点地区</li>
</ul>
</div>
</div>
<!-- 地图部分 -->
<div class="map">
<div class="map1"></div>
<div class="map2"></div>
<div class="map3"></div>
<div class="chart">地图模块</div>
</div>
</div>
<!-- 右侧 -->
<div class="column">
<div class="window">
<h2>航班信息显示</h2>
<div class="flight">
<table id="flight-data">
<tr>
<th>公司-航班</th>
<th>起飞机场/时间</th>
<th>飞行时长</th>
<th>终点机场/时间</th>
<th>早餐供应</th>
<th>票价</th>
</tr>
</table>
</div>
<div class="panel-footer"></div>
</div>
</div>
</section>
<!-- javascript部分 -->
<!-- flexible.js默认会将屏幕宽度十等分,然后将十等分后的一份宽度设为1rem,再通过将rem作为单位来指定某一元素大小,从而相当于设置元素的屏幕占比 -->
<!-- 这里1984px十等分后的198.4px太大,我们将其改为24等分后,1rem = 1984px/24,其值保存在html标签下的font-size样式中 -->
<script src="{{ url_for('static',filename='js/flexible.js') }}"></script>
<!-- 导入echarts的js文件 -->
<script src="{{ url_for('static',filename='js/echarts.min.js') }}"></script>
<!-- 导入jquery的js文件 -->
<script src="{{ url_for('static',filename='js/jquery.js') }}"></script>
<!-- 导入中国地图的js文件 -->
<script src="{{ url_for('static',filename='js/china.js') }}"></script>
<!-- 导入自定义的js文件 -->
<script>
<!--定义变量装载后端数据 -->
var html_data = {{ flask_data }};
<!-- 创建元素节点,用于导入js文件 -->
var newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = "{{url_for('static',filename='js/index.js')}}";
<!-- appendChild()方法可向节点(body)的子节点列表(script)的末尾添加新的子节点 -->
<!-- 插入哪个列表取决于子节点的tag,注意getElementsByTagName返回的是列表 -->
document.getElementsByTagName('body')[0].appendChild(newScript);
</script>
</body>
</html>
- 我们在index.css中设置,为了便于编辑我们使用less语言编写后再用编译器转换为css文件:
// 注意,使用lessc转化less文件为css时,less文件路径不可有中文!!!
// css初始化
* {
margin: 0;
padding: 0;
// border-box固定了我们设定的元素宽与高,内容大小只能小于等于元素大小;
// 通过从已设定的宽度和高度分别减去边框和内边距才能得到内容的宽度和高度;
box-sizing: border-box;
}
li {
// 去除列表前标
list-style: none;
}
// 指明我们自定义的字体名对应的字体文件路径
@font-face {
font-family: electronicFont;
src: url(../font/DS-DIGIT.TTF);
}
body {
background: url("../images/bg.png") // 这一步设置不平铺,靠上居中;
no-repeat top center;
background-size: cover;
line-height: 1.15;
}
// 页面头部盒子
header {
position: relative;
height: 1.25rem;
background: url("../images/head_bg.png") no-repeat;
background-size: 100% 100%;
h1 {
font-size: 0.475rem;
color: #ffffff;
text-align: center;
line-height: 1rem;
}
.showtime {
position: absolute;
right: 0.375rem;
top: 0;
line-height: 0.9rem;
color: rgba(126, 126, 126, 1);
font-size: 0.25rem;
}
}
// 页面主体盒子
.mainbox {
display: flex;
// 面向pc端设置的最小宽度
min-width: 1024px;
// 适配的最大页面大小
max-width: 2400px;
margin: 0 auto;
padding: 0.125rem 0.125rem 0;
// 直接指定flex将等分所有column
.column {
flex: 3;
}
// 单独为第二列指定占比为5,此时盒内比为3:5:3
.column:nth-child(2) {
flex: 5;
margin: 0 0.125rem 0.1875rem;
overflow: hidden;
}
.panel {
position: relative;
height: 3.875rem;
border: 1px solid rgba(255, 255, 255, 0.2);
background: url("../images/line.png") rgba(255, 255, 255, 0.1);
padding: 0 0.1875rem 0.5rem;
margin-bottom: 0.1875rem;
// 通过伪元素,设置上方边框角
&::before {
position: absolute;
top: 0;
left: 0;
width: 10px;
height: 10px;
border-left: 2px solid dimgray;
border-top: 2px solid dimgray;
content: "";
}
&::after {
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 10px;
border-right: 2px solid dimgray;
border-top: 2px solid dimgray;
content: "";
}
// 定义底部区域,通过伪元素,设置下方边框角
.panel-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
&::before {
position: absolute;
bottom: 0;
left: 0;
width: 10px;
height: 10px;
border-left: 2px solid dimgray;
border-bottom: 2px solid dimgray;
content: "";
}
&::after {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
border-right: 2px solid dimgray;
border-bottom: 2px solid dimgray;
content: "";
}
}
h2 {
height: 0.6rem;
color: #ffffff;
line-height: 0.6rem;
text-align: center;
font-size: 0.25rem;
font-weight: 400;
}
}
.info {
.box {
height: 3rem;
table {
/* table-layout用于设置表格的布局算法:
fixed使列宽的显示基于我们设置的表格宽度与列宽;
默认设置是auto,即基于单元格内容动态计算列宽 */
table-layout: fixed;
/* word-break用于设置单元格内容的换行:
设置为break-all,则允许在单词内换行 */
word-break: break-all;
position: relative;
text-align: center;
margin: auto;
border-radius: 0.2rem;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: transparent;
animation: gradual 10s linear infinite alternate;
td {
height: 0.6rem;
width: 1rem;
color: #5A5A5A;
text-align: center;
font-size: 0.15rem;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.2);
}
&::before {
position: absolute;
top: 0;
left: 0;
content: "";
width: 10px;
height: 10px;
border-left: 4px solid dimgray;
border-top: 4px solid dimgray;
}
&::after {
position: absolute;
bottom: 0;
right: 0;
content: "";
width: 10px;
height: 10px;
border-right: 4px solid dimgray;
border-bottom: 4px solid dimgray;
}
}
}
@keyframes gradual {
//0% {
// background-image: linear-gradient(to right, rgba(155, 48, 255, 0.2), rgba(55, 162, 255, 0.2), rgba(0, 255, 255, 0.2));
//}
//50% {
// background-image: linear-gradient(to right, rgba(0, 255, 255, 0.2), rgba(155, 48, 255, 0.2), rgba(55, 162, 255, 0.2));
//}
//100% {
// background-image: linear-gradient(to right, rgba(55, 162, 255, 0.2), rgba(0, 255, 255, 0.2), rgba(155, 48, 255, 0.2));
//}
from {
background-color: rgba(250, 250, 250, 0.2);
}
to {
background-color: rgba(150, 150, 150, 0.2);
}
}
}
.bar {
height: 7.9375rem;
.chart {
height: 6.1875rem;
}
}
.window {
position: relative;
height: 12rem;
border: 1px solid rgba(255, 255, 255, 0.2);
background: url("../images/line.png") rgba(255, 255, 255, 0.1);
padding: 0 0.1875rem 0.5rem;
margin-bottom: 0.1875rem;
// 通过伪元素,设置上方边框角
&::before {
position: absolute;
top: 0;
left: 0;
width: 10px;
height: 10px;
border-left: 2px solid dimgray;
border-top: 2px solid dimgray;
content: "";
}
&::after {
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 10px;
border-right: 2px solid dimgray;
border-top: 2px solid dimgray;
content: "";
}
// 定义底部区域,通过伪元素,设置下方边框角
.panel-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
&::before {
position: absolute;
bottom: 0;
left: 0;
width: 10px;
height: 10px;
border-left: 2px solid dimgray;
border-bottom: 2px solid dimgray;
content: "";
}
&::after {
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
border-right: 2px solid dimgray;
border-bottom: 2px solid dimgray;
content: "";
}
}
h2 {
height: 0.6rem;
color: #ffffff;
line-height: 0.6rem;
text-align: center;
font-size: 0.25rem;
font-weight: 400;
}
.flight {
height: 11.125rem;
overflow: hidden;
table {
table-layout: fixed;
word-break: break-all;
position: relative;
text-align: center;
margin: auto;
border-radius: 0.2rem;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: transparent;
animation: gradual 10s linear infinite alternate;
td {
height: 0.6rem;
width: 1rem;
color: #5A5A5A;
text-align: center;
font-size: 0.15rem;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.2);
}
&::before {
position: absolute;
top: 0;
left: 0;
content: "";
width: 10px;
height: 10px;
border-left: 4px solid dimgray;
border-top: 4px solid dimgray;
}
&::after {
position: absolute;
bottom: 0;
right: 0;
content: "";
width: 10px;
height: 10px;
border-right: 4px solid dimgray;
border-bottom: 4px solid dimgray;
}
}
}
}
}
// 数字部分
.ar {
background: rgba(255, 255, 255, 0.1);
padding: 0.1875rem;
.ar-hd {
position: relative;
border: 1px solid rgba(255, 255, 255, 0.2);
&::before {
position: absolute;
top: 0;
left: 0;
content: "";
width: 30px;
height: 10px;
border-top: 2px solid dimgray;
border-left: 2px solid dimgray;
}
&::after {
position: absolute;
bottom: 0;
right: 0;
content: "";
width: 30px;
height: 10px;
border-right: 2px solid dimgray;
border-bottom: 2px solid dimgray;
}
form {
display: flex;
input {
width: 50%;
position: relative;
flex: 1;
line-height: 1rem;
font-size: 0.875rem;
color: #5B5B5B;
text-align: center;
font-family: electronicFont, serif;
background-color: transparent;
border: 0;
// 给第一列添加伪元素,画中间分界线
&::after {
content: "";
position: absolute;
top: 25%;
right: 0;
height: 50%;
width: 1px;
background: rgba(255, 255, 255, 0.2);
}
}
#submit {
display: none;
}
button {
display: none;
}
}
}
.ar-bd {
ul {
display: flex;
li {
flex: 1;
text-align: center;
color: #5B5B5B;
font-size: 0.225rem;
height: 0.5rem;
line-height: 0.5rem;
padding-top: 0.125rem;
}
}
}
}
.map {
position: relative;
height: 10.125rem;
.map1 {
width: 6.475rem;
height: 6.475rem;
position: absolute;
background: url("../images/map.png");
background-size: 100% 100%;
/* 设置水平垂直居中
第一步使元素左上角对准父一级的中心位置
第二部平移元素自身长宽50%的距离,使元素中心对准父一级的中心位置*/
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// 透明度
opacity: 0.2;
}
// 后定义的图形默认盖在先定义的图形上面
.map2 {
width: 8.0375rem;
height: 8.0375rem;
position: absolute;
background: url("../images/lbx.png");
background-size: 100% 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// 调用动画:名称、执行周期15s、全程匀速、无限次播放
animation: rotate 15s linear infinite;
opacity: 0.4;
}
.map3 {
width: 7.075rem;
height: 7.075rem;
position: absolute;
background: url("../images/jt.png");
background-size: 100% 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// 区别:执行周期10s、新加了一个反向播放
animation: rotate 10s linear infinite reverse;
opacity: 0.6;
}
.chart {
/* 宽度占满(已在mainbox中设定总宽度并划分);
高度等同父一级区块,也能自动伸缩 */
width: 100%;
height: 10.125rem;
position: absolute;
top: 0;
left: 0;
}
/* 定义一个动画rotate1,@keyframes原理:
从from设定的样式逐渐变到to设定的样式,
也可以用0%和100%代替from和to;
设定完后在元素内通过animation调用动画。*/
@keyframes rotate {
from {
// rotate可以使指定元素旋转一个角度,1deg即1度
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
}
/* @media是针对不同的媒体设备,定义不同的CSS样式
此处是针对screen(电脑屏幕、平板、移动设备)做出的样式修改
用于约束页面随屏幕缩放的尺寸,在1024-2400px间不受影响 */
@media screen and (max-width: 1024px) {
html {
/* 当screen设备的显示页面宽度为小于1024px时,将html的font-size设为42px
也就是1rem被固定为了42px,页面不会再随着浏览器窗口缩小而缩放 */
font-size: 42px !important;
}
}
@media screen and (min-width: 2400px) {
html {
/* 当screen设备的显示页面宽度为大于2400px时,将html的font-size设为100px
也就是1rem被固定为了100px,页面不会再随着浏览器窗口放大而增大 */
font-size: 100px !important;
}
}
- 网站中导入的flexible.js、echarts.min.js、china.js是外部导入包,需要在官网自行下载;index.js是我们自己编写的js文件,网页中的点击事件已经echarts图表的设计都在该文件中进行:
// 为了解决图形过多而可能出现option命名的重复,我们使用立即执行函数“(<函数>)()”来进行图表的定义;
// 立即执行函数也叫自调用函数,外层的括号将函数包装为一个“函数表达式”;
// 函数表达式是一个对象,在对象后添加“()”就表示执行该对象,我们定义一般函数时也是生成了函数名对应的对象;
// 由于使用了立即执行函数,所以该js文件必须在页面渲染完成后再调用,不然无法得到我们查找的元素
// 图表模块panel bar
var bar = function (data) {
// 1实例化对象
var myChart = echarts.init(document.querySelector('.bar .chart'));
// 2. 指定配置项和数据
var option;
var yAxisData = [];
var data1 = [];
var data2 = [];
var data3 = [];
if (data) {
yAxisData = data['time_list'];
data1 = data['data_list2'];
data2 = data['data_list1'];
data3 = data['data_list3'];
} else {
for (var i = 46; i < 70; i++) {
yAxisData = ["19", "20", "21", "22", "23", "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"];
data1 = [80, 84, 88, 91, 93, 94, 94, 95, 96, 96, 96, 96, 97, 98, 99, 99, 96, 92, 82, 78, 73, 73, 79, 87, 89];
data2 = [8, 7, 5, 5, 4, 3, 2, 2, 2, 1, 1, 1, 1, 2, 3, 4, 6, 6, 8, 9, 10, 9, 8, 7, 7];
data3 = [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1];
}
}
option = {
tooltip: {
trigger: 'axis',
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
left: '10%',
top: '15%',
right: '10%',
bottom: '0',
//containLabel: true
},
legend: {
data: ['相对湿度-%', '温度-°C', '风力等级-级']
},
xAxis: {
type: 'value',
position: 'top',
splitLine: {
lineStyle: {
type: 'dashed'
}
}
},
yAxis: {
type: 'category',
data: yAxisData,
axisLine: {show: false},
axisLabel: {show: true},
axisTick: {
show: false,
},
splitLine: {show: false},
},
series: [
{
name: '相对湿度-%',
type: 'bar',
data: data1,
barWidth: '70%',
itemStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{
offset: 0,
color: 'rgba(55, 162, 255)'
},
{
offset: 1,
color: 'rgba(116, 21, 219)'
}
])
},
emphasis: {
focus: 'series'
},
animationDelay: function (idx) {
return idx * 10;
}
},
{
name: '温度-°C',
type: 'line',
data: data2,
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
itemStyle:{
opacity: 0.8,
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{
offset: 0,
color: 'rgba(255, 191, 0)'
},
{
offset: 1,
color: 'rgba(224, 62, 76)'
}
])
},
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{
offset: 0,
color: 'rgba(255, 191, 0)'
},
{
offset: 1,
color: 'rgba(224, 62, 76)'
}
])
},
emphasis: {
focus: 'series'
},
animationDelay: function (idx) {
return idx * 10 + 100;
}
},
{
name: '风力等级-级',
type: 'line',
data: data3,
stack: 'Total',
smooth: true,
lineStyle: {
width: 0
},
showSymbol: false,
itemStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}
])
},
areaStyle: {
opacity: 0.8,
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{
offset: 0,
color: 'rgba(128, 255, 165)'
},
{
offset: 1,
color: 'rgba(1, 191, 236)'
}
])
},
emphasis: {
focus: 'series'
},
animationDelay: function (idx) {
return idx * 10 + 100;
}
}
],
animationEasing: 'elasticOut',
animationDelayUpdate: function (idx) {
return idx * 5;
}
};
// 3. 把配置项给实例对象
myChart.setOption(option);
// 4. 让图表跟随屏幕自动的去适应
window.addEventListener("resize", function () {
// 添加一个事件监听:当windows执行resize事件(改变窗体大小)时,执行立即该函数:
// 函数内容是调用mycharts的resize方法来同步修改图表大小
myChart.resize();
});
};
// 中国地图模块map chart
var china_map = function () {
// 1. 实例化对象
var myChart = echarts.init(document.querySelector(".map .chart"));
// 记录了哪些城市有机场,以及这些城市在中国地图上的坐标位置
var geoCoordMap = {
上海: [121.4648, 31.2891],
东莞: [113.8953, 22.901],
东营: [118.7073, 37.5513],
中山: [113.4229, 22.478],
临汾: [111.4783, 36.1615],
临沂: [118.3118, 35.2936],
丹东: [124.541, 40.4242],
丽水: [119.5642, 28.1854],
乌鲁木齐: [87.9236, 43.5883],
佛山: [112.8955, 23.1097],
保定: [115.0488, 39.0948],
兰州: [103.5901, 36.3043],
包头: [110.3467, 41.4899],
北京: [116.4551, 40.2539],
北海: [109.314, 21.6211],
南京: [118.8062, 31.9208],
南宁: [108.479, 23.1152],
南昌: [116.0046, 28.6633],
南通: [121.1023, 32.1625],
厦门: [118.1689, 24.6478],
台州: [121.1353, 28.6688],
合肥: [117.29, 32.0581],
呼和浩特: [111.4124, 40.4901],
咸阳: [108.4131, 34.8706],
哈尔滨: [127.9688, 45.368],
唐山: [118.4766, 39.6826],
嘉兴: [120.9155, 30.6354],
大同: [113.7854, 39.8035],
大连: [122.2229, 39.4409],
天津: [117.4219, 39.4189],
太原: [112.3352, 37.9413],
威海: [121.9482, 37.1393],
宁波: [121.5967, 29.6466],
宝鸡: [107.1826, 34.3433],
宿迁: [118.5535, 33.7775],
常州: [119.4543, 31.5582],
广州: [113.5107, 23.2196],
廊坊: [116.521, 39.0509],
延安: [109.1052, 36.4252],
张家口: [115.1477, 40.8527],
徐州: [117.5208, 34.3268],
德州: [116.6858, 37.2107],
惠州: [114.6204, 23.1647],
成都: [103.9526, 30.7617],
扬州: [119.4653, 32.8162],
承德: [117.5757, 41.4075],
拉萨: [91.1865, 30.1465],
无锡: [120.3442, 31.5527],
日照: [119.2786, 35.5023],
昆明: [102.9199, 25.4663],
杭州: [119.5313, 29.8773],
枣庄: [117.323, 34.8926],
柳州: [109.3799, 24.9774],
株洲: [113.5327, 27.0319],
武汉: [114.3896, 30.6628],
汕头: [117.1692, 23.3405],
江门: [112.6318, 22.1484],
沈阳: [123.1238, 42.1216],
沧州: [116.8286, 38.2104],
河源: [114.917, 23.9722],
泉州: [118.3228, 25.1147],
泰安: [117.0264, 36.0516],
泰州: [120.0586, 32.5525],
济南: [117.1582, 36.8701],
济宁: [116.8286, 35.3375],
海口: [110.3893, 19.8516],
淄博: [118.0371, 36.6064],
淮安: [118.927, 33.4039],
深圳: [114.5435, 22.5439],
清远: [112.9175, 24.3292],
温州: [120.498, 27.8119],
渭南: [109.7864, 35.0299],
湖州: [119.8608, 30.7782],
湘潭: [112.5439, 27.7075],
滨州: [117.8174, 37.4963],
潍坊: [119.0918, 36.524],
烟台: [120.7397, 37.5128],
玉溪: [101.9312, 23.8898],
珠海: [113.7305, 22.1155],
盐城: [120.2234, 33.5577],
盘锦: [121.9482, 41.0449],
石家庄: [114.4995, 38.1006],
福州: [119.4543, 25.9222],
秦皇岛: [119.2126, 40.0232],
绍兴: [120.564, 29.7565],
聊城: [115.9167, 36.4032],
肇庆: [112.1265, 23.5822],
舟山: [122.2559, 30.2234],
苏州: [120.6519, 31.3989],
莱芜: [117.6526, 36.2714],
菏泽: [115.6201, 35.2057],
营口: [122.4316, 40.4297],
葫芦岛: [120.1575, 40.578],
衡水: [115.8838, 37.7161],
衢州: [118.6853, 28.8666],
西宁: [101.4038, 36.8207],
西安: [109.1162, 34.2004],
贵阳: [106.6992, 26.7682],
连云港: [119.1248, 34.552],
邢台: [114.8071, 37.2821],
邯郸: [114.4775, 36.535],
郑州: [113.4668, 34.6234],
鄂尔多斯: [108.9734, 39.2487],
重庆: [107.7539, 30.1904],
金华: [120.0037, 29.1028],
铜川: [109.0393, 35.1947],
银川: [106.3586, 38.1775],
镇江: [119.4763, 31.9702],
长春: [125.8154, 44.2584],
长沙: [113.0823, 28.2568],
长治: [112.8625, 36.4746],
阳泉: [113.4778, 38.0951],
青岛: [120.4651, 36.3373],
韶关: [113.7964, 24.7028]
};
/* 定义不同的航线(双方需要有机场):[始发地,目的地,设置目的地value值]
只有目的地的机场会高亮圆点,value值作为圆点半径,会在目的地和航线上显示 */
// 西安航线数据列表
var XAData = [
[{name: "西安"}, {name: "拉萨", value: 100}],
[{name: "西安"}, {name: "上海", value: 100}],
[{name: "西安"}, {name: "广州", value: 100}],
[{name: "西安"}, {name: "西宁", value: 100}],
[{name: "西安"}, {name: "银川", value: 100}]
];
// 西宁航线数据列表
var XNData = [
[{name: "西宁"}, {name: "北京", value: 100}],
[{name: "西宁"}, {name: "上海", value: 100}],
[{name: "西宁"}, {name: "广州", value: 100}],
[{name: "西宁"}, {name: "西安", value: 100}],
[{name: "西宁"}, {name: "银川", value: 100}]
];
// 银川航线数据列表
var YCData = [
[{name: "银川"}, {name: "上海", value: 100}],
[{name: "银川"}, {name: "西安", value: 100}],
[{name: "银川"}, {name: "西宁", value: 100}],
[{name: "哈尔滨"}, {name: "北京", value: 100}],
[{name: "北京"}, {name: "南昌", value: 100}]
];
// 拉萨数据列表
var WLData = [
[{name: "拉萨"}, {name: "哈尔滨", value: 100}],
[{name: "拉萨"}, {name: "昆明", value: 100}],
[{name: "拉萨"}, {name: "乌鲁木齐", value: 100}],
[{name: "拉萨"}, {name: "西安", value: 100}]
]
// 设置特效图形的样式:矢量路径绘制图形
var planePath =
"path://M1705.06,1318.313v-89.254l-319.9-221.799l0.073-208.063c0.521-84.662-26.629-121.796-63.961-121.491c-37.332-0.305-64.482,36.829-63.961,121.491l0.073,208.063l-319.9,221.799v89.254l330.343-157.288l12.238,241.308l-134.449,92.931l0.531,42.034l175.125-42.917l175.125,42.917l0.531-42.034l-134.449-92.931l12.238-241.308L1705.06,1318.313z";
// var planePath = 'arrow';
// 重写航线数据格式
var convertData = function (data) {
var res = [];
// data对应的是航线数据(如:XNData),是一个的列表
for (var i = 0; i < data.length; i++) {
// 依次获取其中每一条航线信息
var dataItem = data[i];
// 去机场信息字典中查找始发机场坐标与目标机场坐标
var fromCoord = geoCoordMap[dataItem[0].name];
var toCoord = geoCoordMap[dataItem[1].name];
if (fromCoord && toCoord) {
// 如果两个机场都存在,就把该条信息重写为如下格式,然后插入到新列表尾部
res.push({
fromName: dataItem[0].name,
toName: dataItem[1].name,
// 坐标列表:起点坐标与终点坐标
coords: [fromCoord, toCoord],
value: dataItem[1].value
});
}
}
// 返回重写格式后的航线列表
return res;
};
// 定义不同航线的颜色
var color = ["#a6c84c", "#ffa022", "#46bee9", "#9F79EE"];
// 预定义一个系列数组,存放航线数据列表
var series = [];
// 用一个数组封装各组航线数据,再使用forEach()方法调用数组的每个元素,并将元素传递给回调函数
[
["西安", XAData],
["西宁", XNData],
["银川", YCData],
["拉萨", WLData],
].forEach(
// 回调函数:
function (item, i) {
// push()方法可向数组的末尾添加一个或多个元素,并返回新的长度,这里在生成option中series
series.push(
// 箭头——红尾巴
{
// 匹配图例,系名相同的图形(同一系列)可以被一个legend控制
name: item[0] + " Top4",
// 用于带有起点和终点信息的线数据的绘制,主要用于地图上的航线,路线的可视化
type: "lines",
// 用于设置分层,大的zlevel的图形在小的zlevel图形上方
zlevel: 1,
// 线特效的配置
effect: {
show: true,
// 特效动画单次时长(周期)
period: 6,
// 特效轨迹长,数值越大轨迹越长,取值范围0-1
trailLength: 0.7,
// 特效颜色
color: "red",
// 特效的大小
symbolSize: 3
},
// 设置线条样式
lineStyle: {
normal: {
color: color[i],
width: 0,
// 线条边的曲度,取值范围0-1
curveness: 0.2
}
},
// 将航线数据列表作为参数传给回调函数convertData,重写格式
data: convertData(item[1])
},
// 箭头——飞机图标
{
name: item[0] + " Top4",
// 用于带有起点和终点信息的线数据的绘制,主要用于地图上的航线,路线的可视化
type: "lines",
// 盖在尾巴式箭头上方
zlevel: 2,
symbol: ["none", "arrow"],
// 线两端的标记大小
symbolSize: 10,
effect: {
show: true,
period: 6,
trailLength: 0,
// 特效图形样式:标准样式、图片或矢量图形,这里用上方定义的矢量图形
symbol: planePath,
symbolSize: 15
},
lineStyle: {
normal: {
color: color[i],
width: 1,
// 不透明度
opacity: 0.6,
curveness: 0.2
}
},
data: convertData(item[1])
},
// 目标地的高亮圆点
{
// 系列名
name: item[0] + "Top4",
// 带有涟漪特效动画的散点(气泡)图,利用动画特效可以将某些想要突出的数据进行视觉突出。
type: "effectScatter",
// 指定坐标系为地理坐标系geo
coordinateSystem: "geo",
zlevel: 2,
// 涟漪相关配置
rippleEffect: {
// 波纹绘制方式:fill或stroke
brushType: "stroke"
},
label: {
// 设置标签的一般显示样式(似乎属于通用设置?)
normal: {
show: true,
position: "right",
// 标签内容格式器,{b}是将数据名(data里的name属性对应值)设为label
formatter: "{b}"
}
},
// 修改圆点尺寸,参数为数据值(data里的value属性对应值)
symbolSize: function (val) {
// 用我们航线数据里的values值除8作为圆点大小
return val[2] / 8;
},
// 设置圆点样式
itemStyle: {
// 设置一般样式
normal: {
// 指定颜色列表中对应序号的颜色
color: color[i]
},
// 高亮时(选中)图形与标签样式
emphasis: {
scale: true,
// 设置区域颜色,似乎已经无效?
areaColor: "#2B91B7"
}
},
/* map返回数组中,满足回调函数内指定条件的元素,此处功能类似于上面的格式重写,是为了增加数据项;
item[1]是航线数据列表(如XAData),dataItem是其中一条航线数据 */
data: item[1].map(function (dataItem) {
return {
name: dataItem[1].name,
/* concat()方法用于连接两个或多个字符串、数组,
此处将目标地的坐标数组与我们数据中设置的value值连接 */
value: geoCoordMap[dataItem[1].name].concat([dataItem[1].value])
};
})
}
);
});
// 2.指定配置
var option = {
tooltip: {
// 触发提示框的(方式)类型:'item'为数据项图形触发,如地图块,航线,目的地
trigger: "item",
// 提示框内容格式器,将回调函数返回值设为提示框的显示内容,params为series列表
formatter: function (params, ticket, callback) {
// 分别为目标地(effectScatter)、航线(lines)、地图区块指定不同的提示框内容
if (params.seriesType === "effectScatter") {
// 返回该series内data部分对应数据的拼接结果
return "线路:" + params.data.name + "<br/>" + params.data.value[2];
} else if (params.seriesType === "lines") {
return (
// 返回该series内data部分对应数据的拼接结果
params.data.fromName +
"-->" +
params.data.toName +
"<br/>" +
params.data.value
);
} else {
// 返回地图区块名称(即系列名)
return params.name;
}
}
},
legend: {
// 图例列表的布局方向:横向'horizontal'和纵向"vertical"
orient: "vertical",
/* 图例组件离容器上侧的距离,此处将图例置于图表容器底部
如果top的值为'top'、'middle'、'bottom',组件会根据相应的位置自动对齐 */
top: "bottom",
/* 图例组件离容器左侧的距离,此处将图例置于图表容器右侧
如果left的值为'left'、'center'、'right',组件会根据相应的位置自动对齐 */
left: "right",
// 每组航线数据都需要新增一个标签
data: ["西安 Top4", "西宁 Top4", "银川 Top4", "拉萨 Top4"],
textStyle: {
color: "#fff"
},
/* 图例选择的模式,控制是否可以通过点击图例改变系列的显示状态;
默认开启图例选择,可以设成false关闭;
除此之外也可以设成'single'或者'multiple'使用单选或者多选模式。*/
selectedMode: "multiple"
},
// 地理坐标系组件,用于绘制地理坐标图
geo: {
// 引入已经注册的地图js文件:china.js
map: "china",
label: {
emphasis: {
show: true,
// color: "rgba(146, 111, 52, 0.8)",
color: "#fff"
}
},
// 把中国地图放大了1.2倍
zoom: 1.2,
roam: true,
itemStyle: {
normal: {
opacity: 0.6,
// 地图省份的背景颜色
areaColor: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: '#FAFAFA'},
{offset: 0.5, color: '#E6E6E6'},
{offset: 1, color: '#D2D2D2'}
]),
borderColor: "rgba(0,0,0,0.5)",
borderWidth: 1
},
// 高亮设置
emphasis: {
// 选中时的区域颜色
areaColor: "rgba(223, 190, 106, 0.8)"
}
}
},
// 将上面处理后的series作为option的series
series: series
};
// 3. 把配置给实例对象
myChart.setOption(option);
// 3.5 设置点击事件
// 点击前解绑,防止点击事件触发多次
myChart.off('click',);
myChart.on('click', echartsMapClick);
// 4. 让图表跟随屏幕自动的去适应
window.addEventListener("resize", function () {
myChart.resize();
});
};
// 定义地图点击事件函数
var choice = "";
var echartsMapClick = function (params) {
if (!params.name) {
} else {
choice = params.name;
Refresh_chart(params.name);
}
// $("#from_city").on("click",input_click($(this),params.name));
// $("#to_city").on("click",input_click($(this),params.name));
};
// 刷新图表与表格
var Refresh_chart = function (choice) {
var data = {city: choice};
var str = JSON.stringify(data);
$.ajax(
{
url: '/chart',
method: 'post',
data: str,
dataType: 'json',
contentType: 'application/json; charset=UTF-8',
success: function (result) {
// console.log(result['week_data']);
// console.log(result['day_data']);
bar(result['day_data']);
set_table(result['week_data']);
},
error: function (error) {
console.log(error);
}
}
)
};
// 设置天气表格数据
var set_table = function (data){
var td_list = document.getElementsByTagName("td");
console.log(data);
for(var i=0;i<data.length;i++){
td_list[i].innerHTML=data[i]['date'].slice(0,3)+'<br>'+data[i]['date'].slice(3);
td_list[i+7].innerHTML=data[i]['weather'];
td_list[i+14].innerHTML=data[i]['max_tem']+'/'+data[i]['min_tem']
td_list[i+21].innerHTML=data[i]['wind']
}
}
// 聚焦函数
var input_click = function (self) {
self.value = choice;
if (self.name === "to_city") {
document.getElementsByTagName("button")[0].click();
}
};
// ajax请求获取外部页面
var postData = function () {
var from_city = $('#from_city').val();
var to_city = $('#to_city').val();
var data = {
from_city: from_city,
to_city: to_city
};
// 对象转json字符串
var str = JSON.stringify(data)
$.ajax(
{
url: '/flight',
method: 'post',
data: str,
dataType: 'json',
contentType: 'application/json; charset=UTF-8',
success: function (result) {
console.log(result);
insert_table(result)
},
error: function (error) {
console.log(error);
}
}
)
};
var insert_table = function(data) {
var table=document.getElementById("flight-data");
if (table.rows.length>1){
for(var n=table.rows.length;n>1;n--){
table.deleteRow(n-1);
}
}
for(var i=0;i<data.length;i++) {
var row=table.insertRow(table.rows.length);
var cell1=row.insertCell(0);
var cell2=row.insertCell(1);
var cell3=row.insertCell(2);
var cell4=row.insertCell(3);
var cell5=row.insertCell(4);
var cell6=row.insertCell(5);
cell1.innerHTML=data[i]['aircraft']+'<br>'+data[i]['company']
cell2.innerHTML=data[i]['takeoff_airport']+'<br>'+data[i]['takeoff_time'];
cell3.innerHTML=data[i]['duration'];
cell4.innerHTML=data[i]['landing_airport']+'<br>'+data[i]['landing_time'];
cell5.innerHTML=data[i]['breakfast'];
cell6.innerHTML=data[i]['price'];
}
};
// 睡眠函数
function sleep(numberMillis) {
var now = new Date();
var exitTime = now.getTime() + numberMillis;
while (true) {
now = new Date();
if (now.getTime() > exitTime)
return;
}
}
bar()
china_map()
二、爬虫部分:
- 爬虫部分由一个定时爬虫和一个即时爬虫组成;
- 定时爬虫采用xpath方法实现,由管理员设定一个定时器后便可定期爬取数据保存在数据库中:
import re
import pymysql
import requests
from lxml import etree
# import io
# import sys
# sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='utf8') #改变标准输出的默认编码
class mysql:
def __init__(self):
self.connect = pymysql.connect(
host='localhost',
port=3306,
user='root',
password='x20000317',
database='web_work',
charset='utf8mb4'
)
# 以字典的形式获取查询结果
self.cursor = self.connect.cursor(pymysql.cursors.DictCursor)
def save(self,*args):
pass
def read(self,*args):
pass
def exit(self):
self.cursor.close()
self.connect.close()
class weather(mysql):
def __init__(self):
super().__init__()
self.headers = {
'User-Agent':'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Mobile Safari/537.36 Edg/95.0.1020.53'
}
# 定义一个列表存储所有详情页url
self.url_list = []
# 八个地区名称列表,多页爬取
self.area_list = ['hb','db','hd','hz','hn','xb','xn','gat']
# 存储一周内综合天气数据
self.weekdata = []
# 存储一天内各小时数据
self.hourdata = []
# 创建数据库
sql_1 = 'create table week(id int primary key auto_increment,city varchar(20),date char(20),weather varchar(20),max_tem char(10),min_tem char(10),wind char(10))'
sql_2 = 'create table hour(id int primary key auto_increment,city char(15),time char(4),temperature int,humidity int,wind_direction varchar(10),wind_power int)'
try:
self.cursor.execute(sql_1)
except:
pass
try:
self.cursor.execute(sql_2)
except:
pass
# 获取详情页网址
def get_url(self):
for area_name in self.area_list:
# 中国天气预报官网url
main_url = 'http://www.weather.com.cn/textFC/{}.shtml'.format(area_name)
# 获取页面html内容并用etree解析
page_text = requests.get(url=main_url, headers=self.headers).text
tree = etree.HTML(page_text)
td_list = tree.xpath("//div[@class='hanml']/div[1]//td[@class='last']")
for td in td_list:
new_url = td.xpath("./a/@href")
if new_url and (new_url[0] not in self.url_list):
self.url_list.append(new_url[0])
# 正则表达式获取数据
def v_func(self, string):
value_list = []
time_pattern = '"od21":"(.*?)",'
# 字符串列表
time_list = re.findall(time_pattern, string, re.M)
temperature_pattern = '"od22":"(.*?)",'
# 字符串列表
temperature_list = re.findall(temperature_pattern, string, re.M)
humidity_pattern = '"od27":"(.*?)",'
# 字符串列表
humidity_list = re.findall(humidity_pattern, string, re.M)
wind_pattern = '"od24":"(.*?)","od25":"(.*?)",'
# 字符串元组列表
wind_list = re.findall(wind_pattern, string, re.M)
for i in range(len(time_list)):
value_dict = {
'time': time_list[i],
'temperature': temperature_list[i],
'humidity': humidity_list[i],
'wind_direction': wind_list[i][0],
'wind_power': wind_list[i][1]
}
value_list.insert(0, value_dict)
return value_list
# 获取详情页数据
def get_data(self):
self.get_url()
for url in self.url_list:
page = requests.get(url=url, headers=self.headers)
page.encoding="utf-8"
tree = etree.HTML(page.text)
# 城市名信息处理
city_list1 = tree.xpath("//div[@class='crumbs fl']//text()")
city_list2 = [item.strip('全国').strip('>') for item in city_list1]
city_list3 = [item.strip() for item in city_list2 if item.strip() != '']
self.city_info = '-'.join(city_list3)
# 城市一周天气数据处理
li_list = tree.xpath("//ul[@class='t clearfix']/li")
for li in li_list:
text_list1 = li.xpath(".//text()")
# 去除/
text_list2 = [item.strip('/') for item in text_list1]
# 去除\n
text_list3 = [item.strip() for item in text_list2 if item.strip()!='']
text_list3.insert(0,self.city_info)
if len(text_list3)!=6:
text_list3.insert(3,'None')
self.weekdata.append(text_list3)
# 城市小时数据处理
string = tree.xpath("//div[@class='left-div']/script//text()")[0]
data_dict = {
'name':re.findall('"od1":"(.*?)",',string)[0],
'value':self.v_func(string)
}
self.hourdata.append(data_dict)
self.save(self.weekdata,1)
self.save(self.hourdata,2)
# 重写保存方法
def save(self,data,num):
if num == 1:
sql = 'truncate table week'
self.cursor.execute(sql)
self.connect.commit()
sql = 'insert into week(city,date,weather,max_tem,min_tem,wind) values(%s,%s,%s,%s,%s,%s)'
for item in data:
self.cursor.execute(sql,(item[0],item[1],item[2],item[3],item[4],item[5]))
self.connect.commit()
elif num == 2:
sql = 'truncate table hour'
self.cursor.execute(sql)
self.connect.commit()
sql = 'insert into hour(city,time,wind_direction,temperature,humidity,wind_power) values(%s,%s,%s,%s,%s,%s)'
for data_item in data:
for item in data_item['value']:
try:
self.cursor.execute(sql,(data_item['name'],item['time'],item['wind_direction'],item['temperature'],item['humidity'],item['wind_power']))
except Exception as e:
print(e)
print(data_item['name'],item)
self.connect.commit()
# 重写读取方法
def read(self,num,data):
if num == 1:
sql = 'select * from week where city LIKE %s'
self.cursor.execute(sql,('%'+data+'%'))
elif num == 2:
sql = 'select * from hour where city=%s'
self.cursor.execute(sql,(data))
# 当数据库内没有数据时返回的是元组,需要转换为列表
info_list = list(self.cursor.fetchall())
# print(info_list)
return info_list
- 即时爬虫采用selenium方法实现,在获取到前端发来的post请求后,便依据发送来的查询内容爬取相关信息:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
#实现规避检测
from selenium.webdriver import ChromeOptions
class flight:
def __init__(self,data1,data2,data3):
# 实现无可视化界面的操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
# 实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
# 实例化driver
self.bro = webdriver.Chrome(executable_path='driver/chromedriver.exe',chrome_options=chrome_options,options=option)
# 城市名到机场缩写的转换
self.city = {
'阿勒泰': 'AAT',
'兴义': 'ACX',
'百色': 'AEB',
'阿克苏': 'AKU',
'鞍山': 'AOG',
'安庆': 'AQG',
'安顺': 'AVA',
'阿拉善左旗': 'AXF',
'包头': 'BAV',
'毕节': 'BFJ',
'北海': 'BHY',
'北京': 'BJS',
'秦皇岛': 'BPE',
'博乐': 'BPL',
'昌都': 'BPX',
'保山': 'BSD',
'广州': 'CAN',
'承德': 'CDE',
'常德': 'CGD',
'郑州': 'CGO',
'长春': 'CGQ',
'朝阳': 'CHG',
'赤峰': 'CIF',
'长治': 'CIH',
'重庆': 'CKG',
'长沙': 'CSX',
'成都': 'CTU',
'沧源': 'CWJ',
'嘉义': 'CYI',
'常州': 'CZX',
'大同': 'DAT',
'达县': 'DAX',
'白城': 'DBC',
'稻城': 'DCY',
'丹东': 'DDG',
'香格里拉迪庆)': 'DIG',
'大连': 'DLC',
'大理': 'DLU',
'敦煌': 'DNH',
'东营': 'DOY',
'大庆': 'DQA',
'鄂尔多斯': 'DSN',
'张家界': 'DYG',
'额济纳旗': 'EJN',
'恩施': 'ENH',
'延安': 'ENY',
'二连浩特': 'ERL',
'福州': 'FOC',
'阜阳': 'FUG',
'佛山': 'FUO',
'抚远': 'FYJ',
'格尔木': 'GOQ',
'广元': 'GYS',
'固原': 'GYU',
'海口': 'HAK',
'邯郸': 'HDG',
'黑河': 'HEK',
'呼和浩特': 'HET',
'合肥': 'HFE',
'杭州': 'HGH',
'淮安': 'HIA',
'怀化': 'HJJ',
'香港': 'HKG',
'海拉尔': 'HLD',
'乌兰浩特': 'HLH',
'哈密': 'HMI',
'神农架': 'HPG',
'哈尔滨': 'HRB',
'舟山': 'HSN',
'和田': 'HTN',
'惠州': 'HUZ',
'台州': 'HYN',
'汉中': 'HZG',
'黎平': 'HZH',
'银川': 'INC',
'且末': 'IQM',
'庆阳': 'IQN',
'景德镇': 'JDZ',
'加格达奇': 'JGD',
'嘉峪关': 'JGN',
'井冈山': 'JGS',
'西双版纳': 'JHG',
'金昌': 'JIC',
'黔江': 'JIQ',
'九江': 'JIU',
'晋江': 'JJN',
'澜沧': 'JMJ',
'佳木斯': 'JMU',
'济宁': 'JNG',
'锦州': 'JNZ',
'建三江': 'JSJ',
'池州': 'JUH',
'衢州': 'JUZ',
'鸡西': 'JXA',
'九寨沟': 'JZH',
'库车': 'KCA',
'康定': 'KGT',
'喀什': 'KHG',
'南昌': 'KHN',
'凯里': 'KJH',
'昆明': 'KMG',
'金门': 'KNH',
'赣州': 'KOW',
'库尔勒': 'KRL',
'克拉玛依': 'KRY',
'贵阳': 'KWE',
'桂林': 'KWL',
'龙岩': 'LCX',
'伊春': 'LDS',
'临汾': 'LFQ',
'兰州': 'LHW',
'丽江': 'LJG',
'荔波': 'LLB',
'永州': 'LLF',
'吕梁': 'LLV',
'临沧': 'LNJ',
'六盘水': 'LPF',
'芒市': 'LUM',
'拉萨': 'LXA',
'洛阳': 'LYA',
'连云港': 'LYG',
'临沂': 'LYI',
'柳州': 'LZH',
'泸州': 'LZO',
'林芝': 'LZY',
'牡丹江': 'MDG',
'马祖': 'MFK',
'澳门': 'MFM',
'绵阳': 'MIG',
'梅州': 'MXZ',
'南充': 'NAO',
'白山': 'NBS',
'齐齐哈尔': 'NDG',
'宁波': 'NGB',
'阿里': 'NGQ',
'南京': 'NKG',
'宁蒗': 'NLH',
'南宁': 'NNG',
'南阳': 'NNY',
'南通': 'NTG',
'满洲里': 'NZH',
'漠河': 'OHE',
'攀枝花': 'PZI',
'阿拉善右旗': 'RHT',
'日照': 'RIZ',
'日喀则': 'RKZ',
'巴彦淖尔': 'RLK',
'上海': 'SHA',
'沈阳': 'SHE',
'西安': 'SIA',
'石家庄': 'SJW',
'揭阳': 'SWA',
'普洱': 'SYM',
'三亚': 'SYX',
'深圳': 'SZX',
'青岛': 'TAO',
'塔城': 'TCG',
'腾冲': 'TCZ',
'铜仁': 'TEN',
'通辽': 'TGO',
'天水': 'THQ',
'吐鲁番': 'TLQ',
'济南': 'TNA',
'天津': 'TSN',
'唐山': 'TVS',
'黄山': 'TXN',
'太原': 'TYN',
'乌鲁木齐': 'URC',
'榆林': 'UYN',
'潍坊': 'WEF',
'威海': 'WEH',
'遵义(茅台)': 'WMT',
'文山': 'WNH',
'温州': 'WNZ',
'乌海': 'WUA',
'武汉': 'WUH',
'武夷山': 'WUS',
'无锡': 'WUX',
'梧州': 'WUZ',
'万州': 'WXN',
'襄阳': 'XFN',
'西昌': 'XIC',
'锡林浩特': 'XIL',
'厦门': 'XMN',
'西宁': 'XNN',
'徐州': 'XUZ',
'宜宾': 'YBP',
'运城': 'YCU',
'宜春': 'YIC',
'阿尔山': 'YIE',
'宜昌': 'YIH',
'伊宁': 'YIN',
'义乌': 'YIW',
'延吉': 'YNJ',
'烟台': 'YNT',
'盐城': 'YNZ',
'扬州': 'YTY',
'玉树': 'YUS',
'张掖': 'YZY',
'昭通': 'ZAT',
'湛江': 'ZHA',
'中卫': 'ZHY',
'张家口': 'ZQZ',
'珠海': 'ZUH',
'遵义(新舟)': 'ZYI'
}
self.start_end = [self.city[data1],self.city[data2],data3]
# 获取网站信息
def get_info(self):
web_url = f'https://www.ly.com/flights/itinerary/oneway/{self.start_end[0]}-{self.start_end[1]}?date={self.start_end[2]}'
self.bro.get(web_url)
page_text = self.bro.page_source
self.bro.quit()
tree = etree.HTML(page_text)
flight_list = tree.xpath("//div[@class='flight-item']")
info_list = []
for flight in flight_list:
data = flight.xpath(".//text()")
data = [item.strip() for item in data if item.strip() != '']
info = {
'company':data[0],
'aircraft':data[1],
'takeoff_time':data[2],
'takeoff_airport':data[3],
'duration':data[4],
'landing_time':data[5],
'landing_airport':data[6],
'breakfast':data[7],
'price':data[8]
}
info_list.append(info)
# print(info_list)
return info_list
三、后端部分:
- 后端使用flask生成一个服务器,包含三个路由,主路由负责显示网站主页面,其余两个路由接收前端发来的ajax请求并返回响应数据:
from flask import Flask,render_template,request
from crawler import *
import datetime
import json
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False # 解决flask接口中文数据编码问题
data = [100,100,100,100,100]
@app.route('/',methods=['GET','POST'])
def mainwindow():
return render_template('index.html',flask_data=data)
@app.route('/flight',methods=['POST'])
def flightwindow():
# data = request.get_data()
# decode_data = urllib.parse.unquote(data.decode())
data = request.get_json()
# 获取表单中name为from_city的文本域提交的数据
from_city = data['from_city']
# 获取表单中name为to_city的文本域提交的数据
to_city = data['to_city']
# 获取当前年月日
now_time = datetime.datetime.strftime(datetime.datetime.now(),'%Y-%m-%d')
# 实例化爬虫对象
obj = flight(from_city, to_city,now_time)
# 返回静态文件
return json.dumps(obj.get_info())
@app.route('/chart',methods=['POST'])
def chartwindow():
data = request.get_json()
city = data['city']
obj = weather()
# 获取一周天气变化数据
week_data = obj.read(1,city)
for item in week_data:
item['city'] = city
response1 = week_data[0:7:1]
# 获取24小时气候数据
day_data = obj.read(2,city)
response2 = {
'time_list':[],
'data_list1':[],
'data_list2':[],
'data_list3':[],
}
for day in day_data:
response2['time_list'].append(day['time'])
response2['data_list1'].append(day['temperature'])
response2['data_list2'].append(day['humidity'])
response2['data_list3'].append(day['wind_power'])
# 生成返回对象
response = {
'week_data':response1,
'day_data':response2
}
obj.exit()
# 返回对象的json字符串
return json.dumps(response)
if __name__ == '__main__':
app.run(debug=True)
网页展示
基于flask的web项目:出行规划网站
总结分析
一、遇到的问题:
- 前端发送post请求时会刷新当前界面,导致主页面一直重置,无法显示查询信息;
- 前端发送的数据被后端显示为乱码;
二、解决方法:
- 首先需要使用ajax方法发送数据,不要在前端表单处定义submit来提交,而是将按钮绑定我们自定义的函数,然后在该函数中定义一个ajax异步请求提交;
- 在前端定义了一个对象data后,发送时应该使用
JSON.stringify(data)
来将其转化为json字符串发送,同时后端需要使用request.get_json()
方法将其作为json对象接收,然后便可用类似python字典的方式调用对象中的内容;
三、关于知识点分析:
- ajax请求代码分析:
$.ajax(
{
url: '/flight',
method: 'post',
data: str,
dataType: 'json',
contentType: 'application/json; charset=UTF-8',
success: function (result) {
console.log(result);
insert_table(result)
},
error: function (error) {
console.log(error);
}
}
)
- 其中:
1)url是我们要提交ajax请求的目标url(从当前页面发送,如果只用后缀,表示向当前url的同级或子一级url发送请求);
2)method是发送请求的方式;
3)data是请求携带的表单数据;
4)datatype是我们我们预期接收到的响应数据类型,注意,不是我们发送的数据类型;
5)contenttype才是申明我们发送的数据是text文本字符串还是json字符串,以及其解码方式;
6)最后就是定义成功发送ajax请求并接收到响应数据后做的操作,以及在发送请求或接收响应阶段失败时要做的操作。 - 表格数据插入:
var table=document.getElementById("flight-data");
if (table.rows.length>1){
for(var n=table.rows.length;n>1;n--){
table.deleteRow(n-1);
}
}
for(var i=0;i<data.length;i++) {
var row=table.insertRow(table.rows.length);
var cell1=row.insertCell(0);
var cell2=row.insertCell(1);
var cell3=row.insertCell(2);
var cell4=row.insertCell(3);
var cell5=row.insertCell(4);
var cell6=row.insertCell(5);
cell1.innerHTML=data[i]['aircraft']+'<br>'+data[i]['company']
cell2.innerHTML=data[i]['takeoff_airport']+'<br>'+data[i]['takeoff_time'];
cell3.innerHTML=data[i]['duration'];
cell4.innerHTML=data[i]['landing_airport']+'<br>'+data[i]['landing_time'];
cell5.innerHTML=data[i]['breakfast'];
cell6.innerHTML=data[i]['price'];
}
- 其中:
要先获取到table这一级的元素,然后使用insertRow
方法获取到要插入的行号,最后用insertCell
定位到该行的每一列元素后,用innerHTML
实现数据插入。
四、收获:
- 通过这次项目,复习并实际运用到了flask、数据库、html、css与Javascript相关的知识,加深了对其中一些函数、方法的理解,并且拓展学习了echarts与less的使用,收获颇丰。