maven+webmagic爬虫小记

Posted by HWHXY Blog on January 20, 2019

前言

小姑娘的毕设和爬虫有关,是企业工程实习正好使用java进行一下爬虫处理并进行入库操作,之前很少接触java,正好这次也学习学习,在摸索的过程中填一填入门的坑,方便后人进行思考。

0x01 what is maven?

可以参考https://my.oschina.net/huangyong/blog/194583,这篇介绍的很好,简单的来说,maven project和正常的java project有什么区别呢?一句话总结就是统一的工具和jar包,而统一管理,都总结在pom.xml这个配置文件中,你想要什么,就写进去,工具和jar包会缓存在本地仓库。

0x02 what is webmagic?

轻量级爬虫框架,把url和xpath丢进去,就能给你提取信息自动循环,只需要你做好条件控制就能轻易的写出一个爬虫程序! 具体介绍参考http://webmagic.io/docs/zh/posts/ch1-overview/
也就是说我们要考虑的事情很少,就能做出一些很实际的东西。

0x03 代码编写

我想扒下来一个博客网站热门板块的所有文章,并且要求得到的数据入库。

1. maven 项目

关于maven项目的创建,eclipse集成了maven,要创建的时候直接选择maven即可。

2. pom.xml

这是pom.xml配置文件存在很多坑,其中有一个就是mysql的版本问题,由于我的mysql版本是8.0.1,所以pom里面也要说明,us.codecraft是webmagic的公开库,可以直接引用。保存之后,eclipse后台会自动缓存需要的jar包和工具,所以很方便。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>rencai</groupId>
  <artifactId>rencaia</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <dependencies>
  <dependency>
      <groupId>us.codecraft</groupId>
      <artifactId>webmagic-core</artifactId>
      <version>0.6.1</version>
</dependency>
<dependency>
      <groupId>us.codecraft</groupId>
      <artifactId>webmagic-extension</artifactId>
      <version>0.6.1</version>
</dependency>
<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.11</version>
</dependency>
  </dependencies>
</project>

如上是我工程的pom.xml的代码。

3. 爬虫核心代码

3.1 数据结构

数据结构是第一部分 在处理爬虫上,既然我们使用了这个框架,爬虫的部分对我们来说就是轮子,也就是us.codecraft直接能够调用的,Spider.create(),很方便。具体代码借鉴https://blog.csdn.net/little_skeleton/article/details/80996568,毕竟是学习的过程,所以很多code借鉴学习即可。比如我们现在要爬去一个网站的文章,首先我们要清楚我们要爬去的文章的结构是什么,所以我们要定义一个结构体,存储我们爬去的文章。以我们这个项目为例,将爬去的文章结构体定义为:
url、title、author、readNum...
定义一个bloginfo类。如下:

package rencaiwang;
 
public class BlogInfo {
 
    private String url;
    private String title;
    private String author;
    private String readNum;
    private String recommendNum;
    private String blogHomeUrl;
    private String commentNum;
    private String publishTime;
    private String content;
 
    public String getUrl() {
        return url;
    }
 
    public void setUrl(String url) {
        this.url = url;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    public String getAuthor() {
        return author;
    }
 
    public void setAuthor(String author) {
        this.author = author;
    }
 
    public String getReadNum() {
        return readNum;
    }
 
    public void setReadNum(String readNum) {
        this.readNum = readNum;
    }
 
    public String getRecommendNum() {
        return recommendNum;
    }
 
    public void setRecommendNum(String recommendNum) {
        this.recommendNum = recommendNum;
    }
 
    public String getBlogHomeUrl() {
        return blogHomeUrl;
    }
 
    public void setBlogHomeUrl(String blogHomeUrl) {
        this.blogHomeUrl = blogHomeUrl;
    }
 
    public String getCommentNum() {
        return commentNum;
    }
 
    public void setCommentNum(String commentNum) {
        this.commentNum = commentNum;
    }
 
    public String getPublishTime() {
        return publishTime;
    }
 
    public void setPublishTime(String publishTime) {
        this.publishTime = publishTime;
    }
 
    public String getContent() {
        return content;
    }
 
    public void setContent(String content) {
        this.content = content;
    }
}

3.2 爬虫代码

爬虫代码是包含main函数的主要代码,代码如下:

package rencaiwang;
 
import rencaiwang.BlogDao;
import rencaiwang.BlogDaoImpl;
import rencaiwang.BlogInfo;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;


import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * Created by HWHXY
 */
public class BlogPageProcessor implements PageProcessor {
    //抓取网站的相关配置,包括:编码、抓取间隔、重试次数等
    private Site site = Site.me().setRetryTimes(10).setSleepTime(1000);
    //博文数量
    private static int num = 0;
    //数据库持久化对象,用于将博文信息存入数据库
    private BlogDao blogDao = new BlogDaoImpl();
 
