详解Java豆瓣电影爬虫——小爬虫成长记(附源码)

以前也用过爬虫,比如使用nutch爬取指定种子,基于爬到的数据做搜索,还大致看过一些源码。当然,nutch对于爬虫考虑的是十分全面和细致的。每当看到屏幕上唰唰过去的爬取到的网页信息以及处理信息的时候,总感觉这很黑科技。正好这次借助梳理Spring MVC的机会,想自己弄个小爬虫,简单没关系,有些小bug也无所谓,我需要的只是一个能针对某个种子网站能爬取我想要的信息就可以了。有Exception就去解决,可能是一些API使用不当,也可能是遇到了http请求状态异常,又或是数据库读写有问题,就是在这个报exception和解决exception的过程中,JewelCrawler(儿子的小名)已经可以能够独立的爬取数据,并且还有一项基于Word2Vec算法做个情感分析的小技能。

后面可能还会有未知的Exception等着解决,也有一些性能需要优化,比如和数据库的交互,数据的读写等等。但是目测年内没有太多精力放这上面了,所以今天做一个简单的总结,而且前两篇主要侧重的是功能和结果,这篇来说说JewelCrawler是如何诞生的,并将代码放到Github上(源码地址在文章最后),有兴趣的可以关注下(仅供交流学习,请勿他用,考虑下douban君。多一点真诚,少一点伤害)

环境介绍

开发工具:Intellij idea 14

数据库: Mysql 5.5 + 数据库管理工具Navicat(可用来连接查询数据库)

语言:Java

Jar包管理:Maven

版本管理:Git

目录结构

其中

  com.ansj.vec是Word2Vec算法的Java版本实现

  com.jackie.crawler.doubanmovie是爬虫实现模块,其中又包括

有些包是空的,因为这些模块还没有用上,其中

  •     constants包是存放常量类
  •     crawl包存放爬虫入口程序
  •     entity包映射数据库表的实体类
  •     test包存放测试类
  •     utils包存放工具类

resource模块存放的是配置文件和资源文件,比如

  •     beans.xml:Spring上下文的配置文件
  •     seed.properties:种子文件
  •     stopwords.dic:停用词库
  •     comment12031715.txt:爬取的短评数据
  •     tokenizerResult.txt:使用IKAnalyzer分词后的结果文件
  •     vector.mod:基于Word2Vec算法训练的模型数据

test模块是测试模块,用于编写UT.

数据库配置

1. 添加依赖的包

JewelCrawler使用的maven管理,所以只需要在pom.xml中添加相应的依赖就可以了

<dependency>

  <groupId>org.springframework</groupId>

  <artifactId>spring-jdbc</artifactId>

  <version>4.1.1.RELEASE</version>

</dependency>

<dependency>

  <groupId>commons-pool</groupId>

  <artifactId>commons-pool</artifactId>

  <version>1.6</version>

</dependency>

<dependency>

  <groupId>commons-dbcp</groupId>

  <artifactId>commons-dbcp</artifactId>

  <version>1.4</version>

</dependency>

<dependency>

  <groupId>mysql</groupId>

  <artifactId>mysql-connector-java</artifactId>

  <version>5.1.38</version>

</dependency>

<dependency>

  <groupId>mysql</groupId>

  <artifactId>mysql-connector-java</artifactId>

  <version>5.1.38</version>

</dependency>

2. 声明数据源bean

我们需要在beans.xml中声明数据源的bean

 <context:property-placeholder location="classpath*:*.properties"/>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

  <property name="driverClassName" value="${jdbc.driver}"/>

  <property name="url" value="${jdbc.url}"/>

  <property name="username" value="${jdbc.username}"/>

  <property name="password" value="${jdbc.password}"/>

</bean>

注意: 这里是绑定了外部配置文件jdbc.properties,具体数据源的参数从该文件读取。

如果遇到问题“SQL [insert into user(id) values(?)]; Field 'name' doesn't  have a default value;”解决方法是设置表的相应字段为自增长字段。

解析页面遇到的问题

