GitHub爬虫项目详解

news2024/11/18 17:24:30

前言

闲来无事浏览GitHub的时候,看到一个仓库,里边列举了Java的优秀开源项目列表,包括说明、仓库地址等,还是很具有学习意义的。但是大家也知道,国内访问GitHub的时候,经常存在访问超时的问题,于是就有了这篇文章,每日自动把这些数据爬取下来,随时看到热点排行。
仓库地址:https://github.com/akullpp/awesome-java
仓库页面截图:在这里插入图片描述

分析

根据以往爬虫经验,先确定好思路,再开始开发代码效率会更高。那么,第一步,找一下我们的数据来源。
具体步骤:先开启F12,刷新网页,根据关键词搜索,看数据来源是哪个接口(此处以列表里的Maven为例,其他也可以)
在这里插入图片描述
可以看到,项目列表都是来源于这个.md文档的1250行,可以看到,这是一个标准的JSON数据,我们把这行数据复制出来进行分析(由于数据太长,不做展示),继续搜索后发现,我们需要的项目列表和说明,都在其中richText字段里,如下:
在这里插入图片描述
而这个富文本数据都是Unicode编码,为了方便查看结构,我们将其转为中文,可以用如下的正则匹配,批量转换

        richData = richData.replaceAll("/\\\\u([0-9a-f]{3,4})/i", "&#x\\1;");

转换完之后继续看这个富文本数据

在这里插入图片描述
我们需要的东西对应的是一个一个的<li>标签和<a>标签,找到数据源之后就可以正式开始开发了。

项目开发

1、准备工作

  • 开发框架选择SpringBoot,持久层框架使用MyBatis。除必要的基础依赖以外,还需要引入以下依赖:
    jsoup:对网页结构分析,解析数据
    okhttp:HTTP客户端,访问页面使用。
    fastjson:解析JSON数据
  • 关系型数据库选择Mysql,非关系型数据库选择Redis
  • 编辑配置文件
    在这里插入图片描述

2、项目列表解析代码开发

根据前期分析的思路,首先使用okhttp客户端,访问https://github.com/akullpp/awesome-java/blob/master/README.md页面,获取到响应正文。

    public String getPage(String url) {
        try {
            // 1.创建okhttp客户端对象
            OkHttpClient okHttpClient = new OkHttpClient();
            // 2.创建request对象 (用Request的静态类创建)
            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.println("请求失败!");
                return null;
            }
            return response.body().string();
        }catch (Exception e){
            log.error("请求页面出错:{}",e.getMessage());
            return null;
        }

    }

获取到正文后如图所示:
在这里插入图片描述

接着我们使用Jsoup对网页结构进行解析,因为需要的数据处于<Script>标签,因此我们只提取这个标签数据即可,代码为:

Document document = Jsoup.parse(html);
        // 2.使用 getElementsByTag,拿到所有的标签    elements相当于集合类。每个element对应一个标签
Elements elements = document.getElementsByTag("script");

提取之后效果如图:
在这里插入图片描述
需要的数据在列表最后一位,取到之后因其是HTML语法,我们需要将其处理转为标准JSON,然后根据第一步分析的结果,根据key提取richText所在的值,并将Unicode转为中文。

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;");

处理结果为:
在这里插入图片描述
转换完的字符串还是标准的HTML语法,继续用Jsoup解析结构,获取到所有的<li>标签和<a>标签

在这里插入图片描述
将需要的数据提取出来,再根据提取出来的数据继续爬取项目详情页,格式为:https://github.com/作者名/仓库名(因代码基本一致,此处不再赘述),获取项目对应的StartCount、forkCount、IssuesCount,转换为数据库实体对象并存储即可。

3、定时任务

