mapNews项目

本文最后更新于:2022年9月21日 晚上

0.概述

  1. 项目需求:利用地图展示当天各地的新闻(新闻发生点可视化)
    • 想法:龙老师的当代地图学进展,地图是信息的载体
    • 新闻这种信息(时间t、地点x,y、任务z)完美适合地图
    • 之前对地图的信息理解只停留在地理数据上,没有人文数据
    • 暑期项目没有使用到数据库的知识,可能需要学习
    • 想测试一下数据在数据库中处理和在后端代码中处理的速度对比
  2. 项目框架:
    • 前端框架:Angular (真的只是框架,搭了个界面而已)
    • 后端:Nodejs
    • 数据库服务:Sqlite
  3. 具体需求
    • 具有可视化地图功能,利用leafletJS实现
    • 后端自动爬取中国新闻网内容,并将相关数据存储到数据库News.db
      • 标题、内容、发布时间
      • 网页链接、内容摘要、编码格式、爬取时间
    • (提前)建立Position.db数据库,存储地名以及坐标(经纬度)
    • New.db中新闻内容News.contentPosition.db中地名Position.city模糊匹配得到新闻经纬度
    • 前端利用新闻经纬度进行可视化

1.用到的模块

1
2
3
4
5
6
7
8
9
const myCheerio = require('cheerio');
//cheerio 用在服务器端需要对DOM进行操作的地方
const express = require('express');
//用于前后端通信
const axios = require('axios');
//用于前后端通信

const SqliteDB = require('./sqlite.js').SqliteDB;
//用于操作Sqlite数据库,sqlite.js封装好的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//sqlite.js

var fs = require('fs');
var sqlite3 = require('sqlite3').verbose();
var DB = DB || {};

DB.SqliteDB = function (file) {
filePath = '/Users/lingyi/postGraduate/codeMapNews/server/Database/' + file;
//存储路径
DB.db = new sqlite3.Database(filePath);
DB.exist = fs.existsSync(filePath);
if (!DB.exist) {
console.log("Creating db file!");
fs.openSync(filePath, 'w');
};

};

DB.printErrorInfo = function (err) {
console.log("Error Message:" + err.message + " ErrorNumber:");
};

DB.SqliteDB.prototype.createTable = function (sql) {
DB.db.serialize(function () {
DB.db.run(sql, function (err) {
if (null != err) {
DB.printErrorInfo(err);
return;
}
});
});
};

/// insert newsData Format
//[[title, content, publish_date, url, source_name,source_encoding, crawltime]]
DB.SqliteDB.prototype.insertData = function (sql, objects) {
DB.db.serialize(function () {
var stmt = DB.db.prepare(sql);
for (var i = 0; i < objects.length; ++i) {
stmt.run(objects[i]);
}
stmt.finalize();
});

};

//这里对数据库的插入操作进行了改写,后端代码需要等数据库数据读取完之后在进行操作
//写成Promise的格式
DB.SqliteDB.prototype.queryData = function (sql) {
return new Promise(resolve => {
DB.db.all(sql, function (err, rows) {
if (null != err) {
DB.printErrorInfo(err);
return;
}
resolve(rows)
});
})
};

DB.SqliteDB.prototype.executeSql = function (sql) {
DB.db.run(sql, function (err) {
if (null != err) {
DB.printErrorInfo(err);
}
});
};

DB.SqliteDB.prototype.close = function () {
DB.db.close();
};

/// export SqliteDB.
exports.SqliteDB = DB.SqliteDB;

2. 具体功能的实现

1. 服务的启动
  • 使用到了express模块

1
2
3
4
5
const app = express();
app.listen(8082,()=>{
console.log('Server is running on localhost:8082')
})
//在8082端口启动服务
##### 2.响应前端请求

  • 用到的模块:axios
1
2
3
4
5
6
7
8
9
app.get('/server', (req, res) => {
//设置请求头 允许跨域
res.setHeader('Access-Control-Allow-Origin', '*');
reqNewsData().then(data => {
//向前端发送数据
res.send(data)
})
})

3. 数据库的建立
  • 使用到了sqlite.js(见上文)

1
2
3
4
5
6
7
8
9
var file = 'News.db';
var sqliteDBNews = new SqliteDB(file);
//在本地新建名为News.db的数据库
let createNewsTableSql = "create table if not exists News(title TEXT,content TEXT,publish_date TEXT,url TEXT,source_name TEXT,source_encoding TETX,crawltime TEXT)";
//News.db数据库建表规则
let insertNewsSql = "insert OR IGNORE into News(title,content,publish_date,url,source_name,source_encoding,crawltime) values(?,?,?,?,?,?,?)";
//News.db数据库News表数据插入规则
sqliteDBNews.createTable(createNewsTableSql);
//创建News表

4. 新闻数据的处理
  • 使用到了cheerio模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
//设置头文件 避免爬虫被屏蔽
let headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36'
}

//发送请求,获得根页面所有信息,利用reqCallback()函数对根网页中的新闻页面进行进一步解析
function reqNewsData() {
return new Promise(resolve => {
axios.get(seedURL, {
params: {
url: seedURL,
encoding: null,
headers: headers,
timeout: 10000 //
}
}).then(response => {
reqCallback(response.data).then(data => {
resolve(data)
})
})
})
}

