飞道的博客

爬虫项目Java

316人阅读  评论(0)

什么是爬虫?

爬虫就是一个HTTP客户端。当我们在浏览器地址栏输入一个URL时,浏览器就会给服务器发送一个HTTP请求,服务器接收到后会返回给浏览器一个HTTP响应。爬虫就是根据需要构造请求,并且再根据需要简单的解析一下响应数据。爬虫的优势就是可以根据需要批量的获取数据。

爬取的数据

爬取的是GitHub上项目的一些具体信息。例如:star,fork,open_issue。
然后收集这些项目的这些属性,衡量出这些项目中哪些是比较活跃/流行的。

项目最终需求

给awesome-java 这个项目中提到的所有的GitHub上的项目按照活跃程度排次序

整个项目的框架

核心工作

1.需要获取到所有待收集信息的项目列表--------基于OkHttpClient
2.遍历项目列表,一次获取到每个项目的主页信息,进一步就可以知道该项目的star,fork,open_issue--------基于Jsoup
3.把这些数据存储到数据库中--------基于MySQL
4.写一个简单网页/服务器,来展示数据库中的数据(通过图表的形式来呈现)

工作1——获取到所有待收集信息的项目列表


使用爬虫程序,获取到https://github.com/akullpp/awesome-java/blob/master/README.md这个页面的内容,进而就能知道所有的项目链接信息。

ul表示无序列表,li表示列表中的一个项,a表示超链接,a标签里的href表示链接要跳转到哪个页面。
通过分析页面结构,看到入口页面中包含很多ul,每个ul中又有很多的a标签,a标签中href属性里的url就是我们想要获取的内容。

如何获取到页面内容

构造一个HTTP请求发送给服务器即可。
通过使用OKHttp库,就能根据指定的URL获取到对应页面的内容.内容一般是一个html结构的内容。

public String getPage(String url) throws IOException {
        //1.创建一个 OkHttpClient 对象
        okHttpClient = new OkHttpClient();

        //2.创建一个 Request 对象
        //builder 这个类是一个辅助创造 Request 对象的类
        // Builder 中提供的 url 方法能够设定当前请求的 url
        Request request = new Request.Builder().url(url).build();

        //3.创建一个call对象(负责进行一次网络访问操作)
        Call call = okHttpClient.newCall(request);

        //4.发送请求给服务器,获取到 response 对象;
        Response response = call.execute();

        //5.判定响应是否成功
        if (!response.isSuccessful()) {
            System.out.printf("请求失败");
            return null;
        }
        return response.body().string();

    }

如何分析页面结构

通过使用 Jsoup 库来分析网页结构,先获取所有的 li 标签。(里面混杂了一些li 标签,但是他并不是项目的内容),再获取 li 标签中的 a 标签 ,然后获取 href 的内容。存储到一个 Project 类中。

