什么是爬虫?
爬虫就是一个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