//解析根页面中的网页链接,得到具体新闻的链接,交给newsGet()函数处理具体的
function reqCallback(body) {
return new Promise(resolve => {
try {
// console.log(html);
//用cheerio解析html
var $ = myCheerio.load(body, { decodeEntities: true });
} catch (e) {
console.log('读种子页面并转码出错:' + e)
};
var seedurl_news;
try {
seedurl_news = eval(seedURL_format);
// console.log(seedurl_news);
} catch (e) { console.log('url列表所处的html块识别出错:' + e) };
seedurl_news.each(function (i, e) { //遍历种子页面里所有的a链接
var myURL = "";
try {
//得到具体新闻url
var href = "";
href = $(e).attr("href");
if (typeof (href) == "undefined") { // 有些网页地址undefined
return true;
}
if (href.toLowerCase().indexOf('http://') >= 0 || href.toLowerCase().indexOf('https://') >= 0 && href.includes('shtml'))
myURL = href; //http://开头的或者https://开头
else if (href.startsWith('//'))
myURL = 'http:' + href; //开头的
else
myURL = seedURL.substr(0, seedURL.lastIndexOf('/') + 1) + href; //其他
}
catch (e) {
console.log('识别种子页面中的新闻链接出错:' + e)
}
if (!url_reg.test(myURL)) {
//检验是否符合新闻url的正则表达式
// console.log(url_reg.test(myURL));
return;
}
//利用newsGet()函数解析具体的网页
newsGet(myURL).then(data => {
resolve(data)
});
});
})
}

//设置具体新闻网页的解析规范,要自己到具体的网页Elemens里去查找、获取DOM元素
//利用了jQuery eval函数 计算某个字符串,并执行其中的JS代码 因此将jQuery写为String模板
const seedURL_format = "$('a')";
const keywords_format = " $('meta[name=\"keywords\"]').eq(0).attr(\"content\")";
const title_format = "$('title').text()";
const date_format = "$('#pubtime_baidu').text()";
const engDate_format = "$('.downinfo.dottlne').children('span').eq(0).text()"
const author_format = "$('#author_baidu').text()"
const engAuthor_format = "$('.downinfo.dottlne').children('span').eq(2).text()"
const content_format = "$('.left_zw').children('p').text()";
const engContent_format = "$('.content').children('p').text()";
const desc_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";
const source_format = "$('#source_baidu').text()";
//匹配具体的新闻网页格式
const url_reg = /[a-zA-z]+:\/\/[^\s]*[1-9]\d{5}(?!\d).shtml/;

