java百度贴吧爬虫与高校贴吧数据分析

前言

近期工作项目有用到爬虫,便开始学习并写了个demo。采用的是webmagic爬虫框架,爬取的内容有:帖子,帖子回复,用户主页。项目为springboot 1.5.7版本,提供数据持久化,前端采用echart做数据分析图表展示。具体的技术栈如下:

百度贴吧的数据只能爬取到99999页,即不超过10万页,再往后就访问不了了。起初我是想爬取本校贴吧,看看大家都在聊啥,哪年那个帖子最火、词云等等,后来发现,百度贴吧其实风格都一样,这个项目适用爬取所有非https的贴吧,因为之前想爬取李毅吧,发现是https的,不好爬,目前正在想办法解决。希望这个项目能给初学 java 爬虫的有些帮助,大家也可以把环境搭建起来,爬取自己学校的贴吧。^_^

GitHub:https://github.com/chenchaoyun0/tbspider,觉得有用给个start哈~

效果展示

待爬虫线程停止后,访问 http://127.0.0.1:5097/ 就进入导航页。我这里爬的是母校:太原工业学院。

爬取的贴吧名:太原工业学院,帖子总数:94568,回复总数:1085097,用户总数:32754

下面看看一些简单的分析吧~~

 

数据分析

  • 帖子标题的热点词汇(看看大家发帖最频繁的词汇)

有没有、学校、学姐学长...不愧是学校的贴吧哈

  • 发帖与不发帖用户占比(潜水/只回帖用户与常发帖用户占比)

这里能看出所有吧友发帖的人、与不发帖只回复的人的比例。结果出乎我预料,我一直以为是潜水的比较多呢~~

  • 男女比例分布(吧里的男女用户比例,到底是?)

我们学校是工科学校,肯定男多女少了 唉~~,贴吧上是 8:2,当然还有一些人是不上贴吧说话我就不知道啦~~

 

  • 发帖有回复与没回复占比(石沉大海的帖子占比)

大约有百分之10的帖子,发出去没有得到回复噢~

  • 年发帖量(分析近5年来发帖量最多的哪年)

2014年到底发生了什么~2014年是我们学校贴吧发帖的鼎盛时期啊~~从2015年开始就一直在下降,其实也很容易察觉,互联网自媒体的兴起,大家都取玩抖音去啦哈哈

  • 年里的月发帖量(分析每年中,大家都喜欢在几月份发帖)

没想到的是6月和9月发帖量最多,6月份,678高考,估计是学弟学妹们蜂拥而来吧~~

  • 时发帖量(分析大家每天最爱在几点发帖)

这里统计的是2012年到2018年的数据,发现大家在晚上10点发帖是最多的。操作应该是睡前躺床上,刷一波贴吧签个到,顺便发一贴涨经验?

 

  • 年度的十大热帖(按年统计每年讨论最热的帖子)

这里不统计太多,我们就看看,2017年,还有今年2018年,最热的十大帖吧!

2017年

2018年,2018年还没有过完,统计是1月10月份,特别想看看,帖子标题为“怎么一不小心太疯狂”发了啥?

  • 十大活跃用户,按年分组(所谓的贴吧达人/大神)

这里也只统计两年的,看看有没有你在里面嘿嘿!滑稽菌大哥能看到这个帖子吗?

2017年

2018年

  • 粉丝最多的10大用户(吧里人气最高的明星)

看下吧友粉丝最多的是哪位大侠,我也有一些粉丝,不过大多买片儿的咳咳

南宫瑞谦V 是何许人,竟有1万4的粉丝~

  • 大家最喜欢关注的贴吧-词云(分析大家都喜欢关注哪些贴吧) 

不出所料~李毅吧是大家都关注的贴吧啊~,接着是太原理工,中北,考研。。。还有英雄联盟~

  • 十大发帖量最多的用户(看看哪些人最爱在贴吧发帖了)

  • 帖子回复的词云(看所有帖子下大家都在说些什么)

  • 贴吧名词云(大家最喜欢用什么词起名)