public List<Project> parseProjectList(String html) {
        ArrayList<Project> result = new ArrayList<>();
        Document document = Jsoup.parse(html);
        Elements elements = document.getElementsByTag("li");
        for (Element li : elements) {
            Elements allLink = li.getElementsByTag("a");
            if (allLink.size() == 0) {
                //当前 li 标签没有包含 a 标签
                //就直接忽略
                continue;
            }
            Element link = allLink.get(0);

            String url = link.attr("href");
            if (!url.startsWith("https://github.com")) {
                continue;
            }
            if (urlBlackList.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;
    }

存储项目的Project类

package dao;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Project {
    //项目名称,对应 a 标签中的内容
    private String name;

    //项目主页链接,对应 a 标签中的 href 属性
    private String url;

    //项目的描述信息,对应到 li 标签里面的内容
    private String description;

    //以下属性都是要统计到的数据
    //需要根据该项目的 url 进入到对应页面,进而统计数据
    private int starCount;
    private int forkCount;
    private int openIssueCount;
}

通过github官方提供的API进行获取到项目的star,fork,open_issue。

通过项目的URL解析出项目仓库的仓库名和作者名

//这个方法的功能,就是把项目的 url 提取出其中的仓库名字和作者名字
    public String getRepoName(String url) {
        int lastOne = url.lastIndexOf("/");
        int lastTwo = url.lastIndexOf("/", lastOne - 1);
        if (lastOne == -1 || lastTwo == -1) {
            System.out.println("当前url不是一个项目的url! url :" + url);
            return null;
        }
        return url.substring(lastTwo + 1);
    }

然后解析出仓库名和作者名后根据Github广泛提供的API,能得到一个关于此项目的一个JSON格式的文件,通过解析这个文件得到该项目的star,fork,open_issue。

public String getRepoInfo(String respName) throws IOException {
        
        String userName = "你的Github用户名";
        String passWord = "你的Github密码";
        //因为github认证后你的爬取数量可以增加
        //进行身份认证,把用户名密码加密之后得到一个字符串, 把这个字符串放到http header中.
        String credential = Credentials.basic(userName, passWord);

        String url = "https://api.github.com/repos/" + respName;

        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("访问Github API失败! URL + " + url);
            return null;
        }
        return response.body().string();
    }

    //通过这个方法获取到仓库相关信息
    //第一个参数 jsonString 表示获取到的Github API的结果
    //第二个参数 表示将解析的数据存到 project 对象里
    public void parseRepoInfo(String jsonString, Project project) {
        Type type = new TypeToken<HashMap<String, Object>>() {
        }.getType();

        HashMap<String, Object> hashMap = gson.fromJson(jsonString, type);
        Double starCount = (Double)hashMap.get("stargazers_count");
        project.setStarCount(starCount.intValue());
        Double forkCount = (Double)hashMap.get("forks_count");
        project.setForkCount(forkCount.intValue());
        Double issuesCount = (Double) hashMap.get("open_issues_count");
        project.setOpenIssueCount(issuesCount.intValue());

    }

将数据存储到数据库中

设计表结构

主要存储的是 Project 对象。
DButil.sql

create database java_github_crawler;

create table project_table(
	name varchar(50),
	url varchar(1024),
	description varchar(1024),
	startCount int,
	forkCount int,
	openedIssueCount int,
	date carchar(128)
);

将数据储存到Mysql数据库中

public void save(Project project) {

        //通过save方法吧project对象保存到数据库
        //1.获取数据库连接
        Connection connection = DBUtil.getConnection();

        //2.构造 PrepareStatement 对象;
        PreparedStatement statement = null;

        String sql = "insert into project_table values(?, ?, ?, ?, ?, ?, ?)";

        try {
            statement = connection.prepareStatement(sql);
            statement.setString(1, project.getName());
            statement.setString(2, project.getUrl());
            statement.setString(3, project.getDescription());
            statement.setInt(4, project.getStarCount());
            statement.setInt(5, project.getForkCount());
            statement.setInt(6, project.getOpenIssueCount());

            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
            statement.setString(7, simpleDateFormat.format(System.currentTimeMillis()));

            //3.执行sql语句,完成数据库插入
            int ret = statement.executeUpdate();
            if (ret != 1) {
                System.out.println("当前数据库执行插入数据出错");
                return;
            }
            System.out.println("数据插入成功");
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

简单的HTML页面来展示数据,通servlet接口来实现前后端交互

public class AllRankServlet extends HttpServlet {

    private Gson gson = new GsonBuilder().create();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.准备工作
        resp.setContentType("application/json; charset=utf-8");
        //2.解析请求,获取日期参数
        String date = req.getParameter("date");
        if (date == null || date.equals("")) {
            resp.setStatus(404);
            resp.getWriter().write("date 参数错误");
            return;
        }
        //3.从数据库查找数据
        ProjectDao projectDao = new ProjectDao();
        List<Project> projects = projectDao.selectProjectByDate(date);
        String respString = gson.toJson(projects);
        resp.getWriter().write(respString);
        return;
    }
}

同时在web.xml文件中配置通过/allRank路径来访问AllRankServlet类

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1"
         metadata-complete="true">
    <servlet>
        <servlet-name>AllRankServlet</servlet-name>
        <servlet-class>api.AllRankServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AllRankServlet</servlet-name>
        <url-pattern>/allRank</url-pattern>
    </servlet-mapping>
</web-app>

HTML页面编写代码:

<!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) {
        // 告诉 echarts 图表要画到那个 html 标签中.
        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',    // bar 表示柱状图
                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: "allRank?date=" + date,
        type: "get",
        success: function(data, status) {
            // 当请求成功(200) 就会自动执行这个函数
            // data 表示服务器返回数据的 body 内容. status 表示状态码
            var projectNames = [];
            var stars = [];
            // 遍历 data 中的内容.
            for (var index in data) {
                var project = data[index];
                projectNames.push(project.name);
                stars.push(project.starCount);
            }
            drawStars(projectNames, stars);
        }
    })
