爬虫项目(分析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包,在服务器上设只一个定时任务,每天固定时间抓取即可。

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