我统计过很多学校的贴吧,发现大家起名的词汇里,最多都是  “爱” 字,突然怀念青春的爱情哎~~

  • 用户设备分布(到底是苹果用户多,还是安卓用户?)

还是安卓手机的用户占了大多数啊~苹果占百分之11,小米3 和小米2s当然也归属安卓,这里没有统计进去

 

 

项目结构

项目是maven工程,spider-app 为主项目,包含爬取、落库、业务逻辑等,thread-excute为辅助项目,用来异步保存数据库,里面是曾经一同事@姚树礼写的封装的异步阻塞队列,即爬取线程跟落库线程是分开的。

表结构

共有5张表,

帖子信息表:统计吧友们的发帖

吧友信息表:统计吧友信息,贴吧名、行不、吧龄等等

回复信息表:帖子下所有回复信息

吧友关注贴吧表:统计吧友关注的贴吧

分词表:用于词云展示图表

主要代码展示

@Slf4j
public class PostProcessor implements PageProcessor {

  /**
   * 匹配帖子地址
   */
  private static final String POST_URL = "/p/\\d++";
  /**
   * 匹配贴吧首页过滤
   */
  private static final String TB_HOME = "://tieba.baidu.com/f\\?kw=(.*?)";
  /**
   * 匹配贴吧首页帖子分页
   */
  private static final String TB_HOME_PAGE = "://tieba.baidu.com/f?kw={0}&ie=utf-8&pn=";

  /**
   * 匹配用户主页
   */
  private static final String USER_HOME = "/home/main(.*?)";

  /**
   * 爬取起始页,每页为50条帖子
   */
  private long pageNo = 50;

  private BootConfig bootConfig = SpringUtils.getBean(BootConfig.class);

  private long logLoop = 0;

  /**
   * 计数
   */
  public AtomicLong totalPost = new AtomicLong();
  public AtomicLong totalComment = new AtomicLong();
  public AtomicLong totalUser = new AtomicLong();

  /**
   * 方便本地测试
   */
  public PostProcessor() {
    if (bootConfig == null) {
      bootConfig = new BootConfig();
      bootConfig.setSpiderPostSize(10);
      Constant.setSpiderHttpType("http");
      Constant.setTbName("太原工业学院");
    }
  }

  /**
   * 更换字段agent 有可能变成手机客户端,影响爬虫
   */
  private static final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101";

  /**
   * 抓取网站的相关配置,包括编码、抓取间隔、重试次数、代理、UserAgent等
   */
  private Site site = Site.me()//
      .addHeader("Proxy-Authorization", //
          ProxyGeneratedUtil.authHeader(Constant.ORDER_NUM, Constant.SECRET,
              (int) (System.currentTimeMillis() / 1000)))//
      .setDisableCookieManagement(true).setCharset("UTF-8")//
      .setTimeOut(60000)//
      .setRetryTimes(10)//
      .setSleepTime(new Random().nextInt(20) * 100)//
      .setUserAgent(userAgent);

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