编写定时任务代码,每天三点执行爬取任务,因为可能存在连接超时,因此增加五十次失败重试。执行结束后不管成功失败,微信推送执行结果

 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("抓取页面失败,正在发送失败消息!");
                JSONObject re = new JSONObject();
                re.put("本次重试次数:", 50);
                re.put("时间:", MyUtils.nowTime());
                //微信推送执行结果消息
                System.out.println(MyUtils.sendMsgNoUrl(re,MsgToken,"今日任务执行失败,请手动调用接口重新爬取!"));
                return;
            }
        }
        // 2.解析入口页面,获取项目列表
        List<ProjectDTO> projects = crawlerService.parseProjectList(html);
        //发送成功消息
        log.info("抓取页面完成,开始解析!");
        JSONObject re = new JSONObject();
        re.put("时间:", MyUtils.nowTime());
        re.put("本次重试次数:", count);
        re.put("本次项目总数:", projects.size());
        //微信推送执行结果消息
        System.out.println(MyUtils.sendMsgNoUrl(re,MsgToken,"任务执行成功,请去查看效果!"));
        
        if (CollectionUtils.isEmpty(projects)) {
            return;
        }
        // 3.遍历项目列表,利用线程池实现多线程
        // executorService提交任务:1)submit 有返回结果  2)execute 无返回结果
        // 此处使用submit是为了得知是否全部遍历结束,方便进行存到数据库操作
        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);
                        // 解析项目数据
                        parseRepoInfo(jsonString, project);
                        System.out.println("crawling " + project.getName() + "done !");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            taskResults.add(taskResult);
        }

        // 等待所有任务执行结束,再进行下一步
        for (Future<?> taskResult : taskResults) {
            try {
                // 调用get会阻塞,直到该任务执行完毕,才会返回
                if (taskResult != null) taskResult.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //代码到这里,说明所有任务都执行结束,结束线程池
        executorService.shutdown();
        // 4.保存到数据库
        crawlerService.batchSave(projects);
    }

在这里插入图片描述

4、前端调用接口开发

对前端开放两个接口,一个为数据库数据的日期列表接口,一个根据日期查询当日数据接口,同时对参数进行非空验证

    @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 参数错误!");
            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;
    }

在根据日期查询当日数据的接口中,因其每日的数据都是固定的,因此添加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工具类,具体代码如下:

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;
    }

}

因作者对前端不太熟练,只是实现了一些简单的数据处理逻辑,前端效果展示:在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1062819.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云安全之等级保护解决方案及应用场景

等保2.0解决方案背景 适应云计算、移动互联网、大数据、物联网和工业控制等新技术发展&#xff0c;在新的技术场景能够顺利开展等级保护工作;《网络安全法》2016年已正式发布&#xff0c;等级保护2.0为了更好配合《网络安全法》的实施&#xff1b;等级保护1.0&#xff0c;在适…

U盘支持启动区+文件存储区的分区方法

准备新U盘 启动diskgenius &#xff0c;先建立一个主分区&#xff08;7G&#xff09;&#xff0c;剩余空间建立为第二分区&#xff0c;然后设定第二分区激活。 diskgenius格式化 用diskgenius格式化&#xff0c;在格式化的过程中有一个 写入dos系统的选项&#xff0c;在格式…

企业微信机器人对接GPT

现在网上大部分微信机器人项目都是基于个人微信实现的&#xff0c;常见的类库都是模拟网页版微信接口。 个人微信作为我们自己日常使用的工具&#xff0c;也用于支付场景&#xff0c;很怕因为违规而被封。这时&#xff0c;可以使用我们的企业微信机器人&#xff0c;利用企业微信…

抄写Linux源码(Day14:从 MBR 到 C main 函数 (3:研究 head.s) )

回忆我们需要做的事情&#xff1a; 为了支持 shell 程序的执行&#xff0c;我们需要提供&#xff1a; 1.缺页中断(不理解为什么要这个东西&#xff0c;只是闪客说需要&#xff0c;后边再说) 2.硬盘驱动、文件系统 (shell程序一开始是存放在磁盘里的&#xff0c;所以需要这两个东…

linux以太网(三).之netstat命令

引言&#xff1a; netstat命令是一个监控TCP/IP网络的非常有用的工具&#xff0c;它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息 语法选项&#xff1a; netstat [选项] -a或--all&#xff1a;显示所有连线中的Socket&#xff1b; -A<网络类型>或…

JUC第十五讲:JUC集合-ConcurrentHashMap详解(面试的重点)

JUC第十五讲&#xff1a;JUC集合-ConcurrentHashMap详解 本文是JUC第十五讲&#xff1a;JUC集合-ConcurrentHashMap详解。JDK1.7之前的ConcurrentHashMap使用分段锁机制实现&#xff0c;JDK1.8则使用数组链表红黑树数据结构和CAS原子操作实现ConcurrentHashMap&#xff1b;本文…

跨站脚本攻击(XSS)以及如何防止它?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 什么是跨站脚本攻击&#xff08;XSS&#xff09;&#xff1f;⭐ 如何防止XSS攻击&#xff1f;⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#…

复习 --- 消息队列

进程间通信机制(IPC) 简述 IPC&#xff1a;Inter Process Communication 进程和进程之间的用户空间相互独立&#xff0c;但是4G内核空间共享&#xff0c;进程间的通信就是通过这4G的内核空间 分类 传统的进程间通信机制 无名管道&#xff08;pipe&#xff09; 有名管道&…

Linux Vi编辑器基础操作指南

