CSDN爬虫(二)——博客列表分页爬虫+数据表设计

CSDN爬虫(二)——博客列表分页爬虫+数据库设计

说明

  • 开发环境:jdk1.7+myeclipse10.7+win74bit+mysql5.5+webmagic0.5.2+jsoup1.7.2
  • 爬虫框架:webMagic
  • 建议:建议首先阅读webMagic的文档,再查看此系列文章,便于理解,快速学习:http://webmagic.io/
  • 开发所需jar下载(不包括数据库操作相关jar包):点我下载
  • 该系列文章会省略webMagic文档已经讲解过的相关知识。

博客列表爬虫核心代码预览

    package com.wgyscsf.spider;

    import java.util.List;

    import us.codecraft.webmagic.Page;
    import us.codecraft.webmagic.Site;
    import us.codecraft.webmagic.Spider;
    import us.codecraft.webmagic.selector.Html;
    import us.codecraft.webmagic.selector.Selectable;

    import com.wgyscsf.utils.MyStringUtils;

    /**
     * @author wgyscsf 编写日期 2016-9-24下午7:25:36 邮箱 [email protected] 博客
     *         http://blog.csdn.net/wgyscsf TODO
     */
    public class CsdnBlogListSpider extends BaseSpider {
        private Site site = Site
                .me()
                .setDomain("blog.csdn.net")
                .setSleepTime(300)
                .setUserAgent(
                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31");

        @Override
        public void process(Page page) {
            // 列表页: 这里进行匹配,匹配出列表页进行相关处理。在列表页我们获取必要信息。对于全文、评论、顶、踩在文章详情中。
            if ((page.getUrl()).regex(
                    "^http://blog.csdn.net/\\w+/article/list/[0-9]*[1-9][0-9]*$")
                    .match()) {
                // 遍历出页码:遍历出div[@class=\"pagelist\"]节点下的所有超链接,该链接下是页码链接。将其加入到爬虫队列。【核心代码】
                page.addTargetRequests(page
                        .getHtml()
                        .xpath("//div[@class=\"list_item_new\"]//div[@class=\"pagelist\"]")
                        .links().all());
                // 作者
                Selectable links = page.getHtml()
                        .xpath("//div[@class=\"header\"]//div[@id=\"blog_title\"]")
                        .links();
                String blogUrl = links.get();
                String id_author = MyStringUtils.getLastSlantContent(blogUrl);
                id_author = id_author != null ? id_author : "获取作者id失败";
                // System.out.println(TAG + author);
                // 获取列表最外层节点的所有子节点。经过分析可以知道子节点有3个,“置顶文章”列表和“普通文章列表”和分页div。
                List out_div = page.getHtml()
                        .xpath("//div[@class=\"list_item_new\"]/div").all();

                // 判断是否存在置顶文章:如何div的个数为3说明存在置顶文章,否则不存在置顶文章。
                if (out_div.size() == 3) {
                    // 存在
                    processTopArticle(out_div.get(0), id_author);
                    processCommArticle(out_div.get(1), id_author);
                } else if (out_div.size() == 2) {
                    // 不存在
                    processCommArticle(out_div.get(0), id_author);
                } else {
                    System.err.println(TAG + ":逻辑出错");
                }

            } else if (page.getUrl()
                    .regex("http://blog.csdn.net/\\w+/article/details/\\w+")
                    .match()) {
                // 这里的逻辑还没处理,主要是为了获取全文、标签、顶、踩、评论等在列表页获取不到的数据
            }
        }

        /**
         * 处理普通文章列表
         */
        private void processCommArticle(String str, String id_author) {
            // 从列表页获取列表信息
            List all;
            all = new Html(str).xpath("//div[@id=\"article_list\"]/div").all();
            if (!all.isEmpty())
                for (String string : all) {
                    // 这里开始获取具体内容

                    // 单项第一部分:article_title
                    // 文章地址
                    String detailsUrl = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/@href")
                            .toString();
                    // 文章id
                    String id_blog = MyStringUtils.getLastSlantContent(detailsUrl);
                    // 文章标头
                    String title = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/text()")
                            .toString();
                    // 文章类型
                    String type = getArticleType(string);
                    // 单项第二部分:article_description
                    String summary = new Html(string).xpath(
                            "//div[@class='article_description']//text()")
                            .toString();
                    // 单项第三部分:article_manage
                    String publishDateTime = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_postdate']//text()")
                            .toString();
                    // 阅读量
                    String viewNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_view']//text()")
                            .toString();
                    viewNums = MyStringUtils.getStringPureNumber(viewNums);
                    // 评论数
                    String commentNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_comments']//text()")
                            .toString();
                    commentNums = MyStringUtils.getStringPureNumber(commentNums);
                    // 开始组织数据
                    System.out.println(TAG + ":,文章id:" + id_blog + ",文章标头:" + title
                            + ",文章类型('0':原创;'1':转载;'2':翻译):" + type + ",发表日期:"
                            + publishDateTime + ",阅读量:" + viewNums + ",评论数:"
                            + commentNums + ",文章地址:" + detailsUrl + ",文章摘要:"
                            + summary + "");

                }

        }

        /**
         * 处理置顶文章列表
         */
        private void processTopArticle(String topListDiv, String id_author) {
            // 从列表页获取列表信息
            List all;
            all = new Html(topListDiv).xpath("//div[@id=\"article_toplist\"]/div")
                    .all();
            if (!all.isEmpty())
                for (String string : all) {
                    // 单项第一部分:article_title
                    // 文章地址
                    String detailsUrl = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/@href")
                            .toString();
                    // 文章id
                    String id_blog = MyStringUtils.getLastSlantContent(detailsUrl);
                    // 文章标头
                    String title = new Html(string)
                            .xpath("//div[@class='article_title']//span[@class='link_title']//a/text()")
                            .toString();
                    // 文章类型
                    String type = getArticleType(string);
                    // 单项第二部分:article_description
                    String summary = new Html(string).xpath(
                            "//div[@class='article_description']//text()")
                            .toString();
                    // 单项第三部分:article_manage
                    String publishDateTime = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_postdate']//text()")
                            .toString();
                    // 阅读量
                    String viewNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_view']//text()")
                            .toString();
                    viewNums = MyStringUtils.getStringPureNumber(viewNums);
                    // 评论数
                    String commentNums = new Html(string)
                            .xpath("//div[@class='article_manage']//span[@class='link_comments']//text()")
                            .toString();
                    commentNums = MyStringUtils.getStringPureNumber(commentNums);

                    // 开始组织数据
                    System.out.println(TAG + ":,文章id:" + id_blog + ",文章标头:" + title
                            + ",文章类型('0':原创;'1':转载;'2':翻译):" + type + ",发表日期:"
                            + publishDateTime + ",阅读量:" + viewNums + ",评论数:"
                            + commentNums + ",文章地址:" + detailsUrl + ",文章摘要:"
                            + summary + "");

                }
        }

        /**
         * 获取文章类型
         */
        private String getArticleType(String string) {
            String type;
            type = new Html(string)
                    .xpath("//div[@class='article_title']//span[@class='ico ico_type_Original']//text()")
                    .get();// 原创类型
            if (type != null)
                return 0 + "";
            type = new Html(string)
                    .xpath("//div[@class='article_title']//span[@class='ico ico_type_Repost']//text()")
                    .get();// 原创类型
            if (type != null)
                return 1 + "";
            type = new Html(string)
                    .xpath("//div[@class='article_title']//span[@class='ico ico_type_Translated']//text()")
                    .get();// 原创类型
            if (type != null)
                return 2 + "";
            return 3 + "";
        }

        @Override
        public Site getSite() {
            return site;
        }

        public static void main(String[] args) {
            Spider.create(new CsdnBlogListSpider())
                    .addPipeline(null)
                    .addUrl("http://blog.csdn.net/" + "wgyscsf" + "/"
                            + "article/list/1").run();
        }

    }

关键代码解释

  • page.getUrl()).regex("^http://blog.csdn.net/\\w+/article/list/[0-9]*[1-9][0-9]*$").match() ,每次正则表达式都是一个难点。这句话的意思是:http://blog.csdn.net/用户id/article/list/页码,这个网址包含两个可变字符串:用户id和页码,这个正则主要是为了匹配这个规则。如果是爬取单个用户就不用这个麻烦。但是,后期如果我们有很多用户id,这样写才能更加方便的去爬取任意用户。
  • page.addTargetRequests(page.getHtml().xpath("//div[@class=\"list_item_new\"]//div[@class=\"pagelist\"]").links().all())这句话是整个博客列表爬取的核心。它负责找到个页面下所有有效的列表链接,加入到爬虫队列,到爬虫队列,还会走上面所提到的正则进行匹配,加入到列表页的解析,是一个迭代的过程。当然,如果想要更加严谨,也可以做一个正则的匹配,比如我只抓取div[@class=\"pagelist\"]下符合页表页规则的链接。当然,这里是进行了分析,里面只包含有效链接,就没有再进行判断。页码所在的div如下:

    
    
    49条 共4页 首页 上一页 1 2 3 4 下一页 尾页
  • page.getHtml().xpath("//div[@class=\"list_item_new\"]/div").all();这里需要注意的是,需要判断返回List的个数,进而判断是否存在“置顶文章”。如果存在先处理“置顶文章”逻辑,再处理普通文章逻辑。经过分析可以知道子节点有3个,“置顶文章”列表div和“普通文章”列表div和分页div。

  • getArticleType(String string)特别注意这个方法里面关于文章类型的处理,如何判断是何种类型的文章,可以尝试获取对应文章所处的div,如果返回不为null,即说明存在,否则不存在。大致如下:

    String type;
    type = new Html(string).xpath("//div[@class='article_title']//span[@class='ico ico_type_Original']//text()").get();// 原创类型
    if (type != null)
        return 0 + "";// 说明是原创
    

数据库设计

设计原则

  • 全部字段允许为空,包括相关所属唯一id。另外新建一个id,随机生成UUID,作为主键。原因:因为爬虫可能会出现爬不到的数据,或者“脏数据”,所以尽可能使数据库不那么“严谨”,保证程序能够正常走下去。在表中新建一个id作为主键,这个可以保证在获取相应表id失败的情况下,仍然可以正常运行。
  • 每个表的所属id(不是新建的),比如作者id、文章id、个人id等可以标示一行数据的字段,全部加上【索引】。原因:后期需要保存大量用户以及文章数据,保存之前我们需要拿获取到表的id去查询数据库中是否存在,需要有一个查询的过程。查询是一个遍历的过程,加与不加【索引】对程序影响巨大。简单测试如下(不带索引与带索引的查询时间,数据量:80W):
    CSDN爬虫(二)——博客列表分页爬虫+数据表设计_第1张图片
    CSDN爬虫(二)——博客列表分页爬虫+数据表设计_第2张图片
  • 尽可能加上所有直接相关的所有字段,不管现有爬虫技术是否能实现。并且,再加上必要的备用字段。原因:数据表修改麻烦,尽可能后期不要修改。不能直接过去的数据,可能会在其它模块获取,比如“博客详情”与“博客列表”之间的关系。
  • 不使用外键,原因:同第一条。
  • 主键不用自增的,而是采用手动生成。

表结构

CSDN爬虫(二)——博客列表分页爬虫+数据表设计_第3张图片

建表语句

  • 在操作代码中附带

操作代码

点我下载

个人公众号,及时更新技术文章(请移步公众号,文章会被官方删除)

你可能感兴趣的:(爬虫)