利用github pages建立Serverless个人博客

利用github pages建立Serverless个人博客

概述

使用github pages,可以在github上部署静态网站。利用这个功能,可以很方便地实现个人博客的发布托管。

比如我的个人博客:Buttering’s Blog

对应代码仓库:buttering/EasyBlog: 自动将本地博客部署到github中展示

相关技术

1. 博客处理

采用go语言对md格式的博客进行处理,主要目的是处理文档中的图片,将其保存在图床,并将文档中的图片链接替换为图床链接。

2. 图床

采用github仓库直接作为图床。图片和博客存放在同一个仓库中。

3. Hexo博客框架

Hexo是一个快速、简洁且高效的博客框架,能很方便地生成静态的博客网站。采用Fluid主题。

4. CI/CD流程

采用github actions,自动实现博客上传后网站的生成和部署。

5. 更多功能

  • 使用hexo-renderer-markdown-it-plus插件,实现对latex的支持。

  • 使用giscus评论插件,利用github discussions实现博客的评论功能。

  • 使用LeanCloud实现访问统计。

  • 展示网站运行时长

实现

0. 项目框架

项目所有文章和图片都位于同一个github仓库中:

image-20240505100150836

  • .github:存放github action的配置文件
  • asset:存放处理后的博客和图片,pictures起到了图床的作用
  • publish:Hexo部署在此子目录
  • resource:存放githubPub公钥
  • tools:一些自己写的工具函数
  • UploadBlogs:实现博客处理和自动push的脚本

1. 博客预处理

对md格式的博客进行预处理,使用正则表达式提取其中的图片路径:

  • 对于本地图片,直接将其拷贝到asset/pictures目录中;
  • 对于网络图片,将其下载在asset/pictures目录中。

在pictures目录中,对文章名和图片进行了哈希处理,如图:

image-20240505100811570

博客则直接按照原文件名放在blogs目录下。

博客中图片的链接会被替换为形如 https://raw.githubusercontent.com/buttering/EasyBlogs/master/asset/pictures/bdf03a9b9bdacaaf60d1f899c0222865/74d51bfa109b4065ccb3ba37b6922bb3.png的url。

raw.githubusercontent.com 是github用来存储用户上传文件的服务地址。

package main

