发现负面新闻

梁博云实习的第一个项目,发现负面新闻。

任务

这是梁博云实习的第一个项目,发现负面新闻。

实习目标

  1. 学习和掌握json代码的解析和生成
  2. 学习一种http服务的方式
  3. 学习如何使用开放API

第一步、学习和利用现有一个情感计算API http://www.pullword.com/baobian/

调用样例:Linux命令行下执行

1
curl -X POST 'http://baobianapi.pullword.com:9091/get.php' -d'十一月再见,十二月你好,2021年最后一个月总会有不期而遇的温暖,和生生不息的希望' –compressed

返回的json结果要能解析,大于0.5表示正面情感,小于-0.5表明负面情感。或者使用其他自己熟悉的API也可以。推荐使用梁博公开的服务。

第二步,学习提供http服务的工具

C语言选手可以通过搜狗工作流开源工具实现。不会C和C++的同学可以用php代码或者其他自己熟悉的语言实现。

第三步,学习和了解数据源 http://news.baidu.com/

从百度新闻首页获得当天热门新闻,用curl或者wget命令采集首页。然后提取其中的新闻标题和URL

第四步,将今日新闻标题调用情感计算API得到正负面评价,提取负面结果的新闻标题和URL,通过第二步掌握的http服务工具以json格式对外展示。展示的内容包括标题和URL的列表。在自己的linux机器上能访问即可,有公网服务器的可以开放公网URL供我们检查,没有公网服务器的截图即可。

爬取百度新闻

打开百度新闻,F12查看页面源码,可以看到热点新闻的class=mod-tab-pane activebaidu-news 用如下方法得到新闻标题和链接:

1
2
3
4
5
6
// 获取热点新闻HTML代码
Elements hotNews = doc.select("[class=mod-tab-pane active]").select("a");
// 获取新闻标题及链接
for (int i = 0; i < hotNews.size(); i++) {
     news.put(hotNews.get(i).text(), hotNews.get(i).attr("href"));
}

程序的完整逻辑是先得到百度新闻的HTML文件,再从class筛选得到相应的标题和链接,这里Elements类是实现了List<Element>接口,因此是可以遍历的,在遍历时加入存放新闻的标题和链接的哈希表。

 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
package com.jinjin.news.controller;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.util.HashMap;

/**
 * @author 文进
 * @version 1.0
 */
public class News {
    /**
     *
     * @param url 访问路径
     * @return
     */
    public Document getDocument (String url){
        try {
            //5000是设置连接超时时间,单位ms
            return Jsoup.connect(url).timeout(5000).get();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * @param url 访问网站的url
     * @return 返回热点新闻的标题和链接
     */
    public HashMap getNews (String url) {
        Document doc = getDocument(url);
        HashMap<String, String> news = new HashMap<>();
        // 获取热点新闻HTML代码
        Elements hotNews = doc.select("[class=mod-tab-pane active]").select("a");
        // 获取新闻标题及链接
        for (int i = 0; i < hotNews.size(); i++) {
            news.put(hotNews.get(i).text(), hotNews.get(i).attr("href"));
//            System.out.println(hotNews.get(i).text() + "   " + hotNews.get(i).attr("href"));
        }
        return news;
    }
}

参考资料:Java爬取网页内容的简单例子

使用情感计算API

情感计算API链接是:http://www.pullword.com/baobian/

我们要做的是将爬取到的新闻标题用POST方法向上述链接发请求,最后得到一个json结果,提取其中的情感数值,将负的情感值对应的新闻标题保存下来,由于之前存取的新闻是HashMap,因此得到负面新闻的URL也不是难事了。

参考java向指定URL发送GET或POST请求,我们用到了UrlUtils工具类,我们需要用的是public static String sendPost(String url, String param){}这个带参数的发送POST请求的方法。

  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
193
194
195
package com.jinjin.news.controller;

import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @author 文进
 * @version 1.0
 */
public class UrlUtils {
    private static final Logger log = LoggerFactory.getLogger(UrlUtils.class);

    /**
     * 向指定URL发送GET方法的请求
     *
     * @param url   发送请求的URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param) {
        String result = "";
        BufferedReader in = null;
        try {
            String urlNameString = url + "?" + param;
            URL realUrl = new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 获取所有响应头字段
            Map<String, List<String>> map = connection.getHeaderFields();
            // 遍历所有的响应头字段
//            for (String key : map.keySet()) {
//                System.out.println(key + "--->" + map.get(key));
//            }
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            log.error("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 向指定 URL 发送POST方法的请求
     *
     * @param url      发送请求的 URL
     * @param paramMap 请求参数
     * @return 所代表远程资源的响应结果
     */
    public static String sendPost(String url, Map<String, ?> paramMap) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        String param = "";
        Iterator<String> it = paramMap.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            param += key + "=" + paramMap.get(key) + "&";
        }
        try {
            URL realUrl = new URL(url);
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            // 发送请求参数
            out.print(param);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        //使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return result;
    }


    /**
     * 发送post请求 json格式
     *
     * @param url
     * @param param json字符串
     * @return
     */
    public static String sendPost(String url, String param) {
        PrintWriter out = null;
        BufferedReader in = null;
        StringBuilder result = new StringBuilder();
        try {
            URL realUrl = new URL(url);
//            // 忽略https功能,普通使用可以删除
//            if ("https".equalsIgnoreCase(realUrl.getProtocol())) {
//
//                SslUtils.ignoreSsl();
//            }
            // 打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // 获取URLConnection对象对应的输出流
            //out = new PrintWriter(conn.getOutputStream());
            out = new PrintWriter(new OutputStreamWriter(conn.getOutputStream(), "UTF-8"));
            // 发送请求参数
            out.print(param);
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
            String line;
            while ((line = in.readLine()) != null) {
                //result += line;
                result.append(line);
            }
        } catch (Exception e) {
            System.out.println("发送 POST 请求出现异常!" + e);
            e.printStackTrace();
        }
        //使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        //log.info("url返回: " +result);
        return result.toString();
    }
}

创建Spring Boot项目

上述模块都测试好之后,最后就是返回JSON数据格式,参考<后端初学者>Spring Boot 返回 JSON 数据及数据封装,有多种方法,其中一种用Spring Boot中的@RestController注解来返回JSON数据,那么我们创建一个Spring Boot项目,再把上述模块的代码迁移过来就好啦。

创建Spring Boot项目的步骤参考2020入门教程macOS 使用Intellj IDEA 创建spring boot项目

返回JSON数据格式

首先为了方便返回JSON数据格式,我们先将NegativeNews封装成一个类,其主要的属性就两个:新闻的标题title和对应的url。剩下的就是常见的构造器以及get、set方法。

 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
package com.jinjin.news.controller;

/**
 * @author 文进
 * @version 1.0
 */
public class NegativeNews {
    private String title;
    private String url;


