写一个最原始的爬虫,用来爬取新浪新闻的内容
摘要
摘要
在学了这么久的基础之后,感觉可以做一些有成就感的事情了。写一个最基础的Java爬虫,爬取新浪新闻中的新闻,并将其中的内容存入数据库中。虽然是很脆弱简单的一个项目,但是也是花费了自己非常之多的时间,究其原因还是因为自己基础不扎实,一个小问题就能卡半天。总结一下这个过程,便于日后参考。
关键词:Java爬虫、H2数据库、Flywaydb数据库迁移。
先放张效果图:
1.先初始化一个空的Java项目
创建一个空的项目,对我来说最好的方法就是复制一个,然后删除里面对自己没有的内容,这是自己以前总结的过程,“初始化一个空的Java项目”。
2.分析页面结构,初步画出流程图
实现的基本思路在于从新浪的主页出发,获取下面的各个新闻的链接,存入数据库中,再将链接从数据库中拿出,获取该链接下的新闻内容。
初步分析新浪的页面结构后,
3.设计数据库
三张表,一张存放将要处理的链接,一张存放处理完的链接,另一张存放新闻内容。
create table sina_news(
id int not null primary key auto_increment,
url varchar(1000),
title varchar(1000),
content text,
create_time timestamp,
modify_time timestamp);
create table LINKS_ALREADY_PROCESSED(url varchar(1000));
create table LINKS_TOBE_PROCESSED(url varchar(1000));
还需要插入一个初始数据:
insert into LINKS_TOBE_PROCESSED(url)
values ('https://www.sina.cn')
到这,前期的准备工作就基本完成了,接下来就开始实现前面的思路了。
4.逐步实现基本功能
Step1:从数据库中拿到一个初始链接,通过该链接获取所有以(http|https)开头的链接。
光是这一步就涉及两个问题:1.如何在程序中连接一个数据库,并取得里面的数据。2.怎么发送一个http请求到指定网页,并获取响应体,然后对响应体进行解析,获取所需属性。
解决第一个问题:
首先我们知道,一个数据库肯定有他的的地址,那么只要你有这个地址,并且有用户名跟密码,你就可以连上这个数据库。在JDk中,导入H2数据库的Maven引用之后,就可以使用JDBC的DriverManager连接数据库。
Connection connection = DriverManager.getConnection(jdbcUrl, user, password);
在写好对应的sql语句后,执行:
PreparedStatement preparedStatement = connection.prepareStatement
("select url from LINKS_TOBE_PROCESSED");
preparedStatement.executeUpdate();//执行并更新,用作不需要返回值的sql操作.
preparedStatement.executeQuery();//执行并获取返回值,返回结果是一个ResultSet
解决第二个问题:
如何发送http请求,我们可以直接抄官网文档。这时得到的是服务器所给我们的数据流,我们还需要将这个流转换为String,这里需要用到IOutils.toString方法,导入commons-io包,最后我们还需要解析这个String,将其转换为标准化的document文件。使用Jsoup解析html之后,就可以单独的选出我们想要的内容,将这部分内容总结成一个方法,就可以实现从一个网址,到所需新闻内容的过程。
代码如下:
private static Document getUrlDocument(String url) throws IOException {
CloseableHttpClient httpclient = HttpClients.custom()
.setDefaultRequestConfig(RequestConfig.custom()
.setCookieSpec(CookieSpecs.STANDARD).build())
.build();
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("user-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36");
try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
HttpEntity entity1 = response1.getEntity();
InputStream inputStream = entity1.getContent();
String html = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
Document document = Jsoup.parse(html);
EntityUtils.consume(entity1);
return document;
}
}
注意:
1.正常情况下,使用代理的话使用默认的HttpClient即可:
private static List<String> getUrlFromWeb(Document document) {
List<String> result = new ArrayList<>();
List<Element> newsList = document.select("section").select("a");
for (Element e : newsList
) {
String regex = "(http|https)(.*)";
Pattern pattern = Pattern.compile(regex);
String hrefTag = e.attr("href");
if (pattern.matcher(hrefTag).find()) {
result.add(hrefTag);
}
}
return result;
}
将前面的方法串联起来,实现第一步:
@SuppressFBWarnings("OBL_UNSATISFIED_OBLIGATION")
private static void insertLinksTobeProcessedUrlToDataBase(Connection connection) throws SQLException, IOException {
String sqlSelectCommend = "select url from LINKS_TOBE_PROCESSED";
List<String> list = executeSelectSqlCommendAndGetResultSet(connection, sqlSelectCommend);
List<String> result = new ArrayList<>();
for (String url : list
) {
//根据数据库中初始的网站主页,获取主页中各个新闻的链接
Document document = getUrlDocument(url);
//将获取的网址加入结果集中
result.addAll(getUrlFromWeb(document));
}
//将获取的链接,加入待处理数据库中
PreparedStatement preparedStatement = connection.prepareStatement
("insert into LINKS_TOBE_PROCESSED(url) values(?)");
for (String urlFromList : result
) {
preparedStatement.setString(1, urlFromList);
preparedStatement.executeUpdate();
}
}
Step2:从待处理的数据库中拿到所有的链接,逐个判断是不是我们需要的,是的话就解析它,然后将其删除。
代码如下:
@SuppressFBWarnings("SA_LOCAL_SELF_ASSIGNMENT")
private static void filterUrlAndInsertToAlreadyDatabase(Connection connection) throws SQLException {
List<String> resultSetFromTobe = executeSelectSqlCommendAndGetResultSet(connection, "select url from LINKS_TOBE_PROCESSED");
Pattern pattern = Pattern.compile("(\\b(http|https)(.*)(pos=108)\\b)");
while (!resultSetFromTobe.isEmpty()) {
String link = resultSetFromTobe.remove(resultSetFromTobe.size() - 1);
boolean flag = false;
//保证重复的网址不会被插入已经处理完的数据库中
try (PreparedStatement preparedStatement = connection.prepareStatement("select url from LINKS_ALREADY_PROCESSED WHERE URL=?")) {
preparedStatement.setString(1, link);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
flag = true;
}
}
if (flag) {
continue;
}
if (pattern.matcher(link).find()) {
try (PreparedStatement preparedStatement = connection.prepareStatement("insert into LINKS_ALREADY_PROCESSED(url) values ?")) {
preparedStatement.setString(1, link);
preparedStatement.executeUpdate();
}
//这时,可以将待处理的连接池中的这条url从池中删除
try (PreparedStatement preparedStatement = connection.prepareStatement("delete from LINKS_TOBE_PROCESSED where url=?")) {
preparedStatement.setString(1, link);
preparedStatement.executeUpdate();
}
}
}
}
Step3:拿到过滤后的链接,通过对该链接页面中的内容进行解析,获取所需的数据。
代码如下:
private static void getUsefulContentAndInsertIntoSinaNewDataBase(Connection connection) throws SQLException, IOException {
List<String> resultSet = executeSelectSqlCommendAndGetResultSet(connection, "select url from LINKS_ALREADY_PROCESSED");
while (!resultSet.isEmpty()) {
String url = resultSet.remove(resultSet.size() - 1);
Document document = getUrlDocument(url);
String content = getContent(url);
String title = document.select("section").select("article").select("h1").text();
if (!title.isEmpty() && !content.isEmpty()) {
try (PreparedStatement preparedStatement = connection.prepareStatement
("insert into SINA_NEWS(title,url,content,create_time,modify_time) values(?,?,?,current_timestamp,current_timestamp)")) {
preparedStatement.setString(1, title);
preparedStatement.setString(2, url);
preparedStatement.setString(3, content);
preparedStatement.executeUpdate();
System.out.println(url);
}
}
}
}
获取新闻内容:
private static String getContent(String url) throws IOException {
if (url.isEmpty()) {
throw new NullPointerException("传入的连接池为空");
}
Document document = getUrlDocument(url);
return document.select("section").select("p").text();
}
5.使用Flyway数据库迁移化工具进行数据库的初始化
由于每次程序运行之前都需要保证LINKS_TOBE_PROCESSED数据库中有一个新浪首页的新闻,并且其他两个数据库为空。所以,刚开始我调试的时候,十分痛苦,每次都要自己用sql语句初始化数据库。
使用Flyway数据库迁移工具可以做到自动化这个过程。首先在pom.xml中导入Flyway插件后,(别忘了用户名密码)
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>6.0.6</version>
<configuration>
<url>jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler</url>
<user>root</user>
<password>password</password>
</configuration>
</plugin>
注:
官方文档上推荐的版本是6.0.8,但是pom.xml中识别不到该版本,所以修改为6.0.6。其次,h2数据库的依赖可引入最新版。
跟着官网的文档来做即可:
1.在指定目录下新建文件夹,在文件夹中添加指定格式的.sql文件
6.总结
第一次稍微脱离基础知识以外去做一些有用的东西,稍稍一点成就感,学到了一些基本的语句,比如连接数据库的、运行数据库指令、以及解析html文档,开头提到的就不再赘述了,这里总结一些自己学到的知识。
1.连接数据库:
private static final String jdbcUrl = "jdbc:h2:file:H:/github item/SinaCrawler/sina-crawler/SinaCrawler";
private static final String user = "root";
private static final String password = "password";
Connection connection = DriverManager.getConnection(jdbcUrl, user, password);
这个地方数据库的账户跟密码是明文存储的(暂时还不知道怎么加密),所以使用Spotbugs检查的时候,肯定会报错的。可以将这个报错压制掉,导入maven依赖后,在方法前添加标签:
@SuppressFBWarnings("DMI_CONSTANT_DB_PASSWORD")
即可消除报错,当然,最终还是要学会如何解决问题。
2.正则表达式:
这个地方自己写了几个简单的正则,自己在调试过程中发现:
public static void main(String[] args) {
String s = "http://cj.sina.cn/pos=108&his=0";
String regix = "\\b(http|https)(.*)(his=0)\\b";
Pattern pattern = Pattern.compile(regix);
Matcher matcher = pattern.matcher(s);
System.out.println(matcher.group());
}
自己这样写的话,会报错:
if (matcher.find()) {
System.out.println(matcher.group());
}
当然,前面很多低效的、不安全的操作,都可以进行优化,还可以使用多线程什么的,后面在继续更新咯。