import (
	"EasyBlogs/tools"
	"fmt"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport/ssh"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

type markdownPicture struct {
	isUrl             bool
	sourcePicturePath string
	start             int // md中源图片路径的起始偏移量
	end               int
	hashName          string // 均将包含后缀的文件名进行hash,且后拼接上原有后缀名
	targetUrl         string // 修改后在github仓库中的url
}

type Blog struct {
	name          string
	hashName      string
	pictures      []markdownPicture
	directoryPath string // 源文件文件夹路径
	legal         bool   // 成功通过解析
}

var (
	BLOG_PATH      string
	PICTURE_PATH   string
	REPOSITORY_URL string
)

func getBlogList(path string) (blogsList []Blog) {
	blogsList = make([]Blog, 0, 10)

	fileList, err := os.ReadDir(path)
	if err != nil {
		panic(err)
	}

	for _, file := range fileList {
		if !file.IsDir() && filepath.Ext(file.Name()) == ".md" {
			fileName := file.Name()

			blogsList = append(blogsList, Blog{fileName, tools.Hash(fileName), nil, path, false})
		}
	}
	return
}

func extractPicture(blog *Blog) {
	isUrl := func(path string) bool {
		return strings.HasPrefix(path, `http://`) || strings.HasPrefix(path, `https://`)
	}

	content, err := os.ReadFile(filepath.Join(blog.directoryPath, blog.name))
	if err != nil {
		println(err)
		return
	}

	re, _ := regexp.Compile(`!\[.*?]\((.*?)\)`)
	matches := re.FindAllSubmatchIndex(content, -1)

	for _, match := range matches {
		start := match[2]
		end := match[3]

		picturePath := string(content[start:end])
		var pictureName string
		if isUrl(picturePath) {
			u, err := url.Parse(picturePath)
			if err != nil {
				println("解析图片url:", picturePath, " 失败")
				continue
			}
			pictureName = path.Base(u.Path)
		} else if filepath.IsAbs(picturePath) {
			pictureName = filepath.Base(picturePath)
		} else { // 相对路径的本地文件
			picturePath = filepath.Join(blog.directoryPath, picturePath)
			pictureName = filepath.Base(picturePath)
		}
		hashName := tools.Hash(pictureName) + path.Ext(pictureName)

		blog.pictures = append(
			blog.pictures,
			markdownPicture{
				isUrl(picturePath),
				picturePath,
				start,
				end,
				hashName,
				REPOSITORY_URL + "/" + blog.hashName + "/" + hashName,
			},
		)
	}

	blog.legal = true
}

func copyBlog(blog *Blog) {
	fmt.Println("拷贝博客:“" + blog.name + "”")

	blogTargetPath := filepath.Join(BLOG_PATH, blog.name)
	pictureTargetPath := filepath.Join(PICTURE_PATH, blog.hashName)
	if _, err := os.Stat(blogTargetPath); !os.IsNotExist(err) {
		println("文章“" + blog.name + "”已经存在")
		blog.legal = false
		return
	}

	if err := os.Mkdir(pictureTargetPath, 0777); err != nil {
		println("为博客“" + blog.name + "”创建对应picture文件夹失败")
		blog.legal = false
		return
	}

	content, _ := os.ReadFile(filepath.Join(blog.directoryPath, blog.name))

	offset := 0
	for _, picture := range blog.pictures {
		start := picture.start + offset
		end := picture.end + offset
		content = append(content[:start], append([]byte(picture.targetUrl), content[end:]...)...)
		offset += len(picture.targetUrl) - (end - start)
	}

	err := os.WriteFile(blogTargetPath, content, 0644)
	if err != nil {
		println("复制文件“" + blog.name + "”错误")
		blog.legal = false
	}

}

func copyPicture(blog Blog) {
	pictureTargetPath := filepath.Join(PICTURE_PATH, blog.hashName)

	for _, picture := range blog.pictures {
		fmt.Println("导入图片:“" + picture.sourcePicturePath + "”")

		var sourceFile interface{}
		if picture.isUrl {
			for i := 0; i < 5; i++ {
				response, err := http.Get(picture.sourcePicturePath)
				if err == nil && response.StatusCode == http.StatusOK {
					sourceFile = response.Body
					break
				}
				time.Sleep(50 * time.Millisecond)
			}
			if sourceFile == nil {
				println("下载图片“" + picture.sourcePicturePath + "”失败")
				continue
			}

		} else {
			file, err := os.Open(picture.sourcePicturePath)
			if err != nil {
				println("打开图片“" + picture.sourcePicturePath + "”失败")
				continue
			}
			sourceFile = file
		}

		destinationFile, _ := os.Create(filepath.Join(pictureTargetPath, picture.hashName))
		_, err := io.Copy(destinationFile, sourceFile.(io.Reader))
		if err != nil {
			println("复制图片“" + picture.sourcePicturePath + "”失败")
		}
	}
}

func gitOperate(blogList []Blog) {
	if len(blogList) == 0 {
		return
	}
	repositoryPath, _ := filepath.Abs(".")
	r, err := git.PlainOpen(repositoryPath)
	if err != nil {
		println("打开仓库失败")
		return
	}
	w, err := r.Worktree()
	if err != nil {
		println("打开仓库失败")
		println(err.Error())
		return
	}

	_, err = w.Add("./asset")
	if err != nil {
		println("向仓库添加文件失败")
		println(err.Error())
		return
	}
	status, _ := w.Status()
	println("git 状态:")
	println(status.String())

	nameList := tools.Map(blogList, func(blog Blog) string {
		return blog.name
	})
	var summary string
	if len(nameList) == 1 {
		summary = fmt.Sprintf("提交文件 [%s]", blogList[0].name)
	} else {
		summary = fmt.Sprintf(
			"提交 %d 个博客\n"+
				"\n"+
				"文件列表: [%s]",
			len(blogList),
			strings.Join(nameList, ", "),
		)
	}
	commit, err := w.Commit(summary, &git.CommitOptions{
		Author: &object.Signature{
			Name: "Wang",
			When: time.Now(),
		},
	})

	obj, _ := r.CommitObject(commit)
	fmt.Println("提交文件:")
	fmt.Println(obj.String())

	// user必须是"git"。。。困扰了半天,最后查issue发现的。真够郁闷的。
	privateKey, err := ssh.NewPublicKeysFromFile("git", "./resource/githubPublicKey", "")

	if err != nil {
		println(err.Error())
	}

	for i := 0; i < 3; i++ {
		err = r.Push(&git.PushOptions{
			RemoteName: "origin",
			RemoteURL:  `git@github.com:buttering/EasyBlogs.git`,
			Auth:       privateKey,
			Progress:   os.Stdout,
		})
		if err == nil {
			break
		}
		println("第 %d 次上传失败")
	}
	if err != nil {
		println("重试次数已达上限,上传失败")
		return
	}

	fmt.Println("提交成功!")
}

func init() {
	path, _ := filepath.Abs(".")
	BLOG_PATH = filepath.Join(path, "asset", "blogs")
	PICTURE_PATH = filepath.Join(path, "asset", "pictures")
	REPOSITORY_URL = `https://raw.githubusercontent.com/buttering/EasyBlogs/master/asset/pictures`
}

func main() {
	filePath := "E:/desktop/blog"
	//yamlPath := "./asset/blogs-list.yaml"
	blogList := getBlogList(filePath)
	for i := range blogList {
		extractPicture(&blogList[i])
		copyBlog(&blogList[i])
		copyPicture(blogList[i])
	}
	if len(blogList) == 0 {
		return
	}

	// 改用github page进行博客部署,不需要额外记录博客信息
	//yamlOperate(yamlPath, blogList)
	//dbOperate(blogList)
	gitOperate(blogList)

}

2. 引入Hexo框架

Hexo框架的使用方式很简单,只需使用Hexo CLI建立基本框架,而后将博客和相关资源移入指定文件夹,然后运行Hexo服务器即可。这里为实现Serverless,后两部交由github action实现。

安装Hexo并初始化站点

保证电脑中安装了Node.js。Hexo的部署目录为publish。

npm install -g hexo-cli
nexo init publish
cd publish
npm install

执行后,Hexo会自动在publish目录建立一个空白站点,目录如下:

.
├── _config.yml
├── package.json
├── scaffolds
├── source
|   └── _posts
└── themes
  • _config.yml:站点的配置文件
  • _posts:Hexo会自动将其中的文档转化为静态资源
  • themes:主题文件夹
安装fluid主题

初始的Hexo站点已经很完善了,但是为了更加个性化,安装了fluid主题:配置指南 | Hexo Fluid 用户手册 (fluid-dev.com)

# 仍是在publish目录
npm install --save hexo-theme-fluid

然后在博客目录下创建 _config.fluid.yml,将主题的 _config.yml(位于publish\themes\fluid_config.yml)内容复制进去。这个_config.fluid.yml是主题的覆盖配置,之后要自定义主题,只需修改其中的内容即可。

修改publish下的_config.yml如下:

theme: fluid  # 指定主题
language: zh-CN  # 指定语言,会影响主题显示的语言,按需修改

最后创建主题的关于页

hexo new page about
测试博客

自此,一个本地的博客网站即搭建成功,可以尝试手动将文档移入_post文件夹,而后在命令行内的 publish 文件夹下运行 hexo ghexo s ,就可以在浏览器看到博客。

最后记得使用hexo clean 清空生成的文件,避免影响到后面的部署。

3. github actions配置

在预处理函数的最后一步,实现了git的自动提交和推送。在成功向github推送后,就轮到github action实现博客网站的自动部署了。

配置github仓库

首先将本地的git仓库上传至github,且设置为公开。

进入github 项目的设置页面开启page功能。设置SourceGitHub Actions

image-20240505163116047

编写actions脚本文件

回到本地,在项目根目录创建 .github/workflows/xxx.yml 文件。

这里需要一些github actions的知识,简单地说 Actions 就是在设定的时机触发创建一个虚拟云环境,然后执行一连串动作,从而实现自动部署的功能。

可以直接复制以下内容:

name: Deploy Github

# 在master分支发生push事件时触发。
on:
  push:
    branches:
      - master
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: 检出仓库
        uses: actions/checkout@v4

      - name: Install pandoc
        run: |
          cd /tmp
          wget -c https://github.com/jgm/pandoc/releases/download/2.14.0.3/pandoc-2.14.0.3-1-amd64.deb
          sudo dpkg -i pandoc-2.14.0.3-1-amd64.deb

      - name: 安装Node.js 18
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Cache NPM dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          key: ${{ runner.OS }}-npm-cache
          restore-keys: |
            ${{ runner.OS }}-npm-cache

      - name: 安装相关依赖
        working-directory: publish
        run: |
          npm install --silent --no-fund
          npm un hexo-renderer-marked --save
          npm i hexo-renderer-markdown-it-plus --save
          
      - name: 复制博客文件
        run: |
          cd publish
          rm -rf source/_posts
          mkdir source/_posts
          cp -rf ../asset/blogs/* ./source/_posts/

      - name: 构建网站
        working-directory: publish
        run: npm run build

      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./publish/public
          
  deploy:
    needs: build
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

github 工作流在完成了环境的初始化后,会自动将位于 asset/blog 中的博客文件复制到 publish/source/_post 目录内。而后的构建步骤,工作流就会利用 _post 目录中的博客文件生成网站。

修改配置

编辑 _config.yml,将 url: 更改为 <你的 GitHub 用户名>.github.io/<repository 的名字>

提交并自动部署

在提交前,记得编辑.gitignore文件:

/resource/githubPublicKey
/publish/node_modules/
/publish/public/

在之后的每次提交,都会触发actions流程,生成站点。部署完成后,前往 https://<你的 GitHub 用户名>.github.io/<repository 的名字> 查看站点。

4. 开启latex功能

hexo默认的渲染器不支持复杂的数学公式。

Latex的渲染分为前端渲染和后端渲染两种方法,这里选择后端渲染,需要更换渲染器(已经在actions脚本中完成了)。

npm un hexo-renderer-marked --save
npm i hexo-renderer-markdown-it-plus --save

修改 _config.fluid.yml

post:
  math:
    enable: true
    specific: false
    engine: katex

使用hexo-renderer-markdown-it-plus插件进行latex渲染时,如果使用mathjax,一些不完全符合md语法的标题会渲染失败(如一级标题前没有空行),因此要将引擎改为katex。

可以参考配置指南 | Hexo Fluid 用户手册 (fluid-dev.com) 和 CHENXCHEN/hexo-renderer-markdown-it-plus

5. 实现评论功能

使用 Giscus插件,且基于 GitHub Discussion实现评论功能。

开启 GitHub Discussion

进入github仓库->Settings->General->Features,勾选 Discussions

image-20240505170039793

安装 Github Apps

点击链接GitHub Apps - giscus ,为本仓库安装应用。

image-20240505170017470

可以到 giscus仓库 检查自己的仓库是否满足giscus要求的条件。

image-20240505170207236

配置插件

由于使用了fluid主题,因此只需修改配置文件即可开启评论。

修改 _config.fluid.yml

  comments:
    enable: true
    # 指定的插件,需要同时设置对应插件的必要参数
    # The specified plugin needs to set the necessary parameters at the same time
    # Options: utterances | disqus | gitalk | valine | waline | changyan | livere | remark42 | twikoo | cusdis | giscus | discuss
    type: giscus
    
# Giscus
# 基于 GitHub Discussions,类似于 Utterances
# Based on GitHub Discussions, similar to Utterances
# See: https://giscus.app/
giscus:
  repo: buttering/EasyBlog
  repo-id: 
  category: Announcements
  category-id: 
  theme-light: light
  theme-dark: dark
  mapping: title
  reactions-enabled: 1
  emit-metadata: 0
  input-position: bottom
  lang: zh-CN

在配置基于 Github Discussions 的博客评论系统时(例如 giscus),往往需要获取 repo 的 repo-id, category-id 等属性,因此这里介绍一种获取这些信息的方法

首先打开以下 Github Docs 地址:Github Docs Explorer,然后授权 Github 账户,并输入以下内容

{
  repository(owner: "userName", name: "repoName") {
    id
    discussionCategories (first: 5) {
      nodes {
        name
        id
      }
    }
  }
}
  • userName 换成具体的 Github 用户名;
  • repoName 换成保存评论数据的 Repo 名称。

点击运行按钮,即可得到形如 "id": "R_kgDOKjFfn1"DIC_kwdOJPFfnc4CU9... 就是我们需要的 repoIdcategoryId

因为网站中文章的url是直接使用文章名进行定位的,如 https://buttering.github.io/EasyBlog/2022/10/15/使用plotly离线模式报错UnicodeEncodeError:gbk codec can't encode character in position的解决方法/,如果 mapping: 的值设为 pathname 或者 url,其在 github discussions中出现的标题,中文会被base64字符代替,严重影响观感,设为 title 可以解决这个问题。

6. 实现访问统计

参考Hexo-fluid主题设置统计博客阅读量与评论 - 贾明晖的博客 (minghuijia.cn)

到LeanCloud注册一个免费账户,创建一个开发板应用后,再创建一个Class(依次点击数据存储->结构化存储->创建Class),权限给到最大。

然后在设置->应用凭证里找到AppID与AppKey。

修改 _config.fluid.yml

# 网页访问统计
# Analysis of website visitors
web_analytics:  # 网页访问统计
  enable: true
    # LeanCloud 计数统计,可用于 PV UV 展示,如果 `web_analytics: enable` 没有开启,PV UV 展示只会查询不会增加
  leancloud:
    app_id: udXg28eCJ44kXbVz5tZCXqLs-MdYXbMMI
    app_key: I8cwTxLkK5cFUXnRVazQrup5
    # REST API 服务器地址,国际版不填
    server_url:
    # 统计页面时获取路径的属性
    path: window.location.pathname
    # 开启后不统计本地路径( localhost 与 127.0.0.1 )
    ignore_local: false
    
footer:
  # 展示网站的 PV、UV 统计数
  statistics:
    enable: true

7. 网站运行时长

参考Fluid 页脚增加网站运行时长 - Hexo Theme Fluid (fluid-dev.com)

修改 _config.fluid.yml

footer:
  content: '
    <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><span>Hexo</span></a>
    <i class="iconfont icon-love"></i>
    <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener"><span>Fluid</span></a>
    <div style="font-size: 0.85rem">
      <span id="timeDate">载入天数...</span>
      <span id="times">载入时分秒...</span>
      <script src="/js/duration.js"></script>
    </div>
  '

content 前三行是 Fluid 原有的页脚内容,建议不要删除,可稍作修改,保留 Fluid 的超链接,用于向更多人推广主题。

新增的div标签用于实现功能。duration.js 包含的是功能代码,我们在博客目录的 source/js 下创建 duration.js (如果没有js目录,创建即可),

如果直接创建source/js/duration.js 是无效的

针对fluid 1.9.7: 需要将 content<script src="/js/duration.js"> 这行改为 <script src="<项目根目录名>/js/duration.js">,如 <script src="/EasyBlog/js/duration.js"></script>

js文件内容如下:

!(function() {
  /** 计时起始时间,自行修改 **/
  var start = new Date("2020/01/01 00:00:00");

  function update() {
    var now = new Date();
    now.setTime(now.getTime()+250);
    days = (now - start) / 1000 / 60 / 60 / 24;
    dnum = Math.floor(days);
    hours = (now - start) / 1000 / 60 / 60 - (24 * dnum);
    hnum = Math.floor(hours);
    if(String(hnum).length === 1 ){
      hnum = "0" + hnum;
    }
    minutes = (now - start) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
    mnum = Math.floor(minutes);
    if(String(mnum).length === 1 ){
      mnum = "0" + mnum;
    }
    seconds = (now - start) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
    snum = Math.round(seconds);
    if(String(snum).length === 1 ){
      snum = "0" + snum;
    }
    document.getElementById("timeDate").innerHTML = "本站安全运行&nbsp"+dnum+"&nbsp天";
    document.getElementById("times").innerHTML = hnum + "&nbsp小时&nbsp" + mnum + "&nbsp分&nbsp" + snum + "&nbsp秒";
  }

  update();
  setInterval(update, 1000);
})();

