爬蟲項目(分析awesome-java項目流行趨勢)

項目詳情https://github.com/BlackerGod/Crawler
成品展示:點擊訪問
(ps:我在服務器上設置了定時任務每天五點自動爬取數據,但git不穩定,很有可能爬不到數據,可能表格爲空)

一、項目需求

在我們Java開發過程中,需要引用一些項目,或者去學習別人項目中的技巧等。在Github裏面有一個叫awesome-java的項目,他收錄了Github中比較受歡迎的項目,但項目太多了,我們就需要一個工具就分析每個項目的受歡迎程度,我們從(StarCount、ForkCount、openIssueCount)等來衡量一個項目的受歡迎程度。

二、項目分析

我們需要工具先去獲取每個項目的數據,然後保存在數據庫中,寫一個前臺頁面來解析數據庫中的數據,最後以表格的形式顯示在前臺。

三、項目設計

前:建立Maven項目結構

  1. 抓取模塊
  2. 分析模塊
  3. 存儲模塊
  4. 展示模塊

四、編碼

1.抓取模塊

我們需要一個httpClient來構造和發送請求、接收響應。我選擇了okhttp的依賴來構造並獲取響應。

public  String getUrl(String url) throws IOException {
		OkHttpClient okHttpClient = new OkHttpClient();  //創建一個客戶端
        Request request = new Request.Builder().url(url).build(); //根據url構造請求
        Call call = okHttpClient.newCall(request); 
        Response response = call.execute(); //接收響應
        if(!response.isSuccessful()){
            System.out.println("請求失敗");
            return null;
        }
        return response.body().string(); //返回body部分
    }
2.分析模塊

當我們抓取到響應後,我們針對於頁面進行分析。
在這裏插入圖片描述可以看到每一個項目都是在<ul>標籤下的<li>標籤
在這裏插入圖片描述

可以看到<a>裏面的內容爲項目名稱,然後href爲各自項目的標籤,那麼我們可以用到	Jsoup工具來分析頁面,同時
我們也需要一個名單來保存哪些不是github項目的鏈接
 private HashSet<String> htmlBlackList = new HashSet<>();
  {
        htmlBlackList.add("https://github.com/events");
        htmlBlackList.add("https://github.community");
        htmlBlackList.add("https://github.com/about");
        htmlBlackList.add("https://github.com/contact");
        htmlBlackList.add("https://github.com/pricing");
    }

 public  List<Project> parseProjectList(String html){
        ArrayList<Project> result = new ArrayList<>(); //結果
        Document document = Jsoup.parse(html);
        Elements elements = document.getElementsByTag("li"); // 獲取li標籤

        for (Element li : elements){ //遍歷目標
            Elements allLink = li.getElementsByTag("a");//獲取a標籤
            if(allLink.size() == 0){
                continue;
            }
            Element link = allLink.get(0);
            String url = link.attr("href");
            if(!url.startsWith("https://github.com")){
                continue;
            }
            if(htmlBlackList.contains(url)){
                continue;
            }
            Project project = new Project();
            project.setName(link.text());
            project.setUrl(link.attr("href"));
            project.setDescription(li.text());
            result.add(project);
        }
        return result;

    }

到現在我們已經獲取了每一個項目的名稱和它的url,我們現在繼續可以根據各自的url來獲取他們的starCount等數量,但是這樣比較麻煩,我們根據Github的官方API來獲取每個項目的數據。

 根據 https://api.github.com/repos/用戶名/倉庫名 就可以獲取到倉庫的詳細信息

在這裏插入圖片描述根據API返回的結果可以找到對應的數據,但返回的結果是Json格式的,我們就需要去獲取Json裏面的數據,這裏我用的工具爲Gson。(這裏注意,Github在沒登錄情況下一小時只能調用60次)

 public String getRepoName(String url){ //根據項目裏面的url獲取用戶信息和倉庫名
       int lastOne = url.lastIndexOf("/");
       int lastTwo = url.lastIndexOf("/",lastOne-1);
       if(lastOne == -1 || lastTwo == -1){
           System.out.println("url不是一個項目的url"+url);
           return null;
       }
       return url.substring(lastTwo+1);
    }
    /**
    *這裏爲了避免抓取頻繁,我們要登錄,先獲取自己的Authorization,這是登錄後的一個標誌,我們設置到
    *請求頭裏面,這樣就可以一小時爬取5000次,這個函數返回的是json格式的字符串
    */
public String getRepoInfo(String respoName) throws IOException {
        String username = "用戶名";//自己的Git用戶名
        String password = "密碼"; //自己的Git密碼
        //身份認證,把用戶名密碼加密後,得到一個字符串,把這個字符串放在HTTP header裏面
        String credential = Credentials.basic(username,password);
		
        String url = "https://api.github.com/repos/"+respoName;
        Request request = new Request.Builder().url(url).header("Authorization",credential).build();
        Call call = okHttpClient.newCall(request);
        Response response = call.execute();
        if(!response.isSuccessful()){
            System.out.println("獲取項目失敗");
            return null;
        }
        return response.body().string();
    }