//解析新闻页面并进行后续处理
async function newsGet(myURL) {
return new Promise(resolve => {
axios.get(myURL, {
params: {
url: myURL,
encoding: null,
headers: headers,
}
}).then(async response => {
try {
//用cheerio解析html
var $ = myCheerio.load(response.data, { decodeEntities: true });
} catch (e) {
console.log('读新闻页面并转码出错:' + e);
};
// console.log("转码读取成功:" + myURL);
//动态执行format字符串,构建json对象准备写入文件或数据库
var fetch = {};
fetch.title = "";
fetch.content = "";
fetch.publish_date = (new Date()).toFormat("YYYY-MM-DD");
//fetch.html = myhtml;
fetch.url = myURL;
fetch.source_name = source_name;
fetch.source_encoding = myEncoding; //编码
fetch.crawltime = (new Date()).toFormat("YYYY-MM-DD");

//没有关键词就用sourcename
if (eval(keywords_format) == "") {
fetch.keywords = source_name;
} else {
fetch.keywords = eval(keywords_format);
}
//没有title就用空字符串,不过一般都会有
if (eval(title_format) == "") {
fetch.title = ""
} else {
fetch.title = eval(title_format); //标题
}
//日期格式处理
if (eval(date_format) != "") {
fetch.publish_date = eval(date_format); //刊登日期
let regExp = new RegExp(fetch.publish_date)
fetch.publish_date = regExp.exec(fetch.publish_date)[0];
fetch.publish_date = fetch.publish_date.replace('年', '-')
fetch.publish_date = fetch.publish_date.replace('月', '-')
fetch.publish_date = fetch.publish_date.replace('日', '')
// fetch.publish_date = new Date(fetch.publish_date).toFormat("YYYY-MM-DD HH:mm:ss");
} else {
fetch.publish_date = eval(engDate_format);
}
//作者信息处理
if (eval(author_format) == "") {
fetch.author = eval(engAuthor_format).replace("\r\n", ""); //eval(author_format); //作者
} else {
fetch.author = eval(author_format);
}
if (eval(content_format) == "") {
fetch.content = eval(engContent_format);
} else {
fetch.content = eval(content_format).replace("\r\n" + fetch.author, "");
//内容,是否要去掉作者信息自行决定
}
//来源处理
if (eval(source_format) == "") {
fetch.source = fetch.source_name;
} else {
fetch.source = eval(source_format).replace("\r\n", ""); //来源
}
//摘要信息处理
if (eval(desc_format) == "") {
fetch.desc = fetch.title;
} else {
fetch.desc = eval(desc_format); //摘要
}
//如果新闻中没有文字正文,或者没有发布日期,跳过该文章,return
if (fetch.content == "" || fetch.publish_date == "") {
return
} else {
//设置数据库插入元组
var newsCell = [[fetch.title, fetch.content, fetch.publish_date, fetch.url, fetch.source_name, fetch.source_encoding, fetch.crawltime]];
//将文章信息添加进数据库
sqliteDBNews.insertData(insertNewsSql, newsCell);
//设置数据库查询语句,匹配新闻正文中出现的城市与城市所在经纬度
var querySql = `
SELECT
Location.*,News.*
FROM
Location
INNER JOIN News
WHERE
News.content LIKE '%'||Location.city||'%'`;
//执行数据库查询语句,作为Promise返回
await sqliteDBNews.queryData(querySql).then(function (data) {
resolve(data)
})
//存储json
// var filename = source_name + "_" + (new Date()).toFormat("YYYY-MM-DD") +
// "_" + myURL.substr(myURL.lastIndexOf('/') + 1) + ".json";
// fs.writeFileSync('/Users/lingyi/postGraduate/codeMapNews/server/News/' + filename, JSON.stringify(fetch));
}
})
})
// });

5.对执行顺序的一些说明
  1. 后端服务启动 (1)

  2. 前端发送请求进入 (2)

    • 进入(4)函数reqNewsData(),等待reqCallback()函数期约完成
      • resolve reqNewsData()期约返回的Promise数据(数据库运算完成的新闻数据,包含(content、latitude、longitude))
  3. 函数reqNewsData()等待reqCallback()期约完成,

    • resolve reqCallback()期约返回的Promise数据(数据库运算完成的新闻数据,包含(content、latitude、longitude))
  4. 函数reqCallback()等待newsGet()期约完成,

    • resolve newsGet()期约返回的Promise数据(数据库运算完成的新闻数据,包含(content、latitude、longitude))
  5. 函数newsGet()等待sqliteDBNews.queryData()期约完成,

    • resolve sqliteDBNews.queryData()期约返回的Promise数据(数据库运算完成的新闻数据,包含(content、latitude、longitude))

    核心:将数据库返回的数据一层一层传上去,但是由于异步,处理需要时间,所以用Promise一层一层返回

6.前端代码
  • angular-cli生成的,很简单

    1
    2
    <!--app.component.html-->
    <div id="map"> </div>
    1
    2
    3
    4
    5
    6
    /* app.component.css */
    #map {
    height: 100%;
    width: 100%;
    position: absolute;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    //app.Component.ts
    import { Component } from '@angular/core';
    import * as L from 'leaflet';
    import axios from 'axios'

    @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
    })
    export class AppComponent {
    title = 'mapNews';
    map: any;
    constructor() {
    }

    ngOnInit(): void {
    this.initMap()
    }

    async initMap(): Promise<any> {
    this.map = L.map('map', {
    zoom: 7,
    center: [32.10296, 118.91125]
    });

    // 添加图层到地图上
    let tile = L.tileLayer('http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}', {
    maxZoom: 18,
    });
    tile.addTo(this.map);
    //等待拿到数据
    let temp = await this.reqNewsData();
    let newsPos = temp.data;
    var newsLon = null;
    var newsLat = null;
    var newsContent = null;
    //将拿到的经纬度,作为点坐标加载,Content作为bindPipup()提示参数加载
    for (let i = 0; i < newsPos.length; i++) {
    newsLon = newsPos[i].lon;
    newsLat = newsPos[i].lat;
    newsContent = newsPos[i].content;
    L.marker([newsLat, newsLon]).addTo(this.map).bindPopup(newsContent)
    }
    }

    //向后端发送请求
    reqNewsData(): any {
    return new Promise(resolve => {
    axios.get('http://localhost:8082/server').then(res => {
    resolve(res)
    })
    })
    }
    }

3.实现效果

mapNewsIMG1
mapNewsIMG2
mapNewsIMG3
mapNewsIMG4
数据库服务(部分)

4. 目前的不足

  1. 爬取到的新闻本身是没有坐标的,本项目在数据库中根据正文内容实现了城市坐标匹配
    • 匹配的精度、速度取决于所使用的数据库
    • 当一个新闻中有多个地点、或者有类地名的名词时也会匹配到“错误”的城市坐标
    • 解决方法
      • 更改数据库中城市数据组织方式
      • 优化数据库中匹配(查询)方法,对正文中的城市进行更高规格的检索
  2. 前后端通信的时候,有时候数据传输速度很快,有的时候很慢,有的时候后端甚至自己报错停掉了
    • 可能取决于当时的网络情况
    • 后端是用Nodejs写的,存在异步情况。数据处理(存、匹配、取)时间过长导致响应时间过长
  3. 其他 遇到问题再说吧,纯粹突发奇想

mapNews项目
https://anonymouslosty.ink/2022/09/16/mapNews/
作者
Ling yi
发布于
2022年9月16日
更新于
2022年9月21日
许可协议