  /**
   * 爬取处理
   */
  @Override
  public void process(Page page) {
    long start = System.currentTimeMillis();
    String url = page.getRequest().getUrl();
    log.debug("---> url {}", url);
    Html html = page.getHtml();
    try {
      //log
      if (logLoop++ % 30 == 0&&logLoop>1) {
        log.info("---> 当前线程【{}】,爬取URL【{}】", Thread.currentThread().getName(), url);
        log.info("---> 当前爬取论坛第【{}】页,已爬取帖子【{}】条,帖子回复【{}】,用户主页【{}】", (pageNo), totalPost.get(), totalComment.get(), totalUser.get());
        int sizePostQueue = bootConfig.getThreadPoolPost().arrayBlockingQueue.size();
        int sizeCommentQueue = bootConfig.getThreadCommentDivide().arrayBlockingQueue.size();
        int sizeUserQueue = bootConfig.getThreadUserDivide().arrayBlockingQueue.size();
        log.info("---> 当队列堆积 post【{}】,comment【{}】,user【{}】", sizePostQueue, sizeCommentQueue, sizeUserQueue);
      }
      /**
       * 若是贴吧首页则将所有帖子加入待爬取队列
       */
      if (url.matches(Constant.getSpiderHttpType() + TB_HOME)) {
        /**
         * 将所有帖子页面加入队列
         */
        //SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//postlist.html");
        List<String> listPosts = html.links().regex(POST_URL).all();
        //log.info("---> 当前页总帖数 {}", listPosts.size());
        listPosts.forEach(e -> URLGeneratedUtil.generatePostURL(e));
        page.addTargetRequests(listPosts);
      }

      /**
       * 匹配帖子页url
       */
      if (page.getUrl().regex(POST_URL).match()) {
//        SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//postDetail.html");
        String title = html.xpath("/html/head/title/text()").get();
        //有时候获取不到帖子标题
        if (title == null) {
          title = html.xpath("//*[@id=\"j_core_title_wrap\"]/h3/a/text()").toString();
        }
        if (title == null) {
          log.error("title is null...");
          //SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//title-null-post-" + UUID.randomUUID().toString() + ".html");
        }
        // 消失的帖子过滤
        if (StringUtils.isNotBlank(title) && title.indexOf("404") > 0) {
          return;
        }
        crawlPost(page, html);
      }

      /**
       * 用户主页url
       */
      if (page.getUrl().regex(USER_HOME).match()) {
//        SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//userHome.html");
        String title = html.xpath("/html/head/title/text()").get();
        if (StringUtils.isNotBlank(title) && title.indexOf("404") > 0) {
          return;
        }
        crawlUser(page, html);

      }
      /**
       * 贴吧分页,要爬的贴吧分好页,加入待爬取队列
       */
      if (url.matches(Constant.getSpiderHttpType() + TB_HOME)) {
        /**
         * 判断当前爬取页是否超过限制
         */
        if (pageNo < bootConfig.getSpiderPostSize()) {
          log.info("---------> 继续爬取第【{}】页 贴吧 <-----------", pageNo / 50);
          // 将贴吧名编码
          String tieBaName = URLEncoder.encode(Constant.getTbName(), "UTF-8");
          String match = MessageFormat
              .format(Constant.getSpiderHttpType() + TB_HOME_PAGE, tieBaName);
          page.addTargetRequest(match + pageNo);
          pageNo = pageNo + 50;
        }
      }


    } catch (Exception e) {
      log.error("PostDetailProcessor error url {}", url, e);
      String uuid = UUID.randomUUID().toString();
      //SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//" + uuid + ".html");
    } finally {
    }

  }