Linux Vi编辑器基础操作指南 Linux中的Vi是一个强大的文本编辑器&#xff0c;虽然它有一些陡峭的学习曲线&#xff0c;但一旦掌握了基本操作&#xff0c;它就变得非常高效。以下是Vi编辑器的一些基本用法&#xff1a; 打开Vi编辑器&#xff1a; vi 文件名退出Vi编辑器&#xff…

[架构之路-232]:操作系统 - 文件系统存储方法汇总

目录 前言&#xff1a; 一、文件系统存储方法基本原理和常见应用案例&#xff1a; 二、Windows FAT文件系统 2.1 概述 三、Linux EXT文件系统 3.1 基本原理 3.2 索引节点表&#xff08;Inode Table&#xff09; 3.2.1 索引节点表层次结构 3.2.2 间接索引表的大小和表项…

@SpringBootApplication剖析

一、前言 在SpringBoot项目中启动类必须加一个注解SpringBootApplication&#xff0c;今天我们来剖析SpringBootApplication这个注解到底做了些什么。 二、SpringBootApplication简单分析 进入SpringBootApplication源代码如下&#xff1a; 可以看出SpringBootApplication是…

【C语言】动态通讯录(超详细)

通讯录是一个可以很好锻炼我们对结构体的使用&#xff0c;加深对结构体的理解&#xff0c;在为以后学习数据结构打下结实的基础 这里我们想设计一个有添加联系人&#xff0c;删除联系人&#xff0c;查找联系人&#xff0c;修改联系人&#xff0c;展示联系人&#xff0c;排序这几…

数据挖掘(3)特征化

从数据分析角度&#xff0c;DM分为两类&#xff0c;描述式数据挖掘&#xff0c;预测式数据挖掘。描述式数据挖掘是以简介概要的方式描述数据&#xff0c;并提供数据的一般性质。预测式数据挖掘分析数据建立模型并试图预测新数据集的行为。 DM的分类&#xff1a; 描述式DM&#…

arm代码

RISC精简指令集 长度和执行周期固定 长度为一条机器指令在计算机占用的内存大小 指令周期为CPU执行一条机器指令所发费的时间(时钟周期由CPU工作频率决定) CISC复杂指令集 其架构一般用于PC端 X86和X64都是负载指令集CPU 更注重指令的功能性 指令周期和长度都不固定 ar…

014-第二代软件开发

第二代软件开发 文章目录 第二代软件开发项目介绍正式开始我们的Debian Qt 软件开发主题色QSS U盘检测QFileSystemWatcher 屏幕键盘LibUSB 使用 总结 关键字&#xff1a; Qt、 Qml、 U盘检测、 屏幕键盘、 LibUSB 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这…

Qt 综合练习小项目--反金币(2/2)

目录 4 选择关卡场景 4.2 背景设置 4.3 创建返回按钮 4.3 返回按钮 4.4 创建选择关卡按钮 4.5 创建翻金币场景 5 翻金币场景 5.1 场景基本设置 5.2 背景设置 5.3 返回按钮 5.4 显示当前关卡 5.5 创建金币背景图片 5.6 创建金币类 5.6.1 创建金币类 MyCoin 5.6.…

GPT系列论文解读:GPT-2

GPT系列 GPT&#xff08;Generative Pre-trained Transformer&#xff09;是一系列基于Transformer架构的预训练语言模型&#xff0c;由OpenAI开发。以下是GPT系列的主要模型&#xff1a; GPT&#xff1a;GPT-1是于2018年发布的第一个版本&#xff0c;它使用了12个Transformer…

JavaEE 网络原理——TCP的工作机制(中篇 三次握手和四次挥手)

文章目录 一、TCP 内部工作机制——连接管理1. 连接(三次握手)(1).有连接和确认应答之间的关系(2). 通过客户端和服务器详细描述三次握手 2. 断开连接(四次挥手)(1)讨论“四次握手”中间步骤的合并问题。(2) 根据简单的 TCP 代码解释断开连接(3) 四次挥手中的两个重要的 TCP 状…

计算机网络-计算机网络体系结构-物理层

目录 一、通信基础 通信方式 传输方式 码元 传输率 *二 准则 2.1奈氏准则(奈奎斯特定理) 2.2香农定理 三、信号的编码和调制 *数字数据->数字信号 数字数据->模拟信号 模拟数据->数字信号 模拟数据->模拟信号 *四、数据交换方式 电路交换 报文交换…

【刷题笔记10.5】LeetCode:排序链表

LeetCode&#xff1a;排序链表 一、题目描述 给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 二、分析 这题咱们默认要求&#xff1a;空间复杂度为O(1)。所以这把咱们用自底向上的方法实现归并排序&#xff0c;则可以达到O(1) 的空间复杂…