    public static void main(String[] args) throws Exception {
        long startTime, endTime;
        System.out.println("========天善最热博客小爬虫【启动】喽!=========");
        startTime = new Date().getTime();
        Spider.create(new BlogPageProcessor()).addUrl("https://blog.hellobi.com/hot/weekly?page=1").thread(5).run();
        endTime = new Date().getTime();
        System.out.println("========天善最热博客小爬虫【结束】喽!=========");
        System.out.println("一共爬到" + num + "篇博客!用时为:" + (endTime - startTime) / 1000 + "s");
    }
 
    @Override
    public void process(Page page) {
        //1. 如果是博文列表页面 【入口页面】,将所有博文的详细页面的url放入target集合中。
        // 并且添加下一页的url放入target集合中。
        if (page.getUrl().regex("https://blog\\.hellobi\\.com/hot/weekly\\?page=\\d+").match()) {
            //目标链接
            page.addTargetRequests(page.getHtml().xpath("//h2[@class='title']/a").links().all());
            //下一页博文列表页链接
            page.addTargetRequests(page.getHtml().xpath("//a[@rel='next']").links().all());
        }
        //2. 如果是博文详细页面
        else {
//            String content1 = page.getHtml().get();
            try {
                /*实例化BlogInfo,方便持久化存储。*/
                BlogInfo blog = new BlogInfo();
                //博文标题
                String title = page.getHtml().xpath("//h1[@class='clearfix']/a/text()").get();
                //博文url
                String url = page.getHtml().xpath("//h1[@class='clearfix']/a/@href").get();
                //博文作者
                String author = page.getHtml().xpath("//section[@class='sidebar']/div/div/a[@class='aw-user-name']/text()").get();
                //作者博客地址
                String blogHomeUrl = page.getHtml().xpath("//section[@class='sidebar']/div/div/a[@class='aw-user-name']/@href").get();
                //博文内容,这里只获取带html标签的内容,后续可再进一步处理
                String content = page.getHtml().xpath("//div[@class='message-content editor-style']/p").get();
                //推荐数(点赞数)
                String recommendNum = page.getHtml().xpath("//a[@class='agree']/b/text()").get();
                //评论数
                String commentNum = page.getHtml().xpath(
                		"//div[@class='aw-mod']/div/h2/text()").get().split("个")[0].trim();
                //阅读数(浏览数)
                String readNum = page.getHtml().xpath("//div[@class='row']/div/div/div/div/span/text()").get().split(":")[1].trim();
                //发布时间,发布时间需要处理,这一步获取原始信息
                String time = page.getHtml().xpath("//time[@class='time']/text()").get().split(":")[1].trim();
                SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
                Calendar cal = Calendar.getInstance();// 取当前日期。
                cal = Calendar.getInstance();
                String publishTime = null;
                Pattern p = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$");
                Matcher m = p.matcher(time);
                //如果time是“YYYY-mm-dd”这种格式的,则不需要处理
                if (m.matches()) {
                    publishTime = time;
                } else if (time.contains("天")) { //如果time包含“天”,如1天前,
                    int days = Integer.parseInt(time.split("天")[0].trim());//则获取对应的天数
                    cal.add(Calendar.DAY_OF_MONTH, -days);// 取当前日期的前days天.
                    publishTime = df.format(cal.getTime());  //并将时间转换为“YYYY-mm-dd”这个格式
                } else {//time是其他格式,如几分钟前,几小时前,都为当日日期
                    publishTime = df.format(cal.getTime());
                }
                //对象赋值
                blog.setUrl(url);
                blog.setTitle(title);
                blog.setAuthor(author);
                blog.setBlogHomeUrl(blogHomeUrl);
                blog.setCommentNum(commentNum);
                blog.setRecommendNum(recommendNum);
                blog.setReadNum(readNum);
                blog.setContent(content);
                blog.setPublishTime(publishTime);
                num++;//博文数++
 
                System.out.println("num:" + num + " " + blog.toString());//输出对象
                blogDao.saveBlog(blog);//保存博文信息到数据库
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 
    @Override
    public Site getSite() {
        return this.site;
    }
 
}

3.3 数据库操作

在我们爬这样的结构中,要执行入库操作怎么做呢? 上面有一个blogDao.saveBlog(blog);//保存博文信息到数据库这样的语句,所以我们要定义一个入库的函数。

package rencaiwang;
 
import rencaiwang.BlogInfo;
import rencaiwang.BlogDao;
import rencaiwang.DBHelper;
 
import java.util.ArrayList;
import java.util.List;
 
public class BlogDaoImpl implements BlogDao {
 
    @Override
    public int saveBlog(BlogInfo blog) {
        DBHelper dbhelper = new DBHelper();
        StringBuffer sql = new StringBuffer();
        sql.append("INSERT INTO hot_weekly_blogs(url,title,author,readNum,recommendNum,blogHomeUrl,commentNum,publishTime,content)")
                .append("VALUES (? , ? , ? , ? , ? , ? , ? , ? , ? ) ");
        //设置 sql values 的值
        List<String> sqlValues = new ArrayList<String>();
        sqlValues.add(blog.getUrl());
        sqlValues.add(blog.getTitle());
        sqlValues.add(blog.getAuthor());
        sqlValues.add(""+blog.getReadNum());
        sqlValues.add(""+blog.getRecommendNum());
        sqlValues.add(blog.getBlogHomeUrl());
        sqlValues.add(""+blog.getCommentNum());
        sqlValues.add(blog.getPublishTime());
        sqlValues.add(blog.getContent());
        int result = dbhelper.executeUpdate(sql.toString(), sqlValues);
        return result;
    }
}


简单的mysql语句。 为了方便管理,我们可以写个主类:

package rencaiwang;
 
import rencaiwang.BlogInfo;
 
public interface BlogDao {
	
    public int saveBlog(BlogInfo blog);
 
}


3.4 连接数据库

连接数据库:

package rencaiwang;
 
import java.sql.*;
import java.util.List;
 
public class DBHelper {
    public static final String driver_class = "com.mysql.jdbc.Driver";
    public static final String driver_url = "jdbc:mysql://127.0.0.1:3306/yourdb?useUnicode=true&characterEncoding=utf8&useSSL=false";
    public static final String user = "root";
    public static final String password = "yourpassword";
    private static Connection conn = null;
    private PreparedStatement pst = null;
    private ResultSet rst = null;
 
    /**
     * Connection
     */
    public DBHelper() {
        try {
            conn = DBHelper.getConnInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 单例模式
     * 线程同步
     *
     * @return
     */
    private static synchronized Connection getConnInstance() {
        if (conn == null) {
            try {
                Class.forName(driver_class);
                conn = DriverManager.getConnection(driver_url, user, password);
                System.out.println("连接数据库成功");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return conn;
    }
 
    /**
     * close
     */
    public void close() {
 
        try {
            if (conn != null) {
                DBHelper.conn.close();
            }
            if (pst != null) {
                this.pst.close();
            }
            if (rst != null) {
                this.rst.close();
            }
            System.out.println("关闭数据库成功");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * query
     *
     * @param sql
     * @param sqlValues
     * @return ResultSet
     */
    public ResultSet executeQuery(String sql, List<String> sqlValues) {
        try {
            pst = conn.prepareStatement(sql);
            if (sqlValues != null && sqlValues.size() > 0) {
                setSqlValues(pst, sqlValues);
            }
            rst = pst.executeQuery();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return rst;
    }
 
    /**
     * update
     *
     * @param sql
     * @param sqlValues
     * @return result
     */
    public int executeUpdate(String sql, List<String> sqlValues) {
        int result = -1;
        try {
            pst = conn.prepareStatement(sql);
            if (sqlValues != null && sqlValues.size() > 0) {
                setSqlValues(pst, sqlValues);
            }
            result = pst.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
 
        return result;
    }
 
 
    private void setSqlValues(PreparedStatement pst, List<String> sqlValues) {
        for (int i = 0; i < sqlValues.size(); i++) {
            try {
                pst.setObject(i + 1, sqlValues.get(i));
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

上面照搬就行。轮子。

3.5 mysql table创建

为了方便数据库table的创建,直接给出table语句方便复现:

CREATE TABLE `hot_weekly_blogs` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `url` VARCHAR(100) DEFAULT NULL,
  `title` VARCHAR(100) DEFAULT NULL,
  `author` VARCHAR(50) DEFAULT NULL,
  `readNum` INT(11) DEFAULT NULL,
  `recommendNum` INT(11) DEFAULT NULL,
  `blogHomeUrl` VARCHAR(100) DEFAULT NULL,
  `commentNum` INT(11) DEFAULT NULL,
  `publishTime` VARCHAR(20) DEFAULT NULL,
  `content` MEDIUMTEXT,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=69 DEFAULT CHARSET=utf8;

直接执行上述结构就能创建table

0x04 最后的结果

代码结构 运行结果