对于爬到的网页数据需要解析dom结构,拿到自己想要的数据,期间遇到如下错误

org.htmlparser.Node不识别

解决方法:添加jar包依赖

<dependency>

  <groupId>org.htmlparser</groupId>

  <artifactId>htmlparser</artifactId>

  <version>1.6</version>

</dependency>

org.apache.http.HttpEntity不识别

解决方法:添加jar包依赖

<dependency>

  <groupId>org.apache.httpcomponents</groupId>

  <artifactId>httpclient</artifactId>

  <version>4.5.2</version>

</dependency> 

当然这是期间遇到的问题,最后用的是Jsoup做的页面解析。

 maven仓库下载速度慢

之前使用的是默认的maven中央仓库,下载jar包的速度很慢,不知道是我的网络问题还是其他原因,后来在网上找到了阿里云的maven仓库,更新后,相比之前简直是秒下,吐血推荐。

<mirrors>

  <mirror>

   <id>alimaven</id>

   <name>aliyun maven</name>

   <url>http://maven.aliyun.com/nexus/content/groups/public/</url>

   <mirrorOf>central</mirrorOf>    

  </mirror>

</mirrors>

找到maven的settings.xml文件,添加这个镜像即可。

读取resource模块下文件的一种方法

比如读取seed.properties文件

@Test

  public void testFile(){

    File seedFile = new File(this.getClass().getResource("/seed.properties").getPath());

    System.out.print("===========" + seedFile.length() + "===========" );

  }

有关正则表达式

使用regrex正则表达式的时候,如果匹配上了定义的Pattern,则需要先调用matcher的find方法然后才能使用group方法找到子串。直接调用group方法是没有办法找到你想要的结果的。

  我看了下上面Matcher类的源码

package java.util.regex;

import java.util.Objects;

public final class Matcher implements MatchResult {

  /**

   * The Pattern object that created this Matcher.

   */

  Pattern parentPattern;

  /**

   * The storage used by groups. They may contain invalid values if

   * a group was skipped during the matching.

   */

  int[] groups;

  /**

   * The range within the sequence that is to be matched. Anchors

   * will match at these "hard" boundaries. Changing the region

   * changes these values.

   */

  int from, to;

  /**

   * Lookbehind uses this value to ensure that the subexpression

   * match ends at the point where the lookbehind was encountered.

   */

  int lookbehindTo;

  /**

   * The original string being matched.

   */

  CharSequence text;

  /**

   * Matcher state used by the last node. NOANCHOR is used when a

   * match does not have to consume all of the input. ENDANCHOR is

   * the mode used for matching all the input.

   */

  static final int ENDANCHOR = 1;

  static final int NOANCHOR = 0;

  int acceptMode = NOANCHOR;

  /**

   * The range of string that last matched the pattern. If the last

   * match failed then first is -1; last initially holds 0 then it

   * holds the index of the end of the last match (which is where the

   * next search starts).

   */

  int first = -1, last = 0;

  /**

   * The end index of what matched in the last match operation.

   */

  int oldLast = -1;

  /**

   * The index of the last position appended in a substitution.

   */

  int lastAppendPosition = 0;

  /**

   * Storage used by nodes to tell what repetition they are on in

   * a pattern, and where groups begin. The nodes themselves are stateless,

   * so they rely on this field to hold state during a match.

   */

  int[] locals;

  /**

   * Boolean indicating whether or not more input could change

   * the results of the last match.

   *

   * If hitEnd is true, and a match was found, then more input

   * might cause a different match to be found.

   * If hitEnd is true and a match was not found, then more

   * input could cause a match to be found.

   * If hitEnd is false and a match was found, then more input

   * will not change the match.

   * If hitEnd is false and a match was not found, then more

   * input will not cause a match to be found.

   */

  boolean hitEnd;

  /**

   * Boolean indicating whether or not more input could change

   * a positive match into a negative one.

   *

   * If requireEnd is true, and a match was found, then more

   * input could cause the match to be lost.

   * If requireEnd is false and a match was found, then more

   * input might change the match but the match won't be lost.

   * If a match was not found, then requireEnd has no meaning.

   */