  /**
   * 帖子页 获取帖子数据
   */
  private void crawlPost(Page page, Html html) {
    String url = page.getRequest().getUrl();
    try {
      /**
       * 过滤不是本校贴吧
       */
      String tbName = html.xpath("//*[@id=\"container\"]/div/div[1]/div[2]/div[2]/a/text()")
          .toString();
      if (StringUtils.isNotBlank(tbName) && tbName.indexOf(Constant.getTbName()) < 0) {
        return;
      }

      // 查看该帖子有多少页
      String pageSize = html.xpath("//*[@id=\"thread_theme_5\"]/div[1]/ul/li[2]/span[2]/text()")
          .toString();
      // 将帖子的下一页加入待爬
      int size = Integer.parseInt(pageSize == null ? "0" : pageSize);
      if (size >= 2) {
        for (int i = 2; i <= size; i++) {
          if (url.indexOf("pn") < 0) {
            String urlPost = url + "?pn=" + i;
            page.addTargetRequest(urlPost);
          }
        }
      }
      /**
       * 主题信息
       */
      Post post = new Post();
      String data = html.xpath("//*[@id=\"j_p_postlist\"]/div[1]/@data-field").get();
      PostUser postUser = null;
      if (StringUtils.isNotBlank(data)) {
        postUser = JSONObject.parseObject(data, PostUser.class);
      } else {
        // 获取不到信息
        return;
      }
      String time =
          html.xpath(
              "//*[@id=\"j_p_postlist\"]/div[1]/div[3]/div[3]/div[1]/ul[2]/li[2]/span/text()")
              .toString();
      String content = html
          .xpath("//*[@id=\"post_content_" + postUser.getContent().getPost_id() + "\"]/text()")
          .toString();
      String replyNum = html.xpath("//*[@id=\"thread_theme_5\"]/div[1]/ul/li[2]/span[1]/text()")
          .toString();
      String title = html.xpath("//*[@id=\"j_core_title_wrap\"]/div[2]/h1/a/text()").toString();
      String userNickName = html.xpath("//*[@id=\"j_p_postlist\"]/div[1]/div[2]/ul/li[3]/a/text()")
          .toString();
      String userHref = html.xpath("//*[@id=\"j_p_postlist\"]/div/div[2]/ul/li[3]/a/@href").get();
      String userName = postUser.getAuthor().getUser_name();
      // 保存数据
      post.setContent(SpiderStringUtils.xffReplace(content));
      post.setPostUrl(StringUtils.substringBefore(url, "?pn="));
      post.setReplyNum(Integer.parseInt(StringUtils.isBlank(replyNum) ? "0" : replyNum));
      post.setTime(DateConvertUtils
          .parse(postUser.getContent().getDate() == null ? time : postUser.getContent().getDate(),
              DateConvertUtils.DATE_TIME_NO_SS));
      post.setTitle(SpiderStringUtils.xffReplace(title));
      post.setType(1);
      post.setUserName(SpiderStringUtils.xffReplace(userName));
      // 帖子分页不再保存
      if (url.indexOf("pn") < 0) {
        page.putField("post", post);
        totalPost.incrementAndGet();
        /**
         * 用户主页加入队列
         */
        if (userHref != null) {
          String userHome = URLGeneratedUtil.generatePostURL(userHref);
          page.addTargetRequest(userHome);
        }
      }

      /**
       * 回复信息
       */
      commentData(page, html);

    } catch (Exception e) {
      log.error("crawlPost error url {}", url, e);
      String uuid = UUID.randomUUID().toString();
      SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//" + uuid + ".html");
    }
  }


  private void commentData(Page page, Html html) {
    // 当前页回帖数量
    List<String> commentSize = html
        .xpath("//*[@id=\"j_p_postlist\"]/@class=l_post j_l_post l_post_bright/").all();
    if (CollectionUtils.isEmpty(commentSize) || commentSize.size() < 0) {
      return;
    }

    /**
     * 回复信息
     */
    List<Comment> listComment = new ArrayList<>();
    for (int i = 2; i <= commentSize.size(); i++) {
      String dataComment = html.xpath("//*[@id=\"j_p_postlist\"]/div[" + i + "]/@data-field")
          .toString();
      String userHref = html
          .xpath("//*[@id=\"j_p_postlist\"]/div[" + i + "]/div[2]/ul/li[3]/a/@href").get();
      /**
       * 用户主页加入队列
       */
      if (userHref != null) {
        String userHome = URLGeneratedUtil.generatePostURL(userHref);
        page.addTargetRequest(userHome);
      }
      //
      PostUser dataCommentPo = null;
      if (StringUtils.isNotBlank(dataComment)) {
        dataCommentPo = JSONObject.parseObject(dataComment, PostUser.class);
        //
        String contentComment = html.xpath(
            "//*[@id=\"post_content_" + dataCommentPo.getContent().getPost_id() + "\"]/text()")
            .toString();
        String userNameComment = html
            .xpath("//*[@id=\"j_p_postlist\"]/div[" + i + "]/div[2]/ul/li[3]/a/text()").toString();
        //
        Comment comment = new Comment();
        comment.setContent(SpiderStringUtils.xffReplace(contentComment));
        comment.setPostUrl(page.getUrl().toString());
        comment.setUserDevice(dataCommentPo.getContent().getOpen_type());
        comment.setTime(DateConvertUtils
            .parse(dataCommentPo.getContent().getDate(), DateConvertUtils.DATE_TIME_NO_SS));
        comment.setUserName(SpiderStringUtils.xffReplace(userNameComment));
        //
        listComment.add(comment);
      } else {
        continue;
      }
    }
    page.putField("listComment", listComment);
    totalComment.addAndGet(listComment.size());
  }