start 的时间改为自己的时间即可。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MT3516W-ASEMI工业电源专用MT3516W

编辑&#xff1a;ll MT3516W-ASEMI工业电源专用MT3516W 型号&#xff1a;MT3516W 品牌&#xff1a;ASEMI 封装&#xff1a;MTW-5 最大重复峰值反向电压&#xff1a;1600V 最大正向平均整流电流(Vdss)&#xff1a;35A 功率(Pd)&#xff1a;大功率 芯片个数&#xff1a;5…

程序员不会告诉老板的那些神器

目录 1. 持续集成工具&#xff1a;CruiseControl&#xff08;简称CC&#xff09; 2. 代码风格、质量检查工具&#xff1a;StyleCop 3.AI工具 3.1 AI助力编写开发日报 3.2 AI助力编写普适性代码 3.3 AI助力生成代码注释 3.4 AI助力重构代码去掉“坏味道” 3.5 AI助力…

汽车品牌区域营销方案

领克汽车粤海区域营销方案-36P 活动策划信息&#xff1a; 方案页码&#xff1a;36页 文件格式&#xff1a;PPT 方案简介&#xff1a; 车市反弹形势明显&#xff0c;领克销量呈现稳健上涨趋势 品牌 未来市场可观&#xff0c;应 持续扩大品牌声量&#xff0c;保持市场占有优…

