写一个最原始的爬虫,用来爬取新浪新闻页面中的内容

1,182 阅读8分钟

写一个最原始的爬虫,用来爬取新浪新闻的内容

摘要

\quad 在学了这么久的基础之后,感觉可以做一些有成就感的事情了。写一个最基础的Java爬虫,爬取新浪新闻中的新闻,并将其中的内容存入数据库中。虽然是很脆弱简单的一个项目,但是也是花费了自己非常之多的时间,究其原因还是因为自己基础不扎实,一个小问题就能卡半天。总结一下这个过程,便于日后参考。
\quad 关键词:Java爬虫、H2数据库、Flywaydb数据库迁移。
\quad 先放张效果图:

1.先初始化一个空的Java项目

\quad 创建一个空的项目,对我来说最好的方法就是复制一个,然后删除里面对自己没有的内容,这是自己以前总结的过程,“初始化一个空的Java项目”

2.分析页面结构,初步画出流程图

\quad 实现的基本思路在于从新浪的主页出发,获取下面的各个新闻的链接,存入数据库中,再将链接从数据库中拿出,获取该链接下的新闻内容。
\quad初步分析新浪的页面结构后,

可以大概知道:新闻的链接都是再a标签下的href属性下的一个字符串。但是,同样有一些“脏数据”,例如下面这种:
这肯定不是个新闻链接。还有一些广告页面:

都是我们需要排除在外的。
\quad初步设想,数据库中建三个表,一个存放需要被处理的链接,一个存放处理完成后的链接,另一个存放自己需要的内容。
\quad自己初步构想的流程图:

3.设计数据库

\quad 三张表,一张存放将要处理的链接,一张存放处理完的链接,另一张存放新闻内容。

建表语句如下:

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.逐步实现基本功能

\quad Step1:从数据库中拿到一个初始链接,通过该链接获取所有以(http|https)开头的链接。
光是这一步就涉及两个问题:1.如何在程序中连接一个数据库,并取得里面的数据。2.怎么发送一个http请求到指定网页,并获取响应体,然后对响应体进行解析,获取所需属性。
\quad 解决第一个问题:
\quad 首先我们知道,一个数据库肯定有他的的地址,那么只要你有这个地址,并且有用户名跟密码,你就可以连上这个数据库。在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

\quad 解决第二个问题:
\quad 如何发送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;
        }
    }

注意:
\quad 1.正常情况下,使用代理的话使用默认的HttpClient即可:

因为自己在运行的时候会报出一些set-Cookie的警告,十分影响效果,所以搜索了一番,看到这个方法还真的可以去掉警告。
\quad 2.新浪新闻这种不怕你爬的网站,一般没做反爬机制,所以适合我这种新手练习,不过再怎么样,用httpGet.setHeader伪装成浏览器还是需要滴。
\quad 接下来,过滤出所有的href标签中以http/https开头的链接。

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();
        }
    }

\quad 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();
                }
            }
        }
    }

\quad 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数据库迁移化工具进行数据库的初始化

\quad 由于每次程序运行之前都需要保证LINKS_TOBE_PROCESSED数据库中有一个新浪首页的新闻,并且其他两个数据库为空。所以,刚开始我调试的时候,十分痛苦,每次都要自己用sql语句初始化数据库。
\quad 使用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>

注:
\quad 官方文档上推荐的版本是6.0.8,但是pom.xml中识别不到该版本,所以修改为6.0.6。其次,h2数据库的依赖可引入最新版。
跟着官网的文档来做即可:
1.在指定目录下新建文件夹,在文件夹中添加指定格式的.sql文件

2.写入sql命令

3.使用mvn flyway:migrate命令初始化sql

到这一步,就基本上可以实现将获取的内容加入到数据库中的功能,并且实现自动初始化数据库。

6.总结

\quad 第一次稍微脱离基础知识以外去做一些有用的东西,稍稍一点成就感,学到了一些基本的语句,比如连接数据库的、运行数据库指令、以及解析html文档,开头提到的就不再赘述了,这里总结一些自己学到的知识。
\quad 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);

\quad这个地方数据库的账户跟密码是明文存储的(暂时还不知道怎么加密),所以使用Spotbugs检查的时候,肯定会报错的。可以将这个报错压制掉,导入maven依赖后,在方法前添加标签:

@SuppressFBWarnings("DMI_CONSTANT_DB_PASSWORD")

即可消除报错,当然,最终还是要学会如何解决问题。
\quad 2.正则表达式:

\quad 这个地方自己写了几个简单的正则,自己在调试过程中发现:

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());
        }

\quad 当然,前面很多低效的、不安全的操作,都可以进行优化,还可以使用多线程什么的,后面在继续更新咯。