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 文件中
随机推荐
- iOS完美版的UIScrollView无缝循环:你值得一看
可以直接copy运行研究 .m头文件和声明的常量(宏和const) #import "ViewController.h" // UIScrollView的尺寸 const CGFl ...
- OpenStack - liberty CentOS 7
OpenStack私有云部署 Controller Node: em1(10.6.17.11),em2() Computer Node: em1(10.6.17.12),e ...
- [Unity Asset]AssetBundle系列——游戏资源打包
转载:http://www.cnblogs.com/sifenkesi/p/3557231.html 将本地资源打包,然后放到资源服务器上供游戏客户端下载或更新.服务器上包含以下资源列表:(1)游戏内 ...
- LPC1768的usb使用--硬件篇
LPC1768芯片带有USB设备控制器,前面写的文章都是在说比较简单的设备驱动,今天来说复杂一点的 首先是硬件层的配置 #ifndef __USBHW_H__ #define __USBHW_H__ ...
- javascript中最常用的方法
平时在工作中时常需要一些方法,下面列举几个最常用的几个方法. 1. indexOf(searchvalue,fromindex) 该方法用于查找一个字符串是否包含了另一个字符串 indexOf() 方 ...
- 在线文档转换API word,excel,ppt等在线文件转pdf、png
在线文档转换API提供word,excel,ppt等在线文件转pdf.png等,文档:https://www.juhe.cn/docs/api/id/259 接口地址:http://v.juhe.cn ...
- [无关IT]就这样在凌晨写一篇吧~
由于新浪博客广告实在太嚣张,自己也都是转载,故决定搬家至此,一改只转不写的习惯T^T,争取记录一下自己的小成长~日后有时间把脑子里的小东西一点点写出来~(好可怕的说)... 好了,睡了!各位爷早睡~ ...
- 【转】国外程序员整理的 C++ 资源大全
内容包括:标准库.Web应用框架.人工智能.数据库.图片处理.机器学习.日志.代码分析等. 标准库 C++标准库,包括了STL容器,算法和函数等. C++ Standard Library:是一系列类 ...
- [git] 细说commit (git add/commit/diff/rm/reset 以及 index 的概念)
http://kasicass.blog.163.com/blog/static/39561920133294219374/ 创建测试仓库 $ git init $ echo "line o ...
- UVa 10602 - Editor Nottoobad
题目大意:有一个编辑器,它有两种命令,“重复上一个单词” 和 “删除前一个字母”,给出一系列字符串,求最少的敲击键盘的次数. 题目中强调第一个敲的单词必须是给的第一个单词,于是就考虑按照单词与第一个单 ...