【C++】C++11--- lambda表达式

目录 Lambda表达式概述 Lambda表达式语法定义 Lambda表达式参数详解 Lambda捕获列表 捕获列表总结 Lambda参数列表 可变规则mutable lambda表达式原理 Lambda表达式概述 当对自定义类型的数据集合进行排序时&#xff0c;需要根据自定义类型的不同属性去实现不同的排序方…

ESD静电问题 | 手持摄像头整改

【转自微信公众号&#xff1a;柯普伦科技】

用webui.sh安装报错No module named ‘importlib.metadata‘

安装sdweb报错&#xff0c;出现No module named importlib.metadata&#xff1a; glibc version is 2.35 Cannot locate TCMalloc. Do you have tcmalloc or google-perftool installed on your system? (improves CPU memory usage) Traceback (most recent call last):File…

访问网络附加存储:nfs

文章目录 访问网络附加存储一、网络附加存储1.1、存储类型1.3、通过NFS挂载NAS1.4、NFS挂载过程服务端客户端 二、实验&#xff1a;搭建NFS服务端及挂载到nfs客户端服务端客户端测试命令合集服务端客户端 访问网络附加存储 一、网络附加存储 1.1、存储类型 DAS&#xff1a;Di…

TCP四次挥手中为什么 TIME_WAIT 等待的时间是 2MSL?

