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包帶的日誌包衝突,去掉即可。