前言
近期工作项目有用到爬虫,便开始学习并写了个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}
写在最后
后面我还爬取了太原理工大学,中北大学,清华大学~~后续再做一个对比~