TCP 连接断开 1、TCP 四次挥手过程是怎样的&#xff1f;如下图 2、为什么 TIME_WAIT 等待的时间是 2MSL&#xff1f; MSL 是 Maximum Segment Lifetime&#xff0c;报文最大生存时间&#xff0c;它是任何报文在网络上存在的最长时间&#xff0c;超过这个时间报文将被丢弃。因…

激光雷达技术:科技之眼,照亮前行

在科技与人文关怀的交响乐章中&#xff0c;一项名为“蝙蝠避障”使用了激光雷达技术原理及应用的创新成果&#xff0c;正悄然改变着视障朋友们的生活方式&#xff0c;为他们的日常出行铺设了一条充满希望的光明之路。今天&#xff0c;让我们一起深入探讨这项技术如何成为盲人出…

毕业论文怎么写? 推荐4个AI工具

写作这件事一直让我们从小学时期就开始头痛&#xff0c;初高中时期800字的作文让我们焦头烂额&#xff0c;一篇作文里用尽了口水话&#xff0c;拼拼凑凑才勉强完成。 大学时期以为可以轻松顺利毕业&#xff0c;结果毕业前的最后一道坎拦住我们的是毕业论文&#xff0c;这玩意不…

微火快讯:全域运营服务商是什么?赚得多吗?