  /**
   * 帖子页 获取帖子数据
   */
  private void crawlUser(Page page, Html html) {
    String url = page.getRequest().getUrl();
    try {
      String bodyclass = html.xpath("/html/body/@class").get();
      /**
       * 某些用户被屏蔽
       */
      if (StringUtils.isNotBlank(bodyclass) && bodyclass.contains("404")) {
        return;
      }
      String userName = html.xpath("//*[@id=\"userinfo_wrap\"]/div[2]/div[3]/div/span[2]/text()")
          .toString();
      userName = StringUtils.substringAfter(userName, "用户名:");
      String fansCount = html.xpath("//*[@id=\"container\"]/div[2]/div[4]/h1/span/a/text()")
          .toString();
      String followCount = html.xpath("//*[@id=\"container\"]/div[2]/div[3]/h1/span/a/text()")
          .toString();
      String gender = html.xpath("//*[@id=\"userinfo_wrap\"]/div[2]/div[3]/div/span[1]/@class")
          .get();
      String tbAge = html
          .xpath("//*[@id=\"userinfo_wrap\"]/div[2]/div[3]/div/span[2]/span[2]/text()").toString();
      List<String> all = html.xpath("//*[@id=\"container\"]/div[1]/div/div[3]/ul/").all();
      String userHeadUrl = html.xpath("//*[@id=\"j_userhead\"]/a/img/@src").get();
      // 用户关注的贴吧
      List<String> userTiebs = html.xpath("//*[@id=\"forum_group_wrap\"]/").all();
      if (!CollectionUtils.isEmpty(userTiebs)) {
        List<UserTbs> userTbsList = new ArrayList<>();
        for (int i = 1; i <= userTiebs.size(); i++) {
          UserTbs userTbs = new UserTbs();
          String tbName = html.xpath("//*[@id=\"forum_group_wrap\"]/a[" + i + "]/span[1]/text()")
              .toString();
          if (StringUtils.isBlank(tbName)) {
            tbName = html.xpath("//*[@id=\"forum_group_wrap\"]/a[" + i + "]/span[2]/text()")
                .toString();
          }
          //
          String level = html.xpath("//*[@id=\"forum_group_wrap\"]/a[" + i + "]/span[2]/@class")
              .get();
          if (StringUtils.isBlank(level)) {
            level = html.xpath("//*[@id=\"forum_group_wrap\"]/a[3]/span[3]/@class").get();
          }
          //
          String levelInt = StringUtils.substringAfter(level, "forum_level lv");
          if (StringUtils.isBlank(levelInt)) {
            level = html.xpath("//*[@id=\"forum_group_wrap\"]/a[" + i + "]/span[4]/@class").get();
            levelInt = StringUtils.substringAfter(level, "forum_level lv");
          }
          // 实在获取不到用户关注贴吧名跳过
          if (StringUtils.isBlank(levelInt) || StringUtils.isBlank(tbName)) {
            continue;
          }

          userTbs.setTbLevel(Integer.parseInt(levelInt));
          userTbs.setTbName(tbName);
          userTbs.setUserName(SpiderStringUtils.xffReplace(userName));
          userTbsList.add(userTbs);
        }
        page.putField("userTbsList", userTbsList);
      }

      //
      User user = new User();
      user.setUserHomeUrl(page.getRequest().getUrl());
      user.setUserName(SpiderStringUtils.xffReplace(userName));
      user.setFollowCount(Integer.parseInt(StringUtils.isBlank(followCount) ? "0" : followCount));
      user.setFansCount(Integer.parseInt(StringUtils.isBlank(fansCount) ? "0" : fansCount));
      user.setGender("userinfo_sex userinfo_sex_male".equals(gender) ? 1 : 0);
      String numAge = StringUtils.substringBetween(tbAge, "吧龄:", "年");
      user.setTbAge(Double.valueOf(StringUtils.isBlank(numAge) ? "0" : numAge));
      user.setPostCount(CollectionUtils.isEmpty(all) ? 0 : all.size());
      user.setUserHeadUrl(userHeadUrl);
      //
      page.putField("user", user);
      totalUser.incrementAndGet();
    } catch (Exception e) {
      log.error("crawlUser error url {}", url, e);
      String uuid = UUID.randomUUID().toString();
      SpiderFileUtils.writeString2local(html.toString(), "E://tieb-spider//" + uuid + ".html");
    }
  }

}