  boolean requireEnd;

  /**

   * If transparentBounds is true then the boundaries of this

   * matcher's region are transparent to lookahead, lookbehind,

   * and boundary matching constructs that try to see beyond them.

   */

  boolean transparentBounds = false;

  /**

   * If anchoringBounds is true then the boundaries of this

   * matcher's region match anchors such as ^ and $.

   */

  boolean anchoringBounds = true;

  /**

   * No default constructor.

   */

  Matcher() {

  }

/**

 * All matchers have the state used by Pattern during a match.

 */

Matcher(Pattern parent, CharSequence text) {

  this.parentPattern = parent;

  this.text = text;

  // Allocate state storage

  int parentGroupCount = Math.max(parent.capturingGroupCount, 10);

  groups = new int[parentGroupCount * 2];

  locals = new int[parent.localCount];

  // Put fields into initial states

  reset();

}

....

/**

 * Returns the input subsequence matched by the previous match.

 *

 * <p> For a matcher <i>m</i> with input sequence <i>s</i>,

 * the expressions <i>m.</i><tt>group()</tt> and

 * <i>s.</i><tt>substring(</tt><i>m.</i><tt>start(),</tt> <i>m.</i><tt>end())</tt>

 * are equivalent. </p>

 *

 * <p> Note that some patterns, for example <tt>a*</tt>, match the empty

 * string. This method will return the empty string when the pattern

 * successfully matches the empty string in the input. </p>

 *

 * @return The (possibly empty) subsequence matched by the previous match,

 *     in string form

 *

 * @throws IllegalStateException

 *     If no match has yet been attempted,

 *     or if the previous match operation failed

 */

public String group() {

  return group(0);

}

/**

 * Returns the input subsequence captured by the given group during the

 * previous match operation.

 *

 * <p> For a matcher <i>m</i>, input sequence <i>s</i>, and group index

 * <i>g</i>, the expressions <i>m.</i><tt>group(</tt><i>g</i><tt>)</tt> and

 * <i>s.</i><tt>substring(</tt><i>m.</i><tt>start(</tt><i>g</i><tt>),</tt> <i>m.</i><tt>end(</tt><i>g</i><tt>))</tt>

 * are equivalent. </p>

 *

 * <p> <a href="Pattern.html#cg">Capturing groups</a> are indexed from left

 * to right, starting at one. Group zero denotes the entire pattern, so

 * the expression <tt>m.group(0)</tt> is equivalent to <tt>m.group()</tt>.

 * </p>

 *

 * <p> If the match was successful but the group specified failed to match

 * any part of the input sequence, then <tt>null</tt> is returned. Note

 * that some groups, for example <tt>(a*)</tt>, match the empty string.

 * This method will return the empty string when such a group successfully

 * matches the empty string in the input. </p>

 *

 * @param group

 *     The index of a capturing group in this matcher's pattern

 *

 * @return The (possibly empty) subsequence captured by the group

 *     during the previous match, or <tt>null</tt> if the group

 *     failed to match part of the input

 *

 * @throws IllegalStateException

 *     If no match has yet been attempted,

 *     or if the previous match operation failed

 *

 * @throws IndexOutOfBoundsException

 *     If there is no capturing group in the pattern

 *     with the given index

 */

public String group(int group) {

  if (first < 0)

    throw new IllegalStateException("No match found");

  if (group < 0 || group > groupCount())

    throw new IndexOutOfBoundsException("No group " + group);

  if ((groups[group*2] == -1) || (groups[group*2+1] == -1))

    return null;

  return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString();

}

/**

 * Attempts to find the next subsequence of the input sequence that matches

 * the pattern.

 *

 * <p> This method starts at the beginning of this matcher's region, or, if

 * a previous invocation of the method was successful and the matcher has

 * not since been reset, at the first character not matched by the previous

 * match.

 *

 * <p> If the match succeeds then more information can be obtained via the

 * <tt>start</tt>, <tt>end</tt>, and <tt>group</tt> methods. </p>

 *

 * @return <tt>true</tt> if, and only if, a subsequence of the input

 *     sequence matches this matcher's pattern

 */

public boolean find() {

  int nextSearchIndex = last;

  if (nextSearchIndex == first)

    nextSearchIndex++;

  // If next search starts before region, start it at region

  if (nextSearchIndex < from)

    nextSearchIndex = from;

  // If next search starts beyond region then it fails

  if (nextSearchIndex > to) {

    for (int i = 0; i < groups.length; i++)

      groups[i] = -1;

    return false;

  }

  return search(nextSearchIndex);

}

/**

 * Initiates a search to find a Pattern within the given bounds.

 * The groups are filled with default values and the match of the root

 * of the state machine is called. The state machine will hold the state

 * of the match as it proceeds in this matcher.

 *

 * Matcher.from is not set here, because it is the "hard" boundary

 * of the start of the search which anchors will set to. The from param

 * is the "soft" boundary of the start of the search, meaning that the

 * regex tries to match at that index but ^ won't match there. Subsequent

 * calls to the search methods start at a new "soft" boundary which is

 * the end of the previous match.

 */

boolean search(int from) {

  this.hitEnd = false;

  this.requireEnd = false;

  from    = from < 0 ? 0 : from;

  this.first = from;

  this.oldLast = oldLast < 0 ? from : oldLast;

  for (int i = 0; i < groups.length; i++)

    groups[i] = -1;

  acceptMode = NOANCHOR;

  boolean result = parentPattern.root.match(this, from, text);

  if (!result)

    this.first = -1;

  this.oldLast = this.last;

  return result;

}

...

}

