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}

寫在最後

後面我還爬取了太原理工大學,中北大學,清華大學~~後續再做一個對比~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章