</script>
</body>
</html>

项目遗留问题

1.效率问题

单线程解析数据时间过长

获取到Github的入口页面的时间大约4秒,解析页面时间大约0.3秒,通过GithubAPI遍历项目大约2分钟,存储到MySQL数据库消耗时间大约4秒,整个项目大约2分钟左右。我们可以发现调用github API 获取项目信息是最消耗时间的。

//3.遍历项目列表,调用github API 获取项目信息
        for (Project project : projects) {
            try {
                String repoName = crawler.getRepoName(project.getUrl());
                String jsonString = crawler.getRepoInfo(repoName);

                //4.解析JSON数据
                crawler.parseRepoInfo(jsonString, project);
                // 5.保存到数据库中
                projectDao.save(project);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

这是为什么呢?因为网络状况不稳定,并且由于访问的是国外的网站。所以时间消耗大。

解决方法

之前是使用for循环来调用API来获取项目信息,现在使用多线程来发送数据,让每个线程等待自己的响应数据。
1个线程 138秒结束
5个线程 38秒结束
10个线程 22秒结束
15个线程 18秒结束
20个线程 13秒结束
100个线程 9秒结束
线程也不是越多越好,达到一定数量后提升就不明显了

ExecutorService executorService = Executors.newFixedThreadPool(10);
        //ExecutorService有两种提交任务的操作
        //execute:不关注任务提交的结果
        //submit:管住任务提交的结果
        //使用 submit 最主要的目的就是为了能知道任务啥时候全部完成
        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();
            }
        }


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() {
            try {
                String repoName = threadCrawler.getRepoName(project.getUrl());
                String jsonString = threadCrawler.getRepoInfo(repoName);

                threadCrawler.parseRepoInfo(jsonString, project);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

2.完成榜单需求

要完成榜单需求我们需要每天都要抓取数据,并及时更新。

使用Linux定时任务命令来解决

再放置项目的目录上使用 crontab 命令 ,crontab -e后,配置定时任务。
0 5 * * * cd/root/project/java_github_crawler && java -jar java_github_crawler.jar >our 2>err
0表示分钟, 5 表示5点,第一个* 代表每天,第二个* 代表每星期,第三个* 代表每月

pom.xml文件配置代码

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>java_github_crawler</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>java_github_crawler Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.3.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.12.1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <!-- servlet 版本和 tomcat 版本有对应关系,切记 -->
            <version>3.1.0</version>
            <!-- 这个意思是我们只在开发阶段需要这个依赖,部署到 tomcat 上时就不需要了 -->
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.3.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.12.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
        </dependency>

    </dependencies>

    <build>
        <finalName>java_github_crawler</finalName>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.2.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>


转载:https://blog.csdn.net/Huwence/article/details/106075294
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场