Java豆瓣电影爬虫——减少与数据库交互实现批量插入

  节前一个误操作把mysql中record表和movie表都清空了,显然我是没有做什么mysql备份的。所以,索性我把所有的表数据都清空的,一夜回到解放前……

  项目地址:https://github.com/DMinerJackie/JewelCrawler

  在上一个版本中,record表存储了7万多条记录,爬取的有4万多条,但是可以明显的发现爬取的数据量越多的时候,机子就越卡。又一次报错,是有关JDBC的,还有一次机子跑卡死了。

  仔细一琢磨,上个版本的爬虫程序与数据库的读写次数太频繁,存在以下问题:

    1.程序运行,从种子地址开始,对于每次爬取的网站地址先查询数据库是否存在该条记录,如果不存在,则立即插入;

    2.当前网站地址爬取完毕后,查找数据库从中取出第一个crawled为0的记录进行爬取,每次只取一条;

    3.存储电影详情页记录以及短评数据都是采用解析一条则立即存储到数据库。

  显然,上面的这种方式是一目了然的效率低下,所以今天下午对相关代码进行改造,部分实现了批量插入,尽可能减少与数据库的交互,从而降低时空成本。

  在git clone完项目后,发现一个很诡异的现象,JewelCrawler每次都是爬取种子地址,并没有一次查询数据库中crawled字段为0的记录进行一一爬取,但是之前在本机上是完美运行的,可能是在push代码前做了改动影响运行了。

既然问题出现了,就顺着这个版本看看,最终发现问题的原因是对于种子网址并没有存储到mysql的record表中,所以在DoubanCrawler类中

//set boolean value "crawled" to true after crawling this page
sql = "UPDATE record SET crawled = 1 WHERE URL = '" + url + "'";
stmt = conn.createStatement();

 if (stmt.executeUpdate(sql) > 0) {
          //get the next page that has not been crawled yet
           sql = "SELECT * FROM record WHERE crawled = 0";
           stmt = conn.createStatement();
           rs = stmt.executeQuery(sql);
           if (rs.next()) {
                    url = rs.getString(2);
           } else {
                    //stop crawling if reach the bottom of the list
                    break;
           }

           //set a limit of crawling count
           if (count > Constants.maxCycle || url == null) {
                    break;
           }
}

 执行stmt.executeUpdate(sql) > 0是返回的值为0,从而不会从数据库中读取crawled为0的记录,最后就一直在while的循环中爬取种子网站。

解决方法:对于种子网站既然没有存储到record的操作,那么就对种子网站做特殊处理,将if的判断条件改为if (stmt.executeUpdate(sql) > 0 || frontPage.equals(url)),这样对于种子网站即使没有update更新成功操作仍然可以进入读取数据库crawled为0 的操作。

针对第一个问题,采用批量插入操作

实现思路:对于当前爬取的网站地址,解析网页源码,提取出所有的link,对于符合正则表达式过滤的link,将其存到一个list集合中。遍历完当前网址的所有link后,将符合条件的link批量存储到数据库中。

具体实现如下

