前言
閑來無事瀏覽GitHub的時候,看到一個倉庫,里邊列舉了Java的優(yōu)秀開源項(xiàng)目列表,包括說明、倉庫地址等,還是很具有學(xué)習(xí)意義的。但是大家也知道,國內(nèi)訪問GitHub的時候,經(jīng)常存在訪問超時的問題,于是就有了這篇文章,每日自動把這些數(shù)據(jù)爬取下來,隨時看到熱點(diǎn)排行。
倉庫地址:https://github.com/akullpp/awesome-java
倉庫頁面截圖:
分析
根據(jù)以往爬蟲經(jīng)驗(yàn),先確定好思路,再開始開發(fā)代碼效率會更高。那么,第一步,找一下我們的數(shù)據(jù)來源。
具體步驟:先開啟F12,刷新網(wǎng)頁,根據(jù)關(guān)鍵詞搜索,看數(shù)據(jù)來源是哪個接口(此處以列表里的Maven為例,其他也可以)
可以看到,項(xiàng)目列表都是來源于這個.md文檔的1250行,可以看到,這是一個標(biāo)準(zhǔn)的JSON數(shù)據(jù),我們把這行數(shù)據(jù)復(fù)制出來進(jìn)行分析(由于數(shù)據(jù)太長,不做展示),繼續(xù)搜索后發(fā)現(xiàn),我們需要的項(xiàng)目列表和說明,都在其中richText字段里,如下:
而這個富文本數(shù)據(jù)都是Unicode編碼,為了方便查看結(jié)構(gòu),我們將其轉(zhuǎn)為中文,可以用如下的正則匹配,批量轉(zhuǎn)換
richData = richData.replaceAll("/\\\\u([0-9a-f]{3,4})/i", "&#x\\1;");
轉(zhuǎn)換完之后繼續(xù)看這個富文本數(shù)據(jù)
我們需要的東西對應(yīng)的是一個一個的<li>
標(biāo)簽和<a>
標(biāo)簽,找到數(shù)據(jù)源之后就可以正式開始開發(fā)了。
項(xiàng)目開發(fā)
1、準(zhǔn)備工作
- 開發(fā)框架選擇SpringBoot,持久層框架使用MyBatis。除必要的基礎(chǔ)依賴以外,還需要引入以下依賴:
jsoup:對網(wǎng)頁結(jié)構(gòu)分析,解析數(shù)據(jù)
okhttp:HTTP客戶端,訪問頁面使用。
fastjson:解析JSON數(shù)據(jù) - 關(guān)系型數(shù)據(jù)庫選擇Mysql,非關(guān)系型數(shù)據(jù)庫選擇Redis
- 編輯配置文件
2、項(xiàng)目列表解析代碼開發(fā)
根據(jù)前期分析的思路,首先使用okhttp客戶端,訪問https://github.com/akullpp/awesome-java/blob/master/README.md
頁面,獲取到響應(yīng)正文。
public String getPage(String url) {
try {
// 1.創(chuàng)建okhttp客戶端對象
OkHttpClient okHttpClient = new OkHttpClient();
// 2.創(chuàng)建request對象 (用Request的靜態(tài)類創(chuàng)建)
Request request = new Request.Builder().url(url).build();
// 3.創(chuàng)建一個Call對象,負(fù)責(zé)進(jìn)行一次網(wǎng)絡(luò)訪問操作
Call call = okHttpClient.newCall(request);
// 4.發(fā)送請求到服務(wù)器,獲取到response對象
Response response = call.execute();
// 5.判斷響應(yīng)是否成功
if (!response.isSuccessful()) {
System.out.println("請求失?。?);
return null;
}
return response.body().string();
}catch (Exception e){
log.error("請求頁面出錯:{}",e.getMessage());
return null;
}
}
獲取到正文后如圖所示:
接著我們使用Jsoup對網(wǎng)頁結(jié)構(gòu)進(jìn)行解析,因?yàn)樾枰臄?shù)據(jù)處于<Script>
標(biāo)簽,因此我們只提取這個標(biāo)簽數(shù)據(jù)即可,代碼為:
Document document = Jsoup.parse(html);
// 2.使用 getElementsByTag,拿到所有的標(biāo)簽 elements相當(dāng)于集合類。每個element對應(yīng)一個標(biāo)簽
Elements elements = document.getElementsByTag("script");
提取之后效果如圖:
需要的數(shù)據(jù)在列表最后一位,取到之后因其是HTML語法,我們需要將其處理轉(zhuǎn)為標(biāo)準(zhǔn)JSON,然后根據(jù)第一步分析的結(jié)果,根據(jù)key提取richText所在的值,并將Unicode轉(zhuǎn)為中文。
String li = elements.get(elements.size()-1).toString()
.replace("<script type=\"application/json\" data-target=\"react-app.embeddedData\">","")
.replace("</script>","");
JSONObject pageRes = JSONObject.parseObject(li);
String richData = pageRes.getJSONObject("payload").getJSONObject("blob").getString("richText");
richData = richData.replaceAll("/\\\\u([0-9a-f]{3,4})/i", "&#x\\1;");
處理結(jié)果為:
轉(zhuǎn)換完的字符串還是標(biāo)準(zhǔn)的HTML語法,繼續(xù)用Jsoup解析結(jié)構(gòu),獲取到所有的<li>
標(biāo)簽和<a>
標(biāo)簽
將需要的數(shù)據(jù)提取出來,再根據(jù)提取出來的數(shù)據(jù)繼續(xù)爬取項(xiàng)目詳情頁,格式為:https://github.com/作者名/倉庫名(因代碼基本一致,此處不再贅述),獲取項(xiàng)目對應(yīng)的StartCount、forkCount、IssuesCount,轉(zhuǎn)換為數(shù)據(jù)庫實(shí)體對象并存儲即可。
3、定時任務(wù)
編寫定時任務(wù)代碼,每天三點(diǎn)執(zhí)行爬取任務(wù),因?yàn)榭赡艽嬖谶B接超時,因此增加五十次失敗重試。執(zhí)行結(jié)束后不管成功失敗,微信推送執(zhí)行結(jié)果
private static String PageUrl = "https://github.com/akullpp/awesome-java/blob/master/README.md";
//[秒] [分] [小時] [日] [月] [周]
@Scheduled(cron = "0 0 3 * * ?")
public void crawlerTaskFunction() throws InterruptedException {
// 1.獲取入口頁面
int count = 1;
String html = crawlerService.getPage(PageUrl);
if(html == null){
//如果失敗,重試五十次,間隔五秒
for (int i = 0; i < 50; i++) {
Thread.sleep(5000L);
count++;
log.error("抓取頁面失敗,正在第 {} 次重新嘗試",i+1);
html = crawlerService.getPage(PageUrl);
if(html != null){
break;
}
}
if(html == null){
log.error("抓取頁面失敗,正在發(fā)送失敗消息!");
JSONObject re = new JSONObject();
re.put("本次重試次數(shù):", 50);
re.put("時間:", MyUtils.nowTime());
//微信推送執(zhí)行結(jié)果消息
System.out.println(MyUtils.sendMsgNoUrl(re,MsgToken,"今日任務(wù)執(zhí)行失敗,請手動調(diào)用接口重新爬取!"));
return;
}
}
// 2.解析入口頁面,獲取項(xiàng)目列表
List<ProjectDTO> projects = crawlerService.parseProjectList(html);
//發(fā)送成功消息
log.info("抓取頁面完成,開始解析!");
JSONObject re = new JSONObject();
re.put("時間:", MyUtils.nowTime());
re.put("本次重試次數(shù):", count);
re.put("本次項(xiàng)目總數(shù):", projects.size());
//微信推送執(zhí)行結(jié)果消息
System.out.println(MyUtils.sendMsgNoUrl(re,MsgToken,"任務(wù)執(zhí)行成功,請去查看效果!"));
if (CollectionUtils.isEmpty(projects)) {
return;
}
// 3.遍歷項(xiàng)目列表,利用線程池實(shí)現(xiàn)多線程
// executorService提交任務(wù):1)submit 有返回結(jié)果 2)execute 無返回結(jié)果
// 此處使用submit是為了得知是否全部遍歷結(jié)束,方便進(jìn)行存到數(shù)據(jù)庫操作
ExecutorService executorService = Executors.newFixedThreadPool(10); //固定大小10的線程池
List<Future<?>> taskResults = new ArrayList<>();
// for (int i = 0; i < 10; i++) {
for (int i = 0; i < projects.size(); i++) {
ProjectDTO project = projects.get(i);
Future<?> taskResult = executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("crawling " + project.getName() + ".....");
String repoName = getRepoName(project.getUrl());
String jsonString = crawlerService.getRepo(repoName);
// 解析項(xiàng)目數(shù)據(jù)
parseRepoInfo(jsonString, project);
System.out.println("crawling " + project.getName() + "done !");
} catch (Exception e) {
e.printStackTrace();
}
}
});
taskResults.add(taskResult);
}
// 等待所有任務(wù)執(zhí)行結(jié)束,再進(jìn)行下一步
for (Future<?> taskResult : taskResults) {
try {
// 調(diào)用get會阻塞,直到該任務(wù)執(zhí)行完畢,才會返回
if (taskResult != null) taskResult.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
//代碼到這里,說明所有任務(wù)都執(zhí)行結(jié)束,結(jié)束線程池
executorService.shutdown();
// 4.保存到數(shù)據(jù)庫
crawlerService.batchSave(projects);
}
4、前端調(diào)用接口開發(fā)
對前端開放兩個接口,一個為數(shù)據(jù)庫數(shù)據(jù)的日期列表接口,一個根據(jù)日期查詢當(dāng)日數(shù)據(jù)接口,同時對參數(shù)進(jìn)行非空驗(yàn)證
@GetMapping("/list")
public JSONObject verifySign(@RequestParam("time") String time) {
JSONObject resp = new JSONObject();
if(StringUtils.isEmpty(time) || time.equals("null")){
resp.put("code",400);
resp.put("data",null);
resp.put("msg","time 參數(shù)錯誤!");
return resp;
}
resp.put("code",200);
resp.put("msg","請求成功");
resp.put("data",crawlerService.getListByTime(time));
return resp;
}
@GetMapping("/timeList")
public JSONObject timeList() {
JSONObject resp = new JSONObject();
resp.put("code",200);
resp.put("msg","請求成功");
resp.put("data",crawlerService.timeList());
return resp;
}
在根據(jù)日期查詢當(dāng)日數(shù)據(jù)的接口中,因其每日的數(shù)據(jù)都是固定的,因此添加redis緩存,提高性能
String redisKey = "crawler_"+time;
boolean containsKey = redisUtils.containThisKey(redisKey);
if(containsKey){
String value = redisUtils.get(redisKey);
return JSONObject.parseArray(value,ProjectDTO.class);
}
List<ProjectDTO> list = crawlerMapper.getListByTime(time);
redisUtils.set(redisKey,JSONObject.toJSONString(list));
return list;
其中redisUtils
為自己寫的Redis工具類,具體代碼如下:文章來源:http://www.zghlxwxcb.cn/news/detail-734018.html
package com.simon.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Autowired
public StringRedisTemplate redisTemplate;
public String get(String key){
if(StringUtils.isEmpty(key)){
return null;
}
return redisTemplate.opsForValue().get(key);
}
public boolean set(String key,String value){
if(StringUtils.isEmpty(key) || StringUtils.isEmpty(value)){
return false;
}
redisTemplate.opsForValue().set(key,value);
return true;
}
public boolean setTimeOut(String key,String value,Long timeOut){
if(StringUtils.isEmpty(key) || StringUtils.isEmpty(value)){
return false;
}
redisTemplate.opsForValue().set(key,value,timeOut, TimeUnit.SECONDS);
return true;
}
public boolean delete(String key){
if(StringUtils.isEmpty(key) ){
return false;
}
Boolean isDelete = redisTemplate.delete(key);
return isDelete != null ? isDelete : false;
}
public boolean containThisKey(String key){
if(StringUtils.isEmpty(key) ){
return false;
}
Boolean hasKey = redisTemplate.hasKey(key);
return hasKey != null && hasKey;
}
}
因作者對前端不太熟練,只是實(shí)現(xiàn)了一些簡單的數(shù)據(jù)處理邏輯,前端效果展示:文章來源地址http://www.zghlxwxcb.cn/news/detail-734018.html
到了這里,關(guān)于GitHub爬蟲項(xiàng)目詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!