然後根據設置每一個項目的屬性值

public void parseRepoInfo(String jsonString,Project project){
		//反射機制,從json裏面獲取到三個數據
        Type type = new TypeToken<HashMap<String,Object>>(){}.getType();
        HashMap<String,Object> hashMap = gson.fromJson(jsonString,type);
        project.setStarCount(((Double) hashMap.get("stargazers_count")).intValue());
        project.setForkCount(((Double) hashMap.get("forks_count")).intValue());
        project.setOpenIssueCount(((Double) hashMap.get("open_issues_count")).intValue());
    }

但我們知道,一個項目一個項目的爬取太慢了,網絡都耗不起,所以我們使用多線程。

int THREADCOUNT = 10;
  ExecutorService executorService = Executors.newFixedThreadPool(THREADCOUNT);
  建立一個固定大小線程池
        List<Future<?>> taskResults = new ArrayList<>();
        for (Project project: projects){
            Future<?> taskResult = executorService.submit(new CrawlerTask(project,crawler));
            taskResults.add(taskResult);
        }
        //所有線程結束纔可執行下一條
        for (Future<?> taskResult : taskResults){
            //調用Get方法就會阻塞,阻塞到改任務執行完畢,get纔會返回
            try {
                taskResult.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        //所有任務都執行完畢了,關閉線程池,回收資源
        executorService.shutdown();

static class CrawlerTask implements Runnable{

        private Project project;
        private ThreadCrawler threadCrawler;

        public CrawlerTask(Project project, ThreadCrawler threadCrawler) {
            this.project = project;
            this.threadCrawler = threadCrawler;
        }

        @Override
        public void run() {
            //1.調用API獲取項目數據
            //2.解析項目數據
            try {
                System.out.println("craw"+project.getName()+"...");
                String repoName = threadCrawler.getRepoName(project.getUrl());
                String jsonString = threadCrawler.getRepoInfo(repoName);
                threadCrawler.parseRepoInfo(jsonString,project);
            } catch (IOException e) {
                throw new RuntimeException("插入失敗"+project.getUrl());
            }

        }
    }

這樣相當於解析完畢,我們把每一個項目已經初始化結束

3.存儲模塊

先寫一個DButil類用於獲取和關閉連接。

package dao;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 獲取數據庫的連接,使用的單例模式
 * */
public class DBUtil {

    private static final String URL = "jdbc:mysql://127.0.0.1:3306/gitproject?characterEncoding=utf8";
    private static final String USER = "你的數據庫用戶名";
    private static final String PASSWORD = "密碼";
    private static volatile DataSource dataSource = null;
	
    private static DataSource getDataSource() {
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                dataSource = new MysqlDataSource();
                MysqlDataSource mysqlDataSource = (MysqlDataSource) dataSource;
                mysqlDataSource.setURL(URL);
                mysqlDataSource.setUser(USER);
                mysqlDataSource.setPassword(PASSWORD);
            }
        }
        return dataSource;
    }

    public static Connection getConnection(){
        try{
        return getDataSource().getConnection();
        } catch (SQLException e){
            e.printStackTrace();
        }
        return null;
    }

    public static void close(Connection connection , PreparedStatement preparedStatement,ResultSet resultSet){
        try {
            if(resultSet != null) {
                resultSet.close();
            }
            if(preparedStatement != null) {
                preparedStatement.close();
            }
            if(connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

然後就是存儲

public void save(Project project){
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        String sql = "insert into project_table values(?,?,?,?,?,?,?);";
        try {
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,project.getName());
            preparedStatement.setString(2,project.getUrl());
            preparedStatement.setString(3,project.getDescription());
            preparedStatement.setInt(4,project.getStarCount());
            preparedStatement.setInt(5,project.getForkCount());
            preparedStatement.setInt(6,project.getOpenIssueCount());
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
            preparedStatement.setString(7,simpleDateFormat.format(System.currentTimeMillis()));
            //new Date().getTime())
            int ret = preparedStatement.executeUpdate();
            if(ret != 1){
                System.out.println("當前數據庫執行插入數據出錯");
                return;
            }
            //System.out.println("數據插入成功");

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection,preparedStatement,null);
        }
    }
/**
*根據日期來獲取數據庫的數據
*/
 public List<Project> selectProjectByDate(String date){

        List<Project> projects = new ArrayList<>();
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        String sql = "select name,url,starCount,forkCount,openIssueCount " +
                       "from project_table where date = ? order by starCount desc;";
        try {
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1,date);
            resultSet = preparedStatement.executeQuery();

            while (resultSet.next()){
                Project project = new Project();
                project.setName(resultSet.getString("name"));
                project.setUrl(resultSet.getString("url"));
                project.setStarCount(resultSet.getInt("starCount"));
                project.setForkCount(resultSet.getInt("forkCount"));
                project.setOpenIssueCount(resultSet.getInt("openIssueCount"));
                projects.add(project);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection,preparedStatement,resultSet);
        }
        return projects;

    }
4.展示模塊

先寫一個servlet來用於前端和後臺的交互

package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dao.Project;
import dao.ProjectDao;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import java.util.List;

@WebServlet("/AllRankServlet")
public class AllRankServlet extends HttpServlet {

    private Gson gson = new GsonBuilder().create();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        //req.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=utf-8");
        String date = req.getParameter("date");
        if(date == null || date.equals("")){
            resp.setStatus(404);
            resp.getWriter().write("date參數錯誤");
            return;
        }
        ProjectDao projectDao = new ProjectDao();
        List<Project> projects = projectDao.selectProjectByDate(date);
        String respString = gson.toJson(projects);//把數據轉成json格式
        resp.getWriter().write(respString);
        return;

    }
}