随着互联网下半场的开启&#xff0c;公域和私域两大流量池持续不断地迸发出新的活力&#xff0c;一定程度上推动了全域运营赛道和微火全域运营服务商职业的兴起。 所谓全域运营&#xff0c;简而言之&#xff0c;就是所有领域内与商家、品牌运营相关的业务&#xff0c;包括但不限…

Tomcat、MySQL、Redis最大支持说明

文章目录 一、Tomcat二、MySQL三、Redis1、最大连接数2、TPS、QPS3、key和value最大支持 一、Tomcat 查看SpringBoot内置Tomcat的源码&#xff0c;如下&#xff1a; 主要就是看抽象类AbstractEndpoint&#xff0c;可以看到默认的核心线程数10&#xff0c;最大线程数200 通过…

Linux|了解如何使用 awk 内置变量

引言 当我们揭开 Awk 功能部分时&#xff0c;我们将介绍 Awk 中内置变量的概念。您可以在 Awk 中使用两种类型的变量&#xff1a;用户定义的变量和内置变量。 内置变量的值已经在 Awk 中定义&#xff0c;但我们也可以仔细更改这些值&#xff0c;内置变量包括&#xff1a; FILEN…

实时路况信息获取的意义:点亮盲人前行的每一寸道路

在这个日新月异的科技时代&#xff0c;一款名为“蝙蝠避障”的创新应用正以温暖而坚定的步伐&#xff0c;为盲人朋友们的日常出行编织着安全与自由的网络。这款应用的核心——激光雷达技术&#xff0c;正以前所未有的方式&#xff0c;重新定义着实时路况信息获取的意义&#xf…