原因是这样的:这里如果不先调用find方法,直接调用group,可以发现group方法调用group(int group),该方法的方法体中有if first<0,显然这里这个条件是成立的,因为first的初始值就是-1,所以这里会抛异常。但是如果调用find方法,可以发现,最终会调用search(nextSearchIndex),注意这里的nextSearchIndex已被last赋值,而last的值为0,再跳转到search方法中

boolean search(int from) {

  this.hitEnd = false;

  this.requireEnd = false;

  from    = from < 0 ? 0 : from;

  this.first = from;

  this.oldLast = oldLast < 0 ? from : oldLast;

  for (int i = 0; i < groups.length; i++)

    groups[i] = -1;

  acceptMode = NOANCHOR;

  boolean result = parentPattern.root.match(this, from, text);

  if (!result)

    this.first = -1;

  this.oldLast = this.last;

  return result;

}

这个nextSearchIndex传给了from,而from在方法体中被赋值给了first,所以,调用了find方法之后,这个的first就不在是-1,也就不是抛异常了。

源码已经上传至百度网盘:http://pan.baidu.com/s/1dFwtvNz

以上说的问题比较碎,都是在遇到问题和解决问题的时候的一些总结。在具体操作的时候还会遇到其他问题,有问题或者建议的话欢迎提出来^^。

最后放几张截止目前爬取的数据

Record表

其中存储的是79032条,爬取过的网页有48471条

movie表

目前爬取了2964部影视作品

comments表

