1.前言
前言: 在某大佬的公总号上看到使用Python爬取《哪吒》影评的项目小样,心痒痒,我也想用Java来搞一搞,遂动手。
项目基础: 1. JavaClient,Jsoup了解下。 2. WebMagic框架熟悉:WebMagic官网 ,挺简单的。3,老三件套ssm,mybatis用的是mybatis-plus,mysql
项目地址: 戳这里
2.项目
目标: 爬取哪吒豆瓣影评的信息→爬取信息:用户姓名 星星数量 评语 评论时间 赞同数 反对数 正文链接。一共爬取400条信息用于后期分析用途。
目标url: 哪吒豆瓣影评地址
我们看一下我们需要爬取的页面情况:巧了,我们需要爬取的信息都在一个div里,也就一个页面就拥有我们需要的20条信息,本来还想爬取用户信息,头像,地址等的,萌新不知道为啥爬虫死活进不去用户主页,就放弃了。知道的希望告知一下。我们通过使用xpath,css选择方法选择对应标签,获取文本值/属性值就ok了。
项目组成:
下面来看一下项目具体情况吧,config包,controller,log4j的配置不用看。
POM文件: 使用的springboot版本为最新的2.1.7
<dependencies>
<!--WebMagic-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--WebMagic扩展-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml配置信息:
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost/spider?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: xxx
password: xxx
//关闭驼峰转下划线
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
用户信息:
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("film_review")
public class UserInfo {
//用户姓名 星星数量 评语 评论时间 赞同数 反对数 正文链接
private int id;
private String userName;
private int starNumber;
private String comment;
private Date commentTime;
private int agreeStar;
private int againstStar;
private String textLink;
}
Mapper:
@Repository
public interface UserMapper extends BaseMapper<UserInfo> {
}
Service:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 保存UserInfo
* @param userInfo
*/
public void save(UserInfo userInfo){
userMapper.insert(userInfo);
}
/**
* 根据用户名和点赞数量来判断数据是否已存在
* 有数据,返回true,反之返回false
* @param userInfo
* @return
*/
@Transactional
public boolean judgeHaveUserInfo(UserInfo userInfo){
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userName",userInfo.getUserName())
.eq("agreeStar",userInfo.getAgreeStar());
List<UserInfo> result = userMapper.selectList(queryWrapper);
if(result.size()==0){
return false;
}else {
return true;
}
}
}
主要程序:
@Component
public class MyProcessor implements PageProcessor {
@Autowired
private MyPipeline myPipeline;
private static int time;
//设置请求配置
private Site site = Site.me()
.setRetrySleepTime(2000)
.setRetryTimes(3)
.setSleepTime(2000);
//爬虫主要的处理流程
@Override
public void process(Page page) {
Html html = page.getHtml();
//爬取信息:用户姓名 星星数量 评语 评论时间 赞同数 反对数 正文链接
//爬取评论时间
List<String> commentTime = html.css(".main-hd .main-meta", "text").all();
//爬取赞同数量
List<String> agreeStar = html.xpath("//*div[@class='main-bd']/div[3]/a[1]/span/text()").all();
//爬取反对数量
List<String> againstStar = html.xpath("//*div[@class='main-bd']/div[3]/a[2]/span/text()").all();
//爬取正文链接
List<String> textLink = html.xpath("//*div[@class='main review-item']/div/h2").links().all();
//爬取用户姓名
List<String> name = html.css(".main-hd .name", "text").all();
//爬取星星数
List<String> startNumber = html.xpath("//div[@class='main review-item']/header/span[1]/@title").all();
//爬取评论小标题
List<String> comment = html.css(".main-bd h2 a", "text").all();
List<UserInfo> userList = new ArrayList<>();
for (int i = 0; i < name.size(); i++) {
UserInfo userInfo = new UserInfo();
userInfo.setUserName(name.get(i));
//评价星星数str→int
userInfo.setStarNumber(starNumberStrToInt(startNumber.get(i)));
//String→date
userInfo.setCommentTime(strToDate(commentTime.get(i)));
//String→int
userInfo.setAgreeStar(starStrToInt(agreeStar.get(i)));
//string→int
userInfo.setAgainstStar(starStrToInt(againstStar.get(i)));
userInfo.setTextLink(textLink.get(i));
userInfo.setComment(comment.get(i));
userList.add(userInfo);
}
//输出
page.putField("userInfo", userList);
//爬取次数减一,并抓取下一页的超链接
if (--time == 0) return;
List<String> nextPage = html.css(".paginator span.next").links().all();
//添加请求
page.addTargetRequests(nextPage);
}
@Override
public Site getSite() {
return site;
}
/**
* 空字符串处理,爬取的数据可能为""
* @param str
* @return
*/
private int starStrToInt(String str) {
if (str != null && !"".equals(str.trim())) {
//则字符串不为空或空格
return Integer.parseInt(str.trim());
}else {
return 0;
}
}
/**
* 字符串日期处理
* @param str
* @return
*/
private Date strToDate(String str) {
//需要转换的字符串必须和解析的格式相对应
SimpleDateFormat smp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;
try {
date = smp.parse(str);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
/**
* 文字评价转数量
* @param startNumber
* @return
*/
private int starNumberStrToInt(String startNumber) {
switch (startNumber) {
case "很差":
return 1;
case "较差":
return 2;
case "还行":
return 3;
case "推荐":
return 4;
case "力荐":
return 5;
default:
return 0;
}
}
/**
* 爬取次数的设定
* @param time
*/
private static void setTime(int time) {
MyProcessor.time = time;
}
/**
* 开启爬虫的入口
*/
@Scheduled(initialDelay = 1000, fixedDelay = 1000 * 100000)
public void run() {
setTime(20);
System.out.println("爬取哪吒豆瓣影评开始啦--------------");
long start = System.currentTimeMillis() / 1000;
Spider.create(new MyProcessor()) //创建爬虫
.addUrl("https://movie.douban.com/subject/26794435/reviews") // 添加目标页面
.thread(5) //并发数量
.addPipeline(myPipeline) //添加自定义目标
.run(); //运行
long end = System.currentTimeMillis() / 1000;
System.out.println("该次爬取共耗费时间:" + (end - start) + "秒");
}
}
自定义输出:
/**
* 实现Pineline,重写process方法,修改数据的输出方向
*/
@Component
public class MyPipeline implements Pipeline {
@Autowired
private UserService userService ;
@Override
public void process(ResultItems resultItems, Task task) {
List<UserInfo> userInfo = (List<UserInfo>)resultItems.get("userInfo");
if (userInfo != null) {
//进行判断数据库是否已经存在该数据
for (int i = 0; i < userInfo.size(); i++) {
if (!this.userService.judgeHaveUserInfo(userInfo.get(i))){
this.userService.save(userInfo.get(i));
}
}
}
}
}
这里说一下如何运行爬虫,是在启动里面运行呢,还是搞一个监听器运行呢,还是定时运行。我这里采用的是定时运行,约等于只运行一次。
运行: 结果是这样的,下一步准备搞个前端,再搞一个可视化分析。
3.项目问题总结
- 必须打好日志,要不然都不知道哪里报错。
- date与string的装换,str与int的转换,Integer.parse(XXX)的参数接受字符串不能为null或者""。
if (str != null && !"".equals(str.trim())) {
//则字符串不为空或空格
//trim()函数的作用:取出字符串前后的一段连续空格
}else {
}
- xpath与jsoup选择语法,WebMagic官网搞定。
- 日志冲突解决,WebMagic包自带log4j日志包与springboot-web包带的日志包冲突,去掉即可。