主要配置项

见项目application.properties

#爬取线程
spider.thread=${SPIDER_THREAD:80}
spider.tb.name=太原工业学院
spider.run.async=${SPIDER_RUN_ASYNC:true}
#待爬取的贴吧名称
#爬取多少页帖子,百度贴吧封顶展示的就只有到9w数据,再往后也访问不了
#此配置可理解为要爬多少个帖子
spider.post.size=${SPIDER_POST_SIZE:100000}
#贴吧地址是http 还是 https,目前发现李毅吧是https还不好爬
spider.http.type=http


#datasource
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/tbspider-tygy?useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = root123

项目启动

项目代码地址为:https://github.com/chenchaoyun0/tbspider 

  1. 执行 db里的sql脚本,并修改程序连接的url与用户密码
  2. 将webmagic-core-0.7.3.jar 打入本地maven仓库,或私服仓库中(或者修改pom依赖为webmagic官方依赖)
  3. 启动工程,访问http://127.0.0.1:5097/swagger-ui.html ,爬虫接口控制器,startSpider请求

数据爬取需要好一段时间,可以修改配置文件中爬取的页数,自己决定爬多少页的贴吧数据

spider.post.size=${SPIDER_POST_SIZE:100000}

写在最后

后面我还爬取了太原理工大学,中北大学,清华大学~~后续再做一个对比~

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
为了爬取百度吧数据,我们可以使用Python的requests和BeautifulSoup库。具体步骤如下: 1. 导入requests和BeautifulSoup库 ```python import requests from bs4 import BeautifulSoup ``` 2. 构造URL并发送请求 ```python url = 'https://tieba.baidu.com/f?kw=python&ie=utf-8&pn=0' response = requests.get(url) ``` 其中,kw参数指定了要爬取的吧名称,pn参数指定了要爬取的页数。 3. 解析HTML并提取数据 ```python soup = BeautifulSoup(response.text, 'html.parser') post_list = soup.find_all('li', class_='j_thread_list clearfix') for post in post_list: title = post.find('a', class_='j_th_tit').text.strip() author = post.find('span', class_='tb_icon_author').text.strip() reply_num = post.find('span', class_='threadlist_rep_num').text.strip() print('标题:', title) print('作者:', author) print('回复数:', reply_num) ``` 其中,我们使用find_all方法找到所有的帖子,然后使用find方法找到每个帖子的标题、作者和回复数,并打印出来。 完整代码如下: ```python import requests from bs4 import BeautifulSoup url = 'https://tieba.baidu.com/f?kw=python&ie=utf-8&pn=0' response = requests.get(url) soup = BeautifulSoup(response.text, 'html.parser') post_list = soup.find_all('li', class_='j_thread_list clearfix') for post in post_list: title = post.find('a', class_='j_th_tit').text.strip() author = post.find('span', class_='tb_icon_author').text.strip() reply_num = post.find('span', class_='threadlist_rep_num').text.strip() print('标题:', title) print('作者:', author) print('回复数:', reply_num) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值