public static void parseFromString(String content, Connection conn) throws Exception {

        Parser parser = new Parser(content);
        HasAttributeFilter filter = new HasAttributeFilter("href");

        String sql1 = null;
        ResultSet rs1 = null;
        PreparedStatement pstmt1 = null;
        Statement stmt1 = null;

        List<String> nextLinkList = new ArrayList<String>();

        int rowCount = 0;
        sql1 = "select count(*) as rowCount from record";
        stmt1 = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
        rs1 = stmt1.executeQuery(sql1);
        if (rs1.next()) {
            rowCount = rs1.getString("rowCount") != null ? Integer.parseInt(rs1.getString("rowCount")) : 0;
        }

        if (rowCount <= Constants.maxCycle) { //once rowCount is bigger than maxCycle, the new crawled link will not insert into record table
            try {
                NodeList list = parser.parse(filter);
                int count = list.size();

                //process every link on this page
                for (int i = 0; i < count; i++) {
                    Node node = list.elementAt(i);

                    if (node instanceof LinkTag) {
                        LinkTag link = (LinkTag) node;
                        String nextLink = link.extractLink();
                        String mainUrl = Constants.MAINURL;

                        if (nextLink.startsWith(mainUrl)) {
                                //check if the link already exists in the database
                                sql1 = "SELECT * FROM record WHERE URL = '" + nextLink + "'";
                                stmt1 = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
                                rs1 = stmt1.executeQuery(sql1);
                                if (rs1.next()) {

                                } else {
                                    Pattern moviePattern = Pattern.compile(Constants.MOVIE_REGULAR_EXP);
                                    Matcher movieMatcher = moviePattern.matcher(nextLink);

                                    Pattern commentPattern = Pattern.compile(Constants.COMMENT_REGULAR_EXP);
                                    Matcher commentMatcher = commentPattern.matcher(nextLink);

                                    if (movieMatcher.find() || commentMatcher.find()) {
                                        nextLinkList.add(nextLink);
                                    }
                                }
                        }
                    }
                }
                if (nextLinkList.size() > 0) {
                    conn.setAutoCommit(false);
                    //if the link does not exist in the database, insert it
                    sql1 = "INSERT INTO record (URL, crawled) VALUES (?,0)";
                    pstmt1 = conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS);
                    for (String nextLinkStr : nextLinkList) {
                        pstmt1.setString(1, nextLinkStr);
                        pstmt1.addBatch();
                        System.out.println(nextLinkStr);
                    }
                    pstmt1.executeBatch();
                    conn.commit();
                }
            } catch (Exception e) {
                //handle the exceptions
                e.printStackTrace();
                System.out.println("SQLException: " + e.getMessage());
            } finally {
                //close and release the resources of PreparedStatement, ResultSet and Statement
                if (pstmt1 != null) {
                    try {
                        pstmt1.close();
                    } catch (SQLException e2) {
                    }
                }
                pstmt1 = null;

                if (rs1 != null) {
                    try {
                        rs1.close();
                    } catch (SQLException e1) {
                    }
                }
                rs1 = null;

                if (stmt1 != null) {
                    try {
                        stmt1.close();
                    } catch (SQLException e3) {
                    }
                }
                stmt1 = null;
            }
        }
    }

 1.通过正则匹配,找到符合条件的link,并添加到nextLinkList集合中

      2.遍历完后,将数据存到数据库中

    3. 在批量操作中,使用了addBatch()方法和executeBatch()方法,注意需要添加conn.setAutoCommit(false);以及conn.commit()表示手动提交。

针对第二个问题,采用一次查询多条记录

实现思路:将每次只查询一条记录,改为每次查询10条记录,并将这10条记录存放到list集合中,并将原来的String类型的url改为list类型的urlList传入到DouBanHttpGetUtil.getByString()方法里。这样即减少了与数据库的交互,同时也减少了对于getByString方法的调用。

具体实现如下

public static void main(String args[]) throws Exception {

        //load and read seed file
        List<String> seedList = LoadSeed.loadSeed();
        if (seedList == null) {
            log.info("No seed to crawl, please check again");
            return;
        }
        String frontPage = seedList.get(0);

        //connect database mysql
        Connection conn = DBUtils.connectDB();

        //create tables to store crawled data
        DBUtils.createTables();

        String sql = null;
        String url = frontPage;
        Statement stmt = null;
        ResultSet rs = null;
        int count = 0;
        List<String> urlList = new ArrayList<String>();
        urlList.add(url);

        //crawl every link in the database
        while (true) {
            //get page content of link "url"
            DouBanHttpGetUtil.getByString(urlList, conn);
            count++;

            //set boolean value "crawled" to true after crawling this page

            //TODO batch update
            int result = 0;
            conn.setAutoCommit(true);
            for (String urlStr : urlList) {
                sql = "UPDATE record SET crawled = 1 WHERE URL = '" + urlStr + "'";
                stmt = conn.createStatement();
                stmt.executeUpdate(sql);
            }

            urlList.clear();//empty for every loop
            if (stmt.executeUpdate(sql) > 0  || frontPage.equals(url)) {
                //get the next page that has not been crawled yet
                sql = "SELECT * FROM record WHERE crawled = 0 limit 10";
                stmt = conn.createStatement();
                rs = stmt.executeQuery(sql);
                while (rs.next()) {
                    url = rs.getString(2);
                    urlList.add(url);
                }

                //set a limit of crawling count
                if (rs.next() || count > Constants.maxCycle || url == null) {
                    break;
                }
            }
        }
        conn.close();
        conn = null;

        System.out.println("Done.");
        System.out.println(count);
    }