    public NegativeNews(String title, String url) {
        this.title = title;
        this.url = url;
    }


    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

返回JSON数据格式可以选择多种形式,这里由于要返回的新闻有多条,因此我们返回一个List<NegativeNews>

在返回JSON数据格式方法的前面加上注解@RequestMapping("/negativeNews"),在请求该页面时,就会执行其中的代码,因此我们将上述爬取百度新闻标题,向情感计算API发送POST请求,得到负面新闻,综合到该方法中一起执行。

 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
package com.jinjin.news.controller;

import com.sun.net.httpserver.HttpServer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 文进
 * @version 1.0
 */
@RestController
public class HttpServers {
    @RequestMapping("/negativeNews")
    public List<NegativeNews> returnJson( ) {
        // 获取百度新闻中热点新闻的标题和链接
        HashMap<String, String> news = new News().getNews("http://news.baidu.com/");
        // 负面新闻
        HashMap<String, String> negativeNews = new HashMap<>();

        // 通过新闻标题得到的情感值,将负面新闻添加到 negativeNews 中
        for (Map.Entry<String, String> entry : news.entrySet()) {
            // 得到情感API返回的结果,例如 data = {"result":0.739306}
            String data = UrlUtils.sendPost("http://baobianapi.pullword.com:9091/get.php", entry.getKey());
            // 提取结果中的分数值 data = {"result": score}
            String score = data.substring(10, data.length() - 1);
            // 将分数值 score 转换成double数值,方便找到负面情绪的新闻
            double emotion = Double.parseDouble(score);
            // 将负面新闻添加到 negativeNews 中
            if (emotion < -0.5) {
                negativeNews.put(entry.getKey(), entry.getValue());
            }
        }

        // 返回 JSON 数据格式
        List<NegativeNews> negativeNewsList = new ArrayList<>();
        for (Map.Entry<String, String> entry : negativeNews.entrySet()) {
            NegativeNews nNews = new NegativeNews(entry.getKey(), entry.getValue());
            negativeNewsList.add(nNews);
        }
        return negativeNewsList;
    }
}

部署到服务器

部署到服务器有两种方式,参考:

maven项目打包

我们选择打包成jar包,其中,有可能遇到发生一些类找不到的错误,需要将本地jar包打包进去,参考maven项目引入本地jar包史上最详细实践方法

解决如下类找不到的情况:

1
2
import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;

经过跟踪,这两个都在rt.jar类中,用引入本地jar包的方法还是不成功,最终参考Idea本地maven打包,程序包不存在解决了,将rt.jar复制到jdk/jre/lib/ext中。

引入org.json,从中获取相应版本的依赖代码,添加到pom.xml配置文件中。

1
2
3
4
5
6
<dependency>
     <!-- jsoup HTML parser library @ http://jsoup.org/ -->
     <groupId>org.jsoup</groupId>
     <artifactId>jsoup</artifactId>
     <version>1.6.1</version>
</dependency>

添加后,在maven中按刷新按钮,如下图:

maven

打包好后现在本地测试一下,终端进入到jar所在的路径,运行如下代码:

1
java -jar 包名.jar

Linux上运行

参考在Linux上运行springboot项目

为了让服务端关闭SSH连接后依然运行,运行时执行如下命令:

1
nohup java -jar 包名.jar &

杀掉运行中的进程,首先查看:

1
ps aux|grep getCimiss-surf.jar

接着杀掉进程:

1
kill -9 30768

MacOS上查看对应端口的进程:

1
lsof -i tcp:端口号

杀掉对应进程:

1
kill -9 PID号

代码及公网地址

至此整个过程就完成了:)