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的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
Java豆瓣电影爬虫——减少与数据库交互实现批量插入的更多相关文章
- Java豆瓣电影爬虫——抓取电影详情和电影短评数据
一直想做个这样的爬虫:定制自己的种子,爬取想要的数据,做点力所能及的小分析.正好,这段时间宝宝出生,一边陪宝宝和宝妈,一边把自己做的这个豆瓣电影爬虫的数据采集部分跑起来.现在做一个概要的介绍和演示. ...
- Java豆瓣电影爬虫——模拟登录的前世今生与验证码的爱恨情仇
前言 并不是所有的网站都能够敞开心扉让你看个透彻,它们总要给你出些难题让你觉得有些东西是来之不易的,往往,这也更加激发你的激情和斗志! 从<为了媳妇的一张号,我与百度医生杠上了>里就有网友 ...
- Java豆瓣电影爬虫——使用Word2Vec分析电影短评数据
在上篇实现了电影详情和短评数据的抓取.到目前为止,已经抓了2000多部电影电视以及20000多的短评数据. 数据本身没有规律和价值,需要通过分析提炼成知识才有意义.抱着试试玩的想法,准备做一个有关情感 ...
- Java豆瓣电影爬虫——小爬虫成长记(附源码)
以前也用过爬虫,比如使用nutch爬取指定种子,基于爬到的数据做搜索,还大致看过一些源码.当然,nutch对于爬虫考虑的是十分全面和细致的.每当看到屏幕上唰唰过去的爬取到的网页信息以及处理信息的时候, ...
- 转:Scrapy安装、爬虫入门教程、爬虫实例(豆瓣电影爬虫)
Scrapy在window上的安装教程见下面的链接:Scrapy安装教程 上述安装教程已实践,可行.(本来打算在ubuntu上安装Scrapy的,但是Ubuntu 磁盘空间太少了,还没扩展磁盘空间,所 ...
- Scrapy安装、爬虫入门教程、爬虫实例(豆瓣电影爬虫)
Scrapy在window上的安装教程见下面的链接:Scrapy安装教程 上述安装教程已实践,可行.(本来打算在ubuntu上安装Scrapy的,但是Ubuntu 磁盘空间太少了,还没扩展磁盘空间,所 ...
- Mybatis对oracle数据库进行foreach批量插入操作
MySQL支持的语法 INSERT INTO `tableX` ( `a`, `b`, `c`, `d`, `e` ) VALUES <foreach collection ="lis ...
- [Java]在JAVA中使用Oracle的INSERT ALL语法进行批量插入
Oracle也提供了类似MySQL的批量插入语法,只是稍微别扭些,具体代码如下: package com.hy; import java.sql.Connection; import java.sql ...
- python08豆瓣电影 爬虫 BeautifulSoup + Reuqests
主要思路 请求豆瓣的链接获取网页源代码 然后使用 BeatifulSoup 拿到我们要的内容 最后就把数据存储到 excel 文件中
随机推荐
- postgresql 抽样查询
curl -GET 'http://****/query' --data-urlencode "db=db" --data-urlencode "q=SELECT las ...
- 函数(swift)
输入输出参数(In-Out Parameters) 如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Paramet ...
- STL中的所有算法(70个)
STL中的所有算法(70个)----9种类型(略有修改by crazyhacking) 参考自: http://www.cppblog.com/mzty/archive/2007/03/14/1981 ...
- 微信小程序一些简单的快捷键
常用快捷键 格式调整 Ctrl+S:保存文件Ctrl+[, Ctrl+]:代码行缩进Ctrl+Shift+[, Ctrl+Shift+]:折叠打开代码块Ctrl+C Ctrl+V:复制粘贴,如果没有选 ...
- iOS中正则表达式的三种使用方式
1.利用NSPredicate(谓词)匹配 例如匹配有效邮箱: NSString *email = @“nijino_saki@163.com”: NSString *regex = @"[ ...
- FZU 1397 保送
网络流入门题. 源点到每一个学生连一条边,容量为1 每个学校到汇点连一条边,容量为L 符合要求的学生和学校之间连边,容量为1. 从源点到汇点的最大流就是答案. #include<cstdio&g ...
- 组织Golang代码
本月初golang官方blog(需要自己搭梯子)上发布了一篇文章,简要介绍了近几个月Go在一 些技术会议上(比如Google I/O.Gopher SummerFest等)的主题分享并伴有slide链 ...
- MySQL密码过期策略
如果要设置密码永不过期的全局策略,可以这样:(注意这是默认值,配置文件中可以不声明) [mysqld] default_password_lifetime=0 禁用密码过期: ALTER USER ' ...
- 初识Google code jam平台
为了熟悉一下code jam的平台,今天简单试了一下,做了一下Qualification Round Africa 2010的三道题目,都是很基础的. A题:给一个数n和一系列数a[],从a[]中找出 ...
- pypi 的使用
关于本人的package,情况比较简单,所有的.py文件全部放到了一个叫做FundsData的文件夹下(package下),上层目录也叫FundsData(其实叫什么都可以),其下放了setup.py ...