注意: 1.这里采用每次读取10条记录,相应的也需要将这10条记录的crawled字段更新为1,表示爬取过。

     2. mysql不支持top 10 * 这样的语法,但是可以通过代码中所示的limit 10 的方式取出数据。

                 3. 添加conn.setAutoCommit(true);表示更新操作设置为自动提交,这样就可以解决虽然程序执行成功但是数据没有更新到数据库的现象。

针对第三个问题,与第一个问题解决方法相同。

虽然不知道这样做带来的效果有多明显,或有是否有更好的解决方案,但是可以肯定的是上个版本的代码会大量占用内存并频繁与数据库交互。本人是数据库小白,希望有更好的方案可以提出来^_^

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏岑玉海

Hive Tuning(一) 连接策略

群里共享了一本hive调优的书记,名叫《Hive Tunning》,就忍不住开始看了,也顺便记录一下自己学到的东西,备忘! 首先,这是hive的数据摘要,别...

3966
来自专栏tkokof 的技术,小趣及杂念

小话游戏脚本(三)

在此就heSript实现过程中的一些解决方案和自己的想法陈列一番,由于自己编程水平实在拙劣,又没什么实际经验,所以导致相关的代码非常糟糕,所以竭诚欢迎大家批评...

621
来自专栏逢魔安全实验室

SQL注入ByPass的一些小技巧

? 01 — 前言 SQL注入从古至今都是一个经久不衰的影响严重的高危漏洞,但是网络安全发展到现在,如果想通过SQL注入直接获取数据或者权限,多多少少都需要绕...

3969
来自专栏小狼的世界

locale的详细解释

* Thu Sep 27 2001 Bernhard Rosenkraenzer 2.5-0.f.2

844
来自专栏更流畅、简洁的软件开发方式

【测试】两种数据库,四种分页算法的效率比较

分页算法本身没有什么快慢之分,对反应速度起到决定作用的是——能否有效地利用索引! 算法 评价 缺点 适用的数据库 max 效率最高的 只能有一...

2937
来自专栏扎心了老铁

spark-streaming集成Kafka处理实时数据

在这篇文章里,我们模拟了一个场景,实时分析订单数据,统计实时收益。 场景模拟 我试图覆盖工程上最为常用的一个场景: 1)首先,向Kafka里实时的写入订单数据,...

7485
来自专栏Phoenix的Android之旅

Dagger2 Android应用:@Component和@Module

这部分会介绍一下DI的主要概念,包括Component,Module,但不涉及和Android有关的具体代码。

1332
来自专栏Android相关

IjkPlayer初始化过程

最近调研做视频秒开,使用B站开源的ijkplayer作为播放器。ijkplayer基于ffmpeg的播放器。

2751
来自专栏杨建荣的学习笔记

通过pl/sql计算程序的运行时间(r3笔记第77天)

在sqlplus中运行sql语句或者pl/sql的时候如果需要统计运行的时间,只需要开启set timing on选项即可。 SQL> set timing o...

34611
来自专栏技术博客

Entity Framework 关系约束配置

简单的说一下自己的理解,大家应该都很明白ADO.NET,也就是原生态的数据库操作,直接通过拼接SQL语句,表与表之间通过链接(inner join  left ...

1051

扫码关注云+社区

领取腾讯云代金券