爬取了29711条记录

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • java正则表达式简单使用和网页爬虫的制作代码

    正则表达式是一种专门用于对字符串的操作的规则. 1.在String类中就有一些方法是对字符串进行匹配,切割. 判断字符串是否与给出的正则表达式匹配的:boolean matches( String regex); 按照给定的正则表达式对字符串进行切割的:String[]    split(String regex); 将符合正则表达式的字符串替换成我们想要的其他字符串:String  replaceAll(String  regex,String replacement) 2.下面介绍一下正则表

  • JAVA使用爬虫抓取网站网页内容的方法

    本文实例讲述了JAVA使用爬虫抓取网站网页内容的方法.分享给大家供大家参考.具体如下: 最近在用JAVA研究下爬网技术,呵呵,入了个门,把自己的心得和大家分享下 以下提供二种方法,一种是用apache提供的包.另一种是用JAVA自带的. 代码如下: // 第一种方法 //这种方法是用apache提供的包,简单方便 //但是要用到以下包:commons-codec-1.4.jar // commons-httpclient-3.1.jar // commons-logging-1.0.4.jar

  • 零基础写Java知乎爬虫之进阶篇

    说到爬虫,使用Java本身自带的URLConnection可以实现一些基本的抓取页面的功能,但是对于一些比较高级的功能,比如重定向的处理,HTML标记的去除,仅仅使用URLConnection还是不够的. 在这里我们可以使用HttpClient这个第三方jar包. 接下来我们使用HttpClient简单的写一个爬去百度的Demo: import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStr

  • 零基础写Java知乎爬虫之先拿百度首页练练手

    上一集中我们说到需要用Java来制作一个知乎爬虫,那么这一次,我们就来研究一下如何使用代码获取到网页的内容. 首先,没有HTML和CSS和JS和AJAX经验的建议先去W3C(点我点我)小小的了解一下. 说到HTML,这里就涉及到一个GET访问和POST访问的问题. 如果对这个方面缺乏了解可以阅读W3C的这篇:<GET对比POST>. 啊哈,在此不再赘述. 然后咧,接下来我们需要用Java来爬取一个网页的内容. 这时候,我们的百度就要派上用场了. 没错,他不再是那个默默无闻的网速测试器了,他即将

  • 零基础写Java知乎爬虫之抓取知乎答案

    前期我们抓取标题是在该链接下: http://www.zhihu.com/explore/recommendations 但是显然这个页面是无法获取答案的. 一个完整问题的页面应该是这样的链接: http://www.zhihu.com/question/22355264 仔细一看,啊哈我们的封装类还需要进一步包装下,至少需要个questionDescription来存储问题描述: import java.util.ArrayList;public class Zhihu { public St

  • java 爬虫详解及简单实例

    Java爬虫 一.代码 爬虫的实质就是打开网页源代码进行匹配查找,然后获取查找到的结果. 打开网页: URL url = new URL(http://www.cnblogs.com/Renyi-Fan/p/6896901.html); 读取网页内容: BufferedReader bufr = new BufferedReader(new InputStreamReader(url.openStream())); 正则表达式进行匹配: tring mail_regex = "\\w+@\\w+

  • 详解Java豆瓣电影爬虫——小爬虫成长记(附源码)

    以前也用过爬虫,比如使用nutch爬取指定种子,基于爬到的数据做搜索,还大致看过一些源码.当然,nutch对于爬虫考虑的是十分全面和细致的.每当看到屏幕上唰唰过去的爬取到的网页信息以及处理信息的时候,总感觉这很黑科技.正好这次借助梳理Spring MVC的机会,想自己弄个小爬虫,简单没关系,有些小bug也无所谓,我需要的只是一个能针对某个种子网站能爬取我想要的信息就可以了.有Exception就去解决,可能是一些API使用不当,也可能是遇到了http请求状态异常,又或是数据库读写有问题,就是在这

  • 详解Maven 搭建spring boot多模块项目(附源码)

    本文介绍了Maven 搭建spring boot多模块项目,分享给大家,具体如下: 备注:所有项目都在idea中创建 1.idea创建maven项目 1-1: 删除src,target目录,只保留pom.xml 1-2: 根目录pom.xml可被子模块继承,因此项目只是demo,未考虑太多性能问题,所以将诸多依赖.都写在根级`pom.xml`,子模块只需继承就可以使用. 1-3: 根级pom.xml文件在附录1 1-4: 依赖模块 mybatis spring-boot相关模块 2.创建子模块(

  • 详解Java中的OkHttp JSONP爬虫

    目录 什么是JSOUP 什么是OkHttp 爬虫需要掌握的技术 需要的依赖 JSON入门Demo JSOUP常用方法 使用JSOUP 方式连接 User-Agent(随机) 后台爬虫的三大问题 selenium+phantomjs(维护中…内容重新整理) 什么是JSOUP JSOUP 是一款Java 的HTML解析器,可直接解析某个URL地址.HTML文本内容.它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据. 官网 jsoup实现了WHATWG

  • Java中的使用及连接Redis数据库(附源码)

    引言: 本文主要分享了Redis如何在IDEA中部署,运行:模拟加入Redis的操作: 目录结构 1. 在IDEA中搭建Redis 1.1 创建项目 新建含有web的SpringBoot项目: 搭建项目参考:SpringBoot超详细笔记:https://blog.csdn.net/weixin_42601136/article/details/108396511 1.2 添加依赖(不够手动添加) <dependencies> <!-- 1. Jedis--> <depend

  • Java实现天天酷跑小游戏完整代码(附源码)

    目录 首先,写一个需求文档: 一.登录界面 1.界面 2.登录 3.退出 二.开始游戏界面 三.缓冲加载游戏界面 四.游戏主界面 五.结束界面 上代码 首先,写一个需求文档: 一.项目名称:<天天酷跑>(RunDay) 二.功能介绍: 闯关类游戏,玩家登录后,选择进入游戏,通过键盘控制玩家的上下左右移动,来躲避 障碍物和吃金币,玩家躲避的障碍物越多跑酷距离越远,玩家吃的金币越多,得分越高. 三.功能模块: 1.登录界面 用户名(输入框,明文) 密码(输入框,密文) 登录.取消按钮 2.菜单选择

  • Python7个爬虫小案例详解(附源码)中篇

    目录 前言 题目三: 分别使用XPath和Beautiful Soup4两种方式爬取并保存非异步加载的“豆瓣某排行榜”如https://movie.douban.com/top250的名称.描述.评分和评价人数等数据 题目四: 实现某东商城某商品评论数据的爬取(评论数据不少于100条,包括评论内容.时间和评分) 本次的7个python爬虫小案例涉及到了re正则.xpath.beautiful soup.selenium等知识点,非常适合刚入门python爬虫的小伙伴参考学习. 前言 关于Pyth

  • Python7个爬虫小案例详解(附源码)上篇

    目录 前言 题目一: 使用正则表达式和文件操作爬取并保存“百度贴吧”某帖子全部内容(该帖不少于5页) 题目二: 实现多线程爬虫爬取某小说部分章节内容并以数据库存储(不少于10个章节) 本次的7个python爬虫小案例涉及到了re正则.xpath.beautiful soup.selenium等知识点,非常适合刚入门python爬虫的小伙伴参考学习. 前言 关于Python7个爬虫小案例的文章分为三篇,本篇为上篇,共两题,其余两篇内容请关注! 题目一: 使用正则表达式和文件操作爬取并保存“百度贴吧

  • 详解Java如何实现小顶堆和大顶堆

    大顶堆 每个结点的值都大于或等于其左右孩子结点的值 小顶堆 每个结点的值都小于或等于其左右孩子结点的值 对比图 实现代码 public class HeapNode{ private int size;//堆大小 private int[] heap;//保存堆数组 //初始化堆 public HeapNode(int n) { heap = new int[n]; size = 0; } //小顶堆建堆 public void minInsert(int key){ int i = this.

  • 详解Java编写并运行spark应用程序的方法

    我们首先提出这样一个简单的需求: 现在要分析某网站的访问日志信息,统计来自不同IP的用户访问的次数,从而通过Geo信息来获得来访用户所在国家地区分布状况.这里我拿我网站的日志记录行示例,如下所示: 121.205.198.92 - - [21/Feb/2014:00:00:07 +0800] "GET /archives/417.html HTTP/1.1" 200 11465 "http://shiyanjun.cn/archives/417.html/" &qu

  • 详解Java中的Vector

    Vector实现了AbstractList抽象类和List接口,和ArrayList一样是基于Array存储的 Vector 是线程安全的,在大多数方法上存在synchronized关键字 //Vector存放的元素,初始化默认长度为10 protected Object[] elementData; //元素个数 protected int elementCount; //每次扩容大小,默认为0 protected int capacityIncrement; //构造函数,无指定初始化大小和

随机推荐