前言
近期工作項目有用到爬蟲,便開始學習並寫了個demo。採用的是webmagic爬蟲框架,爬取的內容有:帖子,帖子回覆,用戶主頁。項目爲springboot 1.5.7版本,提供數據持久化,前端採用echart做數據分析圖表展示。具體的技術棧如下:
- springboot 1.5.7
- springMVC+Rest+EChart...
- mybatis 3.4.6
- hikari 連接池
- webmagic 0.7.3(修改版,修復https問題與log優化 下載地址:https://download.csdn.net/download/sinat_22767969/10703880)
- mysql 5.7.17 (支持utf8mb4字符編碼)
百度貼吧的數據只能爬取到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
- 執行 db裏的sql腳本,並修改程序連接的url與用戶密碼
- 將webmagic-core-0.7.3.jar 打入本地maven倉庫,或私服倉庫中(或者修改pom依賴爲webmagic官方依賴)
- 啓動工程,訪問http://127.0.0.1:5097/swagger-ui.html ,爬蟲接口控制器,startSpider請求
數據爬取需要好一段時間,可以修改配置文件中爬取的頁數,自己決定爬多少頁的貼吧數據
spider.post.size=${SPIDER_POST_SIZE:100000}
寫在最後
後面我還爬取了太原理工大學,中北大學,清華大學~~後續再做一個對比~