第二证券午评:沪指涨近1%,地产、半导体等板块拉升,锂电池概念活跃

9日早盘&#xff0c;两市股指全线走高&#xff0c;沪指涨近1%&#xff0c;创业板指大涨近2%&#xff1b;北向资金大举出场扫货&#xff0c;半日净买入超100亿元。 到午间收盘&#xff0c;沪指涨0.91%报3156.96点&#xff0c;深成指涨1.63%&#xff0c;创业板指涨1.85%&#xf…

【基于 PyTorch 的 Python深度学习】5 机器学习基础(2)

前言 文章性质&#xff1a;学习笔记 &#x1f4d6; 学习资料&#xff1a;吴茂贵《 Python 深度学习基于 PyTorch ( 第 2 版 ) 》【ISBN】978-7-111-71880-2 主要内容&#xff1a;根据学习资料撰写的学习笔记&#xff0c;该篇主要介绍了如何选择合适的激活函数、损失函数和优化器…

学习大数据,所需更要的shell基础(2)

文章目录 read读取控制台输入函数系统函数bashnamedirname 自定义函数Shell工具&#xff08;重点&#xff09;cutawk 正则表达式入门常规匹配常用特殊字符 read读取控制台输入 1&#xff09;基本语法 read (选项) (参数) ①选项&#xff1a; -p&#xff1a;指定读取值时的提示…

【福利】思科CCNP考试介绍(附CCNP题库下载)

网络行业有两个大神级别的证书&#xff1a;思科认证和华为认证&#xff0c;目前相比思科认证&#xff0c;华为认证在国内更加吃香哦&#xff0c;如果你在国内就业或发展考虑建议考华为的。不过还是有少部分在外企或有出国计划的IT人员考思科的。 那今天小微就来给大家介绍下思科…

SpringBoot启动流程源码解析

目录 一、SpringApplication构造方法解析 1. web应用类型 2. BootstrapRegistryInitializer 3. ApplicationContextInitializer 4. ApplicationListener 5. 推断Main方法所在类 二、SpringApplication.run(String... args)方法解析 1.创建DefaultBootstrapContext 2.获…

回顾5款我非常喜欢的软件,希望大家也能喜欢

​ 我喜欢分享好软件,这就像与老友聊天一样让我感到快乐。在这个过程中,我可以回顾这些实用的小工具,也希望它们可以帮助到更多人。 1.备份工具——Cobian Backup ​ Cobian Backup是一款功能强大的备份软件&#xff0c;支持自动定时备份、增量备份、差异备份等多种备份方式。…