再寫一個前端頁面(前端頁面寫的比較菜,所以我從網站找了一個js製表網站

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>我的Github趨勢</title>
</head>
<body>

<!-- 爲 ECharts 準備一個具備大小(寬高)的 DOM -->
<div id="main" style="width: 100%;height:600px;"></div>



<!--從網絡上下載 JQuery 這個庫-->
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>

<!-- 引入 ECharts 文件 -->
<script src="js/echarts.min.js"></script>


<script>
    function drawStars(projectNames,stars) { //根據echars提供的文檔寫
        var myChart = echarts.init(document.getElementById('main'));
        var option = {
            title: {
                text: 'Star 天榜'
            },
            tooltip: {},
            legend: {
                data:['Star']
            },
            xAxis: {
                //data: ["襯衫","羊毛衫","雪紡衫","褲子","高跟鞋","襪子"]
                data:projectNames
            },
            yAxis: {},
            series: [{
                name: 'Star',
                type: 'bar',//柱狀圖
                //data: [5, 20, 36, 10, 10, 20]
                data:stars
            }],
            dataZoom: [
                {   // 這個dataZoom組件,默認控制x軸。
                    type: 'slider', // 這個 dataZoom 組件是 slider 型 dataZoom 組件
                    start: 0,      // 左邊在 10% 的位置。
                    end: 10         // 右邊在 60% 的位置。
                },
                {   // 這個dataZoom組件,也控制x軸。
                    type: 'inside', // 這個 dataZoom 組件是 inside 型 dataZoom 組件
                    start: 0,      // 左邊在 10% 的位置。
                    end: 10         // 右邊在 60% 的位置。
                }
            ],
        };
        myChart.setOption(option);
    }


    Date.prototype.Format = function (formatStr) {
        var str = formatStr;
        var Week = ['日', '一', '二', '三', '四', '五', '六'];
        str = str.replace(/yyyy|YYYY/, this.getFullYear());
        str = str.replace(/yy|YY/, (this.getYear() % 100) > 9 ? (this.getYear() % 100).toString() : '0' + (this.getYear() % 100));
        str = str.replace(/MM/, this.getMonth() > 9 ? this.getMonth().toString() + 1 : '0' + (this.getMonth() + 1));
        str = str.replace(/M/g, this.getMonth());
        str = str.replace(/w|W/g, Week[this.getDay()]);
        str = str.replace(/dd|DD/, this.getDate() > 9 ? this.getDate().toString() : '0' + this.getDate());
        str = str.replace(/d|D/g, this.getDate());
        str = str.replace(/hh|HH/, this.getHours() > 9 ? this.getHours().toString() : '0' + this.getHours());
        str = str.replace(/h|H/g, this.getHours());
        str = str.replace(/mm/, this.getMinutes() > 9 ? this.getMinutes().toString() : '0' + this.getMinutes());
        str = str.replace(/m/g, this.getMinutes());
        str = str.replace(/ss|SS/, this.getSeconds() > 9 ? this.getSeconds().toString() : '0' + this.getSeconds());
        str = str.replace(/s|S/g, this.getSeconds());
        return str;
    }


    var date = new Date().Format("yyyyMMdd");
    // 這是 JS 常用調試手段, 可以把一段內容打印到瀏覽器的控制檯上.
    console.log(date);

    $.ajax({
        url:"AllRankServlet?date=" + date,
        type:'get',
        success:function (date,status) {
            var projectNames=[];
            var stars = [];
            for(var index in date){
                var  project = date[index];
                projectNames.push(project.name);
                stars.push(project.starCount);
            }
            drawStars(projectNames,stars);
        }
    })
</script>
</body>
</html>

到此就結束了,然後就是打成war包部署。然後把抓取的那個類打成jar包,在服務器上設只一個定時任務,每天固定時間抓取即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章