commit d5475430986c29b41b29b2e37a2c138269f10991 Author: zhangsan <646228430@qq.com> Date: Tue Mar 18 12:46:59 2025 +0800 first commit diff --git a/Java/JavaWeb——前端.md b/Java/JavaWeb——前端.md new file mode 100644 index 0000000..3f3aeb9 --- /dev/null +++ b/Java/JavaWeb——前端.md @@ -0,0 +1,1110 @@ +# JavaWeb + +## JavaWeb学习路线 + +![image-20240227162313041](D:/folder/test/output/56ee13bd-1250-40b6-82c5-58589fb664ee.png) + +## 前后端分离开发 + +![image-20240229125233265](D:/folder/test/output/3a2f79fd-6c6d-4338-96a6-a83e251b92a9.png) + + + +![image-20240229125254635](D:/folder/test/output/98ddfeb8-a696-4eab-a11e-f44f5b812d65.png) + +1. 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。 +2. 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等 +3. 前后台并行开发:各自按照接口文档进行开发,实现需求 +4. 测试:前后台开发完了,各自按照接口文档进行测试 +5. 前后段联调测试:前段工程请求后端工程,测试功能 + +## Html/CSS + +### 标签速记: + +不闭合标签: + + 空格占位符: + +正文格式:line-height:设置行高;text-indent:设置首行缩进;text-align:规定文本的水平对齐方式 + +### CSS引入方式 + +| 名称 | 语法描述 | 示例 | +| -------- | ------------------------------------------------- | ---------------------------------------------- | +| 行内样式 | 在标签内使用style属性,属性值是css属性键值对 | <h1 style="xxx:xxx;">中国新闻网</h1> | +| 内嵌样式 | 定义<style>标签,在标签内部定义css样式 | <style> h1 {...} </style> | +| 外联样式 | 定义<link>标签,通过href属性引入外部css文件 | <link rel="stylesheet" href="css/news.css"> | + +### CSS选择器 + +**1.元素(标签)选择器:** + +- 选择器的名字必须是标签的名字 +- 作用:选择器中的样式会作用于所有同名的标签上 + +~~~ +元素名称 { + css样式名:css样式值; +} +~~~ + +例子如下: + +~~~css + div{ + color: red; + } +~~~ + +**2.id选择器:** + +- 选择器的名字前面需要加上# +- 作用:选择器中的样式会作用于指定id的标签上,而且**有且只有一个**标签(由于id是唯一的) + +~~~ +#id属性值 { + css样式名:css样式值; +} +~~~ + +例子如下: + +~~~css +#did { + color: blue; +} +~~~ + +**3.类选择器:** + +- 选择器的名字前面需要加上 . +- 作用:选择器中的样式会作用于所有class的属性值和该名字一样的标签上,可以是多个 + +~~~ +.class属性值 { + css样式名:css样式值; +} +~~~ + +例子如下: + +~~~css +.cls{ + color: green; + } +~~~ + +这里使用了第二种CSS引入方式,内嵌样式, + + + + 新浪政务 > 正文 + +

焦点访谈:中国底气 新思想夯实大国粮仓

+ +
+ 2023年03月02日 21:50 央视网 +
+ + + +``` + +### 页面布局 + +盒子模型,盒子部分指的是border及以内的部分,不包括margin + +![image-20240228095942357](D:/folder/test/output/9e99deff-c0ab-4331-bf8c-46fc96374722.png) + +![image-20240228100247314](D:/folder/test/output/b7ea4176-c3a6-4fce-886c-bb6bff1bdcfa.png) + + + +- 布局标签:实际开发网页中,会大量频繁的使用 div 和 span 这两个没有语义的布局标签。 + +- 标签:
+ +- 特点: + + - div标签: + + - 一行只显示一个(独占一行) + + - 宽度默认是父元素的宽度,高度默认由内容撑开 + + - 可以设置宽高(width、height) + + - span标签: + + - 一行可以显示多个 + + - 宽度和高度默认由内容撑开 + + - 不可以设置宽高(width、height) + +box-sizing: border-box,此时指定width height为盒子的高宽,而不是content的高宽 + +### 表格标签 + +- table> : 用于定义整个表格, 可以包裹多个 <tr>, 常用属性如下: + - border:规定表格边框的宽度 + - width:规定表格的宽度 + - cellspacing: 规定单元之间的空间 + +- <tr> : 表格的行,可以包裹多个 <td> +- <td> : 表格单元格(普通),可以包裹内容 , 如果是表头单元格,可以替换为 <th> ,th具有加粗居中展示的效果 + +### 表单标签 + +- 表单场景: 表单就是在网页中负责数据采集功能的,如:注册、登录的表单。 + +- 表单标签: <form> + +- 表单属性: + + - action: 规定表单提交时,向何处发送表单数据,表单提交的URL。 + - method: 规定用于发送表单数据的方式,常见为: GET、POST。 + - GET:表单数据是拼接在url后面的, 如: xxxxxxxxxxx?username=Tom&age=12,url中能携带的表单数据大小是有限制的。 + - POST: 表单数据是在请求体(消息体)中携带的,大小没有限制。 + +- 表单项标签: 不同类型的input元素、下拉列表、文本域等。 + + - input: 定义表单项,通过type属性控制输入形式 + + | type取值 | **描述** | + | ------------------------ | ------------------------------------ | + | text | 默认值,定义单行的输入字段 | + | password | 定义密码字段 | + | radio | 定义单选按钮 | + | checkbox | 定义复选框 | + | file | 定义文件上传按钮 | + | date/time/datetime-local | 定义日期/时间/日期时间 | + | number | 定义数字输入框 | + | email | 定义邮件输入框 | + | hidden | 定义隐藏域 | + | submit / reset / button | 定义提交按钮 / 重置按钮 / 可点击按钮 | + + - select: 定义下拉列表 + - textarea: 定义文本域 + + + +``` + + + + + + + HTML-表单项标签 + + + + +
+ 姓名:

+ 密码:

+ 性别: 男 +

+ 爱好: + +

+ 图像:

+ 生日:

+ 时间:

+ 日期时间:

+ 邮箱:

+ 年龄:

+ 学历:

+ 描述:

+ + + + + + +
+
+ + + +``` + +- `name="gender"`:这个属性定义了单选按钮组的名称,它们被分为同一个组,因此只能选择其中的一个按钮。在这种情况下,所有具有相同 `name` 属性值的单选按钮都被视为同一组。 +- `value="1"` 和 `value="2"`:这些是单选按钮的值。当用户选择某个单选按钮时,该单选按钮的值将被提交到服务器。在这种情况下,`value="1"` 表示选择男性,而 `value="2"` 表示选择女性。 +- 用户在浏览器中看到的文本内容是 "男" 和 "女"。 + + 这里的label意味着用户不仅可以点击单选按钮本身来选择选项,当用户单击 "女" 这个标签文本时,与之关联的单选按钮也会被选中。 + + + +## JavaScript + +### JS引入方式 + +**第一种方式:**内部脚本,将JS代码定义在HTML页面中 + +- JavaScript代码必须位于<script></script>标签之间 +- 在HTML文档中,可以在任意地方,放置任意数量的<script> +- 一般会把脚本置于<body>元素的底部,可改善显示速度 + +例子: + +~~~html + +~~~ + +**第二种方式:**外部脚本将, JS代码定义在外部 JS文件中,然后引入到 HTML页面中 + +- 外部JS文件中,只包含JS代码,不包含 +~~~ + +注意:demo.js中只有js代码,没有<script>标签 + + + +### JS基础语法 + +#### 书写语法 + +- 区分大小写:与 Java 一样,变量名、函数名以及其他一切东西都是区分大小写的 + +- 每行结尾的分号可有可无 + +- 大括号表示代码块 + +- 注释: + + - 单行注释:// 注释内容 + + - 多行注释:/* 注释内容 */ + +输出的三种形式: + +| api | 描述 | +| ---------------- | ---------------- | +| window.alert() | 警告框 | +| document.write() | 在HTML 输出内容 | +| console.log() | 写入浏览器控制台 | + +#### 变量 + +| 关键字 | 解释 | +| ------ | ------------------------------------------------------------ | +| var | 早期ECMAScript5中用于变量声明的关键字 | +| let | ECMAScript6中新增的用于变量声明的关键字,相比较var,let只在代码块内生效(大括号) | +| const | 声明常量的,常量一旦声明,不能修改 | + +var:作用域比较大,全局的;可以重复定义,后面的覆盖前面的 + +let:局部变量(代码块内生效{}),不可重复定义。 + +const: const pi=3.14 + +#### 数据类型和运算符 + +| 数据类型 | 描述 | +| --------- | -------------------------------------------------- | +| number | 数字(整数、小数、NaN(Not a Number)) | +| string | 字符串,单双引皆可 | +| boolean | 布尔。true,false | +| null | 对象为空 | +| undefined | 当声明的变量未初始化时,该变量的默认值是 undefined | + +| 运算规则 | 运算符 | +| ---------- | ------------------------------------------------------------ | +| 算术运算符 | + , - , * , / , % , ++ , -- | +| 赋值运算符 | = , += , -= , *= , /= , %= | +| 比较运算符 | > , < , >= , <= , != , == , === **注意 == 会进行类型转换,=== 不会进行类型转换** | +| 逻辑运算符 | && , \|\| , ! | +| 三元运算符 | 条件表达式 ? true_value: false_value | + +![image-20240228122315690](D:/folder/test/output/8f3e1687-e901-4036-b757-07aca6c47df3.png) + +parseint() ,将其他类型转化为数字 + +#### 函数 + +第一种: + +~~~js +function 函数名(参数1,参数2..){ + 要执行的代码 +} +~~~ + +因为JavaScript是**弱数据类型**的语言,所以有如下几点需要注意: + +- 形式参数不需要声明类型,并且JavaScript中不管什么类型都是let或者var去声明,加上也没有意义。 +- 返回值也不需要声明类型,直接return即可 + +如下示例: + +``` +function add(a, b){ + return a + b; +} +``` + +var result=add(10,20)可以接收返回值 + + + +第二种可以通过var去定义函数的名字,具体格式如下: + +``` +var functionName = function (参数1,参数2..){ + //要执行的代码 +} +``` + +如下示例: + +``` +var add = function(a,b){ + return a + b; + } +``` + + var result = add(10,20); 函数的调用不变 + +### JS对象 + +#### Array对象 + +方式1: + +~~~js +var 变量名 = new Array(元素列表); +~~~ + +例如: + +~~~js +var arr = new Array(1,2,3,4); //1,2,3,4 是存储在数组中的数据(元素) +~~~ + +方式2: + +~~~js +var 变量名 = [ 元素列表 ]; +~~~ + +例如: + +~~~js +var arr = [1,2,3,4]; //1,2,3,4 是存储在数组中的数据(元素) +~~~ + +长度可变=》可以直接arr[10]=100,不会报错 + +类型可变=》arr[1]="hello",可以既存数字又存字符串 + + + +属性: + +| 属性 | 描述 | +| :----- | :--------------------------- | +| length | 设置或返回数组中元素的数量。 | + +方法: + +| 方法方法 | 描述 | +| :-------- | :--------------------------------------------------- | +| forEach() | 遍历数组中的每个**有值的**元素,并调用一次传入的函数 | +| push() | 将新元素添加到数组的**末尾**,并返回新的长度 | +| splice() | 从数组中删除元素 | + +普通for循环:会遍历每个数组元素,无论是否有值 + +``` +var arr = [1,2,3,4]; +arr[10] = 50; + for (let i = 0; i < arr.length; i++) { + console.log(arr[i]); +} +``` + +foreach: + +``` +arr.forEach(function(e){ + console.log(e); +}) +``` + +在ES6中,引入箭头函数的写法,语法类似java中lambda表达式,修改上述代码如下: + +``` +arr.forEach((e) => { + console.log(e); +}) +``` + +push: arr.push(7,8,9) 可以一次添加多个元素 + +splice: arr.splice(start,cnt),从start开始,删cnt个元素 + +#### String字符串 + +String对象也提供了一些常用的属性和方法,如下表格所示: + +属性: + +| 属性 | 描述 | +| ------ | -------------- | +| length | 字符串的长度。 | + +方法: + +| 方法 | 描述 | +| ----------- | ---------------------------------------- | +| charAt() | 返回在指定位置的字符。 | +| indexOf() | 检索字符串。 | +| trim() | 去除字符串两边的空格 | +| substring() | 提取字符串中两个指定的索引号之间的字符。 | + +- length属性: + + length属性可以用于返回字符串的长度,添加如下代码: + + ~~~js + //length + console.log(str.length); + ~~~ + +- charAt()函数: + + charAt()函数用于返回在指定索引位置的字符,函数的参数就是索引。添加如下代码: + + ~~~js + console.log(str.charAt(4)); + ~~~ + +- indexOf()函数 + + indexOf()函数用于检索指定内容在字符串中的索引位置的,返回值是索引,参数是指定的内容。添加如下代码: + + ~~~js + console.log(str.indexOf("lo")); + ~~~ + +- trim()函数 + + trim()函数用于去除字符串两边的空格的。添加如下代码: + + ~~~js + var s = str.trim(); + console.log(s.length); + ~~~ + +- substring()函数 + + substring()函数用于截取字符串的,函数有2个参数。 + + 参数1:表示从那个索引位置开始截取。包含 + + 参数2:表示到那个索引位置结束。不包含 + + ~~~js + console.log(s.substring(0,5)); + ~~~ + + + +#### JSON对象 + +##### 自定义对象 + +``` +var 对象名 = { + 属性名1: 属性值1, + 属性名2: 属性值2, + 属性名3: 属性值3, + 函数名称: function(形参列表){} +}; +``` + +我们可以通过如下语法调用属性: + +~~~js +对象名.属性名 +~~~ + +通过如下语法调用函数: + +~~~js +对象名.函数名() +~~~ + +##### json对象 + +JSON对象:**J**ava**S**cript **O**bject **N**otation,JavaScript对象标记法。是通过JavaScript标记法书写的文本。其格式如下: + +~~~js +{ + "key":value, + "key":value, + "key":value +} +~~~ + +其中,**key必须使用引号并且是双引号标记,value可以是任意数据类型。** + +JSON字符串示例: + +``` +var jsonstr = '{"name":"Tom", "age":18, "addr":["北京","上海","西安"]}'; +alert(jsonstr.name); +``` + +注意外层的单引号不要忘记! + +JSON字符串=》JS对象 + +``` +var obj = JSON.parse(jsonstr); +``` + +**对象.属性 就可以获得key对应的值** + +JS对象=》JS字符串 + +``` +var jsonstr=JSON.stringify(obj) +``` + +##### JSON格式数据 + +`{"name":"666"}` 是一个 JSON 对象,`[{"name":"666"},{"name":"li"}]` 是一个 JSON 数组,它们都是 JSON 格式的数据。 + +#### BOM对象 + +重点学习的是Window对象、Location(地址栏)对象 + +##### window对象 + +常用方法:通过可简写,window.alert()->alert() + +| 函数 | 描述 | +| ------------- | -------------------------------------------------- | +| alert() | 显示带有一段消息和一个确认按钮的警告框。 | +| comfirm() | 显示带有一段消息以及确认按钮和取消按钮的对话框。 | +| setInterval() | 按照指定的周期(以毫秒计)来调用函数或计算表达式。 | +| setTimeout() | 在指定的毫秒数后调用函数或计算表达式。 | + +**setInterval**(fn,毫秒值):定时器,用于周期性的执行某个功能,并且是**循环执行**。该函数需要传递2个参数: + +fn:函数,需要周期性执行的功能代码 + +毫秒值:间隔时间 + +``` +//定时器 - setInterval -- 周期性的执行某一个函数 +var i = 0; +setInterval(function(){ + i++; + console.log("定时器执行了"+i+"次"); +},2000); +``` + + + +**setTimeout**(fn,毫秒值) :定时器,只会在一段时间后**执行一次功能**。参数和上述setInterval一致 + +注释掉之前的代码,添加代码如下: + +~~~js +//定时器 - setTimeout -- 延迟指定时间执行一次 +setTimeout(function(){ + alert("JS"); +},3000); +~~~ + +浏览器打开,3s后弹框,关闭弹框,发现再也不会弹框了。 + + + +##### Location对象 + +location是指代浏览器的地址栏对象,对于这个对象,我们常用的是href**属性**,用于获取或者设置浏览器的地址信息,添加如下代码: + +``` +//获取浏览器地址栏信息 +alert(location.href); +//设置浏览器地址栏信息 +location.href = "https://www.itcast.cn"; +``` + +设置后会自动跳转到该地址。 + + + +#### DOM对象 + +##### DOM介绍 + +DOM:Document Object Model 文档对象模型。也就是 JavaScript 将 HTML 文档的各个组成部分封装为对象。 + +封装的对象分为 + +- Document:整个文档对象 +- Element:元素对象 +- Attribute:属性对象 +- Text:文本对象 +- Comment:注释对象 + +![1668796698067](D:/folder/test/output/7b27733d-5754-465f-a262-804e26e0b9f1.png) + +那么我们学习DOM技术有什么用呢?主要作用如下: + +- 改变 HTML 元素的内容 +- 改变 HTML 元素的样式(CSS) +- 对 HTML DOM 事件作出反应 +- 添加和删除 HTML 元素 + +从而达到**动态改变页面效果**目的。 + +##### DOM获取 + +| 函数 | 描述 | +| --------------------------------- | -------------------------------------------- | +| document.getElementById() | 根据id属性值获取,返回单个Element**对象** | +| document.getElementsByTagName() | 根据标签名称获取,返回Element对象**数组** | +| document.getElementsByName() | 根据name属性值获取,返回Element对象**数组** | +| document.getElementsByClassName() | 根据class属性值获取,返回Element对象**数组** | + +示例代码: + +``` + +

+ +
传智教育

+
黑马程序员

+ + 电影 + 旅游 + 游戏 + +``` + +- document.getElementById(): 根据标签的id属性获取标签对象,id是唯一的,所以获取到是单个标签对象。 + +``` + +``` + +- document.getElementsByTagName() : 根据标签的名字获取标签对象,同名的标签有很多,所以返回值是数组。重点! + +``` +var divs = document.getElementsByTagName('div'); +for (let i = 0; i < divs.length; i++) { + alert(divs[i]); +} +``` + +##### DOM修改 + +同上面的例子: + +你想要如何操作获取到的DOM元素,你需要查阅手册,看它支持的属性 + +![image-20240228185708363](D:/folder/test/output/9f45477c-ede5-4b1a-b687-16ad77bdb6cc.png) + +``` +var divs = document.getElementsByClassName('cls'); +var div1 = divs[0]; +div1.innerHTML = "传智教育666"; +``` + +### JS事件 + +JavaScript对于事件的绑定提供了2种方式: + +- 方式1:通过html标签中的事件属性进行绑定 + +``` + + +``` + +- 方式2:通过DOM中Element元素的事件属性进行绑定 + +``` + + +``` + +常见事件: + +| 事件属性名 | 说明 | +| ----------- | ------------------------ | +| onclick | 鼠标单击事件 | +| onblur | 元素失去焦点 | +| onfocus | 元素获得焦点 | +| onload | 某个页面或图像被完成加载 | +| onsubmit | 当表单提交时触发该事件 | +| onmouseover | 鼠标被移到某元素之上 | +| onmouseout | 鼠标从某元素移开 | + +## VUE + +### VUE简介 + +我们引入了一种叫做**MVVM(Model-View-ViewModel)的前端开发思想**,即让我们开发者更加关注数据,而非数据绑定到视图这种机械化的操作。那么具体什么是MVVM思想呢? + +MVVM:其实是Model-View-ViewModel的缩写,有3个单词,具体释义如下: + +- Model: 数据模型,特指前端中通过请求从后台获取的数据 +- View: 视图,用于展示数据的页面,可以理解成我们的html+css搭建的页面,但是没有数据 +- ViewModel: 数据绑定到视图,负责将数据(Model)通过JavaScript的DOM技术,将数据展示到视图(View)上 + +![image-20240228194959643](D:/folder/test/output/1687ae31-9972-4b16-98d2-fda4ce2db1aa.png) + +基于上述的MVVM思想,其中的Model我们可以通过Ajax来发起请求从后台获取;对于View部分,我们将来会学习一款ElementUI框架来替代HTML+CSS来更加方便的搭建View;而今天我们要学习的就是侧重于ViewModel部分开发的vue前端框架,用来替代JavaScript的DOM操作,让数据展示到视图的代码开发变得更加的简单。 + +### VUE快速上手 + +第一步:在VS Code中创建名为12. Vue-快速入门.html的文件,并且在html文件同级创建js目录,将**资料/vue.js文件**目录下得vue.js拷贝到js目录 + +第二步:然后编写<script>标签来引入vue.js文件,代码如下: + +``` + +``` + +第三步:在js代码区域定义vue对象,代码如下: + +``` + +``` + +在创建vue对象时,有几个常用的属性: + +- el: 用来指定哪儿些标签受 Vue 管理。 该属性取值 `#app` 中的 `app` 需要是受管理的标签的id属性值 +- data: 用来定义数据模型 +- methods: 用来定义函数。这个我们在后面就会用到 + +第四步:在html区域编写视图,其中{{}}是插值表达式,用来将vue对象中定义的model展示到页面上的 + +~~~html + +
+ + {{message}} +
+ +~~~ + +### Vue指令 + +**指令:**HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义。 + +| **指令** | **作用** | +| --------- | --------------------------------------------------- | +| v-bind | 为HTML标签绑定属性值,如设置 href , css样式等 | +| v-model | 在表单元素上创建双向数据绑定 | +| v-on | 为HTML标签绑定事件 | +| v-if | 条件性的渲染某元素,判定为true时渲染,否则不渲染 | +| v-else | | +| v-else-if | | +| v-show | 根据条件展示某元素,区别在于切换的是display属性的值 | +| v-for | 列表渲染,遍历容器的元素或者对象的属性 | + +#### V-bind和v-model + +- v-bind: 为HTML标签绑定属性值,如设置 href , css样式等。当vue对象中的数据模型发生变化时,标签的属性值会随之发生变化。**单向**绑定! + +- v-model: 在表单元素上创建**双向**数据绑定。什么是双向? + + - vue对象的data属性中的数据变化,视图展示会一起变化 + + - 视图数据发生变化,vue对象的data属性中的数据也会随着变化。 + + +data属性中数据变化,我们知道可以通过赋值来改变,但是视图数据为什么会发生变化呢?**只有表单项标签!所以双向绑定一定是使用在表单项标签上的**。 + +``` + +
+ 链接1 + 链接2 + +
+ + +``` + +#### v-on + +v-on: 用来给html标签绑定事件的 + +``` + +``` + +简写: + +``` + +``` + +script: + +``` + +``` + +#### v-if和v-show + +``` +年龄经判定,为: +年轻人(35及以下) +中年人(35-60) +老年人(60及以上) +``` + +``` +年龄经判定,为: +年轻人(35及以下) +中年人(35-60) +老年人(60及以上) +``` + +v-show和v-if的作用效果是一样的,只是原理不一样。v-if指令,不满足条件的标签代码直接没了,而v-show指令中,不满足条件的代码依然存在,只是添加了css样式来控制标签不去显示。 + +#### vue-for + +v-for: 从名字我们就能看出,这个指令是用来遍历的。其语法格式如下: + +``` +
+
{{addr}}
+
+
{{index}} : {{addr}}
+
+``` + +``` + +``` + +index从0开始 + +### Vue生命周期 + +| 状态 | 阶段周期 | +| ------------- | -------- | +| beforeCreate | 创建前 | +| created | 创建后 | +| beforeMount | 挂载前 | +| mounted | 挂载完成 | +| beforeUpdate | 更新前 | +| updated | 更新后 | +| beforeDestroy | 销毁前 | +| destroyed | 销毁后 | + +其中我们需要重点关注的是**mounted,**其他的我们了解即可。 + +与methods平级 + +mounted:挂载完成,Vue初始化成功,HTML页面渲染成功。**以后我们一般用于页面初始化自动的ajax请求后台数据** + + + +## Ajax-Axios + +Ajax: 全称Asynchronous JavaScript And XML,异步的JavaScript和XML。其作用有如下2点: + +- 与服务器进行数据交换:通过Ajax可以给服务器发送请求,并获取服务器响应的数据。 +- 异步交互:可以在**不重新加载整个页面**的情况下,与服务器交换数据并**更新部分网页**的技术,如:搜索联想、用户名是否可用的校验等等。 + +现在Ajax已经淘汰,用Axios了,是对Ajax的封装 + +### Axios快速上手 + +Axios的使用比较简单,主要分为2步: + +- 引入Axios文件 + + ~~~html + + ~~~ + +- 使用Axios发送请求,并获取响应结果,官方提供的api很多,此处给出2种,如下 + + - 发送 get 请求 + + ~~~js + axios({ + method:"get", + url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan" + }).then(function (resp){ + alert(resp.data); + }) + ~~~ + + - 发送 post 请求 + + ```js + axios({ + method:"post", + url:"http://localhost:8080/ajax-demo1/aJAXDemo1", + data:"username=zhangsan" + }).then(function (resp){ + alert(resp.data); + }); + ``` + + +推荐以下方法! + +| 方法 | 描述 | +| ---------------------------------- | -------------- | +| axios.get(url [, config]) | 发送get请求 | +| axios.delete(url [, config]) | 发送delete请求 | +| axios.post(url [, data[, config]]) | 发送post请求 | +| axios.put(url [, data[, config]]) | 发送put请求 | + +``` +axios.get("http://yapi.smart-xwork.cn/mock/169327/emp/list").then(result => { + console.log(result.data); +}) +``` + +``` +axios.post("http://yapi.smart-xwork.cn/mock/169327/emp/deleteById","id=1").then(result => { + console.log(result.data); +}) +``` + +### axios使用步骤: + +步骤: + +1. 首先创建文件,提前准备基础代码,包括表格以及vue.js和axios.js文件的引入 +2. 我们需要在vue的mounted钩子函数中发送ajax请求,获取数据 +3. 拿到数据,数据需要绑定给vue的data属性 +4. 在<tr>标签上通过v-for指令遍历数据,展示数据,这里同Vue中的步骤。 + +``` + +``` + +Vue中先定义emps空数组,再axios将数据取到里面 this.emps=xxxx \ No newline at end of file diff --git a/Java/JavaWeb——后端.md b/Java/JavaWeb——后端.md new file mode 100644 index 0000000..b40f0c6 --- /dev/null +++ b/Java/JavaWeb——后端.md @@ -0,0 +1,2414 @@ +# JavaWeb——后端 + +## Java版本解决方案 + +**单个Py文件运行:Edit Configurations** + +- **针对单个运行配置**:每个 Java 运行配置(如主类、测试类等)可以独立设置其运行环境(如 JRE 版本、程序参数、环境变量等)。 +- **不影响全局项目**:修改某个运行配置的环境不会影响其他运行配置或项目的全局设置。 + +**如何调整全局项目的环境** + +- 打开 `File -> Project Structure -> Project`。 +- 在 `Project SDK` 中选择全局的 JDK 版本(如 JDK 17)。 +- 在 `Project language level` 中设置全局的语言级别(如 17)。 + +**Java Compiler** + +- `File -> Settings -> Build, Execution, Deployment -> Compiler -> Java Compiler` + +**Maven Runner** + +- `File -> Settings -> Build, Execution, Deployment -> Build Tools -> Maven -> Runner` + + + +. **三者之间的关系** + +- **全局项目环境** 是基准,决定项目的默认 JDK 和语言级别。 +- **Java Compiler** 控制编译行为,可以覆盖全局的 `Project language level`。 +- **Maven Runner** 控制 Maven 命令的运行环境,可以覆盖全局的 `Project SDK`。 + +**Maven 项目**: + +- 确保 `pom.xml` 中的 `` 和 `` 与 `Project SDK` 和 `Java Compiler` 的配置一致。 +- 确保 `Maven Runner` 中的 `JRE` 与 `Project SDK` 一致。 +- 如果还是不行,pom文件右键点击maven->reload project + + + +## Maven + +![image-20240229132137502](D:/folder/test/output/7ca37eb8-04fd-4b32-a3f1-f50a6eac7306.png) + +![image-20240229133408054](D:/folder/test/output/7654879e-7829-4440-82b4-75293b7369fc.png) + +Maven仓库分为: + +- 本地仓库:自己计算机上的一个目录(用来存储jar包) +- 中央仓库:由Maven团队维护的全球唯一的。仓库地址:https://repo1.maven.org/maven2/ +- 远程仓库(私服):一般由公司团队搭建的私有仓库 + +POM文件导入依赖的时候,先看本地仓库有没有,没有就看私服,再没有就从中央仓库下载。 + + + +### Maven创建/导入项目 + +**创建Maven项目** + +![image-20250307174233390](D:\folder\test\output\image-20250307174233390.png) + +勾选 **Create from archetype**(可选),也可以选择 **maven-archetype-quickstart** 等模版。 + +点击 Next,填写 GAV 坐标 。 + +GroupId:标识组织或公司(通常使用域名反写,如 `com.example`) + +ArtifactId:标识具体项目或模块(如 `my-app`、`spring-boot-starter-web`)。 + +Version:标识版本号(如 `1.0-SNAPSHOT`、`2.7.3`) + + + +**导入Maven项目** + + + +**(一)单独的Maven项目** + +打开 IDEA,在主界面选择 Open(或者在菜单栏选择 File -> Open)。 + +在文件选择对话框中,定位到已有项目的根目录(包含 `pom.xml` 的目录)。 + +选择该目录后,IDEA 会检测到 `pom.xml` 并询问是否导入为 Maven 项目,点击 **OK** 或 **Import** 即可。 + +IDEA 会自动解析 `pom.xml`,下载依赖并构建项目结构。 + + + +**(二)在现有Maven项目中导入独立的Maven项目** + +在已经打开的 IDEA 窗口中,使用 **File -> New -> Module from Existing Sources...** + +选择待导入项目的根目录(其中包含 `pom.xml`),IDEA 会将其导入为同一个工程下的另一个模块(Module)。 + + + +**(三)两个模块有较强的关联** + +1.新建一个上层目录,如下,MyProject1和MyProject2的内容拷贝过去。 + +``` +ParentProject/ +├── pom.xml <-- 父模块(聚合模块) +├── MyProject1/ <-- 子模块1 +│ └── pom.xml +└── MyProject2/ <-- 子模块2 + └── pom.xml +``` + +2.创建父级pom + +父模块 `pom.xml` 示例: + +``` + + 4.0.0 + com.example + ParentProject + 1.0-SNAPSHOT + pom //必写 + + + MyProject1 //必写 + MyProject2 + + + +``` + +3.修改子模块 `pom.xml` ,加上: + +``` + + com.example + ParentProject + 1.0-SNAPSHOT + ../pom.xml + +``` + +如果子模块中无需与父级不同的配置,**可以不写**,就自动继承父级配置;若写了同名配置,则表示你想要**覆盖或合并**父级配置。 + +4.File -> Open选择父级的pom,会自动导入其下面两个项目。 + + + +**(四)通过 Maven 依赖引用(一般导入官方依赖)** + +如果你的两个项目之间存在依赖关系(例如,第二个项目需要引用第一个项目打包后的 JAR),可以采用以下方式: + +在第一个项目(被依赖项目)执行 `mvn install` + +- 这会把打包后的产物安装到本地仓库(默认 `~/.m2/repository`)。 + +在第二个项目的 `pom.xml` 中添加依赖坐标 + +``` + + com.example + my-first-project + 1.0-SNAPSHOT + + +``` + +Maven 重建 + + + +### Maven坐标 + +什么是坐标? + +* Maven中的坐标是==资源的唯一标识== , 通过该坐标可以唯一定位资源位置 +* 使用坐标来定义项目或引入项目中需要的依赖 + +![image-20240302131843540](D:/folder/test/output/b927f172-038e-460f-9f9a-7ae3f67c0bcb.png) + +### 依赖管理 + +可以到mvn的中央仓库(https://mvnrepository.com/)中搜索获取依赖的坐标信息 + +``` + + + + ch.qos.logback + logback-classic + 1.2.11 + + + + junit + junit + 4.12 + test + + +``` + +更改之后可以在界面上看到一个maven刷新按钮,点击一下就开始联网下载依赖了,成功后可以看到 + +![image-20240302133241227](D:/folder/test/output/b0f72966-3d88-4e3e-a5ce-2eb4d1f3980c.png) + +#### 排除依赖 + +A依赖B,B依赖C,如果A不想将C依赖进来,可以同时排除C,被排除的资源**无需指定版本**。 + +``` + + com.itheima + maven-projectB + 1.0-SNAPSHOT + + + + + junit + junit + + + +``` + +#### 依赖范围 + +| **scope**值 | **主程序** | **测试程序** | **打包(运行)** | **范例** | +| --------------- | ---------- | ------------ | ---------------- | ----------- | +| compile(默认) | Y | Y | Y | log4j | +| test | - | Y | - | junit | +| provided | Y | Y | - | servlet-api | +| runtime | - | Y | Y | jdbc驱动 | + +注意!!!这里的scope如果是`test`,那么它的作用范围在`src/test/java`下,在`src/main/java`下无法导包! + + + +### Maven生命周期 + +主要关注以下几个: + +• clean:移除上一次构建生成的文件 (Target文件夹) + +• compile:编译 `src/main/java` 中的 Java 源文件至 `target/classes` + +• test:使用合适的单元测试框架运行测试(junit) + +• package:将编译后的文件打包,如:jar、war等 + +• install:将打包后的产物(如 `jar`)安装到本地仓库 + +#### 单元测试 + +1. 导入依赖junit + + ``` + + junit + junit + 4.12 + test + + ``` + +2. 在src/test/java下创建DemoTest类(*Test) + +3. 创建test方法 + + ``` + @Test + public void test1(){ + System.out.println("hello1"); + } + @Test + public void test2(){ + System.out.println("hello2"); + } + ``` + +4. 双击test生命周期 + +![image-20240302140156166](D:/folder/test/output/f78e5d80-3123-4354-b4ba-74831895fa05.png) + +## HTTP协议 + +### 响应状态码 + +| 状态码分类 | 说明 | +| ---------- | ------------------------------------------------------------ | +| 1xx | **响应中** --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 | +| 2xx | **成功** --- 表示请求已经被成功接收,处理已完成 | +| 3xx | **重定向** --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 | +| 4xx | **客户端错误** --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 | +| 5xx | **服务器端错误** --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 | + + + +| 状态码 | 英文描述 | 解释 | +| ------- | -------------------------------------- | ------------------------------------------------------------ | +| ==200== | **`OK`** | 客户端请求成功,即**处理成功**,这是我们最想看到的状态码 | +| 302 | **`Found`** | 指示所请求的资源已移动到由`Location`响应头给定的 URL,浏览器会自动重新访问到这个页面 | +| 304 | **`Not Modified`** | 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 | +| 400 | **`Bad Request`** | 客户端请求有**语法错误**,不能被服务器所理解 | +| 403 | **`Forbidden`** | 服务器收到请求,但是**拒绝提供服务**,比如:没有权限访问相关资源 | +| ==404== | **`Not Found`** | **请求资源不存在**,一般是URL输入有误,或者网站资源被删除了 | +| 405 | **`Method Not Allowed`** | 请求方式有误,比如应该用GET请求方式的资源,用了POST | +| 428 | **`Precondition Required`** | **服务器要求有条件的请求**,告诉客户端要想访问该资源,必须携带特定的请求头 | +| 429 | **`Too Many Requests`** | 指示用户在给定时间内发送了**太多请求**(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用 | +| 431 | **` Request Header Fields Too Large`** | **请求头太大**,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。 | +| ==500== | **`Internal Server Error`** | **服务器发生不可预期的错误**。服务器出异常了,赶紧看日志去吧 | +| 503 | **`Service Unavailable`** | **服务器尚未准备好处理请求**,服务器刚刚启动,还未初始化好 | + +## SpringBoot + +![image-20240304101816184](D:/folder/test/output/09141a0c-1482-4200-b808-6dd22abe75d2.png) + +### 快速启动 + +1. 新建spring initializr module +2. 删除以下文件 + +![image-20240302142835694](D:/folder/test/output/718cea88-d197-4dbe-a7c6-1e7b1aaf91c0.png) + +新建HelloController类 + +``` +package edu.whut.controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + @RequestMapping("/hello") + public String hello(){ + System.out.println("hello"); + return "hello"; + } +} +``` + +然后启动服务器,main程序 + +``` +package edu.whut; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SprintbootQuickstartApplication { + + public static void main(String[] args) { + SpringApplication.run(SprintbootQuickstartApplication.class, args); + } + +} +``` + +然后浏览器访问 localhost:8080/hello。 + + + +**SpringBoot的WEB默认内嵌了tomcat服务器,非常方便!!!** + +**而外部的Tomcat需要把项目放在webapps文件夹下才可访问。** + +### SpringBoot请求 + +#### 简单参数 + +- 在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。 + +``` +@RestController +public class RequestController { + // http://localhost:8080/simpleParam?name=Tom&age=10 + // 第1个请求参数: name=Tom 参数名:name,参数值:Tom + // 第2个请求参数: age=10 参数名:age , 参数值:10 + + //springboot方式 + @RequestMapping("/simpleParam") + public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致 + System.out.println(name+" : "+age); + return "OK"; + } +} +``` + +- 如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗? + +解决方案:可以使用Spring提供的@RequestParam注解完成映射 + +在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下: + +``` +@RestController +public class RequestController { + // http://localhost:8080/simpleParam?name=Tom&age=20 + // 请求参数名:name + + //springboot方式 + @RequestMapping("/simpleParam") + public String simpleParam(@RequestParam("name") String username , Integer age ){ + System.out.println(username+" : "+age); + return "OK"; + } +} +``` + +#### 实体参数 + +复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下: + +- User类中有一个Address类型的属性(Address是一个实体类) + +复杂实体对象的封装,需要遵守如下规则: + +- **请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。** + +![image-20240303112109981](D:/folder/test/output/f7c54b67-c2f4-4071-87b0-6e3564b70c21.png) + +``` +@RequestMapping("/complexpojo") + public String complexpojo(User user){ + System.out.println(user); + return "OK"; + } +``` + +``` +public class User { + private String name; + private Integer age; + private Address address; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + @Override + public String toString() { + return "User{" + + "name='" + name + '\'' + + ", age=" + age + + ", address=" + address + + '}'; + } +} +``` + +``` +package edu.whut.pojo; + +public class Address { + private String province; + private String city; + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public String toString() { + return "Address{" + + "province='" + province + '\'' + + ", city='" + city + '\'' + + '}'; + } +} + +``` + +#### 数组参数 + +数组参数:**请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数** + +``` +@RestController +public class RequestController { + //数组集合参数 + @RequestMapping("/arrayParam") + public String arrayParam(String[] hobby){ + System.out.println(Arrays.toString(hobby)); + return "OK"; + } +} +``` + +![image-20240303120212254](D:/folder/test/output/6d196007-ad6c-4875-a086-797d67855782.png) + +#### JSON格式参数 + +json数组: + +``` +{ + "退还时间点": [ + "与中标人签订合同后 5日内", + "投标截止时间前撤回投标文件并书面通知招标人的,2日内", + "开标现场投标文件被拒收,开标结束后,2日内", + "招标项目评标结果公示后,2日内退还未进入中标候选人排名的投标人", + "招标失败需重新组织招标或不再招标的,评标结束后,2日内", + "招标人与中标人签订书面合同并报市公共资源交易监督管理局备案后,2日内退还中标人及未中标候选人的" + ], + "employees": [ + { "firstName": "John", "lastName": "Doe" }, + { "firstName": "Anna", "lastName": "Smith" }, + { "firstName": "Peter", "lastName": "Jones" } + ] +} + +``` + +#### JSON 格式的核心特征 + +- **数据为键值对**:数据存储在键值对中,键和值用冒号分隔。在你的示例中,每个对象有两个键值对,如 `"firstName": "John"`。 +- **使用大括号表示对象**:JSON 使用大括号 `{}` 包围对象,对象可以包含多个键值对。 +- **使用方括号表示数组**:JSON 使用方括号 `[]` 表示数组,数组中可以包含多个值,包括数字、字符串、对象等。在你的示例中,`"employees"` 是一个数组,数组中的每个元素都是一个对象。 + + + + + +我们学习JSON格式参数,主要从以下两个方面着手: + +1. Postman在发送请求时,如何传递json格式的请求参数 +2. 在服务端的controller方法中,如何接收json格式的请求参数 + +Postman发送JSON格式数据: + +![image-20240303121028876](D:/folder/test/output/d36fa889-49ec-4f50-86ec-a92c0bf981eb.png) + +服务端Controller方法接收JSON格式数据: + +- 传递json格式的参数,在Controller中会使用实体类进行封装。 +- 封装规则:**JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。** + +``` +@RestController +public class RequestController { + //JSON参数 + @RequestMapping("/jsonParam") + public String jsonParam(@RequestBody User user){ + System.out.println(user); + return "OK"; + } +} +``` + + + +#### JSON格式工具包 + +//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) + String json = JSONObject.toJSONString(responseResult); + +``` + + com.alibaba + fastjson + 1.2.76 + +``` + + + +#### 路径参数 + +在现在的开发中,经常还会直接在请求的URL中传递参数。例如: + +~~~ +http://localhost:8080/user/1 +http://localhost:880/user/1/0 +~~~ + +上述的这种传递请求参数的形式呢,我们称之为:路径参数。 + +``` +@RestController +public class RequestController { + //路径参数 + @RequestMapping("/path/{id}/{name}") + public String pathParam2(@PathVariable Integer id, @PathVariable String name){ + System.out.println(id+ " : " +name); + return "OK"; + } +} +``` + + + +### SpringBoot响应 + +**@ResponseBody注解:** + +- 位置:书写在Controller方法上或类上 +- 作用:将方法返回值直接响应给浏览器 + - 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器 + +@RestController = @Controller + @ResponseBody + + + +**统一响应结果:** + +下图返回值分别是字符串、对象、集合。 + +![image-20221204174052622](D:/folder/test/output/9b8bd336-19d0-421f-954f-17e9e5809e95.png) + +统一的返回结果使用类来描述,在这个结果中包含: + +- 响应状态码:当前请求是成功,还是失败 + +- 状态码信息:给页面的提示信息 + +- 返回的数据:给前端响应的数据(字符串、对象、集合) + +定义在一个实体类Result来包含以上信息。代码如下: + +``` +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Result { + private Integer code;//响应码,1 代表成功; 0 代表失败 + private String msg; //响应信息 描述字符串 + private Object data; //返回的数据 + + //增删改 成功响应 + public static Result success(){ + return new Result(1,"success",null); + } + //查询 成功响应 + public static Result success(Object data){ + return new Result(1,"success",data); + } + //失败响应 + public static Result error(String msg){ + return new Result(0,msg,null); + } +} +``` + + + +### Spring分层架构 + +#### 三层架构 + +Controller层接收请求,调用Service层;Service层先调用Dao层获取数据,然后实现自己的业务逻辑处理部分,最后返回给Controller层;Controller层再响应数据。可理解为递归的过程 + +![image-20221204201342490](D:/folder/test/output/caad9462-2a08-41c4-8734-82db7d296a4b.png) + +**但是**,这样每次要更换ServiceA->ServiceB时,需要修改Controller层的代码! + +``` +private EmpService empService=new EmpServiceA(); //原来 +private EmpService empService=new EmpServiceB(); //现在 +``` + +**软件设计原则:高内聚低耦合。** + +> 高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。 +> +> 低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。 + +#### IOC&DI 分层解耦 + +![image-20240305104036244](D:/folder/test/output/72a36aa0-d0c5-4426-9b25-0e626c328e2a.png) + +任务:完成Controller层、Service层、Dao层的代码解耦 + +- 思路: + 1. 删除Controller层、Service层中new对象的代码 + 2. Service层及Dao层的**实现类**,交给IOC容器管理 + 3. 为Controller及Service注入运行时依赖的对象 + - Controller程序中注入依赖的Service层对象 + - Service程序中注入依赖的Dao层对象 + + + +第1步:删除Controller层、Service层中new对象的代码 + +![image-20221204212807207](D:/folder/test/output/75b3f9f2-1be2-4aa7-8248-3d54deb70ac1.png) + +第2步:Service层及Dao层的实现类,交给IOC容器管理 + +- 使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理 + +![image-20221204213328034](D:/folder/test/output/078c34f1-14b7-4733-ad63-d59e4aef9564.png) + +第3步:为Controller及Service注入运行时依赖的对象 + +- 使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象 + +![image-20221204213859112](D:/folder/test/output/89ff754d-c5fc-4a3a-af00-6becb5ae589b.png) + + + +**如果我有多个实现类,eg:EmpServiceA、EmpServiceB、EmpServiceC,我该如何切换呢?**两种方法 + +1. 只需在需要使用的实现类上加@Component,注释掉不需要用到的类上的@Component。可以把@Component想象成装入盒子,@Autowired想象成拿出来,因此只需改变放入的物品,而不需改变拿出来的这个动作。 +2. 在@Component上面加上@Primary,表明该类优先生效 + + + +Component衍生注解 + +| 注解 | 说明 | 位置 | +| :---------- | -------------------- | -------------------------------------------------- | +| @Controller | @Component的衍生注解 | 标注在控制器类上Controller | +| @Service | @Component的衍生注解 | 标注在业务类上Service | +| @Repository | @Component的衍生注解 | 标注在数据访问类上(由于与mybatis整合,用的少)DAO | +| @Component | 声明bean的基础注解 | 不属于以上三类时,用此注解 | + + + +## 常见的注解!! + +1. `@RequestMapping("/jsonParam")`:这是一个控制器方法级别的注解,用于将HTTP请求映射到相应的处理方法上。在这个例子中,它表示当收到路径为 "/jsonParam" 的HTTP请求时,应该调用这个方法来处理请求。`@RequestMapping` 注解可以用来指定路径、HTTP方法、请求参数等信息,以便Spring框架能够正确地将请求分发到对应的处理方法上。 + + ``` + @RequestMapping("/jsonParam") + public String jsonParam(@RequestBody User user){ + System.out.println(user); + return "OK"; + } + ``` + + + +3. `@RestController`:这是一个类级别的注解,它告诉Spring框架这个类是一个控制器(Controller),并且处理HTTP请求并返回响应数据。与 `@Controller` 注解相比,`@RestController` 注解还会自动将控制器方法返回的数据转换为 JSON 格式,并写入到HTTP响应中,得益于**@ResponseBody** 。因此,`@RestController` 注解通常用于编写 RESTful Web 服务。 + +@RestController = @Controller + @ResponseBody + +3. `@RequestBody`:这是一个方法参数级别的注解,用于告诉Spring框架将请求体的内容解析为指定的Java对象。在这个例子中,`@RequestBody` 注解告诉Spring框架将HTTP请求的主体(即请求体)中的**JSON数据解析**为一个 `User` 对象,并传递给方法的参数 `user`。这样,在方法体内就可以直接使用这个 `User` 对象来处理请求中的数据了。 + +4. `@PathVariable` 注解用于将路径变量 `{id}` 的值绑定到方法的参数 `id` 上。当请求的路径是 "/path/123" 时,`@PathVariable` 会将路径中的 "123" 值绑定到方法的参数 `id` 上,使得方法能够获取到这个值。在这个例子中,方法的参数 `id` 的值将会是整数值 123。 + +``` +public String pathParam(@PathVariable Integer id) { + System.out.println(id); + return "OK"; +} + +参数名与路径名不同 +@GetMapping("/{id}") + public ResponseEntity getUserById(@PathVariable("id") Long userId) { + } +``` + +5. `@RequestParam`,如果方法的参数名与请求参数名不同,需要在 `@RequestParam` 注解中指定请求参数的名字。 + +``` +@RequestParam(defaultValue = "1" Integer page) //若page为null,可以设置page的默认值为1 +``` + +``` +@RequestMapping("/example") +public String exampleMethod(@RequestParam String name, @RequestParam("age") int userAge) { + // 在方法内部使用获取到的参数值进行处理 + System.out.println("Name: " + name); + System.out.println("Age: " + userAge); + return "OK"; +} + +``` + +5. 控制反转与依赖注入: + + @Component ,控制反转 + + @Autowired,依赖注入 + +6. **数据库相关。**@Mapper注解:表示是mybatis中的Mapper接口 + + - 程序运行时:框架会自动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理 + + @Select注解:代表的就是select查询,用于书写select查询语句 + +7. @SpringBootTest:它会启动 Spring 应用程序上下文,并在测试期间模拟运行整个 Spring Boot 应用程序。这意味着你可以在集成测试中使用 Spring 的各种功能,例如**自动装配、依赖注入、配置加载**等。 + +8. lombok的相关注解。非常实用的工具库。**** + +| **注解** | **作用** | +| ------------------- | ------------------------------------------------------------ | +| @Getter/@Setter | 为所有的属性提供get/set方法 | +| @ToString | 会给类自动生成易阅读的 toString 方法 | +| @EqualsAndHashCode | 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 | +| **@Data** | **提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)** | +| @NoArgsConstructor | 为实体类生成无参的构造器方法 | +| @AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 | +| **@Slf4j** | 可以log.info("输出日志信息"); | + +9. @Test,Junit测试单元,可在测试类中定义测试函数,一次性执行所有@Test注解下的函数,不用写main方法 +10. @Override,当一个方法在子类中覆盖(重写)了父类中的同名方法时,为了确保正确性,可以使用 `@Override` 注解来标记这个方法,这样编译器就能够帮助检查是否正确地重写了父类的方法。 +11. @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, + +将日期转化为指定的格式。Spring会尝试将接收到的**字符串参数**转换为控制器方法参数的相应类型。 + +12. @RestControllerAdvice= @ControllerAdvice + @ResponseBody。加上这个注解就代表我们定义了一个全局异常处理器,而且处理异常的方法返回值会转换为json后再响应给前端 +12. @Configuration和@Bean配合使用,可以对第三方bean进行**集中**的配置管理,依赖注入!!@Bean用于方法上 + +加了@Configuration,当Spring Boot应用**启动时,它会执行**一系列的自动配置步骤。 + +12. `@ComponentScan`指定了Spring应该在哪些包下搜索带有`@Component`、`@Service`、`@Repository`、`@Controller`等注解的类,以便将这些类自动注册为Spring容器管理的Bean.`@SpringBootApplication`它是一个便利的注解,组合了`@Configuration`、`@EnableAutoConfiguration`和`@ComponentScan`注解。 + + + +## 开发规范 + +### REST风格 + +在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。 + +什么是REST风格呢? + +- REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。 + +**传统URL风格如下:** + +```url +http://localhost:8080/user/getById?id=1 GET:查询id为1的用户 +http://localhost:8080/user/saveUser POST:新增用户 +http://localhost:8080/user/updateUser PUT:修改用户 +http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户 +``` + +我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。 + +**基于REST风格URL如下:** + +``` +http://localhost:8080/users/1 GET:查询id为1的用户 +http://localhost:8080/users POST:新增用户 +http://localhost:8080/users PUT:修改用户 +http://localhost:8080/users/1 DELETE:删除id为1的用户 +``` + +其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。 + +**在JAVA代码中如何区别不同的请求方式?** + +传统的是@RequestMapping("/depts"),现在: + +@GetMapping("/depts") =>GET请求 + +@PostMapping("/depts") =》POST + +@PutMapping("/depts") =>PUT + +@DeleteMapping("/depts") =>DELETE + + + +### 开发流程 + +![image-20220904125004138](D:/folder/test/output/fd727184-27a7-475f-b1f3-2de432222bad.png) + +1. 查看页面原型明确需求 + - 根据页面原型和需求,进行表结构设计、编写接口文档(已提供) + +2. 阅读接口文档 +3. 思路分析 +4. 功能接口开发 + - 就是开发后台的业务功能,一个业务功能,我们称为一个接口 +5. 功能接口测试 + - 功能开发完毕后,先通过Postman进行功能接口测试,测试通过后,再和前端进行联调测试 +6. 前后端联调测试 + - 和前端开发人员开发好的前端工程一起测试 + + + +## Mybatis + +### 快速创建 + +![image-20240307125505211](D:/folder/test/output/19903ba7-af53-4af3-b647-eccbc3a7215b.png) + +1. 创建springboot工程,并导入 mybatis的起步依赖、mysql的驱动包。创建用户表user,并创建对应的实体类User + +![image-20240307125820685](D:/folder/test/output/da6af3ca-1b16-443b-85da-71a0bb434342.png) + +2. 在springboot项目中,可以编写main/resources/application.properties文件,配置数据库连接信息。 + +``` +#驱动类名称 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +#数据库连接的url +spring.datasource.url=jdbc:mysql://localhost:3306/mybatis +#连接数据库的用户名 +spring.datasource.username=root +#连接数据库的密码 +spring.datasource.password=1234 +``` + +3. 在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper + +![image-20240307132356616](D:/folder/test/output/aea5135b-9c76-418a-8cf8-680f78a6f9cc.png) + +@Mapper注解:表示是mybatis中的Mapper接口 + +- 程序运行时:框架会自动生成接口的**实现类对象(代理对象)**,并交给Spring的IOC容器管理 + + @Select注解:代表的就是select查询,用于书写select查询语句 + +``` +@Mapper +public interface UserMapper { + //查询所有用户数据 + @Select("select * from user") + public List list(); +} +``` + +### 数据库连接池 + +数据库连接池是个容器,负责分配、管理数据库**连接(Connection)** + +- 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象 + +允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个 + +- 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用) + +- 客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象 + +### lombok + +Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码。 + +| **注解** | **作用** | +| ------------------- | ------------------------------------------------------------ | +| @Getter/@Setter | 为所有的属性提供get/set方法 | +| @ToString | 会给类自动生成易阅读的 toString 方法 | +| @EqualsAndHashCode | 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 | +| **@Data** | **提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)** | +| @NoArgsConstructor | 为实体类生成无参的构造器方法 | +| @AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 | + +**使用** + +``` +import lombok.Data; + +@Data +public class User { + private Integer id; + private String name; + private Short age; + private Short gender; + private String phone; +} +``` + +### 日志输出 + +在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。 + +1. 打开application.properties文件 + +2. 开启mybatis的日志,并指定输出到控制台 + +``` +#指定mybatis输出日志的位置, 输出控制台 +mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl +``` + + + +### 增删改 + +- **增删改通用!:返回值为int时,表示影响的记录数,一般不需要可以设置为void!** +- **#{} 表示占位符**,执行SQL时,生成预编译SQL,会自动设置参数值 +- ${} 也是占位符,但直接将参数拼接在SQL语句中,存在SQL注入问题 + +**作用于单个字段** + +``` +@Mapper +public interface EmpMapper { + //SQL语句中的id值不能写成固定数值,需要变为动态的数值 + //解决方案:在delete方法中添加一个参数(用户id),将方法中的参数,传给SQL语句 + /** + * 根据id删除数据 + * @param id 用户id + */ + @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值 + public void delete(Integer id); +} +``` + +![image-20240312122323753](D:/folder/test/output/a1980a1c-adca-447e-82e9-853f019dd722.png) + +上图参数值分离,有效防止SQL注入 + + + +**作用于多个字段** + +``` +@Mapper +public interface EmpMapper { + //会自动将生成的主键值,赋值给emp对象的id属性 + @Options(useGeneratedKeys = true,keyProperty = "id") + @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})") + public void insert(Emp emp); +} +``` + +说明:#{...} 里面写的名称是对象的**属性名**!,函数内的参数是Emp对象 + +useGeneratedKeys = true表示获取返回的主键值,keyProperty = "id"表示主键值存在Emp对象的id属性中,添加这句可以直接获取主键值 + + + +### 查/驼峰命名法 + +表中查询的数据封装到实体类中 + +- 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。 +- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。 + +![image-20221212103124490](D:/folder/test/output/90b9ad4d-c115-4038-a956-9c08448fe5ed.png) + +解决方法: + +1. 起别名 +2. 结果映射 +3. **开启驼峰命名** +4. **属性名和表中字段名保持一致** + +**开启驼峰命名(推荐)**:如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射 + +> 驼峰命名规则: abc_xyz => abcXyz +> +> - 表中字段名:abc_xyz +>- 类中属性名:abcXyz + +```properties +# 在application.properties中添加: +mybatis.configuration.map-underscore-to-camel-case=true +``` + +> 要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。 + + + +eg:通过页面原型以及需求描述我们要实现的查询: + +- **姓名:要求支持模糊匹配** +- 性别:要求精确匹配 +- 入职时间:要求进行范围查询 +- 根据最后修改时间进行降序排序 + +重点在于模糊查询时where name like '%#{name}%' 会报错。 + +解决方案: + +``` +@Mapper +public interface EmpMapper { + + @Select("select * from emp " + + "where name like concat('%',#{name},'%') " + + "and gender = #{gender} " + + "and entrydate between #{begin} and #{end} " + + "order by update_time desc") + public List list(String name, Short gender, LocalDate begin, LocalDate end); + +} +``` + +**使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%')** + + + +### XML配置文件规范 + +使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。 + +在Mybatis中使用XML映射文件方式开发,需要符合一定的规范: + +1. XML映射**文件的名称**与Mapper**接口名称**一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名) + +2. XML映射文件的**namespace属性**为Mapper接口**全限定名**一致 + +3. XML映射文件中sql语句的**id**与Mapper接口中的**方法名**一致,并保持返回类型一致。 + +![image-20221212153529732](D:/folder/test/output/4c5d2450-105a-4fc4-93fc-92bd16ab06dd.png) + +\ + select * from emp + where name like concat('%',#{name},'%') + and gender = #{gender} + and entrydate between #{begin} and #{end} + order by update_time desc + + ``` + + XML映射文件中sql语句的id与Mapper接口中的**方法名**一致,并保持**返回类型一致**(也是**全限定名**!!).里面的查询语句与之前的一模一样,仅仅单独写到一个xml文件中罢了。 + + **注意:**返回类型指的是单挑记录的类型,是Emp,不是list + + + +**这里有bug!!!concat('%',#{name},'%')这里应该用 标签对name是否为''或null进行判断** + +`''`和`null`虽然在某些上下文中可能看起来相似,但它们代表了不同的概念:一个是具有明确的(虽然是空的)值,另一个是完全没有值。 + +### 动态SQL + +#### SQL-if,where + +``:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。 + +~~~xml + + 要拼接的sql语句 + +~~~ + +``只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR,**加了总比不加好** + + + +``` + +``` + +#### SQL-foreach + +mapper接口: + +``` +@Mapper +public interface EmpMapper { + //批量删除 + public void deleteByIds(List ids); +} +``` + +xml: + +语法: + +``` + + +``` + + + +``` + + delete from emp where id in + + #{id} + + +``` + + + +## 案例实战 + +### 分页查询 + +传统员工分页查询分析: + +![image-20221215153413290](D:/folder/test/output/676d3cca-2740-4246-95ca-f221bd7052d8.png) + +**采用分页插件PageHelper:** + +![image-20221215170038833](D:/folder/test/output/10c5184c-b74c-45b3-89c8-fa7aa6a25e4a.png) + +**在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢?** + +分页插件帮我们完成了以下操作: + +1. 先获取到要执行的SQL语句:select * from emp +2. 把SQL语句中的字段列表,变为:count(*) +3. 执行SQL语句:select count(*) from emp //获取到总记录数 +4. 再对要执行的SQL语句:select * from emp 进行改造,在末尾添加 limit ? , ? +5. 执行改造后的SQL语句:select * from emp limit ? , ? + +**使用方法:** + +当使用了PageHelper分页插件进行分页,就**无需再Mapper中进行手动分页**了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前**设置分页参数**,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。 + +1、在pom.xml引入依赖 + +```xml + + com.github.pagehelper + pagehelper-spring-boot-starter + 1.4.2 + +``` + +2、EmpMapper + +```java +@Mapper +public interface EmpMapper { + //获取当前页的结果列表 + @Select("select * from emp") + public List list(); +} +``` + +3、EmpServiceImpl + +```java +@Override +public PageBean page(Integer page, Integer pageSize) { + // 设置分页参数 + PageHelper.startPage(page, pageSize); //page是页号,不是起始索引 + // 执行分页查询 + List empList = empMapper.list(); + // 获取分页结果 + Page p = (Page) empList; + //封装PageBean + PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); + return pageBean; +} +``` + +4、Controller + +``` +@Slf4j +@RestController +@RequestMapping("/emps") +public class EmpController { + + @Autowired + private EmpService empService; + + //条件分页查询 + @GetMapping + public Result page(@RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer pageSize) { + //记录日志 + log.info("分页查询,参数:{},{}", page, pageSize); + //调用业务层分页查询功能 + PageBean pageBean = empService.page(page, pageSize); + //响应 + return Result.success(pageBean); + } +} +``` + + + +### 条件分页查询 + +思路分析: + +![image-20221215180528415](D:/folder/test/output/c4504af4-0542-45d8-a7bb-a278e770fb36.png) + +``` + +``` + + + +### 文件上传 + +#### 本地存储 + +文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。下面呢,我们就需要完成将上传的文件保存在服务器的本地磁盘上。 + +代码实现: + +1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录) +2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下 + +> MultipartFile 常见方法: +> +> - String getOriginalFilename(); //获取原始文件名 +> - void transferTo(File dest); //将接收的文件转存到磁盘文件中 +> - long getSize(); //获取文件的大小,单位:字节 +> - byte[] getBytes(); //获取文件内容的字节数组 +> - InputStream getInputStream(); //获取接收到的文件内容的输入流 + +``` +@Slf4j +@RestController +public class UploadController { + + @PostMapping("/upload") + public Result upload(String username, Integer age, MultipartFile image) throws IOException { + log.info("文件上传:{},{},{}",username,age,image); + + //获取原始文件名 + String originalFilename = image.getOriginalFilename(); + + //构建新的文件名 + String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名 + String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名 + + //将文件存储在服务器的磁盘目录 + image.transferTo(new File("E:/images/"+newFileName)); + + return Result.success(); + } + +} +``` + +在SpringBoot中,文件上传时默认单个文件最大大小为1M + +那么如果需要上传大文件,可以在application.properties进行如下配置: + +``` +#配置单个文件最大上传大小 +spring.servlet.multipart.max-file-size=10MB + +#配置单个请求最大上传大小(一次请求可以上传多个文件) +spring.servlet.multipart.max-request-size=100MB +``` + +**不推荐!** + + + +#### 云存储 + +pom文件中添加如下依赖: + +```plaintext + + com.aliyun.oss + aliyun-sdk-oss + 3.15.1 + + + javax.xml.bind + jaxb-api + 2.3.1 + + + javax.activation + activation + 1.1.1 + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.3 + +``` + + + +上传文件的工具类 + +``` +package edu.whut.utils; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.common.auth.CredentialsProviderFactory; +import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; +import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.model.PutObjectResult; +import com.aliyuncs.exceptions.ClientException; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.util.UUID; + + +/** + * 阿里云 OSS 工具类 + */ +@Component +public class AliOSSUtils { + + private String endpoint = "https://oss-cn-hangzhou.aliyuncs.com"; + private String bucketName = "zyjavaweb"; + + /** + * 实现上传图片到OSS + */ + public String upload(MultipartFile file) throws IOException, ClientException { + + InputStream inputStream = file.getInputStream(); + // 避免文件覆盖 + String originalFilename = file.getOriginalFilename(); + String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名 + String fileName = UUID.randomUUID().toString() + extname; + + //上传文件到 OSS + EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); + OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream); + PutObjectResult result = ossClient.putObject(putObjectRequest); + + //文件访问路径 + String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName; + // 关闭ossClient + ossClient.shutdown(); + return url;// 把上传到oss的路径返回 + } + +} + +``` + +使用时传入MultipartFile类型的文件 + + + +### 配置文件 + +**properties** + +将配置信息写在application.properties,用注解@Value获取配置文件中的数据 + +![image-20230102173905913](D:/folder/test/output/bd30346f-bb4b-4296-b870-b281e9bfad85.png) + +@Value("${aliyun.oss.endpoint}") + + + +**yml配置文件** + +![image-20230102181215809](D:/folder/test/output/4b624d01-7099-4d2a-90a6-7becabec47a0.png) + +了解下yml配置文件的基本语法: + +- 大小写敏感 +- 数据前边必须有空格,作为分隔符 +- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格) +- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 +- `#`表示注释,从这个字符一直到行尾,都会被解析器忽略 + +对象/map集合 + +``` +user: + name: zhangsan + age: 18 + password: 123456 +``` + +数组/List/Set集合 + +``` +hobby: + - java + - game + - sport +``` + + + +**@ConfigurationProperties** + +前面获取配置项中的属性值,需要通过@Value注解,有时过于繁琐!!! + +``` +@Component +public class AliOSSUtils { + + @Value("${aliyun.oss.endpoint}") + private String endpoint; + + @Value("${aliyun.oss.accessKeyId}") + private String accessKeyId; + + @Value("${aliyun.oss.accessKeySecret}") + private String accessKeySecret; + + @Value("${aliyun.oss.bucketName}") + private String bucketName; + + //省略其他代码... + } +``` + +Spring提供的简化方式套路: + +1. 需要创建一个实现类,且实体类中的**属性名**和配置文件当中**key**的名字必须要一致 + + > 比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法 **==》@Data** + +2. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象 **==>@Component** + +3. 在实体类上添加`@ConfigurationProperties`注解,并通过**perfix属性**来指定配置参数项的前缀 + +![image-20230103210827003](D:/folder/test/output/bcfee8e8-44cf-4fde-8a31-e0d45e64914f.png) + +4. (可选)引入依赖pom.xml + +``` + + org.springframework.boot + spring-boot-configuration-processor + +``` + +**使用:** + +![image-20240327124923629](D:/folder/test/output/2129321a-7793-4982-9b26-1c0b73da4666.png) + + + +## 登录校验 + +### 会话技术 + +![image-20230105203827355](D:/folder/test/output/47e3a07f-cc82-44de-84c2-ba76e004ced7.png) + +会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。 + +会话跟踪技术有两种: + +1. Cookie(客户端会话跟踪技术) + - 数据存储在客户端浏览器当中 +2. Session(服务端会话跟踪技术) + - 数据存储在储在服务端 +3. 令牌技术 + +**Cookie** + +- 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的) +- 缺点: + - 移动端APP(Android、IOS)中无法使用Cookie + - 不安全,用户可以自己禁用Cookie + - Cookie不能跨域 + +**Session** + +- 优点:Session是存储在服务端的,安全 +- 缺点: + - 服务器集群环境下无法直接使用Session + - 移动端APP(Android、IOS)中无法使用Cookie + - 用户可以自己禁用Cookie + - Cookie不能跨域 + +**令牌(推荐)** + +- 优点: + - 支持PC端、移动端 + - 解决集群环境下的认证问题 + - 减轻服务器的存储压力(无需在服务器端存储) +- 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验) + + + +### JWT令牌 + +![image-20240320191446966](D:/folder/test/output/6fde07d1-4168-42b2-a8f0-c69d1bc6c28b.png) + +**生成和校验** + +引入依赖 + +``` + + io.jsonwebtoken + jjwt + 0.9.1 + +``` + +生成与解析: + +``` +public class JwtUtils { + + private static String signKey = "itheima"; + private static Long expire = 43200000L; + + /** + * 生成JWT令牌 + * @param claims JWT第二部分负载 payload 中存储的内容 + * @return + */ + public static String generateJwt(Map claims){ + String jwt = Jwts.builder() + .addClaims(claims) + .signWith(SignatureAlgorithm.HS256, signKey) + .setExpiration(new Date(System.currentTimeMillis() + expire)) + .compact(); + return jwt; + } + + /** + * 解析JWT令牌 + * @param jwt JWT令牌 + * @return JWT第二部分负载 payload 中存储的内容 + */ + public static Claims parseJWT(String jwt){ + Claims claims = Jwts.parser() + .setSigningKey(signKey) + .parseClaimsJws(jwt) + .getBody(); + return claims; + } +} +``` + +**注意:**一旦生成token,在有效期内,关闭浏览器再重新打开,仍然可以通过登录校验,因为token仍然存在浏览器的“storage里面”。 + +在有效期内,发送的每个请求头部都会带上token + + + +**令牌可以存储当前登录用户的信息:id、username等等,传入claims** + +``` +Map claims = new HashMap<>(); +claims.put("id",emp.getId()); +claims.put("name",e.getName()); +claims.put("username",e.getUsername()); +String jwt=JwtUtils.generateJwt(claims); +``` + +**解析令牌:** + +``` +@Autowired +private HttpServletRequest request; + + String jwt = request.getHeader("token"); + Claims claims = JwtUtils.parseJWT(jwt); +``` + + + +### 拦截器(Interceptor) + +在拦截器当中,我们通常也是做一些通用性的操作,比如:**我们可以通过拦截器来拦截前端发起的请求**,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。 + +1.定义工具类:生成、解析JWT令牌 + +2.登录时生成JWT令牌 + +3.定义拦截器,要实现怎样的功能 + +4.注册配置拦截器,哪些方法前要加拦截器=》校验JWT + + + +#### 快速入门 + +1. **定义拦截器,实现HandlerInterceptor接口,并重写其所有方法** + +``` +//自定义拦截器 +@Component +public class LoginCheckInterceptor implements HandlerInterceptor { + //目标资源方法执行前执行。 返回true:放行 返回false:不放行 + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + System.out.println("preHandle .... "); + + return true; //true表示放行 + } + + //目标资源方法执行后执行 + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + System.out.println("postHandle ... "); + } + + //视图渲染完毕后执行,最后执行 + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + System.out.println("afterCompletion .... "); + } +} +``` + +注意: + +​ preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行 + +​ postHandle方法:目标资源方法执行后执行 + +​ afterCompletion方法:视图渲染完毕后执行,最后执行 + +2. **注册配置拦截器,实现WebMvcConfigurer接口,并重写addInterceptors方法** + +``` +@Configuration +public class WebConfig implements WebMvcConfigurer { + + //自定义的拦截器对象 + @Autowired + private LoginCheckInterceptor loginCheckInterceptor; + + + @Override + public void addInterceptors(InterceptorRegistry registry) { + //注册自定义拦截器对象 + registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求) + } +} +``` + +#### 拦截路径 + +addPathPatterns指定拦截路径; + +调用`excludePathPatterns("不拦截路径")`方法,指定哪些资源不需要拦截。 + +| 拦截路径 | 含义 | 举例 | +| --------- | -------------------- | --------------------------------------------------- | +| /* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 | +| /** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 | +| /depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts | +| /depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 | + +#### 登录校验 + +主要在preHandle中写逻辑 + +``` +@Override //目标资源方法执行前执行。 返回true:放行 返回false:不放行 + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + System.out.println("preHandle .... "); + //1.获取请求url + //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行 + + //3.获取请求头中的令牌(token) + String token = request.getHeader("token"); + log.info("从请求头中获取的令牌:{}",token); + + //4.判断令牌是否存在,如果不存在,返回错误结果(未登录) + if(!StringUtils.hasLength(token)){ + log.info("Token不存在"); + + //创建响应结果对象 + Result responseResult = Result.error("NOT_LOGIN"); + //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) + String json = JSONObject.toJSONString(responseResult); + //设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8) + response.setContentType("application/json;charset=utf-8"); + //响应 + response.getWriter().write(json); + + return false;//不放行 + } + + //5.解析token,如果解析失败,返回错误结果(未登录) + try { + JwtUtils.parseJWT(token); + }catch (Exception e){ + log.info("令牌解析失败!"); + + //创建响应结果对象 + Result responseResult = Result.error("NOT_LOGIN"); + //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) + String json = JSONObject.toJSONString(responseResult); + //设置响应头 + response.setContentType("application/json;charset=utf-8"); + //响应 + response.getWriter().write(json); + + return false; + } + + //6.放行 + return true; + } +``` + + + +### 拦截器&&全局异常处理 + +**执行时机不同**:拦截器 (`HandlerInterceptor`) 主要在请求处理的前后进行拦截和处理,而全局异常处理器在控制器方法抛出异常后进行捕获和处理。 + +**作用不同**:拦截器用于拦截请求,可以进行权限验证、日志记录等预处理操作;全局异常处理器专注于异常的统一处理和返回错误信息,确保异常不会导致程序崩溃或未处理的异常信息泄露给客户端。 + + + +## 全局异常处理 + +- 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解**@RestControllerAdvice**,加上这个注解就代表我们定义了一个全局异常处理器。 +- 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解**@ExceptionHandler**。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。 + +``` +@RestControllerAdvice +public class GlobalExceptionHandler { + + //处理异常 + @ExceptionHandler(Exception.class) //指定能够处理的异常类型,Exception.class捕获所有异常 + public Result ex(Exception e){ + e.printStackTrace();//打印堆栈中的异常信息 + //捕获到异常之后,响应一个标准的Result + return Result.error("对不起,操作失败,请联系管理员"); + } +} +``` + + + +## 事务 + +### Spring事务日志开关 + +```yml +logging: + level: + org.springframework.jdbc.support.JdbcTransactionManager: debug +``` + +### Transactional注解 + +@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。一般会在**业务层(Service)**当中来控制事务 + +@Transactional注解书写位置: + +- 方法 + - 当前方法交给spring进行事务管理 +- 类 + - 当前类中所有的方法都交由spring进行事务管理 +- 接口 + - 接口下所有的实现类当中所有的方法都交给spring 进行事务管理 + +默认情况下,只有出现**RuntimeException(运行时异常)**才会回滚事务。假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的**rollbackFor**属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。 + +```java +@Transactional(rollbackFor=Exception.class) + public void delete(Integer id){ + //根据部门id删除部门信息 + deptMapper.deleteById(id); + + //模拟:异常发生 + int num = id/0; + + //删除部门下的所有员工信息 + empMapper.deleteByDeptId(id); + } +``` + + + +在@Transactional注解的后面指定一个属性**propagation**,通过 propagation 属性来指定传播行为。可以在嵌套的子事务上加入。 + +``` +@Transactional(propagation = Propagation.REQUIRES_NEW) +``` + +| **属性值** | **含义** | +| ------------ | ------------------------------------------------------------ | +| REQUIRED | 【默认值】有父事务则加入,**若父事务报异常则一起回滚**;无父事务则创建新事务 | +| REQUIRES_NEW | 需要新事务,无论有无,**总是创建新事务** | + +- REQUIRED :大部分情况下都是用该传播行为即可。 + +- REQUIRES_NEW :当我们**不希望事务之间相互影响**时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。 + + + +## AOP + +AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。 + +我们要想完成统计各个业务方法执行耗时的需求,我们只需要定义一个**模板方法**,将记录方法执行耗时这一部分公共的逻辑代码,定义在模板方法当中,在这个方法开始运行之前,来记录这个方法运行的开始时间,在方法结束运行的时候,再来记录方法运行的结束时间,**中间就来运行原始的业务方法**。 + +### 快速入门 + +**实现步骤:** + +1. 导入依赖:在pom.xml中导入AOP的依赖 + +``` + + org.springframework.boot + spring-boot-starter-aop + +``` + +2. 编写AOP程序:针对于特定方法根据业务需要进行编程 + +``` +@Component +@Aspect //当前类为切面类 +@Slf4j +public class TimeAspect { + ////第一个星号表示任意返回值,第二个星号表示类/接口,第三个星号表示所有方法。 + @Around("execution(* com.itheima.service.*.*(..))") + public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { + //记录方法执行开始时间 + long begin = System.currentTimeMillis(); + + //执行原始方法 + Object result = pjp.proceed(); + + //记录方法执行结束时间 + long end = System.currentTimeMillis(); + + //计算方法执行耗时,pjp.getSignature()获得函数名 + log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin); + + return result; + } +} +``` + +我们通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下: + +- 记录系统的操作日志 +- 权限控制 +- 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务 + + + +### 核心概念 + +**1. 连接点:JoinPoint**,可以被AOP控制的方法 + + + +**2. 通知:Advice**,指哪些重复的逻辑,也就是共性功能 (必须的) + +**3. 切入点表达式:PointCut**,匹配连接点的条件,通知仅会在切入点方法执行时被应用 (必须的) + + + +**4. 切面:Aspect**,描述通知与切入点的对应关系(通知+切入点表达式) + +**5.目标对象:Target**,通知所应用的对象 + + + +### 通知类型 + +Spring中AOP的通知类型: + +- **@Around**:环绕通知,此注解标注的通知方法在目标方法**前、后都被执行** +- @Before:前置通知,此注解标注的**通知方法在目标方法前被执行** +- @After :后置通知,此注解标注的通知方法在目标方法后被执行,**无论是否有异常都会执行** +- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,**有异常不会执行** +- @AfterThrowing : 异常后通知,此注解标注的通知方法**发生异常后执行** + +在使用通知时的注意事项: + +- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行 +- @Around环绕通知方法的返回值,**必须指定为Object**,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。 + + + + + +通知的执行顺序大家主要知道两点即可: + +1. 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的 +2. 可以在**切面类上**面加上**@Order注解**,来控制不同的切面类通知的执行顺序。切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行) + +eg:@Order(1) + + + +### 切入点表达式 + +#### 公共表示 + +先定义一个公共的pt(),然后可以直接引用。 + +![image-20240322105617334](D:/folder/test/output/62ff534c-bbb8-466e-832b-c55acb3b9bdf.png) + +切入点表达式: + +- 描述切入点方法的一种表达式 + +- 作用:主要用来**决定项目中的哪些方法需要加入通知** + +- 常见形式: + + 1. execution(……):根据方法的签名来匹配 + 2. @annotation(……) :根据注解匹配 + +#### execution + +execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为: + +``` +execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) +``` + +其中带`?`的表示可以省略的部分 + +- 访问修饰符:可省略(比如: public、protected) + +- 包名.类名: 可省略,**但不建议** + +- throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) + +eg: + +``` +@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))") +``` + + + +可以**使用通配符描述切入点** + +- `*` :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、**任意类型的一个参数**,也可以通配包、类、方法名的一部分 + +``` +execution(* edu.*.service.*.update*(*)) +``` + +这里update后面的'星'即通配方法名的一部分,()中的'星'表示有且仅有一个任意参数 + +- `..` :**多个连续的任意符号**,可以通配任意层级的包,或任意类型、任意个数的参数 + + + +#### annotation + +那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。 + +实现步骤: + +1. **新建anno包,在这个包下**编写自定义注解 + +``` +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 定义注解 +@Retention(RetentionPolicy.RUNTIME) // 定义注解的生命周期 +@Target(ElementType.METHOD) // 定义注解可以应用的Java元素类型 +public @interface MyLog { + // 定义注解的元素(属性) + String description() default "This is a default description"; + int value() default 0; +} + +``` + +2. 在业务类要做为连接点的**方法上添加**自定义注解 + +``` + @MyLog //自定义注解(表示:当前方法属于目标方法) + public void delete(Integer id) { + //1. 删除部门 + deptMapper.delete(id); + } +``` + + + +3. aop切面类上使用类似如下的切面表达式: + +``` +@Before("@annotation(edu.whut.anno.MyLog)") +``` + + + +### 连接点 + +![image-20240322131745283](D:/folder/test/output/430dd5be-28c9-4e6c-ab30-fe34f13324fd.png) + +**`getSignature()`**: 返回一个`Signature`类型的对象,这个对象包含了被拦截点的签名信息。在方法调用的上下文中,这包括了方法的名称、声明类型等信息。 + +- **方法名称**:可以通过调用`getName()`方法获得。 +- **声明类型**:方法所在的类或接口的**完全限定名**,可以通过`getDeclaringTypeName()`方法获取。 +- **返回类型**(对于方法签名):可以通过将`Signature`对象转换为更具体的`MethodSignature`类型,并调用`getReturnType()`方法获取。 + + + +Object[] args = joinPoint.**getArgs()**; 可以获取调用方法时传递的参数数组 + + + +## SpringBoot原理 + +### 容器启动 + +在 Spring 框架的上下文中,提到的“容器启动”通常指的是 Spring 应用上下文(ApplicationContext)的初始化和启动过程。这个过程涉及到多个关键步骤,其中包括配置解析、Bean 定义的加载、Bean 的实例化和初始化以及依赖注入等。具体来说,容器启动的时机包括以下几个关键点: + +#### 1. 应用启动时 + +当你启动一个 Spring 应用时,无论是通过直接运行一个包含 `main` 方法的类,还是部署到一个 Servlet 容器中,Spring 的应用上下文都会被创建和初始化。这个过程包括: + +- **读取配置**:加载配置文件或注解指定的配置信息,这些配置指定了哪些组件需要被 Spring 管理。 +- **Bean 定义的注册**:Spring 将在配置中找到的所有 Bean 定义加载到容器中。 +- **Bean 的实例化**:Spring 根据 Bean 的定义创建实例。默认情况下(非懒加载),所有的单例 Bean 在容器启动时即被创建。 +- **依赖注入**:Spring 解析 Bean 之间的依赖关系,并将相应的依赖注入到 Bean 中。 + + + +### 配置优先级 + +在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。通过以上的测试,我们也得出了优先级(从低到高): + +- application.yaml(忽略) +- application.yml +- application.properties +- java系统属性(-Dxxx=xxx) +- 命令行参数(--xxx=xxx) + +如果项目已经打包上线了,这个时候我们又如何来设置Java系统属性和命令行参数呢? + +``` +java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010 +``` + + + +### Bean + +#### 获取bean + +**如何从Spring IoC容器中获取Bean** + +- **@Autowired注解**:最常见的方式是使用@Autowired注解自动装配Bean。Spring会自动在其容器中查找匹配类型的Bean并注入到被@Autowired标注的字段或方法中。 + +``` +@Service +public class MyService { + @Autowired + private MyRepository myRepository; // 自动装配MyRepository Bean +} + +``` + +- **ApplicationContext获取**:你也可以通过Spring的ApplicationContext来手动获取Bean。ApplicationContext是Spring的IoC容器,通过它你可以访问容器中的任何Bean。 + +```java +class SpringbootWebConfig2ApplicationTests { + @Autowired + private ApplicationContext applicationContext; //IOC容器对象 + //获取bean对象 + @Test + public void testGetBean(){ + //根据bean的名称获取 + DeptController bean1 = (DeptController) applicationContext.getBean("deptController"); + System.out.println(bean1); + } +} +``` + +默认是饿汉模式,通过依赖注入设置的类,会在容器**启动时自动初始化**,除非设置了@Lazy注解,懒汉模式,第一次使用bean对象时,才会创建bean对象并交给ioc容器管理。 + +#### bean的作用域 + +| **作用域** | **说明** | +| ---------- | ---------------------------------------------- | +| singleton | 容器内同名称的bean只有一个实例(单例)(默认) | +| prototype | 每次使用该bean时会创建新的实例(非单例) | + +**使用方法:** + +在bean类上加注解 + +@Scope("prototype") + + + +#### 第三方bean + +那么我们应该怎样使用并定义第三方的bean呢? + +- 如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到**@Bean**注解。 +- 如果需要定义第三方Bean时, 通常会单独定义一个**配置类** + +``` +@Configuration //配置类 (在配置类当中对第三方bean进行集中的配置管理) +public class CommonConfig { + + //声明第三方bean + @Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean + //通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名 + public SAXReader reader(DeptService deptService){ + System.out.println(deptService); + return new SAXReader(); + } +} +``` + +在**方法上**加上一个@Bean注解,Spring 容器在启动的时候,它会自动的调用这个方法,并将方法的返回值声明为Spring容器当中的Bean对象。 + + + +### SpirngBoot原理 + +#### 起步依赖 + +如果我们使用了SpringBoot,我们只需要引入一个依赖就可以了,那就是web开发的起步依赖:springboot-starter-web。 + +为什么我们只需要引入一个web开发的起步依赖? + +- 因为Maven的依赖传递。 + +> - 在SpringBoot给我们提供的这些起步依赖当中,已提供了当前程序开发所需要的所有的常见依赖(官网地址:https://docs.spring.io/spring-boot/docs/2.7.7/reference/htmlsingle/#using.build-systems.starters)。 +> +> - 比如:springboot-starter-web,这是web开发的起步依赖,在web开发的起步依赖当中,就集成了web开发中常见的依赖:json、web、webmvc、tomcat等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。 + +**结论:起步依赖的原理就是Maven的依赖传递。** + + + +#### 自动配置 + +SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明。 + +**如何让第三方bean以及配置类生效?** + +@Import导入 + +- 导入形式主要有以下几种: + 1. 导入普通类 + 2. 导入配置类 + 3. 导入ImportSelector接口实现类 + + + +**导入普通类:** + +``` +@Component +public class TokenParser { + + public void parse(){ + System.out.println("TokenParser ... parse ..."); + } + +} +``` + +``` +@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中 +@SpringBootApplication +public class SpringbootWebConfig2Application { + public static void main(String[] args) { + SpringApplication.run(SpringbootWebConfig2Application.class, args); + } +} +``` + +**导入配置类:** + +- 配置类 + +~~~java +@Configuration +public class HeaderConfig { + @Bean + public HeaderParser headerParser(){ + return new HeaderParser(); + } + + @Bean + public HeaderGenerator headerGenerator(){ + return new HeaderGenerator(); + } +} +~~~ + +- 启动类 + +~~~java +@Import(HeaderConfig.class) //导入配置类 +@SpringBootApplication +public class SpringbootWebConfig2Application { + public static void main(String[] args) { + SpringApplication.run(SpringbootWebConfig2Application.class, args); + } +} +~~~ + + + +怎么让**第三方依赖**自己指定bean对象和配置类? + +- 比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以@EnableXxxx开头的注解,**注解中封装的就是@Import注解** + + 使用第三方依赖提供的 @EnableXxxxx注解 + +- 第三方依赖中提供的注解 + +~~~java +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类 +public @interface EnableHeaderConfig { +} +~~~ + +- 在使用时只需在启动类上加上@EnableXxxxx注解即可 + +``` +@EnableHeaderConfig //使用第三方依赖提供的Enable开头的注解 +@SpringBootApplication +public class SpringbootWebConfig2Application { + public static void main(String[] args) { + SpringApplication.run(SpringbootWebConfig2Application.class, args); + } +} +``` + + + + + +**Springboot自动配置** + +自动配置原理源码入口就是@SpringBootApplication注解,在这个注解中封装了3个注解,分别是: + +- @SpringBootConfiguration + - 声明当前类是一个配置类 +- @ComponentScan + - 进行组件扫描(SpringBoot中默认扫描的是启动类所在的当前包及其子包) +- @EnableAutoConfiguration + - 封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类) + - 在实现类重写的selectImports()方法,读取当前项目下所有依赖jar包中META-INF/spring.factories、META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports两个文件里面定义的配置类(配置类中定义了@Bean注解标识的方法)。 + + 当SpringBoot程序启动时,就会加载配置文件当中所定义的配置类,并将这些配置类信息(类的全限定名)封装到String类型的数组中,最终通过@Import注解将这些配置类全部加载到Spring的IOC容器中,交给IOC容器管理。 + +**那么所有自动配置类的中声明的bean都会加载到Spring的IOC容器中吗?** + +其实并不会,因为这些配置类中在声明bean时,通常都会添加@Conditional开头的注解,这个注解就是进行条件装配。而Spring会根据Conditional注解有选择性的进行bean的创建。 + + + +## WEB开发总体图 + +![image-20240326111230747](D:/folder/test/output/20b69417-9b5c-401d-bf1b-e077789a921b.png) diff --git a/Java/Java笔记本.md b/Java/Java笔记本.md new file mode 100644 index 0000000..42681ae --- /dev/null +++ b/Java/Java笔记本.md @@ -0,0 +1,1639 @@ +## Java笔记本 + +### IDEA基础操作 + +Intellij Ideav创建Java项目: + +1. 创建空项目 +2. 创建Java module +3. 创建包 package edu.whut.xx +4. 创建类,类名首字母必须大写! + +IDEA快捷键: + +| `Ctrl + Alt + L` | 格式化代码 | +| ---------------- | ------------------------ | +| `Ctrl + /` | 注释/取消注释当前行 | +| `Ctrl + D` | 复制当前行或选中的代码块 | +| `Ctrl + Y` | 删除当前行 | +| `Ctrl + N` | 查找类 | +| `Ctrl+shift+F` | 在文件中查找代码 | + +调试快捷键: + +| 快捷键 | 功能 | +| :----------- | :--------------------- | +| `Shift + F9` | 调试当前程序 | +| `F8` | 单步执行(不进入方法) | +| `F7` | 单步执行(进入方法) | +| `Shift + F8` | 跳出当前方法 | +| `Alt + F9` | 运行到光标处 | +| `Ctrl + F2` | 停止调试 | + + + +| 缩写 | 生成的代码 | 说明 | +| :--------------- | :------------------------------------------ | :------------------ | +| `psvm` | `public static void main(String[] args) {}` | 生成 `main` 方法 | +| `sout` | `System.out.println();` | 打印到控制台 | +| `fori` | `for (int i = 0; i < ; i++) {}` | 生成 `for` 循环 | +| `iter` | `for (Type item : iterable) {}` | 生成增强 `for` 循环 | +| `new Test().var` | `Test test = new Test();` | 自动补全变量声明 | + + + +**从exsiting file中导入模块**: + +**方法一**:复制整个模块到项目文件夹,并导入模块的 *.iml 文件,这种方式保留了模块原有的配置信息。 + +**方法二**:新建一个模块,然后将原模块的 src 文件夹下的包复制过去,这种方式更灵活,可以手动调整模块设置。 + +删除模块: + +模块右键,remove module,这只是把它从项目中移除,然后!!打开模块所在文件夹,物理删除,才是真正完全删除。 + + + +### 转义符的作用 + +**防止字符被误解**: + +- 在字符串中,一些字符(如 `"` 和 `\`)有特殊的含义。例如,双引号用于标识字符串的开始和结束,反斜杠通常用于转义。所以当你希望在字符串中包含这些特殊字符时,你需要使用转义符来告诉解析器这些字符是字符串的一部分,而不是特殊符号。 +- 例如,`\"` 表示在字符串中包含一个双引号字符,而不是字符串的结束标志。 + +`"Hello \"World\""` => 结果是:`Hello "World"` (双引号被转义) + +`"C:\\Program Files\\App"` => 结果是:`C:\Program Files\App`(反斜杠被转义) + +如果只是"C:\\Program Files\\App" 那么路径就会报错 + + + +**表示非打印字符**: + +- 转义符可以用于表示一些不可见的或非打印的控制字符,如换行符(`\n`)、制表符(`\t`)等。这些字符无法直接通过键盘输入,所以使用转义符来表示它们。 + + + +### Java基础语法 + +1. 二进制:0b 八进制:0 十六进制:0x + +2. 在 `System.out.println()` 方法中,"ln" 代表 "line",表示换行。因此,`println` 实际上是 "print line" 的缩写。这个方法会在输出文本后自动换行. + + ``` + System.out.println("nihao "+1.3331); #Java 会自动将数值转换为字符串 + ``` + + 当直接打印一个没有重写 `toString()` 方法的对象时,Java 默认会调用 `Object` 类的 `toString()` 方法,其输出格式通常为: + + ``` + java.lang.Object@15db9742 + ``` + + + + 当打印重写`toString()` 方法的对象时: + + ``` + class Person { + private String name; + private int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return "Person{name='" + name + "', age=" + age + "}"; + } + } + + public class Main { + public static void main(String[] args) { + Person person = new Person("Alice", 30); + System.out.println(person); + } + } + + ``` + + ``` + Person{name='Alice', age=30} + ``` + + + +3. 一维数组创建: + + ``` + // 方式1:先声明,再指定长度(默认值为0、null等) + int[] arr1 = new int[10]; // 创建一个长度为10的int数组 + + // 方式2:使用初始化列表直接创建数组 + int[] arr2 = {1, 2, 3, 4, 5}; // 创建并初始化一个包含5个元素的int数组 + + String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"}; + + // 方式3:结合new关键字和初始化列表创建数组(常用于明确指定类型时) + int[] arr3 = new int[]{1, 2, 3, 4, 5}; // 与方式2效果相同 + + + + ``` + +4. 字符串创建 + + ``` + String str = "Hello, World!"; //(1)直接赋值 + + String str = new String("Hello, World!"); //使用 new 关键字 + + char[] charArray = {'H', 'e', 'l', 'l', 'o'}; + String str = new String(charArray); //通过字符数组创建 + ``` + + + +#### 日期 + +在Java中: + +- 代表年月日的类型是 `LocalDate`。`LocalDate` 类位于 `java.time` 包下,用于表示没有时区的日期,如年、月、日。 +- 代表年月日时分秒的类型是 `LocalDateTime`。`LocalDateTime` 类也位于 `java.time` 包下,用于表示没有时区的日期和时间,包括年、月、日、时、分、秒。 + +LocalDateTime.now(),获取当前时间 + +#### 访问修饰符 + +**public(公共的)**: + +使用public修饰的成员可以被任何其他类访问,无论这些类是否属于同一个包。 +例如,如果一个类的成员被声明为public,那么其他类可以通过该类的对象直接访问该成员。 + +**protected(受保护的)**: + +使用protected修饰的成员可以被**同一个包**中的其他类访问,也可以被**不同包中的子类**访问。 +与包访问级别相比,protected修饰符提供了更广泛的访问权限。 + +**default (no modifier)(默认的,即包访问级别)**: + +如果没有指定任何访问修饰符,则默认情况下成员具有包访问权限。 +在同一个包中的其他类可以访问默认访问级别的成员,但是在不同包中的类不能访问。 + +**private**(私有的): + +使用private修饰的成员只能在声明它们的**类内部**访问,其他任何类都不能访问这些成员。 +这种访问级别提供了最高的封装性和安全性。 + +如果您在另一个类中实例化了包含私有成员的类,那么您无法直接访问该类的私有成员。但是,您可以通过**公共方法**来间接地访问和操作私有成员。 + + + +则每个实例都有自己的一份拷贝,只有当变量被声明为 static 时,变量才是类级别的,会被所有实例共享。 + + + +``` +// 文件:com/example/PrivateExample.java +package com.example; + +public class PrivateExample { + private int privateVar = 30; + + // 公共方法,用于访问私有成员 + public int getPrivateVar() { + return privateVar; + } +} + +``` + + + +修饰符不仅可以用来修饰成员变量和方法,也可以用来**修饰类**。顶级类只能使用 `public` 或默认(即不写任何修饰符,称为包访问权限)。内部类可以使用所有访问修饰符(`public`、`protected`、`private` 和默认),这使得你可以更灵活地控制嵌套类的访问范围。 + +``` +public class OuterClass { + // 内部类使用private,只能在OuterClass内部访问 + private class InnerPrivateClass { + // ... + } + + // 内部类使用protected,同包以及其他包中的子类可以访问 + protected class InnerProtectedClass { + // ... + } + + // 内部类使用默认访问权限,只在同包中可见 + class InnerDefaultClass { + // ... + } + + // 内部类使用public,任何地方都可访问(但访问时需要通过OuterClass对象) + public class InnerPublicClass { + // ... + } +} + +``` + + + +#### 四种内部类 + +下面是四种内部类(成员内部类、局部内部类、静态内部类和匿名内部类)的示例代码,展示了如何用每一种方式来实现`Runnable`的`run()`方法并创建线程。 + +1. **成员内部类** + + 定义位置:成员内部类定义在外部类的**成员位置**。 + + 访问权限:可以无限制地访问外部类的所有成员,**包括私有成员**。 + + 实例化方式:需要先创建外部类的实例,然后才能创建内部类的实例。 + + 修改限制:不能有静态字段和静态方法(除非声明为常量`final static`)。**成员内部类属于外部类的一个实例,不能独立存在于类级别上。** + + 用途:适用于内部类与外部类关系密切,需要频繁访问外部类成员的情况。 + +``` +public class OuterClass { + class InnerClass implements Runnable { + // static int count = 0; // 编译错误 + public static final int CONSTANT = 100; // 正确:可以定义常量 + public void run() { + System.out.println("成员内部类中的线程正在运行..."); + } + } + + public void startThread() { + InnerClass inner = new InnerClass(); + Thread thread = new Thread(inner); + thread.start(); + } + + public static void main(String[] args) { + OuterClass outer = new OuterClass(); + outer.startThread(); + } +} + +``` + +2. **局部内部类** + + 定义位置:局部内部类定义在**一个方法或任何块内**(如:if语句、循环语句内)。 + + 访问权限:只能访问**所在方法**的`final`或事实上的`final`(即不被后续修改的)局部变量和外部类的成员变量(同成员内部类)。 + + 实例化方式:只能在定义它们的块中创建实例。 + + 修改限制:同样不能有静态字段和方法。 + + 用途:适用于只在方法或代码块中使用的类,有助于将实现细节隐藏在方法内部。 + +``` +public class OuterClass { + public void startThread() { + class LocalInnerClass implements Runnable { + public void run() { + System.out.println("局部内部类中的线程正在运行..."); + } + } + LocalInnerClass localInner = new LocalInnerClass(); + Thread thread = new Thread(localInner); + thread.start(); + } + + public static void main(String[] args) { + OuterClass outer = new OuterClass(); + outer.startThread(); + } +} + +``` + +3. **静态内部类** + + 定义位置:定义在外部类内部,但使用`static`修饰。 + + 访问权限:只能直接访问外部类的静态成员,访问非静态成员需要通过外部类实例。 + + 实例化方式:**可以直接创建,不需要外部类的实例**。 + + 修改限制:可以有自己的静态成员。 + + 用途:适合当内部类工作不依赖外部类实例时使用,常用于实现与外部类关系不那么密切的帮助类。 + +``` +public class OuterClass { + // 外部类的静态成员 + private static int staticVar = 10; + // 外部类的实例成员 + private int instanceVar = 20; + + // 静态内部类 + public static class StaticInnerClass { + public void display() { + // 可以直接访问外部类的静态成员 + System.out.println("staticVar: " + staticVar); + // 下面这行代码会报错,因为不能直接访问外部类的实例成员 + // System.out.println("instanceVar: " + instanceVar); + + // 如果确实需要访问实例成员,可以通过创建外部类的对象来访问 + OuterClass outer = new OuterClass(); + System.out.println("通过外部类实例访问 instanceVar: " + outer.instanceVar); + } + } + + public static void main(String[] args) { + // 直接创建静态内部类的实例,不需要外部类实例 + OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass(); + inner.display(); + } +} + +``` + +4. **匿名内部类** + + 使用匿名内部类实现抽象类相当于临时创建了一个**未命名的子类**,并且立即实例化了这个子类的对象。 + + 定义位置:在需要使用它的地方立即定义和实例化。 + + 访问权限:类似局部内部类,只能访问`final`或事实上的`final`局部变量。 + + 实例化方式:在定义时就实例化,不能显式地命名构造器。 + + 修改限制:不能有任何静态成员。 + + 用途:适用于创建一次性使用的实例,通常用于接口或抽象类的实现。 + +``` +//eg1 +public class OuterClass { + public static void main(String[] args) { + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("匿名内部类中的线程正在运行..."); + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } +} + +//eg2 安卓开发中用过很多次! +import javax.swing.*; +import java.awt.event.*; + +public class GUIApp { + public static void main(String[] args) { + JFrame frame = new JFrame("Demo"); + JButton button = new JButton("Click Me!"); + + // 匿名内部类用于事件监听 + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + System.out.println("Button was clicked!"); + } + }); + + frame.add(button); + frame.setSize(300, 200); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + } +} + +``` + +1. 创建`ActionListener`实例 + +``` +new ActionListener() { + public void actionPerformed(ActionEvent e) { + System.out.println("Button was clicked!"); + } +} + +``` + +这部分代码是匿名内部类的核心。这里发生的事情包括: + +- **`new ActionListener()`**:这表示创建了`ActionListener`接口的一个实现。由于`ActionListener`是一个接口,我们不能直接实例化它,而是需要提供该接口的一个**具体实现**。在这种情况下,我们通过创建一个匿名内部类来提供实现。 +- **大括号 `{ ... }`**:在这对大括号内,我们定义了接口中需要实现的方法。对于`ActionListener`接口,必须实现`actionPerformed(ActionEvent e)`方法。 +- **方法实现**: + - **`public void actionPerformed(ActionEvent e)`**:这是`ActionListener`接口要求实现的方法,用于响应事件。这里的方法定义了当事件发生时(例如,用户点击按钮)执行的操作。 + - **`System.out.println("Button was clicked!");`**:这是`actionPerformed`方法的具体实现,即当按钮被点击时,控制台将输出一条消息。 + +2. 将匿名内部类添加为事件监听器 + +``` +button.addActionListener(...); +``` + +**`button.addActionListener(EventListener)`**:这是`JButton`类的一个方法,用于添加事件监听器。这个方法的参数是`ActionListener`类型的对象,这里传入的正是我们刚刚创建的匿名内部类的实例。 + + + +#### Lambda表达式 + +Lambda表达式特别适用于**只有单一抽象方法**的接口(也即**函数式接口**)。lambda 表达式主要用于**实现**函数式接口。 + +**`@FunctionalInterface` 注解**:这是一个可选的注解,用于表示接口是一个函数式接口。虽然不是强制的,但它可以帮助编译器识别意图,并检查接口是否确实只有一个抽象方法。 + +``` +public class LambdaExample { + // 定义函数式接口,doSomething 有两个参数 + @FunctionalInterface + interface MyInterface { + void doSomething(int a, int b); + } + + public static void main(String[] args) { + // 使用 Lambda 表达式实现接口方法 + MyInterface obj = (a, b) -> { + System.out.println("参数a: " + a + ", 参数b: " + b); + }; + obj.doSomething(5, 10); + } +} +``` + +**lambda表达式格式**:`(参数列表) -> { 代码块 }` + +​ 或 (参数列表) ->表达式; + +在上述Lambda表达式中,因为`MyInterface`接口的`doSomething()`方法不接受任何参数并且没有返回值,所以Lambda表达式的参数列表为空(`()`),后面跟的是执行的代码块。 + + + +**以下是lambda表达式的重要特征:** + +可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。 +可选的参数圆括号:**一个参数无需定义圆括号**,但无参数或多个参数需要定义圆括号。 +可选的大括号:如果主体只有一个语句,可以不使用大括号。 +可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,使用大括号需显示retrun;如果函数是void则不需要返回值。 + +``` +// 定义一个函数式接口 +interface Calculator { + int add(int a, int b); +} + +public class LambdaReturnExample { + public static void main(String[] args) { + // 例子1:单个表达式,不使用大括号和 return 关键字 + Calculator calc1 = (a, b) -> a + b; + System.out.println("calc1: " + calc1.add(5, 3)); // 输出:8 + + // 例子2:使用大括号,需要显式使用 return 关键字 + Calculator calc2 = (a, b) -> { + return a + b; + }; + System.out.println("calc2: " + calc2.add(5, 3)); // 输出:8 + } +} + +``` + + + +示例1: + +`list.forEach`这个方法接受一个**函数式接口**作为参数。它只有一个抽象方法 `accept(T t)`因此,可以使用 lambda 表达式来**实现**。 + +``` +import java.util.Arrays; +import java.util.List; + +public class Main { + public static void main(String[] args) { + List list = Arrays.asList("Apple", "Banana", "Cherry", "Date"); + + // 使用 Lambda 表达式迭代列表 + list.forEach(item -> System.out.println(item)); + } +} + +``` + + + +示例2: + +`Collections.sort` 方法用于对列表进行排序,它接受两个参数 + +第一个参数:要排序的列表(这里是一个 `List`)。 + +第二个参数:一个实现了 `Comparator` 接口的比较器,用于指定排序规则。 + +​ 如果返回负数,表示 `a` 应该排在 `b` 的前面; + +​ 如果返回正数,则 `a` 应该排在 `b` 的后面。 + +``` +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class Main { + public static void main(String[] args) { + List names = Arrays.asList("John", "Jane", "Adam", "Dana"); + + // 使用Lambda表达式排序 + Collections.sort(names, (String a, String b) -> a.compareTo(b)); + + // 输出排序结果 + names.forEach(name -> System.out.println(name)); + } +} +``` + + + + + +#### 静态成员变量的初始化 + +静态成员变量属于类级别,在类加载时完成初始化。初始化方式主要有两种: + +**1.静态初始化块(Static Initialization Block)** + +``` +public class MyClass { + static int num1, num2; + + // 第一个静态代码块 + static { + num1 = 1; + System.out.println("静态代码块1执行"); + } + + // 主方法 + public static void main(String[] args) { + System.out.println("main方法执行"); + } + + // 第二个静态代码块 + static { + num2 = 3; + System.out.println("静态代码块2执行"); + } +} + +``` + +输出: + +``` +静态代码块1执行 +静态代码块2执行 +main方法执行 +``` + +**说明:** +类加载时依次执行所有静态代码块,然后执行 `main` 方法。 + + + +**2.在声明时直接初始化** + +``` +public class MyClass { + // 直接在声明时初始化静态成员变量 + public static int staticVariable = 42; +} +``` + + + +静态成员变量的访问不需要创建 `MyClass` 的实例,可以直接通过类名访问: + +``` +int value = MyClass.staticVariable; +MyClass obj = new MyClass(); +System.out.println("obj.num1 = " + obj.staticVariable); #通过示例访问也可以 +``` + + + +#### 静态方法 + +静态方法属于**类级别**,不依赖于任何具体实例 + +静态方法访问规则: + +- **可以直接访问:** + + - 类中的其他静态成员变量。 + - 类中的静态方法。 + + **不能直接访问:** + + - 非静态成员变量。 + - 非静态方法(必须通过对象实例访问)。 + +``` +public class MyClass { + private static int staticVar = 10; + private int instanceVar = 20; + + // 静态方法:可以直接访问静态成员 + public static void staticMethod() { + System.out.println(staticVar); // 正确:访问静态成员变量 + // System.out.println(instanceVar); // 错误:不能直接访问非静态成员变量 + + // 如需要访问非静态成员,必须先创建对象实例 + MyClass obj = new MyClass(); + System.out.println(obj.instanceVar); // 正确:通过对象实例访问非静态成员变量 + } + + // 非静态方法:可以访问所有成员 + public void instanceMethod() { + System.out.println(staticVar); // 正确:访问静态成员变量 + System.out.println(instanceVar); // 正确:访问非静态成员变量 + } +} + +``` + +调用静态方法: + +``` +MyClass.staticMethod(); // 通过类名直接调用静态方法 +``` + + + +#### super关键字 + +`super` 关键字有两种主要的使用方法:访问父类的成员和调用父类的构造方法。 + +1. 访问父类的成员 + + 可以使用 `super` 关键字来引用父类的字段或方法。这在子类中**存在同名的字段或方法**时特别有用。 + + 因为父类的成员变量和方法都是默认的访问修饰符,可以继承给子类,而子类也定义了同名的xxx,发生了**变量隐藏**(shadowing)。 + + ``` + class Parent { + int num = 10; + void display() { + System.out.println("Parent class method"); + } + } + + class Child extends Parent { + int num = 20; + void display() { + System.out.println("Child class method"); + } + void print() { + System.out.println("Child class num: " + num); // 访问子类中的num + System.out.println("Parent class num: " + super.num); // 使用super关键字访问父类中的num + super.display(); // 调用父类中的display方法 + } + } + + public class Main { + public static void main(String[] args) { + Child obj = new Child(); + obj.print(); + } + } + + ``` + + 输出: + + ``` + Child class num: 20 + Parent class num: 10 + Parent class method + ``` + +2. 调用父类的构造方法 + +可以使用 `super` 关键字调用父类的构造方法。这通常在子类的构造方法中使用,用于显式地调用父类的构造方法。 + +``` +class Parent { + Parent() { + System.out.println("Parent class constructor"); + } +} + +class Child extends Parent { + Child() { + super(); // 调用父类的构造方法 + System.out.println("Child class constructor"); + } +} + +public class Main { + public static void main(String[] args) { + Child obj = new Child(); + } +} + +``` + +输出: + +``` +Parent class constructor +Child class constructor +``` + + + +#### final关键字 + +final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点: + +1. 修饰类:类不能继承,final 类中的所有成员方法都会被隐式的指定为 final 方法; +2. 修饰变量:该变量为常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。 +3. 修饰符方法:方法不能重写 + + + +#### 变量修饰符的顺序 + +在Java中,变量的修饰符应该按照规定的顺序出现,通常是这样的: + +1. **访问修饰符**:public、protected、private,或者不写(默认为包级访问)。 +2. **非访问修饰符**:final、static、abstract、synchronized、volatile等。 +3. **数据类型**:变量的数据类型,如int、String、class等。 +4. **变量名**:变量的名称。 + +``` +public static final int MAX_COUNT = 100; #定义常量 +protected static volatile int counter; #定义成员变量 +``` + + + +### JAVA面向对象 + +#### **JAVA**三大特性 + +**封装** + +封装指隐藏对象的状态信息(属性),不允许外部对象直接访问对象的内部信息(private实现)。但是可以提供一些可以被外界访问的方法(public)来操作属性。 + + + +**继承** + +``` +[修饰符] class 子类名 extends 父类名{ + 类体部分 +} + +//class C extends A, B { } // 错误:C 不能同时继承 A 和 B +``` + +Java只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。 + +Java支持多层继承(A → B → C )。 + +Java继承了父类**非私有**的成员变量和成员方法,但是请注意:子类是无法继承父类的构造方法的。 + + + +**多态** + +指在面向对象编程中,同样的消息(方法调用)可以在不同的对象上触发不同的行为。 + +1. **方法重写(Override)**:动态多态;子类从父类继承的某个实例方法无法满足子类的功能需要时,需要在子类中对该实例方法进行重新实现,这样的过程称为重写,也叫做覆写、覆盖。 + + **要求**: + + - 必须存在继承关系(子类继承父类)。 + - 子类重写的方法的访问修饰符不能比父类更严格(可以相同或更宽松)。 + - 方法名、参数列表和返回值类型必须与父类中的方法完全相同(Java 5 以后支持协变返回类型,即允许返回子类型)。 + +2. **向上转型(Upcasting)**:动态多态;子类对象可以赋值给父类引用,这样做可以隐藏对象的真实类型,只能调用**父类中声明的方法**。 + + ``` + class Animal { + public void makeSound() { + System.out.println("Animal makes sound"); + } + } + + class Dog extends Animal { + @Override + public void makeSound() { + System.out.println("Dog barks"); + } + + public void fetch() { + System.out.println("Dog fetches the ball"); + } + } + + public class Test { + public static void main(String[] args) { + Animal animal = new Dog(); // 向上转型 + animal.makeSound(); // 调用的是 Dog 重写的 makeSound() 方法 + // animal.fetch(); // 编译错误:Animal 类型没有 fetch() 方法 + } + } + ``` + + **多态实现总结**:继承 + 重写 + 父类引用指向子类对象 = 多态 + +3. **方法重载(Overload)**:静态多态;在一个类中,可以定义多个同名方法,但参数列表不同。当调用这些方法时,会根据传递的参数类型或数量选择相应的方法。 + + **与重写的区别**: + + - **重载**发生在同一个类中,与继承无关; + - **重写**发生在子类中,依赖继承关系,实现运行时多态。 + +``` +class Calculator { + int add(int a, int b) { + return a + b; + } + + double add(double a, double b) { + return a + b; + } + +``` + + + +#### 抽象类 + +**定义** +抽象类是包含至少一个抽象方法的类。抽象方法没有实现,只定义了方法的签名。 +**注意:** 抽象类不能被实例化。 + +**必须实现抽象方法** +如果一个子类继承了抽象类,通常必须实现抽象类中的所有抽象方法,否则该子类也必须声明为抽象类。例如: + +``` +abstract class Animal { + // 抽象方法,没有方法体 + public abstract void makeSound(); + + // 普通方法 + public void sleep() { + System.out.println("Sleeping..."); + } +} + +// 正确:子类实现了所有抽象方法 +class Dog extends Animal { + @Override + public void makeSound() { + System.out.println("Dog barks"); + } +} + +// 错误:如果不实现 makeSound() 方法,则 Dog 必须也声明为抽象类 + +``` + +**如何使用抽象类** +由于抽象类不能直接实例化,我们通常有两种方法来使用抽象类: + +1. **定义一个新的子类** + 创建一个子类继承抽象类并实现所有抽象方法,然后使用子类实例化对象: + + ``` + Animal animal = new Dog(); + animal.makeSound(); // 输出:Dog barks + ``` + +2. **使用匿名内部类** + 使用匿名内部类实现抽象类相当于临时创建了一个**未命名的子类**,并且立即实例化了这个子类的对象。 + + ``` + Animal animal = new Animal() { + @Override + public void makeSound() { + System.out.println("Anonymous animal sound"); + } + }; + animal.makeSound(); // 输出:Anonymous animal sound + + ``` + + + +#### 接口 + +**接口(Interface)**: +定义了一组方法的规范,侧重于行为的约定。接口中的所有方法默认是抽象的(Java 8 之后可包含默认方法和静态方法),不包含成员变量(除了常量)。 + +**抽象类(Abstract Class)**: +可以包含抽象方法和具体实现的方法,还可以拥有成员变量和构造方法,适用于需要部分通用实现的情况。 + +1. *方法实现*: + + *接口*: + + - Java 8 前:所有方法都是抽象方法,只包含方法声明。 + - Java 8 及以后:可包含默认方法(default methods)和静态方法。 + + *抽象类*: + + - 可以同时包含抽象方法(不提供实现)和具体方法(提供实现)。 +2. *继承:* + + - 类实现接口时,使用关键字 `implements`。 + - 类继承抽象类时,使用关键字 `extends`。 +3. *多继承*: + + - 类可以实现多个接口(多继承)。 + - 类只能继承一个抽象类(单继承)。 + +``` +// 定义接口 +interface Flyable { + void fly(); +} + +interface Swimmable { + void swim(); +} + +// 实现多个接口的类 +class Bird implements Flyable, Swimmable { + // 实现接口中的方法 + public void fly() { + System.out.println("Bird is flying"); + } + + public void swim() { + System.out.println("Bird is swimming"); + } +} + +// 主类 +public class Main { + public static void main(String[] args) { + // 创建实现多个接口的对象 + Bird bird = new Bird(); + + // 调用实现的方法 + bird.fly(); // 输出: Bird is flying + bird.swim(); // 输出: Bird is swimming + } +} + +``` + + + +### 容器 + +#### Collection + +在 Java 中,`Collection` 是一个接口,它表示一组对象的集合。`Collection` 接口是 Java 集合框架中最基本的接口之一,定义了一些操作集合的**通用方法**,例如添加、删除、遍历等。 + +所有集合类(例如 List、Set、Queue 等)都直接或间接地继承自 Collection 接口。 + +- `boolean add(E e)`:将指定的元素添加到集合中(可选操作)。 +- `boolean remove(Object o)`:从集合中移除指定的元素(可选操作)。 +- `boolean contains(Object o)`:如果集合中包含指定的元素,则返回 `true`。 +- `int size()`:返回集合中的元素个数。 +- `void clear()`:移除集合中的所有元素。 +- `boolean isEmpty()`:如果集合为空,则返回 `true`。 + +``` +import java.util.ArrayList; +import java.util.Collection; + +public class CollectionExample { + public static void main(String[] args) { + // 创建一个 Collection 对象,使用 ArrayList 作为实现类 + Collection fruits = new ArrayList<>(); + + // 添加元素到集合中 + fruits.add("Apple"); + fruits.add("Banana"); + fruits.add("Cherry"); + System.out.println("添加元素后集合大小: " + fruits.size()); // 输出集合大小 + + // 检查集合是否包含某个元素 + System.out.println("集合中是否包含 'Banana': " + fruits.contains("Banana")); + + // 从集合中移除元素 + fruits.remove("Banana"); + System.out.println("移除 'Banana' 后集合大小: " + fruits.size()); + + // 清空集合 + fruits.clear(); + System.out.println("清空集合后,集合是否为空: " + fruits.isEmpty()); + } +} + +``` + + + +#### Iterator + +在 Java 中,`Iterator` 是一个接口,遍历集合元素。Collection 接口中定义了 iterator() 方法,返回一个 Iterator 对象。 + +`Iterator` 接口中包含以下主要方法: + +1. `hasNext()`:如果迭代器还有下一个元素,则返回 `true`,否则返回 `false`。 +2. `next()`:返回迭代器的下一个元素,并将迭代器移动到下一个位置。 +3. `remove()`:从迭代器当前位置删除元素。该方法是可选的,不是所有的迭代器都支持。 + +``` +import java.util.ArrayList; +import java.util.Iterator; + +public class Main { + public static void main(String[] args) { + // 创建一个 ArrayList 集合 + ArrayList list = new ArrayList<>(); + list.add(1); + list.add(2); + list.add(3); + int size = list.size(); // 获取列表大小 + System.out.println("Size of list: " + size); // 输出 3 + + // 获取集合的迭代器 + Iterator iterator = list.iterator(); + + // 使用迭代器遍历集合并输出元素 + while (iterator.hasNext()) { + Integer element = iterator.next(); + System.out.println(element); + } + } +} +``` + + + +#### ArrayList + +`ArrayList` 是 List 接口的一种实现,而 List 接口又继承自 Collection 接口。包括 `add()`、`remove()`、`contains()` 等。 + +![image-20240227133714509](D:/folder/test/output/2e517d04-cc78-4b53-874b-ad66ce023237.png) + +#### HashSet + +![image-20240227150219184](D:/folder/test/output/c8b543e5-8807-4892-8c2c-f4527d8788bd.png) + +#### HashMap + +![image-20240227152019078](D:/folder/test/output/7a225569-a6d5-47e2-b8e9-c737188b4020.png) + +``` + // 使用 entrySet() 方法获取 Map 中所有键值对的集合,并使用增强型 for 循环遍历键值对 + System.out.println("Entries in the map:"); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Integer value = entry.getValue(); + System.out.println("Key: " + key + ", Value: " + value); + } +``` + + + +#### PriorityQueue + +默认是小根堆,输出1,2,5,8 + +``` +import java.util.PriorityQueue; + +public class Main { + public static void main(String[] args) { + // 创建一个 PriorityQueue 对象 + PriorityQueue pq = new PriorityQueue<>(); + + // 添加元素到队列 + pq.offer(5); + pq.offer(2); + pq.offer(8); + pq.offer(1); + + // 打印队列中的元素 + System.out.println("Elements in the priority queue:"); + while (!pq.isEmpty()) { + System.out.println(pq.poll()); + } + } +} +``` + +1. `offer()` 方法用于将元素插入到队列中 +2. `poll()` 方法用于移除并返回队列中的头部元素 +3. `peek()` 方法用于返回队列中的头部元素但不移除它。 + + + +### JAVA异常处理 + +``` +public class ExceptionExample { + // 方法声明中添加 throws 关键字,指定可能抛出的异常类型 + public static void main(String[] args) throws SomeException, AnotherException { + try { + // 可能会抛出异常的代码块 + if (someCondition) { + throw new SomeException("Something went wrong"); + } + } catch (SomeException e) { + // 处理 SomeException 异常 + System.out.println("Caught SomeException: " + e.getMessage()); + } catch (AnotherException e) { + // 处理 AnotherException 异常 + System.out.println("Caught AnotherException: " + e.getMessage()); + } finally { + // 不管是否发生异常,都会执行的代码块 + System.out.println("End of try-catch block"); + } + } +} + // 自定义异常类,继承自 Exception 类 +public class SomeException extends Exception { + // 构造方法,用于设置异常信息 + public SomeException(String message) { + // 调用父类的构造方法,设置异常信息 + super(message); + } +} + +``` + +## 好用的方法 + +### toString() + +Arrays.toString() + +作用:方便地输出数组。 +这个方法是是用来将数组转换成String类型输出的,入参可以是long,float,double,int,boolean,byte,object 型的数组。 + +``` +import java.util.Arrays; + +public class Main { + public static void main(String[] args) { + // 一维数组示例 + int[] oneD = {1, 2, 3, 4, 5}; + System.out.println("一维数组输出: " + Arrays.toString(oneD)); + + // 二维数组示例 + int[][] twoD = { + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + // 使用 Arrays.deepToString() 输出二维数组 + System.out.println("二维数组输出: " + Arrays.deepToString(twoD)); + } +} + +``` + + + +### 类加载器和获取资源文件路径 + +在Java中,类加载器的主要作用是根据**类路径(Classpath)**加载类文件以及其他资源文件。 + +**类路径**是JVM在运行时用来查找类文件和资源文件的一组目录或JAR包。在许多项目(例如Maven或Gradle项目)中,`src/main/resources`目录下的内容在编译时会被复制到输出目录(如`target/classes`),`src/main/java` 下编译后的 class 文件也会放到这里。 + +``` +MyProject/ +├── src/ +│ └── main/ +│ └── java/ +│ └── com/ +│ └── example/ +│ └── Main.java +└── resources/ + ├── emp.xml + └── static/ + └── tt.img + +``` + +``` +// 获取 resources 根目录下的 emp.xml 文件路径 +String empFile = this.getClass().getClassLoader().getResource("emp.xml").getFile(); + +// 获取 resources/static 目录下的 tt.img 文件路径 +URL resourceUrl = getClass().getClassLoader().getResource("static/tt.img"); +String ttImgPath = resourceUrl != null ? resourceUrl.getFile() : null; + +``` + +1. `this.getClass()`:获取当前对象(即调用该代码的对象)的 Class 对象。 +2. `.getClassLoader()`:获取该 Class 对象的类加载器(ClassLoader)。 +3. `.getResource("emp.xml")`:从类路径中获取名为 "emp.xml" 的资源,并返回一个 URL 对象,该 URL 对象指向 "emp.xml" 文件的位置。 +4. `.getFile()`:从 URL 对象中获取文件路径部分,即获取 "emp.xml" 文件的绝对路径字符串。 + + + +**类路径(Classpath)**是 Java 虚拟机(JVM)用于查找类文件和其他资源文件的一组路径。 + +类加载器的主要作用之一就是从类路径中加载类文件和其他资源文件。 + + + +### 反射 + +反射技术,指的是加载类的字节码到内存,并以编程的方法解刨出类中的各个成分(**成员变量、方法、构造器**等)。 + +![1668575796295](D:/folder/test/output/2e1ccb45-af4e-4b31-8341-2c79145b0012.png) + +**反射技术例子**:IDEA通过反射技术就可以获取到类中有哪些方法,并且把方法的名称以提示框的形式显示出来,所以你能看到这些提示了。 + +![1668576426355](D:/folder/test/output/c7f42472-eebf-4d17-8c29-55acebc98ed3.png) + +**1.获取类的字节码(Class对象)**:有三种方法 + +``` +public class Test1Class{ + public static void main(String[] args){ + Class c1 = Student.class; + System.out.println(c1.getName()); //获取全类名:edu.whut.pojo.Student + System.out.println(c1.getSimpleName()); //获取简单类名: Student + + Class c2 = Class.forName("com.itheima.d2_reflect.Student"); //全类名 + System.out.println(c1 == c2); //true + + Student s = new Student(); + Class c3 = s.getClass(); + System.out.println(c2 == c3); //true + } +} +``` + + + +**2.获取类的构造器** + +``` +public class Cat{ + private String name; + private int age; + + public Cat(){ + + } + + private Cat(String name, int age){ + + } +} +``` + + + +- 获取构造器列表 + +``` +public class TestConstructor { + + @Test + public void testGetAllConstructors() { + // 1. 获取类的 Class 对象 + Class c = Cat.class; + + // 2. 获取类的全部构造器(包括public、private等) + Constructor[] constructors = c.getDeclaredConstructors(); + + // 3. 遍历并打印构造器信息 + for (Constructor constructor : constructors) { + System.out.println( + constructor.getName() + + " --> 参数个数:" + constructor.getParameterCount() + ); + } + } +} + +``` + +`c.getDeclaredConstructors()` 会返回所有声明的构造器(包含私有构造器),而 `c.getConstructors()` 只会返回公共构造器。 + +`constructor.getParameterCount()` 用于获取该构造器的参数个数。 + + + +获取某个构造器:**指定参数类型!** + +```java +public class Test2Constructor(){ + @Test + public void testGetConstructor(){ + //1、反射第一步:必须先得到这个类的Class对象 + Class c = Cat.class; + + /2、获取private修饰的有两个参数的构造器,第一个参数String类型,第二个参数int类型 + Constructor constructor = + c.getDeclaredConstructor(String.class,int.class); + + constructor.setAccessible(true); //禁止检查访问权限,可以使用private构造函数 + Cat cat=(Cat)constructor.newInstance("叮当猫",3); //初始化Cat对象 + + } +} +``` + +`c.getDeclaredConstructor(String.class, int.class)`:根据参数列表获取特定的构造器。 + +如果构造器是private修饰的,先需要调用`setAccessible(true)` 表示禁止检查访问控制,然后再调用`newInstance(实参列表)` 就可以执行构造器,完成对象的初始化了。 + + + +**3.获取类的成员变量** + +![1668579517323](D:/folder/test/output/1d017d0b-986d-4fd7-b963-5d225100a5d2.png) + +![1668580075962](D:/folder/test/output/bb6ed24c-148d-4024-8b84-ebab8c914b94.png) + +不管是设置值还是获取值,都需要: + +1. 拿到 `Field` 对象。 +2. 指定操作**哪个对象**的该字段。 +3. 对于私有字段,还需要调用 `setAccessible(true)` 来关闭访问检查。 + + + +**4.获取类的成员方法** + +![1668580761089](D:/folder/test/output/9a80d395-6ae1-43bb-83b5-86c2bc1d09db.png) + +获取**单个**指定的成员方法:第一个参数填**方法名**、第二个参数填方法中的**参数类型** + +![1668581678388](D:/folder/test/output/09bebd8f-0d24-452f-8c01-8253f4b49568.png) + +执行:第一个参数传入一个**对象**,然后是若干方法参数(无参可不写)... + +![1668581800777](D:/folder/test/output/153f2ab3-f562-4c2d-913f-5a791e157bb0.png) + + + +示例:`Cat` 类与测试类 + +``` +public class Cat { + private String name; + public int age; + + public Cat() { + this.name = "Tom"; + this.age = 1; + } + + public void meow() { + System.out.println("Meow! My name is " + this.name); + } + + private void purr() { + System.out.println("Purr... I'm a happy cat!"); + } +} +``` + +``` +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class FieldReflectionTest { + + @Test + public void testFieldAccess() throws Exception { + // 1. 获取 Cat 类的 Class 对象 + Class catClass = Cat.class; + + // 2. 创建 Cat 对象实例 + Cat cat = new Cat(); + + // ---------------------- + // A. 获取 public 字段 + // ---------------------- + Field ageField = catClass.getField("age"); // 只能获取public字段 + System.out.println("初始 age = " + ageField.get(cat)); // 读取 age 的值 + + // 设置 age 的值 + ageField.set(cat, 5); + System.out.println("修改后 age = " + ageField.get(cat)); + + // ---------------------- + // B. 获取 private 字段 + // ---------------------- + Field nameField = catClass.getDeclaredField("name"); // 获取私有字段 + nameField.setAccessible(true); // 关闭权限检查 + System.out.println("初始 name = " + nameField.get(cat)); + + // 设置 name 的值 + nameField.set(cat, "Jerry"); + System.out.println("修改后 name = " + nameField.get(cat)); + } + + @Test + public void testMethodAccess() throws Exception { + // 1. 获取 Cat 类的 Class 对象 + Class catClass = Cat.class; + + // 2. 创建 Cat 对象实例 + Cat cat = new Cat(); + + // ---------------------- + // A. 获取并调用 public 方法 + // ---------------------- + // 获取名为 "meow"、无参数的方法 + Method meowMethod = catClass.getMethod("meow"); + // 调用该方法 + meowMethod.invoke(cat); + + // ---------------------- + // B. 获取并调用 private 方法 + // ---------------------- + // 获取名为 "purr"、无参数的私有方法 + Method purrMethod = catClass.getDeclaredMethod("purr"); + purrMethod.setAccessible(true); // 关闭权限检查 + purrMethod.invoke(cat); + } +} + +``` + + + +### Junit 单元测试 + +![image-20240307172717512](D:/folder/test/output/0dee7d7b-471a-46af-91ee-273a73454a79.png) + +``` + @Test + public void testListUser(){ + Listlist=userMapper.list(); + for(User user:list){ + System.out.println(user); + } + } +``` + +写了@Test注解,那么该测试函数就可以直接运行!若一个测试类中写了多个测试方法,可以全部执行! + +![image-20240307173454288](D:/folder/test/output/25badfe2-74f4-4e85-a427-2504b3639ee5.png) + +原理可能是: + +``` +//自定义注解 +@Retention(RetentionPolicy.RUNTIME) //指定注解在运行时可用,这样才能通过反射获取到该注解。 +@Target(ElementType.METHOD) //指定注解可用于方法上。 +public @interface MyTest { +} + +public class AnnotationTest4 { + + @MyTest + public void test() { + System.out.println("===test4==="); + } + + public static void main(String[] args) throws Exception { + AnnotationTest4 a = new AnnotationTest4(); + Class c = AnnotationTest4.class; + + // 获取当前类中声明的所有方法 + Method[] methods = c.getDeclaredMethods(); + + // 遍历方法,检查是否使用了 @MyTest 注解 + for (Method method : methods) { + if (method.isAnnotationPresent(MyTest.class)) { + // 如果标注了 @MyTest,就通过反射调用该方法 + method.invoke(a); + } + } + } +} +``` + + + +在Springboot中,如何快速生成单元测试? + +选中类名,右键: +image-20240815093428359 + + + +### 注解 + +定义: + +``` +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 定义注解 +@Retention(RetentionPolicy.RUNTIME) // 定义注解的生命周期 +@Target(ElementType.METHOD) // 定义注解可以应用的Java元素类型 +public @interface MyAnnotation { + // 定义注解的元素(属性) + String description() default "This is a default description"; + int value() default 0; +} + +``` + +@Target(ElementType.METHOD) //方法上的注解 + +@Target(ElementType.CLASS) //类上的注解 + + + +**简化使用**:当注解只有一个元素需要设置时,且该元素的名字是`value`,在使用该注解时可以不用显式地指定元素名 + +``` +@MyAnnotation(5) // 等同于 @MyAnnotation(value = 5) +public void someMethod() { + // 方法实现 +} + +``` + + + +如果要同时设置`description`,则不能省略元素名: + +``` +@MyAnnotation(value = 5, description = "Specific description") +public void anotherMethod() { + // 方法实现 +} + +``` + + + +**获得注解上的value:**反射 + +``` +public class MyClass { + + @MyAnnotation(value = "specific value") + public void myMethod() { + // 方法实现 + } +} + +``` + + + +``` +import java.lang.reflect.Method; + +public class AnnotationReader { + + public static void main(String[] args) throws NoSuchMethodException { + // 获取MyClass的Class对象 + Class obj = MyClass.class; + + // 获取myMethod方法的Method对象 + Method method = obj.getMethod("myMethod"); + + // 获取方法上的MyAnnotation注解实例 + MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); + + if (annotation != null) { + // 输出注解的value值 + System.out.println("注解的value: " + annotation.value()); + } + } +} + +``` + + + +### 对象拷贝属性 + +```java + public void save(EmployeeDTO employeeDTO) { + Employee employee = new Employee(); + //对象属性拷贝 + BeanUtils.copyProperties(employeeDTO, employee); + } +``` + diff --git a/Java/Jupyter notebook快速上手.md b/Java/Jupyter notebook快速上手.md new file mode 100644 index 0000000..071681b --- /dev/null +++ b/Java/Jupyter notebook快速上手.md @@ -0,0 +1,237 @@ +# Jupyter notebook快速上手 + +> 张子豪 同济大学研究生 + +的Bilibili视频教程:[同济子豪兄](https://space.bilibili.com/1900783/#/)
+ +知乎专栏:[人工智能小技巧](https://zhuanlan.zhihu.com/c_1032626015746502656)
+ +简书专栏:[人工智能小技巧](https://www.jianshu.com/u/38cccf09b515)
+ +子豪兄的粉丝答疑交流QQ群:953712961 + +[TOC] + +# 为什么学Jupyter notebook? + +![jupyter notebook](D:/folder/test/output/2d843df3-ff59-4766-9b85-798f58bd9859.png) + +能够编写、运行python文件的程序很多,比如python安装自带的IDLE、程序员喜爱的pycharm、数据科学全家桶Anaconda,还有Spyder、Thonny等。 + +而我,独爱jupyter notebook。 + +Jupyter notebook是用python进行数据科学、机器学习的必备工具。 + +突出优点: + +- 学习Jupyter notebook非常容易,按照我的视频教程一步步做,再自己尝试一下,之后写代码即可健步如飞。 + +- 能够独立运行一个、几个或全部python代码块,更容易看到中间变量的值,从而进行调试 +- 可以插入Markdown说明文字和Latex数学公式,让枯燥的代码充满颜值,可读性爆表 +- 能够调用Ipython丰富的“魔法函数”,比如程序计时、重复运行、显示图片等 +- 写好的代码和文档能够以网页和ppt的形式在线分享。[在线看Jupyter notebook文件]()
+- 可以在云端远程服务器运行,不需本地安装配置各种环境。[体验一下]()
+ +比如下图,包含了Markdown说明文档、代码块、代码运行结果、图片嵌入等元素,特别适合Python数据科学和机器学习撰写文档。 + +![jupyter notebook代码分块运行、嵌入Markdown文档和图片](D:/folder/test/output/0cefdcea-2b6e-4d3a-9232-0e1f3c637cb7.png) + +吴恩达的《深度学习》慕课的课后编程作业、大数据竞赛网站Kaggle上的代码文档、美国大学的数据科学课程的课后资料及编程作业,都是以jupyter notebook文件的形式给出的,也就是`.ipynb`文件。 + +> 其实Jupyter notebook不止可以运行python,还可以运行julia、R、Javascript等语言,这也是`jupyter`这个名字的由来。[Jupyter notebook支持的编程语言]()
+> +> Jupyter notebook集编程和写作于一身,这就叫做“文学编程”。 + +# Jupyter notebook——在浏览器网页中运行python + +## Ipython内核——更高级的python解释器 + +Jupyter notebook是基于Ipython内核的,在浏览器中以网页形式运行Python代码的工具,十分方便。 + +> Ipython是啥? +> +> Ipython可以理解成更高级的python解释器,相比原生的python交互式命令行,Ipython有更强大的命令计数、自动补全等交互功能。 +> +> Spyder和Jupyter notebook都是以Ipython为内核的。 + +![ipython](D:/folder/test/output/156887fb-fbf6-45f7-983e-955f68a39b5d.png) + +## 安装Jupyter notebook + +如果你安装了python数据科学全家桶Anaconda,那么其中自带了Jupyter notebook。 + +如果你没安装Anaconda,可以直接在命令行里运行这行命令 + +```shell +pip install jupyter -i https://pypi.tuna.tsinghua.edu.cn/simple +``` + +## 运行Jupyter notebook + +打开命令行,输入`jupter notebook`,回车。稍等片刻即可跳出浏览器网页。 + +![jupyter notebook打开界面](D:/folder/test/output/f44ab651-248c-41a3-847c-05c499a0d479.png) + +点击右边的New-Python3即可创建python文档。 + +点击New-Folder可以创建新文件夹。 + +点击New-Text File可以创建空的`.txt`文件。 + +点击New-Terminal可以打开操作系统命令行,你可以使用操作系统对应的命令行进行目录切换、解压文件等操作。 + +勾选文件夹,点击rename即可重命名 + +![勾选文件夹,点击rename重命名](D:/folder/test/output/20a9b5fc-69c2-4067-9a79-ee66f374a38a.png) + +最常用的是点击右边的New-Python3,创建python文档。 + +# 用Jupyter notebook写python代码 + +## 写下并运行第一行python代码 + +点击左上角`Untitled`给新建的python文档文件重新命名。 + +![重命名](D:/folder/test/output/e0acc191-e3d5-4dfb-8de2-a08ce82990ca.png) + +在代码框中输入第一行python代码,shift+回车运行 + +![输入第一行python代码](D:/folder/test/output/404bfeeb-88d2-4bf7-b1d5-37367d11e301.png) + +![运行第一行python代码](D:/folder/test/output/59f5a60d-74e4-4167-9198-260821273989.png) + + + +## 蓝绿两模式:命令模式、编辑模式 + +Jupyter notebook中,代码和文档都存在于一个个单元格中,每个单元格都有蓝色和绿色两种状态。 + +- 命令模式(蓝色):用于执行键盘输入的快捷命令(新增单元格、剪切、复制等等)。通过 `Esc` 键从绿色的编辑模式切换到蓝色的命令模式,此时单元左侧显示蓝色竖线。 + +- 编辑模式(绿色):编辑文本和代码。选中单元并按 `Enter` 键进入编辑模式,此时单元左侧显示绿色竖线。 + +> 命令模式和编辑模式,其实是源自于著名的vim编辑器,vim编辑器以特别难学和学成之后可以超神而闻名于世。 + +## 两种单元格:代码单元格和Markdown单元格 + +Jupyter notebook中,有两种单元格:代码单元格和Markdown单元格。 + +- 代码单元格:这里是你编写代码的地方,通过按 `Shift + Enter` 运行代码,其结果显示在本单元下方。代码单元左边有 `In [1]:` 这样的序列标记,方便人们查看代码的执行次序。在蓝色命令模式下,按`y`键可以将Markdown单元格转换为代码单元格。 +- Markdown 单元格:在这里对文本进行编辑,采用 markdown 的语法规范,可以设置文本格式、插入链接、图片甚至数学公式。同样使用 `Shift + Enter` 运行 markdown 单元来显示渲染后的文本。在蓝色命令模式下按`m`键可以将代码单元格转换为Markdown单元格。 + +> Markdown是程序员通用的撰写文档的语法,可以轻松实现标题、引用、链接、图片等,非常简洁易学,Github代码托管网站、有道云笔记、简书、知乎、CSDN论坛、电子邮件等都支持Markdown语法。 +> +> 学习Markdown,推荐我制作的博客和视频教程: +> +> [二十分钟精通排版神器Markdown,从此word和秀米是路人]()
+> +> [Bilibili视频:二十分钟精通排版神器Markdown]()
+ +编辑Markdown单元格,输入以下内容: + +```markdown +# 我是Markdown一号标题 +## 我是Markdown二号标题 +### 我是Markdown三号标题 +>我是引用,我这行开头有一个灰色竖杠 + +[我是外部链接,点我上百度](www.baidu.com) +![我是图片](D:/folder/test/output/dea28a7f-ecf7-4e6f-b9ac-6a86db641f26.webp) +``` + +![编辑Markdown单元格](D:/folder/test/output/817d3d27-0383-4d08-be35-0bf59f20847f.png) + +然后按`shift`+`Enter`运行该单元格。 + +![Markdown渲染效果](D:/folder/test/output/40503d86-d643-4568-bf43-97ef206e7cbe.png) + + + +# 抛弃鼠标,只用键盘 + +下面介绍Jupyter notebook快捷键,掌握这些快捷键之后,你将彻底解放你拿鼠标的那只手,更专注、高效地敲代码了。 + +## 最常用快捷键(必会) + +h 查看所有快捷键 + +Enter 从命令模式进入编辑模式 + +Esc 从编辑模式退回到命令模式 + +m 将代码单元格转换为Markdown单元格 + +y 将Markdown单元格转换为代码单元格 + +shift+Enter 运行本单元格,选择下面的代码块 + +ctrl+Enter 运行本单元格 + +alt+Enter 运行本单元格,在下方新建一个单元格 + +a 在上方新建一个单元格(above) + +b 在下方新建一个单元格(below) + +d 删除选中的单元格(delete) + +x 剪切本单元格 + +c 复制本单元格 + +shift v 粘贴到上面 + +v 粘贴到下面 + +l 显示代码行号 + + + +## 所有快捷键 + +h 查看所有快捷键 + +![命令模式快捷键](D:/folder/test/output/de233993-56fb-4eb0-9bae-49a90e08e822.png) + +![编辑模式快捷键](D:/folder/test/output/1745af46-3cf9-41b7-b092-889bf97965e2.png) + +## 在Markdown单元格中输入数学公式 + +分别在两个Markdown单元格内输入以下内容: + +```latex +这是爱因斯坦的质能转换方程$E=mc^2$,揭示了质量和能量之间的关系 +``` + +```latex +这是一元二次方程求解公式 +$$x = \frac{-b\pm \sqrt{b^2-4ac}}{2a}$$ +初中数学内容 +``` + +![在Markdown单元格中输入Latex公式](D:/folder/test/output/1830a73c-3974-4290-b04d-1863b14ae967.png) + +按`shift`+`Enter`渲染运行: + +![渲染之后的Latex数学公式](D:/folder/test/output/12d7f799-0937-4811-8914-1ac822a39e0f.png) + +# 数据分析与可视化实战案例:学习时间与成绩的关系(线性回归) + +先用excel把玩数据 + +![excel中的线性回归](D:/folder/test/output/750531ac-b3b6-440f-8fab-1863ca83ebd3.png) + +观察数据、导入数据、划分特征和标签、划分训练集和测试集、构建模型,模型可视化 + +# 用Jupyter notebook制作ppt并在线分享 + + + +# 参考博客 + +[左手代码,右手写作:你必须会的Jupyter Notebook]()
+ +[二十分钟精通排版神器Markdown,从此word和秀米是路人]()
+ +[Bilibili视频:二十分钟精通排版神器Markdown]()
+ diff --git a/Java/Mysql数据库.md b/Java/Mysql数据库.md new file mode 100644 index 0000000..c5133c3 --- /dev/null +++ b/Java/Mysql数据库.md @@ -0,0 +1,817 @@ +# Mysql数据库 + +## 安装启动Mysql + +### 启动Mysql + +``` +net start mysql // 启动mysql服务 +net stop mysql // 停止mysql服务 +``` + +### 修改root账户密码 + +``` +mysqladmin -u root password 123456 +``` + +### 登录 + +``` +mysql -u用户名 -p密码 [-h数据库服务器的IP地址 -P端口号] +``` + +``` +mysql -uroot -p123456 +``` + +-h 参数不加,默认连接的是本地 127.0.0.1 的MySQL服务器 + +-P 参数不加,默认连接的端口号是 3306 + +## Mysql简介 + +### 通用语法 + +1、SQL语句可以单行或多行书写,以分号结尾。 + +2、SQL语句可以使用空格/缩进来增强语句的可读性。 + +3、MySQL数据库的SQL语句不区分大小写。 + +4、注释: + +- 单行注释:-- 注释内容 或 # 注释内容(MySQL特有) +- 多行注释: /* 注释内容 */ + +### 分类 + +| **分类** | **全称** | **说明** | +| -------- | --------------------------- | ------------------------------------------------------ | +| DDL | Data Definition Language | 数据定义语言,用来定义数据库对象(数据库,表,字段) | +| DML | Data Manipulation Language | 数据操作语言,用来对数据库表中的数据进行增删改 | +| DQL | Data Query Language | 数据查询语言,用来查询数据库中表的记录 | +| DCL | Data Control Language | 数据控制语言,用来创建数据库用户、控制数据库的访问权限 | + +### 数据类型 + +char:声明的字段如果数据类型为char,则该字段占据的长度固定为声明时的值,例如:char(4),存入值 'ab',其长度仍为4. + +varchar:声明字段时,字段占据的实际长度等于存储内容的实际长度+记录长度的字节(一般是一个字节或者两个字节)例如:varchar(100)表示可以存100,但是存储值'ab'时,占用长度是3字节。 + +日期时间类型: + +| 类型 | 大小 | 范围 | 格式 | 描述 | +| -------- | ---- | ------------------------------------------ | ------------------- | ---------------- | +| DATE | 3 | 1000-01-01 至 9999-12-31 | YYYY-MM-DD | 日期值 | +| TIME | 3 | -838:59:59 至 838:59:59 | HH:MM:SS | 时间值或持续时间 | +| DATETIME | 8 | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 | + +**字符串和日期型数据都应包含在引号中** + +## DDL + +### 数据库操作 + +**查询所有数据库:** + +```mysql +show databases; +``` + +**创建一个itcast数据库。** + +```mysql +create database itcast; +``` + +**切换到itcast数据** + +```mysql +use itcast; +``` + +**查询当前使用的数据库:** + +```mysql +select database(); +``` + +**删除itcast数据库** + +~~~mysql +drop database if exists itcast; -- itcast数据库存在时删除,不存在也不报错 +~~~ + +### 表操作 + +**查询当前数据库下所有表** + +``` +show tables; +``` + +**查看指定表的结构(字段)** + +``` +desc tb_tmps; ( tb_tmps为表名) +``` + +**创建表** + +```mysql +create table 表名( + 字段1 字段1类型 [约束] [comment 字段1注释 ], + 字段2 字段2类型 [约束] [comment 字段2注释 ], + ...... + 字段n 字段n类型 [约束] [comment 字段n注释 ] +) [ comment 表注释 ] ; +``` + +> 注意: [ ] 中的内容为可选参数; 最后一个字段后面没有逗号 + +eg: + +![image-20220829143005524](D:/folder/test/output/11ea44e4-304a-45ab-bb7b-4dcd6edeefa5.png) + +``` +create table tb_user ( + id int comment 'ID,唯一标识', # id是一行数据的唯一标识(不能重复) + username varchar(20) comment '用户名', + name varchar(10) comment '姓名', + age int comment '年龄', + gender char(1) comment '性别' +) comment '用户表'; +``` + +#### 约束 + +| **约束** | **描述** | **关键字** | +| ------------ | ------------------------------------------------ | ----------- | +| 非空约束 | 限制该字段值不能为null | not null | +| 唯一约束 | 保证字段的所有数据都是唯一、不重复的 | unique | +| 主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | primary key | +| 默认约束 | 保存数据时,如果未指定该字段值,则采用默认值 | default | +| **外键约束** | 让两张表的数据建立连接,保证数据的一致性和完整性 | foreign key | + +``` +create table tb_user ( + id int primary key auto_increment comment 'ID,唯一标识', + username varchar(20) not null unique comment '用户名', + name varchar(10) not null comment '姓名', + age int comment '年龄', + gender char(1) default '男' comment '性别' +) comment '用户表'; +``` + +auto_increment: + +- 每次插入新的行记录时,数据库自动生成id字段(主键)下的值 +- 具有auto_increment的数据列是一个正数序列开始增长(从1开始自增) + +**设计表的字段时,还应考虑:** + +id:主键,唯一标志这条记录 + +create_time :插入记录的时间 now()函数可以获取当前时间 +update_time:最后修改记录的时间 + +## DML(增删改) + +DML英文全称是Data Manipulation Language(数据操作语言),用来对数据库中表的数据记录进行增、删、改操作。 + +- 添加数据(INSERT) +- 修改数据(UPDATE) +- 删除数据(DELETE) + +### INSERT + +insert语法: + +- 向**指定**字段添加数据 + + ~~~mysql + insert into 表名 (字段名1, 字段名2) values (值1, 值2); + ~~~ + +- 全部字段添加数据 + + ~~~mysql + insert into 表名 values (值1, 值2, ...); + ~~~ + +- 批量添加数据(指定字段) + + ~~~mysql + insert into 表名 (字段名1, 字段名2) values (值1, 值2), (值1, 值2); + ~~~ + +- 批量添加数据(全部字段) + + ~~~mysql + insert into 表名 values (值1, 值2, ...), (值1, 值2, ...); + ~~~ + +### UPDATE + +update语法: + +```sql +update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , .... [where 条件] ; +``` + +案例1:将tb_emp表中id为1的员工,姓名name字段更新为'张三' + +```sql +update tb_emp set name='张三',update_time=now() where id=1; +``` + +案例2:将tb_emp表的所有员工入职日期更新为'2010-01-01' + +```sql +update tb_emp set entrydate='2010-01-01',update_time=now(); +``` + +**注意!**不带where会更新表中所有记录! + +### DELETE + +delete语法: + +```SQL +delete from 表名 [where 条件] ; +``` + +案例1:删除tb_emp表中id为1的员工 + +```sql +delete from tb_emp where id = 1; +``` + +案例2:删除tb_emp表中所有员工(记录) + +```sql +delete from tb_emp; +``` + +DELETE 语句不能删除某一个字段的值(可以使用UPDATE,将该字段值置为NULL即可)。 + +## DQL(查询) + +DQL英文全称是Data Query Language(数据查询语言),用来查询数据库表中的记录。 + +查询关键字:SELECT + +查询操作是所有SQL语句当中最为常见,也是最为重要的操作。 + +### 语法 + +``` +SELECT + 字段列表 +FROM + 表名列表 ----基本查询 +WHERE + 条件列表 ----条件查询 +GROUP BY + 分组字段列表 +HAVING + 分组后条件列表 ----分组查询 +ORDER BY + 排序字段列表 ----排序查询 +LIMIT + 分页参数 ----分页查询 +``` + +### 基本查询 + +- 查询多个字段 + + ~~~mysql + select 字段1, 字段2, 字段3 from 表名; + ~~~ + +- 查询所有字段(通配符) + + ~~~mysql + select * from 表名; + ~~~ + +- 设置别名 + + ~~~mysql + select 字段1 [ as 别名1 ] , 字段2 [ as 别名2 ] from 表名; + ~~~ + +- 去除重复记录 + + ~~~mysql + select distinct 字段列表 from 表名; + eg:select distinct job from tb_emp; + ~~~ + +### 条件查询 + +| **比较运算符** | **功能** | +| -------------------- | ---------------------------------------- | +| between ... and ... | 在某个范围之内(含最小、最大值) | +| in(...) | 在in之后的列表中的值,多选一 | +| like 占位符 | 模糊匹配(_匹配单个字符, %匹配任意个字符) | +| is null | 是null | +| = | 等于 | + +| **逻辑运算符** | **功能** | +| -------------- | --------------------------- | +| and 或 && | 并且 (多个条件同时成立) | +| or 或 \|\| | 或者 (多个条件任意一个成立) | +| not 或 ! | 非 , 不是 | + +案例:查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息 + +``` +select * +from tb_emp +where entrydate between '2000-01-01' and '2010-01-01' + and gender = 2; +``` + +案例8:查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息 + +``` +select * +from tb_emp +where job in (2,3,4); +``` + +案例9:查询 姓名 为两个字的员工信息 + +~~~mysql +select * +from tb_emp +where name like '__'; # 通配符 "_" 代表任意1个字符 +~~~ + + + +### 聚合函数 + +之前我们做的查询都是横向查询,就是根据条件一行一行的进行判断,而使用聚合函数查询就是**纵向查询**,它是对一列的值进行计算,然后返回一个结果值。(将一列数据作为一个整体,进行纵向计算) + +语法: + +~~~mysql +select 聚合函数(字段列表) from 表名 ; +~~~ + +> 注意 : 聚合函数会忽略空值,对NULL值不作为统计。 + +``` +# count(*) 推荐此写法(MySQL底层进行了优化) +select count(*) from tb_emp; +``` + + + +### 分组查询 + +分组: 按照某一列或者某几列,把相同的数据进行合并输出。 + +> 分组其实就是按列进行分类(指定列下相同的数据归为一类),然后可以对分类完的数据进行合并计算。 +> +> 分组查询通常会使用**聚合函数**进行计算。 + +``` +select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤条件]; +``` + +例如,假设我们有一个名为 `orders` 的表,其中包含 `customer_id` 和 `amount` 列,我们想要计算每个客户的订单总金额,可以这样写查询: + +``` +SELECT customer_id, SUM(amount) AS total_amount +FROM orders +GROUP BY customer_id; +``` + +在这个例子中,`GROUP BY customer_id` 将结果按照 `customer_id` 列的值进行分组,并对每个客户的订单金额求和,生成每个客户的总金额。 + + + +``` +SELECT customer_id, SUM(amount) AS total_amount +FROM orders +GROUP BY customer_id +HAVING total_amount > specified_amount; +``` + +在这个查询中,`HAVING` 子句用于筛选出消费金额(`total_amount`)大于指定数目(`specified_amount`)的记录。你需要将 `specified_amount` 替换为你指定的金额数目。 + + + +**注意事项:** + +​ • 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义 + +​ • 执行顺序:where > 聚合函数 > having + +### 排序查询 + +语法: + +```mysql +select 字段列表 +from 表名 +[where 条件列表] +[group by 分组字段 ] +order by 字段1 排序方式1 , 字段2 排序方式2 … ; +``` + +- 排序方式: + + - ASC :升序(默认值) + + - DESC:降序 + +### 分页查询 + +``` +select 字段列表 from 表名 limit 起始索引, 每页显示记录数 ; +``` + +前端传过来的一般是页码,要计算起始索引 + +注意事项: + +1. 起始索引从0开始。 计算公式 : 起始索引 = (查询页码 - 1)* 每页显示记录数 + +2. 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT + +3. 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 条数 + +## 多表设计 + +### 外键约束 + +外键约束的语法: + +```mysql +-- 创建表时指定 +create table 表名( + 字段名 数据类型, + ... + [constraint] [外键名称] foreign key (外键字段名) references 主表 (主键名) +); + + +-- 建完表后,添加外键 +alter table 表名 add constraint 外键名称 foreign key(外键字段名) references 主表(主表列名); +``` + + + +### 一对多 + +![image-20221206230156403](D:/folder/test/output/cd36d561-606c-408f-9d2a-d975941a3e1d.png) + +**一对多关系实现:在数据库表中多的一方,添加外键字段,来关联'一'这方的主键。** + + + +### 一对一 + +一对一关系表在实际开发中应用起来比较简单,通常是用来做单表的拆分。一对一的应用场景: 用户表=》基本信息表+身份信息表 + +- 基本信息:用户的ID、姓名、性别、手机号、学历 +- 身份信息:民族、生日、身份证号、身份证签发机关,身份证的有效期(开始时间、结束时间) + +**一对一 :在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)** + + + +### 多对多 + +多对多的关系在开发中属于也比较常见的。比如:学生和老师的关系,一个学生可以有多个授课老师,一个授课老师也可以有多个学生。 + +案例:学生与课程的关系 + +- 关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择 + +- 实现关系:建立第三张中间表(选课表),中间表至少包含两个外键,分别关联两方主键 + + + +## 多表查询 + +### 分类 + +多表查询可以分为: + +1. 连接查询 + + - 内连接:相当于查询A、B交集部分数据 + + ![image-20221207165446062](D:/folder/test/output/6dad0071-1bec-406c-8f3a-9c20cc3843a7.png) + +2. 外连接 + + - 左外连接:查询左表所有数据(包括两张表交集部分数据) + + - 右外连接:查询右表所有数据(包括两张表交集部分数据) + +3. 子查询 + +### 内连接 + +隐式内连接语法: + +``` mysql +select 字段列表 from 表1 , 表2 where 条件 ... ; +``` + +显式内连接语法: + +``` mysql +select 字段列表 from 表1 [ inner ] join 表2 on 连接条件 ... ; +``` + +[inner]可省略 + +案例:查询员工的姓名及所属的部门名称 + +- 隐式内连接实现 + +~~~mysql +select tb_emp.name , tb_dept.name -- 分别查询两张表中的数据 +from tb_emp , tb_dept -- 关联两张表 +where tb_emp.dept_id = tb_dept.id; -- 消除笛卡尔积 +~~~ + +- 显示内连接 + +``` +select tb_emp.name , tb_dept.name +from tb_emp inner join tb_dept +on tb_emp.dept_id = tb_dept.id; +``` + + + +### 外连接 + +左外连接语法结构: + +```mysql +select 字段列表 from 表1 left [ outer ] join 表2 on 连接条件 ... ; +``` + +> 左外连接相当于查询表1(左表)的所有数据,当然也包含表1和表2交集部分的数据。 + +右外连接语法结构: + +```mysql +select 字段列表 from 表1 right [ outer ] join 表2 on 连接条件 ... ; +``` + + + +``` +-- 右外连接 +select dept.name , emp.name +from tb_emp AS emp right join tb_dept AS dept + on emp.dept_id = dept.id; +``` + +![image-20240306190305575](D:/folder/test/output/c78ba59b-309e-4a13-819d-0f37e72c6f0c.png) + +### 子查询 + +SQL语句中嵌套select语句,称为嵌套查询,又称子查询。 + +```sql +SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2 ... ); +``` + +> 子查询外部的语句可以是insert / update / delete / select 的任何一个,最常见的是 select。 + +#### 标量子查询 + +子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询称为标量子查询。 + +常用的操作符: = <> > >= < <= + +案例1:查询"教研部"的所有员工信息 + +> 可以将需求分解为两步: +> +> 1. 查询 "教研部" 部门ID +> 2. 根据 "教研部" 部门ID,查询员工信息 + +```mysql +-- 1.查询"教研部"部门ID +select id from tb_dept where name = '教研部'; #查询结果:2 +-- 2.根据"教研部"部门ID, 查询员工信息 +select * from tb_emp where dept_id = 2; + +-- 合并出上两条SQL语句 +select * from tb_emp where dept_id = (select id from tb_dept where name = '教研部'); +``` + +#### 列子查询 + +子查询返回的结果是**一列(可以是多行,即多条记录)**,这种子查询称为列子查询。 + +常用的操作符: + +| **操作符** | **描述** | +| ---------- | ---------------------------- | +| IN | 在指定的集合范围之内,多选一 | +| NOT IN | 不在指定的集合范围之内 | + +案例:查询"教研部"和"咨询部"的所有员工信息 + +> 分解为以下两步: +> +> 1. 查询 "销售部" 和 "市场部" 的部门ID +> 2. 根据部门ID, 查询员工信息 + +```mysql +-- 1.查询"销售部"和"市场部"的部门ID +select id from tb_dept where name = '教研部' or name = '咨询部'; #查询结果:3,2 +-- 2.根据部门ID, 查询员工信息 +select * from tb_emp where dept_id in (3,2); + +-- 合并以上两条SQL语句 +select * from tb_emp where dept_id in (select id from tb_dept where name = '教研部' or name = '咨询部'); +``` + +#### 行子查询 + +子查询返回的结果是**一行(可以是多列,即多字段)**,这种子查询称为行子查询。 + +常用的操作符:= 、<> 、IN 、NOT IN + +案例:查询与"韦一笑"的入职日期及职位都相同的员工信息 + +> 可以拆解为两步进行: +> +> 1. 查询 "韦一笑" 的入职日期 及 职位 +> 2. 查询与"韦一笑"的入职日期及职位相同的员工信息 + +```mysql +-- 查询"韦一笑"的入职日期 及 职位 +select entrydate , job from tb_emp where name = '韦一笑'; #查询结果: 2007-01-01 , 2 +-- 查询与"韦一笑"的入职日期及职位相同的员工信息 +select * from tb_emp where (entrydate,job) = ('2007-01-01',2); + +-- 合并以上两条SQL语句 +select * from tb_emp where (entrydate,job) = (select entrydate , job from tb_emp where name = '韦一笑'); +``` + +#### 表子查询 + +子查询返回的结果是多行多列,常作为临时表,这种子查询称为表子查询。 + +案例:查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息 + +> 分解为两步执行: +> +> 1. 查询入职日期是 "2006-01-01" 之后的员工信息 +> 2. 基于查询到的员工信息,在查询对应的部门信息 + +~~~mysql +select * from emp where entrydate > '2006-01-01'; + +select e.*, d.* from (select * from emp where entrydate > '2006-01-01') e left join dept d on e.dept_id = d.id ; +~~~ + +## 事务 + +简而言之:事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 + +手动提交事务使用步骤: + +- 第1种情况:开启事务 => 执行SQL语句 => 成功 => 提交事务 +- 第2种情况:开启事务 => 执行SQL语句 => 失败 => 回滚事务 + +` + +``` +-- 开启事务 +start transaction ; + +-- 删除学工部 +delete from tb_dept where id = 1; + +-- 删除学工部的员工 +delete from tb_emp where dept_id = 1; +``` + +` + +- 上述的这组SQL语句,如果如果执行成功,则提交事务 + +```sql +-- 提交事务 (成功时执行) +commit ; +``` + +- 上述的这组SQL语句,如果如果执行失败,则回滚事务 + +```sql +-- 回滚事务 (出错时执行) +rollback ; +``` + +面试题:事务有哪些特性? + +- 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。 +- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。(部门和该部门下的员工数据全部删除) +- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行(事务还没commit,那么别的窗口就看不到该修改 )。 +- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。 + +> 事务的四大特性简称为:ACID + +## 索引 + +索引(index):是帮助数据库高效获取数据的数据结构 。 + +**创建索引** + +``` +-- 添加索引 +create index idx_sku_sn on tb_sku (sn); #在添加索引时,也需要消耗时间 + +-- 查询数据(使用了索引) +select * from tb_sku where sn = '100000003145008'; +``` + +**查看索引** + +~~~mysql +show index from 表名; +~~~ + +案例:查询 tb_emp 表的索引信息 + +~~~mysql +show index from tb_emp; +~~~ + +**删除索引** + +~~~mysql +drop index 索引名 on 表名; +~~~ + +案例:删除 tb_emp 表中name字段的索引 + +~~~mysql +drop index idx_emp_name on tb_emp; +~~~ + +优点: + +1. 提高数据查询的效率,降低数据库的IO成本。 +2. 通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗。 + +缺点: + +1. 索引会占用存储空间。 +2. 索引大大提高了查询效率,同时却也降低了insert、update、delete的效率。 + +因为插入一条数据,要重新维护索引结构 + + + +**注意事项:** + +- 主键字段,在建表时,会自动创建主键索引 (primarily key) + +- 添加唯一约束时,数据库实际上会添加唯一索引 (unique约束) + +### 结构 + +musql默认采用B+树来作索引 + +采用二叉搜索树或者是红黑树来作为索引的结构有什么问题? + +
+ 答案 + 最大的问题就是在数据量大的情况下,树的层级比较深,会影响检索速度。因为不管是二叉搜索数还是红黑数,一个节点下面只能有两个子节点。此时在数据量大的情况下,就会造成数的高度比较高,树的高度一旦高了,检索速度就会降低。 +
+ + + +> 说明:如果数据结构是红黑树,那么查询1000万条数据,根据计算树的高度大概是23左右,这样确实比之前的方式快了很多,但是如果高并发访问,那么一个用户有可能需要23次磁盘IO,那么100万用户,那么会造成效率极其低下。所以为了减少红黑树的高度,那么就得增加树的宽度,就是不再像红黑树一样每个节点只能保存一个数据,可以引入另外一种数据结构,一个节点可以保存多个数据,这样宽度就会增加从而降低树的高度。这种数据结构例如BTree就满足。 + +下面我们来看看B+Tree(多路平衡搜索树)结构中如何避免这个问题: + +![image-20221208181315728](D:/folder/test/output/6b80d339-af26-4000-879d-91ef6460b60e.png) + +B+Tree结构: + +- 每一个节点,可以存储多个key(有n个key,就有n个指针) +- 节点分为:叶子节点、非叶子节点 + - 叶子节点,就是最后一层子节点,所有的数据都存储在叶子节点上 + - 非叶子节点,不是树结构最下面的节点,用于索引数据,存储的的是:key+指针 +- 为了提高范围查询效率,叶子节点形成了一个双向链表,便于数据的排序及区间范围查询 + diff --git a/Java/Redis.md b/Java/Redis.md new file mode 100644 index 0000000..dad8cc7 --- /dev/null +++ b/Java/Redis.md @@ -0,0 +1,109 @@ +# Redis + +Redis是一个基于**内存**的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的**存储中间件**。 + +Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。它存储的value类型比较丰富,也被称为结构化的**NoSql数据库**(非关系型)。 + +**主要特点:** + +- 基于内存存储,读写性能高 +- 适合存储热点数据(热点商品、资讯、新闻) +- 企业应用广泛 + +**服务启动/停止:** + +redis-server.exe redis.windows.conf + +ctrl+c + +**客户端连接:** + +redis-cli.exe (本地) + +redis-cli.exe -h localhost -p 6379 -a 123456 (指定连接) + +exit + +## Redis数据类型 + +image-20221130190150749 + +**解释说明:** + +- 字符串(string):普通字符串,Redis中最简单的数据类型 +- 哈希(hash):也叫散列,类似于Java中的HashMap结构。(套娃) +- 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList +- 集合(set):无序集合,没有重复元素,类似于Java中的HashSet(求交集) +- 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素(排行榜) + + + +## Redis常用命令 + +### 字符串 + +Redis 中字符串类型常用命令: + +- **SET** key value 设置指定key的值 +- **GET** key 获取指定key的值 +- **SETEX** key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒(验证码) +- **SETNX** key value 只有在 key 不存在时设置 key 的值 + +### 哈希操作 + +Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令: + +- **HSET** key field value 将哈希表 key 中的字段 field 的值设为 value +- **HGET** key field 获取存储在哈希表中指定字段的值 +- **HDEL** key field 删除存储在哈希表中的指定字段 +- **HKEYS** key 获取哈希表中所有字段 +- **HVALS** key 获取哈希表中所有值 + +image-20221130193121969 + +### 列表操作 + +Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令: + +- **LPUSH** key value1 [value2] 将一个或多个值插入到列表头部 +- **LRANGE** key start stop 获取列表指定范围内的元素 +- **RPOP** key 移除并获取列表最后一个元素 +- **LLEN** key 获取列表长度 +- **BRPOP** key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止 + +image-20221130193332666 + +### 集合操作 + +Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令: + +- **SADD** key member1 [member2] 向集合添加一个或多个成员 +- **SMEMBERS** key 返回集合中的所有成员 +- **SCARD** key 获取集合的成员数 +- **SINTER** key1 [key2] 返回给定所有集合的交集 +- **SUNION** key1 [key2] 返回所有给定集合的并集 +- **SREM** key member1 [member2] 移除集合中一个或多个成员 + +image-20221130193532735 + +### 有序集合 + +Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令: + +常用命令: + +- **ZADD** key score1 member1 [score2 member2] 向有序集合添加一个或多个成员(score代表分数) +- **ZRANGE** key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员 +- **ZINCRBY** key increment member 有序集合中对指定成员的分数加上**增量** increment +- **ZREM** key member [member ...] 移除有序集合中的一个或多个成员 + +image-20221130193951036 + +### 通用命令 + +Redis的通用命令是不分数据类型的,都可以使用的命令: + +- KEYS pattern 查找所有符合给定模式( pattern)的 key +- EXISTS key 检查给定 key 是否存在 +- TYPE key 返回 key 所储存的值的类型 +- DEL key 该命令用于在 key 存在是删除 key \ No newline at end of file diff --git a/Java/Untitled.md b/Java/Untitled.md new file mode 100644 index 0000000..e69de29 diff --git a/Java/anaconda基础命令.md b/Java/anaconda基础命令.md new file mode 100644 index 0000000..6024ce2 --- /dev/null +++ b/Java/anaconda基础命令.md @@ -0,0 +1,107 @@ +## anaconda基础命令 + + + +cuda版本 12.3.1 驱动版本 546.26 + +打开anaconda prompt(普通命令行cmd也可以): +conda -V 查看版本 +conda env list 查看已安装的环境 *代表当前环境 +conda list 列出当前环境所安装的包 +conda updata +conda create -n 新环境名字 python=3.7 (若只有python则下载最新版python) +conda activate 新环境名字 可以切换环境 +conda deactivate 退出环境到base +conda search numpy 可以查看numpy有哪些版本 +conda install numpy 可以指定版本,默认最新版 +也可以pip install -r requirements.txt (激活环境后) +conda remove numpy 删除numpy以及所有依赖关系的包 +conda remove -n 新环境名字 --all 删除创建的环境(先deactivate退出) + +结合pycharm:可以先在conda中创建好虚拟环境,然后在pycharm中使用exsiting conda环境。 +project interpreter,show all可以看到pycharm记住的所有解释器,也可以右边加号添加 + +查看激活的环境的python版本: python --version + + + + + + + +这是添加conda镜像 + +conda config --add channels http://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ +conda config --add channels http://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ +conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free +conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge +conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/mysys2/ +conda config --set show_channel_urls yes + + + +这是添加Pypi镜像,适用于pip安装 + +清华:https://pypi.tuna.tsinghua.edu.cn/simple + +阿里云:https://mirrors.aliyun.com/pypi/simple/ + +中国科技大学: https://pypi.mirrors.ustc.edu.cn/simple/ + +华中理工大学:https://pypi.hustunique.com/ + +山东理工大学:https://pypi.sdutlinux.org/ + +豆瓣:https://pypi.douban.com/simple/ + + + +pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + + + +在conda中导出pip requirements.txt: + +pip freeze > requirements.txt + + + +## Conda环境与Pycharm环境的大坑 + +如果你的pycharm中使用conda环境,那么你在pycharm的终端中所用的可能不是conda环境! + +![image-20240729155102345](D:/folder/test/output/dd0e8c0c-08f2-4202-b137-5031b62ecb3c.png) + +解决办法: shell path改为 cmd.exe +这样虚拟环境就默认设置为conda环境了 + +![image-20240729161200801](D:/folder/test/output/2949e4bf-056c-483b-a74e-79c95a894a77.png) + +![image-20240729161242191](D:/folder/test/output/51ab0afc-4c62-418f-b90f-58816bb0aa6a.png) + + + + + +如果命令行cd到项目根目录,所用的也并不是conda环境!这里用的是conda的默认环境? + +![image-20240729155148576](D:/folder/test/output/e9508c58-d415-4af5-a8e4-c6dc343d01ed.png) + + + +正确方法: + +1.使用anaconda prompt打开 + +2.conda activate env 激活环境 + +3.cd到项目根目录 + +4.输入命令 + +![image-20240729155310049](D:/folder/test/output/3245d28c-a993-4896-b419-feb7ed443f88.png) + + + + + diff --git a/Java/docker/Docker指南.md b/Java/docker/Docker指南.md new file mode 100644 index 0000000..a361787 --- /dev/null +++ b/Java/docker/Docker指南.md @@ -0,0 +1,1269 @@ +## Docker + +### docker基础知识 + +#### 镜像和容器 + +Docker中有几个重要的概念: + +**镜像(Image)**:Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像,是只读的。 + +**容器(Container)**:镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器进程做隔离,对外不可见。因此一个镜像可以启动多次,形成多个容器进程。 + +一切应用最终都是代码组成,都是硬盘中的一个个的字节形成的**文件**。只有运行时,才会加载到内存,形成进程。 + + + +Docker为了解决依赖的兼容问题的,采用了两个手段: + +- 将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包 + +- 将每个应用放到一个隔离**容器**去运行,避免互相干扰 + +image-20210731142219735 + +这样打包好的应用包中,既包含应用本身,也保护应用所需要的Libs、Deps,无需在操作系统上安装这些,自然就不存在不同应用之间的兼容问题了。 + + + +#### DockerHub + +开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像,例如Redis、MySQL镜像放到网络上,共享使用,就像GitHub的代码共享一样。 + +- DockerHub:DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。 + +- 国内也有类似于DockerHub 的公开服务,比如 [网易云镜像服务](https://c.163yun.com/hub)、[阿里云镜像库](https://cr.console.aliyun.com/)等。 + +注意:很多国内的镜像现在也不能用了!需要换源! + + + +#### Docker架构 + +我们要使用Docker来操作镜像、容器,就必须要安装Docker。 + +Docker是一个CS架构的程序,由两部分组成: + +- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等 + +- 客户端(client):通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。 + +如图: + +![image-20210731154257653](D:/folder/test/output/5d572263-851b-41b2-be45-ee94c76afe48.png) + + + +#### 镜像操作 + +![image-20210731155649535](D:/folder/test/output/4771e3c2-fc4c-474a-91f1-c605599aa377.png) + +1. docker push,将本地镜像上传到远程仓库(例如 Docker Hub) + +``` +docker login #docker hub登录 +# 假设已有本地镜像 myimage,需要先打上标签: +docker tag myimage yourusername/myimage:latest +# 上传镜像到远程仓库: +docker push yourusername/myimage:latest +``` + +2. docker pull ,从远程仓库拉取镜像到本地。 + +``` +docker pull yourusername/myimage:latest +``` + +3. docker save,将本地镜像保存为 tar 文件,方便备份或传输 + +``` +docker save -o myimage.tar yourusername/myimage:latest +``` + +4. docker load,从 tar 文件中加载镜像到本地 Docker。 + +``` +docker load -i myimage.tar +``` + +5. docker images ,查看本地镜像 + +``` +docker images +``` + +6. docker build ,构建镜像 -t后面跟镜像名 + +``` +docker build -t yourusername/myimage:latest . +``` + + + +#### 容器操作 + +![image-20210731161950495](D:/folder/test/output/ba96cf38-81f0-4705-8395-21f47c73ae40.png) + +1. docker run 创建并运行一个新容器 + +​ -d:以后台模式运行容器,不会占用当前终端。 + +​ --name <容器名> :为容器指定一个自定义名称,便于后续管理。 + +​ -p <宿主机端口>:<容器端口> : 将容器内部的端口映射到宿主机,使外部可以访问容器提供的服务。 + +​ --restart <策略> :设置容器的重启策略,如 `no`(默认不重启)、`on-failure`(失败时重启)、`always`(总是重启)或 `unless-stopped`。 + +​ -v <宿主机目录>:<容器目录>` 或 `--volume : 如 -v /host/data:/app/data + +``` +docker run --name test-container -d test:latest +``` + +2. docker exec 在正在运行的 test-container 内执行命令 + +​ -it : 给当前进入的容器创建一个标准输入、输出终端 + +``` +docker exec -it test-container sh +``` + +3. docker logs ,查看 test-container 的日志输出: + +``` +docker logs --since 1h test-container #查看最近1h +``` + +4. docker stop 停止正在运行的 test-container: + +``` +docker stop test-container +``` + +5. docker start 启动一个已停止的 test-container: + +``` +docker start test-container +``` + +6. docker cp 复制文件(或目录)到容器内部,先cd到文件所在目录 + +``` +docker cp localfile.txt test-container:/target_dir/ +``` + +7. docker stats ,查看docker中运行的所有容器的运行状态(CPU 内存占用) + +``` +docker stats +``` + +8. docker container ls,查看运行容器的创建时间、端口映射等 + +``` + docker container ls +``` + +9. docker ps 查看 Docker 容器的状态,默认情况下,它只显示正在运行的容器 + +``` +docker ps -a #查看所有容器,包括已经停止或启动失败的容器 +``` + + + +#### 数据卷操作 + +**数据卷(volume)**是一个虚拟目录,指向宿主机文件系统中的某个目录。 + +![image-20210731173541846](D:/folder/test/output/b6234372-df07-42e6-9fb0-fab8cc190c11.png) + +一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。 + +这样,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了 + +有两种挂载方式: + +**绑定挂载(Bind Mounts)更加推荐!** + +- **原理**:直接将宿主机上的一个目录或文件挂载到容器内。 + +- **特点** + + - 数据存储在宿主机上,容器可以直接访问宿主机的文件系统。 + - 适合需要在开发过程中频繁修改代码或数据共享的场景。 + - 依赖宿主机的目录结构,移植性较低。 + +- **示例**:将宿主机的 `/path/on/host` 挂载到容器内的 `/app/data`: + + ``` + docker run -v /path/on/host:/app/data your_image + ``` + +**命名卷(Docker Volumes)** + +- **原理**:由 Docker 管理的数据卷,存储在 Docker 的默认目录(通常在 `/var/lib/docker/volumes/`),或通过 Docker 卷插件存储到其他位置。 + +- **特点** + + - Docker 负责管理这些卷,提供更好的隔离和数据持久性。 + - 与宿主机的具体目录结构无关,便于迁移和备份。 + - 常用于生产环境中数据的持久化。 + +- **示例**:创建并挂载名为 `my_volume` 的卷到容器内的 `/app/data`: + + ``` + docker run -v my_volume:/app/data your_image + ``` + +创建命名卷 + +``` +docker volume create my_volume +``` + +查看命名卷,这将列出所有 Docker 管理的卷。 + +``` +docker volume ls +``` + +显示该命名卷的详细信息 + +``` +zy123@hcss-ecs-588d:~/zbparse$ sudo docker volume inspect html +[ + { + "CreatedAt": "2025-02-25T18:46:10+08:00", + "Driver": "local", + "Labels": null, + "Mountpoint": "/var/lib/docker/volumes/html/_data", + "Name": "html", + "Options": null, + "Scope": "local" + } +] +``` + +Mountpoint是宿主机上的路径,也就是 Docker 存储该数据卷数据的**实际位置** + + + +#### docker网络 + +Docker 网络的主要作用是实现容器之间的**通信和隔离**,同时也能控制容器与外部主机或网络的连接。通过创建自定义网络,你可以让属于同一网络的容器通过名称互相访问,而不必暴露所有服务到外部网络,这既提升了安全性又简化了容器间的交互。 + +**举例说明** + +假设你有两个容器,一个运行 MySQL 数据库,另一个运行 Web 应用程序。你希望 Web 应用能够通过数据库容器的别名来访问 MySQL,而不需要硬编码 IP 地址。 + +1.创建自定义网络 ,名为 `app-net` + +``` +docker network create app-net +``` + +2.启动 MySQL 容器,并加入 `app-net` 网络,同时为其指定别名 `db` + +``` +docker run -d --name mysql \ + --network app-net \ + --network-alias db \ + -e MYSQL_ROOT_PASSWORD=my-secret-pw \ + mysql:latest +``` + +**Docker CLI 命令**(如 `docker ps`、`docker exec`)**需要使用 `container_name` 来操作容器**。 + +但如果 **另一个容器需要访问它,不能直接用 `mysql`容器名,而要用 IP 地址或 `network-alias`**。 + +`--network-alias db`(网络别名),只在特定网络中生效 + +注意:不使用docker-compose就没有服务名,可以通过容器名,**网络别名**实现同网络的容器间通信 + + + + + +3.启动 Web 应用容器,加入同一个 `app-net` 网络 + +``` +docker run -d --name webapp \ + --network app-net \ + your_webapp_image:latest +``` + +4.验证容器间通信,进入 Web 应用容器,尝试通过别名 `db` 连接 MySQL: + +``` +docker exec -it webapp bash +# 在 webapp 容器内执行,比如使用 ping 测试网络连通性: +ping db +``` + +举个例子,如果你的 Java 应用运行在容器 B 中,而数据库容器 A 已经通过 `--network-alias db` 起了别名,那么在 Java 应用中,你只需要写: + +``` +String dbUrl = "jdbc:mysql://db:3306/your_database"; +``` + +而不必关心数据库容器的实际 IP 地址。 + +否则: + +``` +String dbUrl = "jdbc:mysql://<宿主机IP或localhost>:3306/your_database"; + +``` + +因为会通过**宿主机IP映射到容器内**的IP + +5.连接一个正在运行或已创建的容器到网络 + +接时可以使用 `--alias` 参数为容器在该网络中设置别名 + +``` +docker network connect app-net mysql --alias db +``` + +6.断开连接 + +``` +docker network disconnect app-net mysql +``` + +7.删除网络 + +需要注意的是,只有当网络中没有容器连接时才能删除。 + +``` +docker network rm app-net +docker network prune #删除所有未使用的网络 +``` + + + +### docker安装: + +**1.卸载旧版** + +首先如果系统中已经存在旧的Docker,则先卸载: + +```Shell +sudo apt-get remove docker docker-engine docker.io containerd runc +``` + +**说明**:该命令会删除系统中现有的 Docker 相关包,但不会删除 Docker 镜像、容器、卷等数据。如果需要彻底清理,可以手动删除相关目录。 + + + +**2.安装 Docker 依赖** + +在安装新版 Docker 前,需要更新 apt 源并安装一些依赖包,以便能够通过 HTTPS 协议访问 Docker 官方仓库。 + +``` +sudo apt-get update +sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release +``` + +**说明**:这些软件包包括用于 HTTPS 传输的支持库、CA 证书、curl 工具、GPG 密钥管理工具以及 Debian 版本识别工具。 + + + +**3.添加 Docker 官方 GPG 密钥** + +``` +curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +``` + +**说明**:此命令从 Docker 官方获取 GPG 密钥并保存为二进制格式,供后续验证软件包使用。 + + + +**4.设置 Docker 仓库** + +使用以下命令将 Docker 稳定版仓库添加到 apt 源列表中: + +这是最关键的一步,配置docker官方地址,往往**很难下载**!!! + +``` +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +``` + +推荐使用以下阿里云的镜像加速源 + +``` +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +``` + +cat /etc/apt/sources.list.d/docker.list 可以查看是否配置成功 + + + +**5.安装 Docker Engine** + +更新 apt 缓存后,安装最新版的 Docker Engine、CLI 工具和 containerd: + +``` +sudo apt update #如果是docker官方,这一步可能失败! +sudo apt install docker-ce docker-ce-cli containerd.io +``` + + + +**6.启动和校验** + +```Bash +# 启动Docker +systemctl start docker +# 停止Docker +systemctl stop docker +# 重启 +systemctl restart docker +# 设置开机自启 +systemctl enable docker +# 执行docker ps命令,如果不报错,说明安装启动成功 +docker ps +``` + +以上docker安装,最关键的就是**第4步**和**第5步** + + + +linux vim: + +**finalshell中粘贴会出现粘贴的文本一行比一行靠右,看起来乱成一团。比较快的解决办法是,在粘贴文档前,在命令行模式下,输入** +**:set paste** + +删除全文: 控制模式下 %d + + + +### docker配置代理 + +``` +sudo vim /etc/systemd/system/docker.service.d/http-proxy.conf +``` + +``` +[Service] +Environment="HTTP_PROXY=http://127.0.0.1:7890" +Environment="HTTPS_PROXY=http://127.0.0.1:7890" +Environment="NO_PROXY=localhost,127.0.0.1" +``` + + + +``` +sudo systemctl daemon-reload //加载配置文件,更新环境变量 +sudo systemctl restart docker +``` + +``` +systemctl show --property=Environment docker //验证是否配置成功 +``` + + + +### docker配置镜像: + +1.编辑 Docker 配置文件,如果该文件不存在,可以创建一个新的文件。 + +``` +sudo vim /etc/docker/daemon.json +``` + +2.添加多个镜像仓库配置 + +``` +{ + "registry-mirrors": [ + "http://hub-mirror.c.163.com", + "https://mirrors.tuna.tsinghua.edu.cn", + "https://ustc-edu-cn.mirror.aliyuncs.com", + "https://rnsxnws9.mirror.aliyuncs.com", + "https://registry.docker-cn.com", + "https://reg-mirror.qiniu.com" + ] +} + +``` + +3.重启 Docker 服务以应用 + +``` +sudo systemctl restart docker +``` + +4.验证配置 + +``` +docker info +``` + + + +### Dockerfile语法 + +我们只需要告诉Docker,我们的镜像的组成,需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么,将来Docker会帮助我们构建镜像。 + +而描述上述信息的文件就是Dockerfile文件 。![image-20210731180321133](D:/folder/test/output/b8a1a956-cbd1-480d-8a16-bb726af62b6a.png) + +`EXPOSE 8090` 是一个声明性的指令,`EXPOSE` 本身不会进行端口映射 + +在 Dockerfile 中,RUN 指令用于在构建镜像的过程中执行命令,这些命令会在镜像的一个临时容器中执行,然后将执行结果作为新的镜像层保存下来。常见的用途包括安装软件包、修改系统配置、编译代码等。 + +``` +RUN cd $JAVA_DIR \ + && tar -xf ./jdk8.tar.gz \ + && mv ./jdk1.8.0_144 ./java8 +``` + + + +**减少重复构建镜像** + +当你修改原镜像时,只需使用相同的镜像名执行: + +``` +docker build -t zbparse . +``` + +Docker 会根据 **Dockerfile 和上下文的变化**来判断哪些层需要重建,只重建受影响的部分,而未变的层会使用缓存。 + +**优化建议**: + +- 把变化较少的步骤(如 `FROM` 和设置工作目录和requirements.txt)放在前面。 +- 将容易变化的步骤(比如 `COPY . .`)放在后面。 + +这样,即使修改了 `项目的代码`,其他层仍可复用缓存,从而减少重复构建的开销。 + + + +这样会有一个问题,如果新镜像与旧镜像名字一致,那么旧的镜像名会变成none! + +以下方法可以删除none镜像。 + +``` +# 查找无标签镜像 +docker images -f "dangling=true" +# 删除无标签镜像 +docker rmi $(docker images -f "dangling=true" -q) +``` + + + +在构建命令中使用 `--no-cache` 选项可以强制 Docker 重新执行所有步骤,这在某些情况下是必要的,但通常应避免使用它以利用缓存。 + + + +### docker启动服务全流程 + +1. 编写dockerfile文件 + + ``` + # 使用官方 Python 运行时作为父镜像 + FROM python:3.8-slim + + # 设置工作目录 + WORKDIR /flask_project + + # 复制requirements文件到容器中 + COPY requirements.txt . + + # 关闭pip的进度条以减少日志输出量 + RUN pip config set global.progress_bar off + + # 安装依赖 + RUN pip install --upgrade pip --default-timeout=200 \ + && pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt + + # 将当前目录的内容复制到容器的 /flask_project 中 + COPY . . + + # 将 flask_project 添加到 PYTHONPATH + ENV PYTHONPATH=/flask_project:$PYTHONPATH + + + # 暴露端口 + EXPOSE 5000 + + # 在容器启动时运行你的应用 + CMD ["python", "flask_app/run_serve.py"] + ``` + +2. pip freeze > requirements.txt 导出requirements文件 + 这里有个问题,这里生成的requirements可能包含诸多不需要的库,有另一种方式。做加法,先pip freeze生成,然后打开项目中的每个文件,查看import的包,手动写requirements,具体的包的版本从pip freeze生成的内容摘取。 + +3. requirements和dockerfile都放在项目的根目录下 + +5. 构造镜像 -t后面是镜像名 ,最后的点号 (`.`) 代表当前目录 + +``` +docker build -t zbparse . +``` + +5. 运行容器 -p后面,第一个5000代表宿主机端口,第二个5000代表容器内端口 ,zbparse-container为创建的容器名,zbparse是使用的镜像名字 + +``` +docker run -d -p 5000:5000 --name zbparse-container zbparse +``` + +6. 查看日志 ,若无报错贼容器正常启动 + +``` +docker logs zbparse-container +docker logs --tail 10 [容器ID或名称] 查看最近10条日志 +docker logs --since 1h [容器ID或名称] 查看最近1小时的日志 +``` + +7. 停止和删除容器,先停止后删除 + +``` +docker stop zbparse-container +docker rm zbparse-container +``` + +8. 删除镜像 + +``` +docker rmi zbparse +``` + +9. 进入容器,可以查看容器内的数据 + +``` +docker exec -it zbparse-container /bin/bash +``` + +10. 保存镜像为tar文件 (最简单的应该是上传docker hub镜像库,但是现在貌似被墙了) + +``` +docker save -o zbparse.tar zbparse +``` + +11. 使用scp传输文件 + +``` +scp zbparse.tar root@118.178.236.139:/home/zy +``` + +这条命令使用 `scp`(安全复制)将本地生成的 `zbparse.tar` 文件传输到远程服务器 `118.178.236.139` 上,目标路径为 `/home/zy`,使用的登录用户名是 `root`。 + +12. 加载镜像 + +``` +sudo docker load -i zbparse.tar +``` + +13. 上传镜像 + +``` +docker login #输入邮箱 密码 +docker tag zbparse yourusername/zbparse #标记你的 Docker 镜像 +#其中yourusername/zbparse是标记名 +docker push yourusername/zbparse #推送镜像到 Docker Hub + +``` + +**注意!denied: requested access to the resource is denied** + +原因:docker hub上只能使用一个命名空间,也就是说 + +docker tag zbparse 646228430smile/zbparse:latest 这里的646228430smile是用户名,保持不变 + +![微信截图_20250225163059](D:/folder/test/output/1d57397b-fa97-4aa0-8316-11b6eb1ffd32.png) + +14. 查看镜像 + +``` +docker images +``` + + + +### **docker可能遇到的问题:** + +- **linux中构建镜像问题:** + +``` +RuntimeError: can‘t start new thread。 +``` + +解释原因:线程资源限制: Docker 容器可能有默认或显式设置的资源限制,如 CPU 限制、内存限制和可用线程数限制。在这种情况下,pip 的进度条尝试创建新线程来处理进度条更新,这可能会超出容器允许的线程数上限。 + +解决方法:在dockerfile文件中增加一行关闭pip进度条展示 +RUN pip config set global.progress_bar off + + + +``` + => [flask_app internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 982B 0.0s + => ERROR [flask_app internal] load metadata for docker.io/library/python:3.8-slim 60.4s +------ + > [flask_app internal] load metadata for docker.io/library/python:3.8-slim: +------ +failed to solve: python:3.8-slim: failed to resolve source metadata for docker.io/library/python:3.8-slim: unexpected status from HEAD request to https://ustc-edu-cn.mirror.aliyuncs.com/v2/library/python/manifests/3.8-slim?ns=docker.io: 403 Forbidden +exit status 1 + +``` + +原因:在构建镜像时,Docker 在尝试从 ustc-edu-cn 镜像站获取 `python:3.8-slim` 镜像的元数据时被拒绝了(403 Forbidden) + +解决方法:1.单独拉取该镜像 2.添加别的镜像源(可能清华源镜像有问题!!) + + + + + +- **docker运行权限问题** + +``` +OpenBLAS blas_thread_init: pthread_create failed for thread 1 of 4: Operation not permitted +OpenBLAS blas_thread_init: RLIMIT_NPROC -1 current, -1 max +``` + +解决方法: + +``` +docker run --name zbparse-container --security-opt seccomp=unconfined zbparse +``` + + + +## Docker-Compose + +### docker-compose安装: + +**方式1:**从 Docker 20.10 开始,Docker 官方就将 Docker Compose 作为插件集成在 Docker Engine 中,所以在安装 Docker Engine 时,它也会一并安装 Docker Compose 插件。无需额外安装 + +验证安装 + +``` +docker compose version +``` + +**方式2:**安装独立版 Docker Compose 二进制文件 + +下载二进制文件(或者下载别人的镜像复制到服务器中的/usr/local/bin下) + +``` +sudo curl -L "https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +``` + +赋予执行权限 + +``` +sudo chmod +x /usr/local/bin/docker-compose +``` + +验证安装 + +``` +docker-compose --version +``` + + + +这两者功能基本一致,大部分命令和参数都是相同的,**只是命令前缀不同**。 + + + +### docker-compose.yml语法 + +**1.services** + +**常见子配置**: + +- **image**:指定服务使用的镜像。 +- **build**:指定构建上下文或 Dockerfile 路径。 +- **ports**:映射容器端口到主机端口。 +- **environment**:设置环境变量。 +- **volumes**:挂载主机目录或数据卷到容器。 +- **depends_on**:定义服务启动顺序的依赖关系。 + +**2.networks** + +定义和管理自定义网络,方便容器间通信。 + +**3.volumes** + +定义数据卷,用于数据持久化或者在多个容器间共享数据。 + + + +``` +version: '3' + +services: + web_app: + build: + context: ./web_app # 指定web_app的上下文 + dockerfile: Dockerfile # 在./web_app下寻找Dockerfile + container_name: web_app + ports: + - "8080:80" # 将主机8080端口映射到容器的80端口 + environment: + - DATABASE_HOST=db + - DATABASE_USER=root + - DATABASE_PASSWORD=root + - DATABASE_NAME=my_database + depends_on: + - db + networks: + - my_network + + db: + image: mysql:8 + container_name: mysql_db + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: my_database + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql # 将命名数据卷挂载到MySQL数据目录 + networks: + - my_network + restart: always + +networks: + my_network: + driver: bridge # 使用桥接网络驱动 + +volumes: + db_data: + +``` + + + +``` +build: + context: ./web_app + dockerfile: Dockerfile + +build: ./web_app #两种写法是等效的 +``` + + + +**** + +### 4.docker-compose常用命令 + +**构建镜像:**这个命令根据 docker-compose.yml 中各服务的配置构建镜像。如果你修改了 Dockerfile 或者项目代码需要打包进镜像时,就需要运行该命令来构建新的镜像。 + +``` +docker-compose build +``` + +**启动容器:**这个命令用于启动服务,参数 `-d` 表示以后台守护进程的方式运行。如果镜像不存在,它会自动构建镜像;但如果镜像已经存在,则默认直接使用现有的镜像启动容器。 + +``` +docker-compose up -d +``` + +**更新并重启容器** + +``` +docker-compose up --build -d +``` + + + +***!!注意在使用docker-compose命令时,可以指定服务名,而无需完整的容器名*** + + + +**查看服务的日志输出** + +``` +docker-compose logs flask_app --since 1h #只显示最近 1 小时 +``` + +**停止并删除所有由 docker-compose 启动的容器、网络等(默认不影响挂载卷)。** + +``` +docker-compose down #不能单独指定 +``` + +**删除停止的容器** + +``` +docker-compose rm +docker-compose rm flask_app +``` + +asdf + +**停止运行的容器** + +``` +docker-compose stop +docker-compose stop flask_app #指定某个服务 +``` + +**启动服务** + +``` +docker-compose start #启动所有停止的.. +docker-compose start flask_app +``` + + **重启服务(停止+启动)** + +``` +docker-compose restart +docker-compose restart flask_app #指定某个服务 +``` + + + +当使用docker-compose build 构建镜像时,镜像的标签格式通常是 `项目名_服务名` + +docker-compose up生成的容器名默认是 `项目名_服务名_索引号` + +**索引号(Index Number)**:这是一个用于区分多个相同服务实例的数字索引。第一次启动时为 `1`,后续实例依次递增。 + +**服务名**是指你在docker-compose中设置的服务名称(services:下的名称)。 + +**项目名**默认是**当前目录名**,如果你使用了 `-p` 选项指定项目名,则使用指定的项目名 + +如 + +``` +docker-compose -p my_custom_project up -d +``` + +在docker-compose.yml中指定容器名 + +``` +version: '3' +services: + web: + build: . + container_name: my_custom_web_container + restart: always 设置自动启动 + ports: + - "5005:5005" + db: + image: mysql:5.7 + container_name: my_custom_db_container + environment: + MYSQL_ROOT_PASSWORD: example + +``` + +启动docker-compose中的单独一个服务:docker-compose up -d web web为里面的一个服务名 + + + +### 关于Docker-compose的重要说明 + +**一个Docker-compose文件定义的各个服务默认使用同一个网络**。不同的Docker-compose文件可能使用相同的镜像,如mysql8.0,此时docker会自动进行镜像复用,而不会重复下载。 + +一个**基础镜像可以构造多个容器**,互不影响, + +``` +db: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: 123456 + MYSQL_DATABASE: db_project1 + ports: + - "3306:3306" + volumes: + - db_data_project1:/var/lib/mysql +``` + +``` +db: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: 123456 + MYSQL_DATABASE: db_project2 + ports: + - "3307:3306" # 使用不同的主机端口以避免冲突 + volumes: + - db_data_project2:/var/lib/mysql +``` + + + +| **问题类型** | **可能的冲突** | **解决方案** | +| -------------- | ------------------------------------------ | ------------------------------------------------------------ | +| **端口冲突** | 容器监听相同的宿主机端口(如 `3306:3306`) | 在不同 `docker-compose.yml` 中映射不同端口(如 `3307:3306`) | +| **数据卷冲突** | 多个 MySQL 实例共享相同的 `/var/lib/mysql` | 使用不同的 `volume` 名称,或只运行一个 MySQL 实例 | +| **网络冲突** | 默认网络可能导致 DNS 解析失败 | 在 `docker-compose.yml` 里创建独立的 `network` | +| **镜像冲突** | 镜像不会冲突,但多个实例可能导致资源竞争 | 镜像可共享,但实例应该使用独立配置 | + +**最佳实践** + +1. 如果只需要一个 MySQL 服务,建议让多个 Compose 连接到同一个 MySQL 容器,而不是启动多个 MySQL 容器。 +2. **所有服务都在一个docker-compose中,包括mysql** +3. 如果必须运行多个 MySQL 实例: + - 为不同的 MySQL 实例分配不同的宿主机端口(如 `3306`、`3307`)。 + - 使用不同的数据卷,避免数据损坏。 + - 确保不同的实例在不同的 Docker 网络中运行,防止冲突。 + +**最佳实践1:** + +**让 MySQL 作为一个单独的 Compose 服务** + +**在其他 `docker-compose.yml` 里连接到这个 MySQL 容器的网络** + +1.创建一个 Docker 网络 + +``` +docker network create my_shared_network +``` + +2.创建 MySQL 的 `docker-compose-mysql.yml` + +``` +version: '3' +services: + mysql: + image: mysql:8 + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: my_database + ports: + - "3306:3306" + networks: + - my_shared_network +networks: + my_shared_network: + external: true + +``` + +3.在 `docker-compose-app.yml` 里连接这个 MySQL + +``` +version: '3' +services: + web_app: + image: my_web_app + environment: + - DATABASE_HOST=mysql + - DATABASE_USER=root + - DATABASE_PASSWORD=root + - DATABASE_NAME=my_database + ports: + - "8080:8080" + networks: + - my_shared_network +networks: + my_shared_network: + external: true + +``` + + + +| **方式** | **能否被其他容器访问?** | **生效范围** | **优缺点** | +| ------------------------- | ------------------------ | ----------------------------------------------------- | ------------------------------------------ | +| **`network-alias`(db)** | ✅ **推荐** | 仅限同一个 `network`(不同compose文件共享网络时也生效) | **最稳定,不受 `container_name` 变更影响** | +| **`服务名`(mysql)** | ✅ **默认推荐** | 仅限 **同一个**`docker-compose.yml` | **简单,自动解析,不需要额外配置** | + + + +服务名和网络别名可用于同一网络内容器之间的通信 + +容器名主要用于管理和运维:`docker ps`、`docker logs`、`docker exec` 等命令时指定目标容器。 + + + +4.启动 MySQL 容器 + +``` +docker-compose -f docker-compose-mysql.yml up -d +``` + +5.启动 Web 应用 + +``` +docker-compose -f docker-compose-app.yml up -d +``` + +**问题:但你需要确保其他服务不会产生不必要的相互访问,因为相当于都在my_shared_network中。** + + + +**最佳实践** + +- **如果所有服务都在同一个 `docker-compose.yml`,直接用 `服务名`(如 `mysql`),因为 Docker Compose 会自动解析。** +- **如果是多个 `docker-compose.yml` 共享网络,建议用 `network-alias`**,这样即使 `container_name` 改变,服务依然可以正确解析。 + + + +### 实践:部署微服务集群 + +**需求**:将之前学习的cloud-demo微服务集群利用DockerCompose部署 + + + +**实现思路**: + +① 查看课前资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件 + +② 修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名 + +③ 使用maven打包工具,将项目中的每个微服务都打包为app.jar + +④ 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中 + +⑤ 将cloud-demo上传至虚拟机,利用 docker-compose up -d 来部署 + +#### compose文件 + +查看课前资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件,而且每个微服务都准备了一个独立的目录: + +![image-20210731181341330](D:/folder/test/output/6ab58e62-3f4a-4639-beda-2da6816feaad.png) + +内容如下: + +```yaml +version: "3.2" + +services: + nacos: + image: nacos/nacos-server + environment: + MODE: standalone + ports: + - "8848:8848" + mysql: + image: mysql:5.7.25 + environment: + MYSQL_ROOT_PASSWORD: 123 + volumes: + - "$PWD/mysql/data:/var/lib/mysql" + - "$PWD/mysql/conf:/etc/mysql/conf.d/" + userservice: + build: ./user-service + orderservice: + build: ./order-service + gateway: + build: ./gateway + ports: + - "10010:10010" +``` + +可以看到,其中包含5个service服务: + +- `nacos`:作为注册中心和配置中心 + - `image: nacos/nacos-server`: 基于nacos/nacos-server镜像构建 + - `environment`:环境变量 + - `MODE: standalone`:单点模式启动 + - `ports`:端口映射,这里暴露了8848端口 +- `mysql`:数据库 + - `image: mysql:5.7.25`:镜像版本是mysql:5.7.25 + - `environment`:环境变量 + - `MYSQL_ROOT_PASSWORD: 123`:设置数据库root账户的密码为123 + - `volumes`:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据 +- `userservice`、`orderservice`、`gateway`:都是基于Dockerfile临时构建的 + + + +查看mysql目录,可以看到其中已经准备好了cloud_order、cloud_user表: + +![image-20210801095205034](D:/folder/test/output/7441e84f-ee75-4073-894c-b3e299d8cd23.png) + +查看微服务目录,可以看到都包含Dockerfile文件: + +![image-20210801095320586](D:/folder/test/output/21b40ad4-f2b0-40f4-b78b-c4ce2e474a9a.png) + +内容如下: + +```dockerfile +FROM java:8-alpine +COPY ./app.jar /tmp/app.jar +ENTRYPOINT java -jar /tmp/app.jar +``` + + + +#### 修改微服务配置 + +因为微服务将来要部署为docker容器,而容器之间互联不是通过IP地址,而是通过容器名。这里我们将order-service、user-service、gateway服务的mysql、nacos地址都修改为基于服务名的访问。 + +如下所示: + +```yaml +spring: + datasource: + url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false + username: root + password: 123 + driver-class-name: com.mysql.jdbc.Driver + application: + name: orderservice + cloud: + nacos: + server-addr: nacos:8848 # nacos服务地址 +``` + + + +#### 打包 + +接下来需要将我们的每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar,因此我们的每个微服务都需要用这个名称。 + +可以通过修改pom.xml中的打包名称来实现,每个微服务都需要修改: + +```xml + + + app + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +打包后: + +![image-20210801095951030](D:/folder/test/output/83693759-d10d-4d08-b551-e10186a0433e.png) + +#### 拷贝jar包到部署目录 + +编译打包好的app.jar文件,需要放到Dockerfile的同级目录中。注意:每个微服务的app.jar放到与服务名称对应的目录,别搞错了。 + +user-service: + +![image-20210801100201253](D:/folder/test/output/c527513d-3381-41de-be43-0447393c3b17.png) + +order-service: + +![image-20210801100231495](D:/folder/test/output/1cbfc1b5-6519-433c-83c4-8aa1efb9d63a.png) + +gateway: + +![image-20210801100308102](D:/folder/test/output/204b20e7-a843-42e1-93f3-47d3a1fd0fb7.png) + +#### 部署 + +最后,我们需要将文件整个cloud-demo文件夹上传到虚拟机中,理由DockerCompose部署。 + +上传到任意目录: + +![image-20210801100955653](D:/folder/test/output/8c29ca6b-6f59-4f8c-b124-a60e63b4eb9b.png) + +部署: + +进入cloud-demo目录,然后运行下面的命令: + +```sh +docker-compose up -d +``` + + + diff --git a/Java/docker/assets/image-20210731141907366.png b/Java/docker/assets/image-20210731141907366.png new file mode 100644 index 0000000..4f3d37c Binary files /dev/null and b/Java/docker/assets/image-20210731141907366.png differ diff --git a/Java/docker/assets/image-20210731142219735.png b/Java/docker/assets/image-20210731142219735.png new file mode 100644 index 0000000..c8e523c Binary files /dev/null and b/Java/docker/assets/image-20210731142219735.png differ diff --git a/Java/docker/assets/image-20210731143401460.png b/Java/docker/assets/image-20210731143401460.png new file mode 100644 index 0000000..04ad8b6 Binary files /dev/null and b/Java/docker/assets/image-20210731143401460.png differ diff --git a/Java/docker/assets/image-20210731144304990.png b/Java/docker/assets/image-20210731144304990.png new file mode 100644 index 0000000..213b9be Binary files /dev/null and b/Java/docker/assets/image-20210731144304990.png differ diff --git a/Java/docker/assets/image-20210731144458680.png b/Java/docker/assets/image-20210731144458680.png new file mode 100644 index 0000000..b6d6320 Binary files /dev/null and b/Java/docker/assets/image-20210731144458680.png differ diff --git a/Java/docker/assets/image-20210731144820638.png b/Java/docker/assets/image-20210731144820638.png new file mode 100644 index 0000000..b50dda3 Binary files /dev/null and b/Java/docker/assets/image-20210731144820638.png differ diff --git a/Java/docker/assets/image-20210731145914960.png b/Java/docker/assets/image-20210731145914960.png new file mode 100644 index 0000000..368d228 Binary files /dev/null and b/Java/docker/assets/image-20210731145914960.png differ diff --git a/Java/docker/assets/image-20210731152243765.png b/Java/docker/assets/image-20210731152243765.png new file mode 100644 index 0000000..3003cc7 Binary files /dev/null and b/Java/docker/assets/image-20210731152243765.png differ diff --git a/Java/docker/assets/image-20210731153059464.png b/Java/docker/assets/image-20210731153059464.png new file mode 100644 index 0000000..1d5854e Binary files /dev/null and b/Java/docker/assets/image-20210731153059464.png differ diff --git a/Java/docker/assets/image-20210731153743354.png b/Java/docker/assets/image-20210731153743354.png new file mode 100644 index 0000000..ab6584d Binary files /dev/null and b/Java/docker/assets/image-20210731153743354.png differ diff --git a/Java/docker/assets/image-20210731154257653.png b/Java/docker/assets/image-20210731154257653.png new file mode 100644 index 0000000..254344b Binary files /dev/null and b/Java/docker/assets/image-20210731154257653.png differ diff --git a/Java/docker/assets/image-20210731155002425.png b/Java/docker/assets/image-20210731155002425.png new file mode 100644 index 0000000..fced121 Binary files /dev/null and b/Java/docker/assets/image-20210731155002425.png differ diff --git a/Java/docker/assets/image-20210731155141362.png b/Java/docker/assets/image-20210731155141362.png new file mode 100644 index 0000000..ce93189 Binary files /dev/null and b/Java/docker/assets/image-20210731155141362.png differ diff --git a/Java/docker/assets/image-20210731155649535.png b/Java/docker/assets/image-20210731155649535.png new file mode 100644 index 0000000..10f5fdf Binary files /dev/null and b/Java/docker/assets/image-20210731155649535.png differ diff --git a/Java/docker/assets/image-20210731155844368.png b/Java/docker/assets/image-20210731155844368.png new file mode 100644 index 0000000..9363466 Binary files /dev/null and b/Java/docker/assets/image-20210731155844368.png differ diff --git a/Java/docker/assets/image-20210731155856199.png b/Java/docker/assets/image-20210731155856199.png new file mode 100644 index 0000000..3c96c3e Binary files /dev/null and b/Java/docker/assets/image-20210731155856199.png differ diff --git a/Java/docker/assets/image-20210731155903037.png b/Java/docker/assets/image-20210731155903037.png new file mode 100644 index 0000000..f66e2d1 Binary files /dev/null and b/Java/docker/assets/image-20210731155903037.png differ diff --git a/Java/docker/assets/image-20210731161104732.png b/Java/docker/assets/image-20210731161104732.png new file mode 100644 index 0000000..10ef865 Binary files /dev/null and b/Java/docker/assets/image-20210731161104732.png differ diff --git a/Java/docker/assets/image-20210731161354344.png b/Java/docker/assets/image-20210731161354344.png new file mode 100644 index 0000000..79797b3 Binary files /dev/null and b/Java/docker/assets/image-20210731161354344.png differ diff --git a/Java/docker/assets/image-20210731161746245.png b/Java/docker/assets/image-20210731161746245.png new file mode 100644 index 0000000..12a19cb Binary files /dev/null and b/Java/docker/assets/image-20210731161746245.png differ diff --git a/Java/docker/assets/image-20210731161950495.png b/Java/docker/assets/image-20210731161950495.png new file mode 100644 index 0000000..9eb17f6 Binary files /dev/null and b/Java/docker/assets/image-20210731161950495.png differ diff --git a/Java/docker/assets/image-20210731163255863.png b/Java/docker/assets/image-20210731163255863.png new file mode 100644 index 0000000..d5513d4 Binary files /dev/null and b/Java/docker/assets/image-20210731163255863.png differ diff --git a/Java/docker/assets/image-20210731164159811.png b/Java/docker/assets/image-20210731164159811.png new file mode 100644 index 0000000..75c067e Binary files /dev/null and b/Java/docker/assets/image-20210731164159811.png differ diff --git a/Java/docker/assets/image-20210731164455818.png b/Java/docker/assets/image-20210731164455818.png new file mode 100644 index 0000000..2a5fe19 Binary files /dev/null and b/Java/docker/assets/image-20210731164455818.png differ diff --git a/Java/docker/assets/image-20210731164717604.png b/Java/docker/assets/image-20210731164717604.png new file mode 100644 index 0000000..2913f92 Binary files /dev/null and b/Java/docker/assets/image-20210731164717604.png differ diff --git a/Java/docker/assets/image-20210731172440275.png b/Java/docker/assets/image-20210731172440275.png new file mode 100644 index 0000000..5d053f7 Binary files /dev/null and b/Java/docker/assets/image-20210731172440275.png differ diff --git a/Java/docker/assets/image-20210731173541846.png b/Java/docker/assets/image-20210731173541846.png new file mode 100644 index 0000000..89ec8e6 Binary files /dev/null and b/Java/docker/assets/image-20210731173541846.png differ diff --git a/Java/docker/assets/image-20210731173746910.png b/Java/docker/assets/image-20210731173746910.png new file mode 100644 index 0000000..90dfd44 Binary files /dev/null and b/Java/docker/assets/image-20210731173746910.png differ diff --git a/Java/docker/assets/image-20210731173809877.png b/Java/docker/assets/image-20210731173809877.png new file mode 100644 index 0000000..d6ad6b8 Binary files /dev/null and b/Java/docker/assets/image-20210731173809877.png differ diff --git a/Java/docker/assets/image-20210731175155453.png b/Java/docker/assets/image-20210731175155453.png new file mode 100644 index 0000000..c9db3dd Binary files /dev/null and b/Java/docker/assets/image-20210731175155453.png differ diff --git a/Java/docker/assets/image-20210731175806273.png b/Java/docker/assets/image-20210731175806273.png new file mode 100644 index 0000000..7f16680 Binary files /dev/null and b/Java/docker/assets/image-20210731175806273.png differ diff --git a/Java/docker/assets/image-20210731180321133.png b/Java/docker/assets/image-20210731180321133.png new file mode 100644 index 0000000..73cc210 Binary files /dev/null and b/Java/docker/assets/image-20210731180321133.png differ diff --git a/Java/docker/assets/image-20210731180921742.png b/Java/docker/assets/image-20210731180921742.png new file mode 100644 index 0000000..e9f4c48 Binary files /dev/null and b/Java/docker/assets/image-20210731180921742.png differ diff --git a/Java/docker/assets/image-20210731181341330.png b/Java/docker/assets/image-20210731181341330.png new file mode 100644 index 0000000..e40221a Binary files /dev/null and b/Java/docker/assets/image-20210731181341330.png differ diff --git a/Java/docker/assets/image-20210801095205034.png b/Java/docker/assets/image-20210801095205034.png new file mode 100644 index 0000000..20e4745 Binary files /dev/null and b/Java/docker/assets/image-20210801095205034.png differ diff --git a/Java/docker/assets/image-20210801095320586.png b/Java/docker/assets/image-20210801095320586.png new file mode 100644 index 0000000..b4994f0 Binary files /dev/null and b/Java/docker/assets/image-20210801095320586.png differ diff --git a/Java/docker/assets/image-20210801095951030.png b/Java/docker/assets/image-20210801095951030.png new file mode 100644 index 0000000..c22bc80 Binary files /dev/null and b/Java/docker/assets/image-20210801095951030.png differ diff --git a/Java/docker/assets/image-20210801100201253.png b/Java/docker/assets/image-20210801100201253.png new file mode 100644 index 0000000..5e31877 Binary files /dev/null and b/Java/docker/assets/image-20210801100201253.png differ diff --git a/Java/docker/assets/image-20210801100231495.png b/Java/docker/assets/image-20210801100231495.png new file mode 100644 index 0000000..199fc7c Binary files /dev/null and b/Java/docker/assets/image-20210801100231495.png differ diff --git a/Java/docker/assets/image-20210801100308102.png b/Java/docker/assets/image-20210801100308102.png new file mode 100644 index 0000000..4475667 Binary files /dev/null and b/Java/docker/assets/image-20210801100308102.png differ diff --git a/Java/docker/assets/image-20210801100955653.png b/Java/docker/assets/image-20210801100955653.png new file mode 100644 index 0000000..4bc6f65 Binary files /dev/null and b/Java/docker/assets/image-20210801100955653.png differ diff --git a/Java/docker/assets/image-20210801101207444.png b/Java/docker/assets/image-20210801101207444.png new file mode 100644 index 0000000..c6357d9 Binary files /dev/null and b/Java/docker/assets/image-20210801101207444.png differ diff --git a/Java/docker/assets/image-20210801101314816.png b/Java/docker/assets/image-20210801101314816.png new file mode 100644 index 0000000..d91344e Binary files /dev/null and b/Java/docker/assets/image-20210801101314816.png differ diff --git a/Java/docker/assets/image-20210801101410200.png b/Java/docker/assets/image-20210801101410200.png new file mode 100644 index 0000000..5e8e833 Binary files /dev/null and b/Java/docker/assets/image-20210801101410200.png differ diff --git a/Java/docker/assets/image-20210801101455590.png b/Java/docker/assets/image-20210801101455590.png new file mode 100644 index 0000000..92b27cc Binary files /dev/null and b/Java/docker/assets/image-20210801101455590.png differ diff --git a/Java/git基本操作.md b/Java/git基本操作.md new file mode 100644 index 0000000..7902422 --- /dev/null +++ b/Java/git基本操作.md @@ -0,0 +1,390 @@ +# Git + +**linux上安装Git** + +``` +sudo apt update +sudo apt install git +``` + + + +Git Bash:与linux风格接近,使用最多,推荐 + +Git CMD:windows风格 + +Git GUI:图形界面,不推荐 + + + +查看配置 + +git config -l :查看所有配置 + +git config --global --list 查看用户配置的 + +git config --system --list :系统配置的 + + + +## 核心原理 + +![](D:/folder/test/output/72874011-821a-4da8-92d0-6ad9322ca40c.png) + +index是暂存区 + +respository 是本地代码仓库,保存着本地的各个版本代码 + +remote是远程仓库,通常是github/gitee码云 + + + +## 如何克隆别人已有的项目 + +1. 获取项目地址http +2. 打开本地文件系统,选择你需要保存项目的位置并打开cmd +3. git clone xxx,这个过程可能需要验证身份,输入用户名密码 + + + +## 如何将本地仓库与远程仓库连接?从零开始 + +**法1:** + +- 首先在Github上新建一个代码仓库,拷贝地址 eg:https://github.com/zhangww-web/JianShu.git + +- 在你本地文件夹下鼠标右键git bash here +- git init 新建本地仓库,生成.git隐藏文件,点进去有head文件。 +- git remote add origin url,可以绑定远程仓库 + +![](D:/folder/test/output/cbe51a46-0337-46eb-a5e3-6314ed0031ce.png) + +- 继续输入git pull origin master(若你远程仓库为空,不需要这一步) +- git add .(.表示所有的,注意这个‘点’)—将本地文件提交至暂存区 +- git commit -m '提交信息' +- git push origin master + + + +**法2:** + +第2种方法比较简单,直接用把远程仓库拉到本地,然后再把自己本地的项目拷贝到仓库中去。然后push到远程仓库上去即可。要求远程仓库为新建的空仓库!!! + +- 在空文件夹中git bash here(不用init!) + +- 首先git clone https://github.com/zhangww-web/JianShu.git + +- ##### 然后复制自己项目的所有文件到刚刚克隆下来的仓库中 + +- git push -u origin master + + + +**法3:在android studio中集成** + +1. **配置Git** + +![image-20240425104918549](D:/folder/test/output/4a40efd9-1864-46ae-b7ce-39f8645380ff.png) + +2. **关联自己的github** + +![image-20240425104950049](D:/folder/test/output/587fb99d-078c-49b0-8047-7979566d3d03.png) + +**对于任何安卓项目,前两步都是通用的!!!** + +3. + +![image-20240425105318414](D:/folder/test/output/44425f1e-39c6-4189-b9cb-f7e907263004.png) + +![image-20240425105341246](D:/folder/test/output/d77962b8-3dd8-4c5f-88e4-71728181a528.png) + +这里的Remote填远程仓库地址,eg: https://github.com/zhangww-web/JianShu.git + +**IDEA** + +方法类似,先init本地仓库, + +点击顶部菜单栏的 `Git -> Manage Remotes`。 + +![image-20250308102802964](D:\folder\test\output\image-20250308102802964.png) + + + + + + + +**如果Share project on github一直失败,可以这样:** +**前面步骤不变,现在去github上新建一个空白仓库,获得一个remote url,然后本地仓库提交到该空白仓库,即可建立链接。** + + + +## 迁移代码仓库 + +法一(推荐):访问令牌(Access Token)获取:点击右上角头像,选择“Settings”(设置)。 + +在左侧菜单中找到“Developer settings”(开发者设置),然后点击“Personal access tokens”(个人访问令牌)。点击“Generate new token”(生成新令牌),按照需要选择相应的权限(通常建议勾选 repo、read:org 等,根据你的实际需求)。生成后将令牌复制下来,填入 Gitea 的迁移界面中。 + +**直接推完整git信息,推荐** + + + +法二: + +本地有完整的代码已经git提交记录,不想丢失,推送到新的远程仓库: + +在本地文件夹下git bash here + +git remote add origin <新仓库地址> + +git push -u origin --all + +注意:只会将**你本地仓库已经 checkout 出来的所有分支**(即本地存在的分支)推送到新远程仓库中。 + + + + + +## Git 常用命令 + +### 提交代码 + +git add--> git commit --> git push + + + +git add . 提交所有文件到暂存区,此时git status显示changes to be commited + +git commit -m "describe描述性内容" 提交到本地仓库 + + + +### 分支操作 + +**git banch -a** 可以查看所有分支 + +git branch dev 可以新建dev分支 + +**git checkout dev** 可以切换到dev分支 + +git branch -d dev 删除dev分支 + +git branch -m dev cs :将dev分支修改名称为cs分支 + +![](D:/folder/test/output/c6e7b13c-e1ac-4dbc-b976-f4ca8c8b0572.png) + + + +### push失败问题 + +首先,科学上网一定能打开github的官网,但是idea中仍然无法push + +此时查看你的代理服务器的端口,发现是7890 + +image-20240802181935378 + +然后打开git bash,输入以下代码: + +**使用http代理** + +git config --global http.proxy http://127.0.0.1:7890 +git config --global https.proxy https://127.0.0.1:7890 + +**使用socks5代理** + +git config --global http.proxy socks5://127.0.0.1:7890 +git config --global https.proxy socks5://127.0.0.1:7890 + + + +**或者使用国产的gitee或者自己服务器上搭建gitea进行代码托管!!!** + + + +### 拉取代码与解决冲突 + +`git pull` 命令用于从远程仓库拉取(fetch)并合并(merge)最新的更改到本地仓库。它实际上**执行了两个操作**: + +1. `git fetch`: 这个操作会从远程仓库下载最新的更改到本地仓库,但不会自动合并到当前分支。它将远程仓库的更改存储在本地仓库中,使你能够查看它们,但不会更改你当前工作目录中的文件。 +2. `git merge`: 这个操作将远程仓库的更改合并到当前分支。如果有**冲突**,你需要解决冲突后再次提交合并的结果 + + + +**情况1:本地修改代码未提交,拉取代码的时候就会报错:** + +*错误:您对下列文件的本地修改将被合并操作覆盖:* + *docker-compose.yaml* +*请在合并前提交或贮藏您的修改。* +*正在终止* + +**暂存未提交的更改(git stash)** + +如果还不想提交本地修改,可以将更改暂存起来: + +``` +git stash +git pull +git stash pop +``` + +`git stash` 会将当前工作区的改动保存到一个栈中,拉取完成后通过 `git stash pop` 恢复修改。如果恢复过程中出现冲突,同样需要手动解决。 + + + +**舍弃本地修改** + +如果确定不需要这些修改,可以放弃它们: + +``` +git reset --hard +git pull +``` + + + +**但是推荐先提交本地的代码!!!** + +``` +git add . +git commit -m "描述本次修改的提交信息" +git pull +``` + + + +**情况2:权限校验问题** + +一般可以保持git网页端账号的登录状态,再pull,会有弹窗出来输入git的用户名和密码,成功后即可拉取。 + + + +**情况3 合并冲突,以pycharm为例** + +**1.触发冲突解决界面** +当你执行 `git pull` 后如果出现冲突,PyCharm 会在右下角或版本控制工具窗口中提醒有冲突文件。你可以点击提示信息或在版本控制面板中找到冲突文件。 + +**2.启动合并工具** +双击冲突文件后,PyCharm 会自动打开三方合并工具。界面通常分为三部分: + +- **左侧:本地修改**(当前分支的更改) +- **右侧:远程修改**(要合并进来的更改) +- **中间:合并结果**(你需要编辑的区域) + +**3.手动选择并合并代码** +在合并工具中,你可以逐个查看冲突部分: + +- 点击左侧或右侧的按钮来选择保留哪部分内容。 +- 如果需要,**你也可以手动编辑中间**的合并结果区域,直接输入合适的代码。 +- 合并工具通常会有跳转到下一个冲突的按钮,方便你逐个解决。 + +**4.保存合并结果并标记解决** +合并完成后,点击工具窗口上的“Apply”或“Accept Merge”按钮,保存你的修改。此时,冲突文件会标记为已解决。 + +**5.提交合并后的更改** +返回主界面后,你可以在版本控制面板中看到已解决的文件。检查确认无误后,通过 VCS 菜单或直接点击工具栏中的提交按钮,将合并结果提交到仓库。 + + + +## 其他Git相关 + +公私钥生成 + +在linux中,使用账号密码链接github报错如下: + +remote: Support for password authentication was removed on August 13, 2021. remote: Please see **https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls** for information on currently recommended modes of authentication. 致命错误:'https://github.com/zhangww-web/reptile.git/' 鉴权失败 + +*原因是linux不支持账号密码链接!!* + +配置ssh,可以在git push的时候直接推送,github会通过ssh来验证你的身份。 + +如何在linux中配置私钥? + +**生成 SSH 密钥**: + +- 如果你还没有 SSH 密钥,可以使用以下命令生成: + + ``` + ssh-keygen -t rsa -b 4096 -C "your_email@example.com" + ``` + +- 按照提示保存密钥文件。 + +**添加 SSH 密钥到 GitHub**: + +- **复制生成的公钥内容**(通常在 `~/.ssh/id_rsa.pub` 文件中)。 +- 登录到 GitHub。 +- 进入 [SSH and GPG keys](https://github.com/settings/keys) 页面。 +- 点击 "New SSH key" 按钮,粘贴公钥内容并保存。 + +**使用 SSH URL 克隆仓库**: + +``` +git clone git@github.com:zhangww-web/reptile.git +``` + + + +SSH 连接 GitHub 并触发身份验证,流程如下: + +1. **GitHub 发送一个随机挑战(Challenge)** + GitHub 服务器会向你的 Linux 服务器发送一个随机字符串,并用 **你的公钥** 进行加密。 +2. **你的 Linux 服务器用私钥解密** + 你的 SSH 客户端(`ssh` 命令或 `git`)会自动使用本地的 **私钥(id_rsa)** 进行解密。如果解密成功,证明你拥有匹配的私钥。 +3. **返回解密后的数据** + 你的客户端将解密后的数据返回给 GitHub。 +4. **GitHub 验证解密结果** + GitHub 服务器检查解密结果是否匹配它最初发送的随机挑战。如果匹配,则认证成功。 + +**身份验证逻辑**:GitHub 发送加密数据 → 你的私钥解密 → 返回结果 → GitHub 确认一致性 → 认证成功。 + + + +### .gitignore(忽略某些文件) + +**如果不小心commit了如何撤销?** + +到项目根目录,git bash here + +``` +git rm -r --cached 'dictory'/ +``` + +![](D:/folder/test/output/52b3bb61-86aa-48c3-bb77-c241170fd570.png)在.gitignore文件进行添加 + +**为什么`.gitignore`文件不放在`.git`文件夹中?** + +- **用途不同**:`.git`文件夹由Git自动创建,用于存储Git的内部数据,包括所有提交记录、配置和对象等。用户一般不需要手动修改这个文件夹里的内容。而`.gitignore`文件是用户创建和维护的,用于定义哪些文件和目录应被Git忽略。 +- **便于版本控制**:`.gitignore`文件放在项目的根目录中,可以和项目代码一起被版本控制,这样其他协作开发者也能看到和使用相同的忽略规则。如果把`.gitignore`放在`.git`文件夹中,它就不会被版本控制系统追踪到。 + + + + + +### 撤销Git版本控制 + +直接把项目文件夹中的.git文件夹删除即可(开启查看隐藏文件夹可看到) + +若idea/pycharm报错: + +COVID-19-Detector is registered as a Git root, but no Git repositories were found there. + +![image-20240729113527314](D:/folder/test/output/2da238ef-d379-483d-b92f-0747e9c1cf7f.png) + + + + + +### 添加协作者 + +**协作者权限** + +如果不使用组织的话,你也可以单独为每个仓库添加协作者。这样做的话,公钥仍然应该添加到你的个人设置中,但是你可以在每个仓库的设置中单独管理协作者访问权限。 + +**设置步骤包括:** + +- 打开你想要添加协作者的仓库。 +- 导航到仓库设置中的“Manage access”(管理访问)或“Collaborators”(协作者)部分。 +- 添加协作者的GitHub用户名,并设置他们的访问级别。 + +![image-20240524100231379](D:/folder/test/output/57e3e2b0-18db-4f5b-8f8c-121794124771.png) + diff --git a/Java/jupyter笔记本.md b/Java/jupyter笔记本.md new file mode 100644 index 0000000..75619a2 --- /dev/null +++ b/Java/jupyter笔记本.md @@ -0,0 +1,27 @@ +# jupyter笔记本 + +### 如何打开jupyter? + +1. 打开anaconda navigator图形界面,lauch jupyter +2. 打开cmd 敲 jupyter notebook + +%matplotlib inline 使matplotlib绘制的图像嵌入在jupyter notebook单元格中 + +## 常用快捷键! + +- esc进入命令模式 回车进入编辑模式 + +- 在命令模式下输入M可以进行markdown格式编写,输入Y可以进行python代码编写 + +- shift+回车:运行当前代码块并跳转到下一块 +- ctrl+回车:只运行当前代码块 不跳转 +- B:往下增加一行代码块 +- A:往上新加一行代码块 +- DD:删除一行 +- L:标一个代码块内的行数 +- 公式撰写 + +$$ +x=\frac{-b\pm \sqrt{b^2-4_ac}}{2a} +$$ + diff --git a/Java/linux/assets/1.png b/Java/linux/assets/1.png new file mode 100644 index 0000000..af7a32f Binary files /dev/null and b/Java/linux/assets/1.png differ diff --git a/Java/linux/assets/2.png b/Java/linux/assets/2.png new file mode 100644 index 0000000..2046a65 Binary files /dev/null and b/Java/linux/assets/2.png differ diff --git a/Java/linux/assets/d0de0e49-ecef-4776-9a98-e000c3576326.png b/Java/linux/assets/d0de0e49-ecef-4776-9a98-e000c3576326.png new file mode 100644 index 0000000..af7a32f Binary files /dev/null and b/Java/linux/assets/d0de0e49-ecef-4776-9a98-e000c3576326.png differ diff --git a/Java/linux/assets/image-20250306140720509-1741241342632-2.png b/Java/linux/assets/image-20250306140720509-1741241342632-2.png new file mode 100644 index 0000000..f8f6d31 Binary files /dev/null and b/Java/linux/assets/image-20250306140720509-1741241342632-2.png differ diff --git a/Java/linux/assets/image-20250306140720509.png b/Java/linux/assets/image-20250306140720509.png new file mode 100644 index 0000000..f8f6d31 Binary files /dev/null and b/Java/linux/assets/image-20250306140720509.png differ diff --git a/Java/linux/linux服务器.md b/Java/linux/linux服务器.md new file mode 100644 index 0000000..ba1751c --- /dev/null +++ b/Java/linux/linux服务器.md @@ -0,0 +1,1010 @@ +## llinux服务器(以debian为例) + +### 预先准备 + +**域名购买与解析** + +购买:[Low-Cost Domain Names & Hosting from $0.99 | NameSilo](https://www.namesilo.com/?rid=c562892jb) + +教程:[【服务器、域名购买】Namesilo优惠码和域名解析教程(附带服务器购买推荐和注意事项) | 爱玩实验室](https://iwanlab.com/namesilo/) + +DNS解析:可能需等待几分钟生效 + +![](D:/folder/test/output/4da01fd4-bfe9-47bd-89fa-8bc19ffeadf7.png) + +买的域名的r2studying.top 这里的HOSTNAME相当于二级域名,如npm.r2studying.top + + + +现改为CloudFlare来做DNS解析,不用namesilo,它有两种模式: + +**Proxied(橙色云朵)**:当你将某个 DNS 记录设置为 Proxied 时,Cloudflare 会作为反向代理服务器处理该域名的 HTTP/HTTPS 流量。这样做有几个效果: + +- **隐藏真实 IP**:用户访问时看到的是 Cloudflare 的 Anycast IP,而不是你服务器的真实 IP,从而提高安全性。 +- **提供 CDN 加速**:Cloudflare 会缓存静态资源,并通过全球节点加速内容传输,提升网站响应速度。 +- **附加安全防护**:包括 DDoS 防护和 Web 应用防火墙等功能。 + +**DNS Only(灰色云朵)**:如果你设置为 DNS Only,Cloudflare 只负责 DNS 解析,不会对流量进行代理或处理。也就是说,用户访问时会直接获取并连接到你服务器的真实 IP。 + + + + + +**安全组设置** + +登录云服务器的控制台设置 + +**入站规则** + +入站规则(Inbound Rules)是指防火墙中用来控制外部流量进入服务器的规则。通过这些规则,你可以指定允许或拒绝哪些类型的网络流量(例如特定协议、端口号、IP地址等)进入你的服务器。 + +| 类型 | 协议端口 | 源地址 | 描述 | +| ---- | ----------- | --------- | ------------------------------------------------------------ | +| IPv4 | TCP : 22 | 0.0.0.0/0 | 允许外部访问安全组内实例的SSH(22)端口,用于远程登录Linux实例。 | +| IPv4 | TCP : 3389 | 0.0.0.0/0 | 允许外部访问安全组内实例的RDP(3389)端口,用于远程登录Windows实例。 | +| IPv4 | TCP : 80 | 0.0.0.0/0 | 允许外部访问安全组内实例的HTTP(80)端口,用于通过HTTP协议访问网站。 | +| IPv4 | TCP : 443 | 0.0.0.0/0 | 允许外部访问安全组内实例的HTTPS(443)端口,用于通过HTTPS协议访问网站。 | +| IPv4 | TCP : 20-21 | 0.0.0.0/0 | 允许通过FTP上传和下载文件。 | +| IPv4 | ICMP: 全部 | 0.0.0.0/0 | 允许外部使用ping命令验证安全组内实例的网络连通性。 | + +**出站规则** + +出站规则(Outbound Rules)则是指控制服务器向外部发送流量的规则。你可以通过出站规则来限制服务器发出的数据包,比如限制服务器访问某些外部服务或 IP 地址。 + + + +**凡是服务正常启动但是浏览器上无法访问的,都首先想想防火墙端口是否打开!!!** + + + + + +### 常用的命令 + +- **cat** 用于查看文本文件的内容 + +- head 查看前n行 + + ``` + head -n 100 文件名 + ``` + + + +- **touch** 新建文本文件,如touch /home/hello.py 将在home 文件夹下新建hello.py + +- **ls** 列出所有文件,但默认只是显示出最基础的文件和文件夹,如果需要更详细的信息,则使用ls -la,这将列出包括隐藏文件在内的所有文件和文件夹,并且给出对应的权限、大小和日期等信息。 + + ``` + zy123@hcss-ecs-588d:~$ ls -la + total 44 + drwxr-xr-x 6 zy123 zy123 4096 Feb 26 08:53 . + drwxr-xr-x 3 root root 4096 Feb 24 16:33 .. + -rw------- 1 zy123 zy123 6317 Feb 25 19:41 .bash_history + -rw-r--r-- 1 zy123 zy123 220 Feb 24 16:33 .bash_logout + -rw-r--r-- 1 zy123 zy123 3526 Feb 24 16:33 .bashrc + drwx------ 3 zy123 zy123 4096 Feb 24 16:35 .config + drwxr-xr-x 3 zy123 zy123 4096 Feb 24 16:36 .local + -rw-r--r-- 1 zy123 zy123 807 Feb 24 16:33 .profile + drwxr-xr-x 5 zy123 zy123 4096 Feb 26 08:53 zbparse + drwxr-xr-x 3 root root 4096 Feb 26 08:54 zbparse_output + ``` + + 权限与文件类型(第一列): + + - 第一个字符表示文件类型: + - “d”表示目录(directory) + - “-”表示普通文件(regular file) + - “l”表示符号链接(symbolic link) + - 后续的9个字符分为3组,每组三个字符,分别代表所有者、所属组和其他用户的权限(读、写、执行)。 + + 硬链接数(第二列): + 表示指向该文件的硬链接数量。对于目录来说,这个数字通常会大于1,因为“.”和“..”也算在内。 + + 所有者(第三列): + 显示该文件或目录的拥有者用户名。 + + 所属组(第四列): + 显示该文件或目录所属的用户组。 + + 文件大小(第五列): + 以字节为单位显示文件的大小。对于目录,通常显示的是目录文件占用的磁盘空间(一般为4096字节)。 + + 最后修改日期和时间(第六列): + 显示文件最后一次修改的日期和时间(可能包含月、日和具体时间或年份)。 + + 文件名或目录名(第七列): + 显示文件或目录的名称。 + +- **cd** 进入指定文件夹,如cd /home 将进入home目录。返回上层目录的命令是*cd ..*,返回刚才操作的目录的命令是*cd -* + +- **mkdir** 新建文件夹,如mkdir /home/Python 将在home 文件夹下新建一个Python 文件夹。 + +- **mv** 移动文件和文件夹,也可以用来修改名称,如: + + ``` + mv /home/hello.py /home/helloworld.py + ``` + + 将上文的hello.py重命名为helloworld.py, + + ``` + mv /home/helloworld.py /home/Python/helloworld.py + ``` + + 将helloworld.py 由home文件夹移动到了次级的Python文件夹。 + + ``` + mv /home/hello.py . + ``` + + 将/home/hello.py 移动到当前目录下 + +- **cp** 复制文件 + + ``` + cp /home/Python/hellowrold.py /home/Python/HelloWorld.py + ``` + + 将helloworld.py复制为HelloWolrd.py。注意:Linux系统严格区分大小写,helloworld.py和HelloWolrd.py是两个文件。 + +- **rm** 删除,即江湖传说中rm -rf ,r为递归,可以删除文件夹中的文件,f为强制删除。rm /home/Python/helloworld.py 可以删除刚才的helloworld.py 文件,而想删除包括Python 在内的所有文件,则是rm -rf /home/Python 。 + +- **du** -h 查看当前文件夹下,各文件、文件夹的大小,h是让文件自动使用K/M/G显示而不是只有K。“disk usage”(磁盘使用情况) + +- **grep **是用于在文件或标准输入中搜索**符合条件的行**的命令。 + + ``` + grep "pattern" filename #pattern可以是一个正则表达式 + ``` + + `-i`忽略大小写 `-n`显示行号 + +- **awk** 是一个功能强大的文本处理工具,它可以对文本文件进行分列处理、模式匹配和报告生成。它的语法类似一种简单的脚本语言。 + + ``` + awk 'pattern { action }' filename + ``` + + **pattern**:用于匹配文本的条件(可以省略,默认对所有行生效)。 + + **action**:在匹配的行上执行的操作。 + + `$0`:代表整行内容。 + + `$1, $2, ...`:代表各个字段(默认分隔符是空白字符,可以通过 `-F` 参数指定其他分隔符)。 + + ``` + awk '{print $1, $3}' filename #印指定列 + ``` + +- 管道 `|` 是将一个命令的输出直接传递给另一个命令作为输入的一种机制。 + 示例:将 grep 与 awk 联合使用:假设有一个日志文件 `access.log`,需要先用 grep 过滤出包含 "ERROR" 的行,再用 awk 提取时间字段: + + ```access.log + 127.0.0.1 27/Feb/2025:10:30:25 "GET /index.html HTTP/1.1" 200 1024 + 192.168.1.1 27/Feb/2025:10:31:45 "POST /login HTTP/1.1" 302 512 + 10.0.0.5 27/Feb/2025:10:32:10 "GET /error_page HTTP/1.1" 500 2048 ERROR + ``` + + ``` + grep 'ERROR' access.log | awk '{print $2}' + ``` + + 输出:27/Feb/2025:10:32:10 + +- **lsof**"List Open Files"显示系统中当前打开的文件。 `-i`*会显示所有正在使用网络连接的进程* + + ``` + lsof -i :80 #查看 80 端口上的进程,或者判断80端口是否被占用! + ``` + +- **usermode** 修改用户账户信息的命令 + + ``` + sudo usermod -aG docker zy123 #-aG一起用,添加zy123到group组 + ``` + +- **chmod** 命令用于修改文件或目录的权限 + + 1. 数字方式:数字方式使用三个(或四个)数字来表示所有者、组用户和其他用户的权限。每个数字代表读 (4)、写 (2)、执行 (1) 权限的和。 + + ``` + chmod 644 filename + #所有者:读 + 写 = 6 组用户:读 = 4 其他用户:读 = 4 + ``` + + 2. 符号方式 + + ``` + chmod [用户类别][操作符][权限] filename + ``` + + *用户类别:* + + - `u`:所有者(user) + - `g`:组用户(group) + - `o`:其他用户(others) + - `a`:所有用户(all),等同于 `u+g+o` + + *操作符:* + + - `+`:增加权限 + - `-`:去掉权限 + - `=`:直接设置权限 + + *权限:* + + - `r`:读权限 + - `w`:写权限 + - `x`:执行权限 + + ``` + chmod u+x filename #为所有者增加执行权限 + ``` + + + +### 文本编辑器 + +**nano**:Debian 11自带了简便易用的nano文本编辑器 + +``` +nano /etc/apt/sources.list #打开sources.list文件 +``` + +![](D:/folder/test/output/d0de0e49-ecef-4776-9a98-e000c3576326.png) + +Ctrl+O:保存修改 ->弹出询问-> Y则保存,N则不保存,ctrl+c 取消操作。 + +Ctrl+X:退出 + + + +**vim** + +**普通模式(Normal Mode):** + 打开 Vim 后默认进入普通模式。在该模式下,可以执行移动光标、删除、复制、粘贴等命令。 + +1. 光标移动: + + `0`:跳到当前行行首 + + `$`:跳到当前行行尾 + + `gg`: 将光标移到文件开头 + +2. 文本操作: + + **`dd`:删除(剪切)整行** + + `yy`:复制(拷贝)整行 + `p`:在光标后粘贴(把剪切或复制的内容贴上) + **`u`:撤销上一步操作** + `Ctrl + r`:重做上一步撤销的操作 + +**插入模式(Insert Mode):** + 在普通模式下按 `i`、`I`、`a`、`A` 等键可进入插入模式,此时可以像普通编辑器那样输入文本。 + +​ 按 `ESC` 键退出插入模式,返回普通模式。 + + +**可视模式(Visual Mode):** + 用于选中一段文本,自动进入可视模式。选中文本后可以进行复制、剪切等操作。 + +​ 在可视模式下选中后按 `d` 删除 + +​ 按 `y` 复制选中区域 + +**命令行模式(Command-Line Mode):** + 在普通模式下,输入 `:` 进入命令行模式,可以执行保存、退出、查找等命令。 + +​ `:w`:保存当前文件 + +​ **`:q`:退出 Vim(如果有未保存的修改会警告)** + +​ **`:wq` 或 `:x`:保存文件并退出** + +​ `:q!`:不保存强制退出 + +​ `:set number`:显示行号 + +​ `:set paste`:适用于代码或格式敏感的文本,确保粘贴操作不会破坏原有的布局和缩进。 + +​ **`:%d`: 删除全文** + + + +### 抓包 + +``` + sudo tcpdump -nn -i any port 1000 //查看请求端口1000的源 IP 地址 +``` + + + +### SSH + +生成密钥 + +``` +ssh-keygen -t rsa -b 4096 -C "your_email@example.com" +``` + +默认会在当前用户的主目录下的 `~/.ssh/` 文件夹中生成两个文件: + +- **私钥(id_rsa)**:必须保存在本地,切勿泄露或放到服务器上。它是用来证明你身份的重要凭证。 +- **公钥(id_rsa.pub)**:需要复制到目标服务器上,通常放入服务器用户主目录下的 `~/.ssh/authorized_keys` 文件中,用于验证连接时与私钥匹配。 + +根据我的理解: + +1.服务器从github上通过ssh拉取代码时,需要把服务器的公钥放在github上,因为拉代码时,github需要认证服务器的身份,就要用服务器给它提供的公钥加密。 + +2.通过windows上的finalshell连接linux服务器时,需要在windows环境下生成一对密钥,私钥用于ssh登录的时候输入,公钥放在linux服务器的`~/.ssh/authorized_keys`文件中。因为ssh登录的时候服务器需要验证你的身份,需要用你提供给它的公钥加密。 + +除此之外,还需要给文件和相关文件夹合适的权限: + +``` +chmod 600 authorized_keys +chmod 700 ~/.ssh +``` + + + +### 文件系统 + +在 Linux 系统中,整个文件系统从根目录 `/` 开始,下面简单介绍一些主要目录及其存放的文件类型: + +**/bin** +存放系统启动和运行时必需的用户二进制可执行文件,如常用的 shell 命令(例如 `ls`、`cp`、`mv` 等)。 + +**/boot** +包含启动加载器(如 GRUB)的配置文件和内核映像(kernel image),这些文件用于系统启动过程。 + +**/dev** +包含设备文件,这些文件代表系统中的各种硬件设备(例如硬盘、终端、USB 设备等),以及一些伪设备。 + +**/etc** +存放系统范围内的配置文件,例如网络配置、用户账户信息、服务配置等。这些文件通常由管理员维护。 + +**/home** +为普通用户提供的家目录,每个用户在这里有一个独立的子目录,存放个人文件和配置。 + +**/lib 和 /lib64** +存放系统和应用程序所需的共享库文件,这些库支持 `/bin`、`/sbin` 及其他程序的运行。 + +**/media** +通常用于挂载移动介质,如光盘、U 盘或其他可移动存储设备。 + +**/mnt** +提供一个临时挂载点,一般供系统管理员在需要手动挂载文件系统时使用。 + +**/opt** +用于安装附加的应用程序软件包,通常是第三方提供的独立软件,不与系统核心软件混合。 + +**/proc** +这是一个虚拟文件系统,提供内核和进程信息,例如系统资源使用情况、硬件信息、内核参数等。这里的文件不占用实际磁盘空间。 + +**/root** +系统管理员(root 用户)的家目录,与普通用户的 `/home` 分开存放。 + +**/run** +存储系统启动后运行时产生的临时数据,比如 PID 文件、锁文件等,系统重启后会清空该目录。 + +**/sbin** +存放系统管理和维护所需的二进制文件(系统级命令),例如网络配置、磁盘管理工具等,这些通常只由 root 用户使用。 + +**/srv** +用于存放由系统提供的服务数据,如 FTP、HTTP 服务的数据目录等。 + +**/tmp** +用于存放临时文件,许多应用程序在运行时会将临时数据写入这里,系统重启后通常会清空该目录。 + +**/usr** +存放大量用户应用程序和共享资源,其子目录包括: + +- **/usr/bin**:大部分用户命令和应用程序。 +- **/usr/sbin**:非基本系统维护工具,主要供系统管理员使用。 +- **/usr/lib**:程序库文件。 +- **/usr/share**:共享数据,如文档、图标、配置样本等。 + +**/var** +存放经常变化的数据,如日志文件、缓存、邮件、打印队列和临时应用数据等。 + + + +### Bash + +``` +#!/bin/bash + +# 定义变量,注意等号两边不能有空格 +name="World" + +# 如果脚本传入了参数,则使用第一个参数覆盖默认值 +if [ $# -ge 1 ]; then # $# 表示传入脚本的参数个数 + name=$1 # $1 表示第一个参数。 +fi + +# 输出问候语 +echo "Hello, $name!" #变量引用时要用 $ 符号,如 $name。 + +# 循环示例:遍历1到5的数字 +for i in {1..5}; do + echo "当前数字:$i" +done + +# 定义一个函数,函数名为greet +greet() { + echo "函数内问候: Hello, $1!" +} + +# 调用函数,并传入变量name作为参数 +greet $name +``` + +**如何运行?** + +赋予可执行权限 + +``` +chmod +x hello_world.sh +``` + +执行 + +``` +./hello_world.sh +或者 +./hello_world.sh Alice #传参 +``` + +在 Linux 系统中,默认情况下当前目录(`.`)并不包含在 PATH 环境变量中。这意味着,如果你在终端中直接输入脚本名(例如 `hello_world.sh`),系统不会在当前目录下查找这个脚本,而是只在 PATH 中指定的目录中查找可执行程序。使用 `./hello_world.sh` 表示“在当前目录下执行 hello_world.sh”,从而告诉系统正确的路径。 + + + +**如何设置定时任务?** + +``` +sudo crontab -e +#在里面添加: +10 0 * * * /path/toyour/xx.sh #让gpt写 +``` + + + + + +## 反向代理神器——Nginx Proxy Manager + +[【Docker系列】一个反向代理神器——Nginx Proxy Manager-我不是咕咕鸽](https://blog.laoda.de/archives/nginxproxymanager) + +**概念** + +**正向代理**是代理客户端的行为,即代理服务器代表客户端向目标服务器发出请求。客户端将自己的请求先发送给代理服务器,由代理服务器转发给真正的目标服务器,然后再将返回结果传递给客户端。 + +**特点:** + +- **保护访问者(客户端)的信息**:目标服务器只会看到代理服务器的请求,无法直接获知真正发起请求的客户端是谁。 +- **应用场景**:当客户端出于隐私、访问控制或跨越网络限制的目的,需要隐藏自己的真实IP或身份时,会使用正向代理。 + + + +**反向代理**是代理服务器代表目标服务器接收客户端请求,并将请求转发给内部的真实服务器,然后将结果返回给客户端。客户端只与代理服务器通信,而不知道背后实际处理请求的服务器。 + +**特点:** + +- **保护服务器端的信息**:客户端看不到真正的服务器细节,只知道代理服务器。这样可以隐藏真实服务器的 IP 和其他内部结构信息,从而增强安全性和负载均衡等功能。 +- **应用场景**:在大型网站或应用中,为了防止恶意攻击或实现负载均衡,通常会在真实服务器前部署一个反向代理服务器。 + + + +**docker 部署** + +``` +version: '3' +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + - '80:80' # 保持默认即可,不建议修改左侧的80 + - '81:81' # 冒号左边可以改成自己服务器未被占用的端口 + - '443:443' # 保持默认即可,不建议修改左侧的443 + volumes: + - ./data:/data # 冒号左边可以改路径,现在是表示把数据存放在在当前文件夹下的 data 文件夹中 + - ./letsencrypt:/etc/letsencrypt # 冒号左边可以改路径,现在是表示把数据存放在在当前文件夹下的 letsencrypt 文件夹中 +``` + + +NPM后台管理网站运行在81号端口,NPM服务本身监听80(http)和443(https)端口 + +**工作原理** + +**用户访问网站(80/443端口)** + +- 当用户访问 `https://blog.test.com` 时,请求到达服务器的443端口。 +- Nginx根据域名匹配代理规则(由NPM配置),将请求转发到后端服务(如 `192.168.1.100:8080`)。 + +**管理员访问后台(81端口)** + +- 管理员通过 `http://服务器IP:81` 访问NPM管理界面,配置代理规则。 +- 配置完成后,NPM会自动生成Nginx配置文件并重启Nginx服务,使新规则生效。 + + + +**两种方法实现SSL安全连接** + +**1. 开启橙云+Cloudflare Origin CA**:[网站SSL证书自动续期又又又失败了?试试CloudFlare的免费证书,15年有效期!-我不是咕咕鸽](https://blog.laoda.de/archives/try-cloudflare-free-15-year-ssl-certificate) + +- cloudflare解析DNS,开启橙云 +- Cloudflare Full (strict)模式(**灵活模式下无需以下步骤**) +- 在 Nginx Proxy Manager(NPM)添加 Cloudflare Origin CA +- 配置 Proxy Host 使其使用 + 选择刚刚添加的 Cloudflare Origin CA 证书 + 选择 Force SSL(强制 HTTPS) + 启用 HTTP/2 支持 + +Cloudflare Origin CA作用:实现cloudflare与源服务器之间的流量加密 + +**2.通配符SSL证书**:[【Docker系列】反向代理神器NginxProxyManager——通配符SSL证书申请-我不是咕咕鸽](https://blog.laoda.de/archives/nginxproxymanager-ssl-wildcardcerts-for-your-entire-domain) +更推荐第二种!!!目前正在使用,但是3个月续签一次证书! + + + +**浏览器** → **(HTTPS)** → **NPM** → **(HTTP 或 HTTPS)** → **Gitea** + +这就是反向代理常见的工作流程。 + +默认经常是 NPM 做 SSL 终止(内网用 HTTP 转发给 Gitea)。如果你想内外全程加密,就要让 NPM -> Gitea 这段也走 HTTPS,并在 Gitea 上正确配置证书。 + + + +## 正向代理(用于拉取镜像) + +下载Clash客户端:[Release Clash-Premium · DustinWin/proxy-tools](https://github.com/DustinWin/proxy-tools/releases/tag/Clash-Premium) + +或者[Index of /Linux/centOS/](https://app.chongjin01.icu/Linux/centOS/) + +我是windows上下载clashpremium-release-linux-amd64.tar.gz 然后FTP上传到linux服务器上。 + + + +下载配置文件config.yaml:每个人独一无二的 + +wget -O /home/zy123/VPN/config.yaml "https://illo1.no-mad-world.club/link/2zXAEzExPjAi6xij?clash=3" + +类似这样: + +``` +port: 7890 +socks-port: 7891 +allow-lan: false +mode: Rule //Global +log-level: info +external-controller: 127.0.0.1:9090 +unified-delay: true +hosts: + time.facebook.com: 17.253.84.125 + time.android.com: 17.253.84.125 +dns: + enable: true + use-hosts: true + nameserver: + - 119.29.29.29 + - 223.5.5.5 + - 223.6.6.6 + - tcp://223.5.5.5 + - tcp://223.6.6.6 + - tls://dns.google:853 + - tls://8.8.8.8:853 + - tls://8.8.4.4:853 + - tls://dns.alidns.com + - tls://223.5.5.5 + - tls://223.6.6.6 + - tls://dot.pub + - tls://1.12.12.12 + - tls://120.53.53.53 + - https://dns.google/dns-query + - https://8.8.8.8/dns-query + - https://8.8.4.4/dns-query + - https://dns.alidns.com/dns-query + - https://223.5.5.5/dns-query + - https://223.6.6.6/dns-query + - https://doh.pub/dns-query + - https://1.12.12.12/dns-query + - https://120.53.53.53/dns-query + default-nameserver: + - 119.29.29.29 + - 223.5.5.5 + - 223.6.6.6 + - tcp://119.29.29.29 + - tcp://223.5.5.5 + - tcp://223.6.6.6 +proxies: + - {name: 🇭🇰 香港Y01, server: qvhh1-g03.hk01-ae5.entry.v50307shvkaa.art, port: 19273, type: ss, cipher: aes-256-gcm, password: 6e4124c4-456e-36a3-b144-c0e1a618d04c, udp: true} +``` + + + +注意,魔戒vpn给的是一个订阅地址!!!还需要解码 + +简便方法:windows上将订阅链接导入,自动解析成yaml配置文件,然后直接把该文件传到服务器上! + +![image-20250309173756312](D:\folder\test\output\image-20250309173756312.png) + + + +**启动Clash** + +``` +./CrashCore -d . & //后台启动 +``` + +**为Clash创建服务** + +1.创建 `systemd` 服务文件 + +``` +sudo vim /etc/systemd/system/clash.service +``` + +2.在文件中添加以下内容: + +``` +[Unit] +Description=Clash Proxy Service +After=network.target + +[Service] +ExecStart=/home/zy123/VPN/CrashCore -d /home/zy123/VPN +WorkingDirectory=/home/zy123/VPN +Restart=always +User=zy123 +Group=zy123 + +[Install] +WantedBy=multi-user.target +``` + +这段配置将 Clash 配置为: + +- 在网络服务启动后运行。 +- 在启动时自动进入后台,执行 `CrashCore` 服务。 +- 如果服务意外停止,它将自动重启。 +- 以 `zy123` 用户身份运行 Clash。 + +启动服务: + +``` +sudo systemctl start clash +``` + +停止服务: + +``` +sudo systemctl stop clash +``` + +查看服务状态: + +``` +sudo systemctl status clash +``` + + + +**配置YACD** + +YACD 是一个基于 **Clash** 的 Web 管理面板,用于管理您的 Clash 配置、查看流量和节点信息等。 + +**直接用现成的**:https://yacd.haishan.me/ + +**服务器上部署**:目前是npm手动构建安装启动的。 + +0. 下载yacd + + ``` + git clone https://github.com/haishanh/yacd.git + ``` + +1. 安装npm + +2. 安装pnpm + +3. 构建yacd + + ``` + cd ~/VPN/yacd + pnpm install + pnpm build + ``` + +4. 启动yacd + + ``` + nohup pnpm serve --host 0.0.0.0 & //如果不是0.0.0.0 不能在windows上打开 + ``` + + + +通过http://124.71.159.195:4173/,手动添加crash服务所在的ip:端口 + + + +使用代理:curl -x http://127.0.0.1:7890 https://www.google.com + + + +## File Browser 文件分享 + +[Docker 部署 File Browser 文件管理系统_filebrowser docker-CSDN博客](https://blog.csdn.net/qq_41906909/article/details/144726676) + +1.创建数据目录 + +``` +mkdir -p /data/filebrowser/{srv,config,db} +``` + +2.目录授权 + +``` +chmod -R 777 /data/filebrowser/ +``` + +3.编辑 docker-compose.yaml 文件 + +``` +version: '3' +services: + filebrowser: + image: filebrowser/filebrowser:s6 + container_name: filebrowser + restart: always + ports: + - "2000:80" # 将容器的80端口映射到宿主机2000端口 + volumes: + - /data/filebrowser/srv:/srv #保存用户上传的文件 + - /data/filebrowser/config:/config # 配置文件存储路径 + - /data/filebrowser/db:/database #数据库存储路径 + +``` + + + +## Gitea + +[Gitea Docker 安装与使用详解:轻量级自托管 Git 服务教程-CSDN博客](https://blog.csdn.net/m0_70878103/article/details/144908188) + +``` +version: "3" +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + ports: + - "3000:3000" # 将宿主机的3000端口映射到容器的3000端口 + volumes: + - /data/gitea:/data # 持久化存储Gitea数据(包括仓库、配置、日志等) + +``` + + + +## EasyImage + +[【好玩儿的Docker项目】10分钟搭建一个简单图床——Easyimage-我不是咕咕鸽](https://blog.laoda.de/archives/docker-compose-install-easyimage) + +github地址:[icret/EasyImages2.0: 简单图床 - 一款功能强大无数据库的图床 2.0版](https://github.com/icret/EasyImages2.0) + +``` +sudo -i # 切换到root用户 + +apt update -y # 升级packages + +apt install wget curl sudo vim git # Debian系统比较干净,安装常用的软件 + +``` + + + +``` +version: '3.3' +services: + easyimage: + image: ddsderek/easyimage:latest + container_name: easyimage + ports: + - '1000:80' + environment: + - TZ=Asia/Shanghai + - PUID=1000 + - PGID=1000 + volumes: + - '/root/data/docker_data/easyimage/config:/app/web/config' + - '/root/data/docker_data/easyimage/i:/app/web/i' + restart: unless-stopped + +``` + +网页打开显示bug: + +``` +cd /data/easyimage/config/config.php +``` + +![image-20250315101454031](D:\folder\test\output\image-20250315101454031.png) + +这里添加上https + + + +网站域名 图片域名设置可以改变图片的url:IP或域名 + +![image-20250306183642549](D:\folder\test\output\image-20250306183642549.png) + + + +**picgo安装:** + +[Releases · Molunerfinn/PicGo](https://github.com/Molunerfinn/PicGo/releases) + +1.右下角小窗打开 + +2.插件设置,搜索web-uploader 1.1.1 (自定义web图床) + 旧版有搜索不出来的情况!建议直接安装最新版! + +3.配置如下,API地址从easyimage-设置-API设置中获取![image-20250306141151376](D:\folder\test\output\image-20250306141151376.png) + + + +**typora设置** + +左上角文件-偏好设置-图像-插入图片时{ + +**上传图片**-picgo服务器-填写picgo安装路径 + +} + +ps:还可以选择上传到./assets,每个md文件独立 + +或者上传到指定路径如/image,多个md文件共享 + + + +py脚本1:将所有md文件中的图片路径改为本地,统一保存到**本地**output文件夹中 + +py脚本2:将每个md文件及其所需图片单独保存,保存到**本地**,但每个md文件有自己独立的assets文件夹 + +py脚本3:将本地图片上传到easyimage图床并将链接返回替换md文件中的本地路径 + + + + + +## Typecho + +[【好玩儿的Docker项目】10分钟搭建一个Typecho博客|太破口!念念不忘,必有回响!-我不是咕咕鸽](https://blog.laoda.de/archives/docker-compose-install-typecho) + +``` +version: "3" + +services: + nginx: + image: nginx + ports: + - "4000:80" # 左边可以改成任意没使用的端口 + restart: always + environment: + - TZ=Asia/Shanghai + volumes: + - ./typecho:/var/www/html + - ./nginx:/etc/nginx/conf.d + - ./logs:/var/log/nginx + depends_on: + - php + networks: + - web + + php: + build: php + restart: always + expose: + - "9000" # 不暴露公网,故没有写9000:9000 + volumes: + - ./typecho:/var/www/html + environment: + - TZ=Asia/Shanghai + depends_on: + - mysql + networks: + - web + + mysql: + image: mysql:5.7 + restart: always + environment: + - TZ=Asia/Shanghai + expose: + - "3306" # 不暴露公网,故没有写3306:3306 + volumes: + - ./mysql/data:/var/lib/mysql + - ./mysql/logs:/var/log/mysql + - ./mysql/conf:/etc/mysql/conf.d + env_file: + - mysql.env + networks: + - web + +networks: + web: + +``` + + + +卸载: + +``` +sudo -i # 切换到root + +cd /root/data/docker_data/typecho # 进入docker-compose所在的文件夹 + +docker-compose down # 停止容器,此时不会删除映射到本地的数据 + +cd ~ + +rm -rf /root/data/docker_data/typecho # 完全删除映射到本地的数据 +``` + + + +主题:https://github.com/HaoOuBa/Joe + + + +## qBittorrent + +[【好玩的Docker项目】10分钟搭建你专属的下载神器——qbittorrent-我不是咕咕鸽](https://blog.laoda.de/archives/docker-install-qbittorrent) + +``` +docker pull linuxserver/qbittorrent +``` + +``` +cd ~ +mkdir /root/data/docker_data/qBittorrent #创建qbitorrent数据文件夹 +cd /root/data/docker_data/qBittorrent +mkdir config downloads #创建配置文件目录与下载目录 +nano docker-compose.yml #创建并编辑文件 + +``` + +``` +services: + qbittorrent: + image: linuxserver/qbittorrent + container_name: qbittorrent + environment: + - PUID=1000 + - PGID=1000 + - TZ=Asia/Shanghai # 你的时区 + - UMASK_SET=022 + - WEBUI_PORT=8081 # 将此处修改成你欲使用的 WEB 管理平台端口 + volumes: + - ./config:/config # 绝对路径请修改为自己的config文件夹 + - ./downloads:/downloads # 绝对路径请修改为自己的downloads文件夹 + ports: + # 要使用的映射下载端口与内部下载端口,可保持默认,安装完成后在管理页面仍然可以改成其他端口。 + - 6881:6881 + - 6881:6881/udp + # 此处WEB UI 目标端口与内部端口务必保证相同,见问题1 + - 8081:8081 + restart: unless-stopped + +``` + diff --git a/Java/mongodb_base.pdf b/Java/mongodb_base.pdf new file mode 100644 index 0000000..b9d665d Binary files /dev/null and b/Java/mongodb_base.pdf differ diff --git a/Java/test.md b/Java/test.md new file mode 100644 index 0000000..a988038 --- /dev/null +++ b/Java/test.md @@ -0,0 +1,82 @@ +```c +int main(){ + return 0; +} +``` + +# ![测试](D:/folder/test/output/96115587-d917-43bf-b965-886379af8fda.png) + +# 一级标题:# xx + +## 二级标题:## xx + +无序列表:- xx + +- 1 +- 2 + +有序列表:1. xx + +1. first +2. second + +任务列表:- [ ] xx + +- [ ] xx +- [ ] xxx + +斜体:星号xx星号 + +*斜体* +加粗:星号星号xx星号星号 + +**加粗** + +删除:波浪线波浪线xx波浪线波浪线 + +~~删除~~ + +--- + + 分割线:--- +$$ +\frac{\partial f}{\partial x}=2\sqrt{a}x +$$ +注释: + +[百度](www.baidu.com "一个搜索引擎") + +[谷歌][id] + +[id]: google.com ""谷歌"" + +[请参考标题一](#一级标题:# xx ) + +![ww]() + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Java/zbparse/md_files/0.png b/Java/zbparse/md_files/0.png new file mode 100644 index 0000000..fff6000 Binary files /dev/null and b/Java/zbparse/md_files/0.png differ diff --git a/Java/zbparse/md_files/1.gif b/Java/zbparse/md_files/1.gif new file mode 100644 index 0000000..79cce4c Binary files /dev/null and b/Java/zbparse/md_files/1.gif differ diff --git a/Java/zbparse/md_files/1.png b/Java/zbparse/md_files/1.png new file mode 100644 index 0000000..b919152 Binary files /dev/null and b/Java/zbparse/md_files/1.png differ diff --git a/Java/zbparse/md_files/10.png b/Java/zbparse/md_files/10.png new file mode 100644 index 0000000..ff65b42 Binary files /dev/null and b/Java/zbparse/md_files/10.png differ diff --git a/Java/zbparse/md_files/11.png b/Java/zbparse/md_files/11.png new file mode 100644 index 0000000..5814aa4 Binary files /dev/null and b/Java/zbparse/md_files/11.png differ diff --git a/Java/zbparse/md_files/12.png b/Java/zbparse/md_files/12.png new file mode 100644 index 0000000..6c25a0d Binary files /dev/null and b/Java/zbparse/md_files/12.png differ diff --git a/Java/zbparse/md_files/13.png b/Java/zbparse/md_files/13.png new file mode 100644 index 0000000..08346e9 Binary files /dev/null and b/Java/zbparse/md_files/13.png differ diff --git a/Java/zbparse/md_files/14.png b/Java/zbparse/md_files/14.png new file mode 100644 index 0000000..eea2ca3 Binary files /dev/null and b/Java/zbparse/md_files/14.png differ diff --git a/Java/zbparse/md_files/16.png b/Java/zbparse/md_files/16.png new file mode 100644 index 0000000..42a2265 Binary files /dev/null and b/Java/zbparse/md_files/16.png differ diff --git a/Java/zbparse/md_files/17.png b/Java/zbparse/md_files/17.png new file mode 100644 index 0000000..a179f55 Binary files /dev/null and b/Java/zbparse/md_files/17.png differ diff --git a/Java/zbparse/md_files/18.png b/Java/zbparse/md_files/18.png new file mode 100644 index 0000000..41adf33 Binary files /dev/null and b/Java/zbparse/md_files/18.png differ diff --git a/Java/zbparse/md_files/19.png b/Java/zbparse/md_files/19.png new file mode 100644 index 0000000..2c1f426 Binary files /dev/null and b/Java/zbparse/md_files/19.png differ diff --git a/Java/zbparse/md_files/2.png b/Java/zbparse/md_files/2.png new file mode 100644 index 0000000..a3b3c77 Binary files /dev/null and b/Java/zbparse/md_files/2.png differ diff --git a/Java/zbparse/md_files/20.png b/Java/zbparse/md_files/20.png new file mode 100644 index 0000000..22ad0b5 Binary files /dev/null and b/Java/zbparse/md_files/20.png differ diff --git a/Java/zbparse/md_files/21.png b/Java/zbparse/md_files/21.png new file mode 100644 index 0000000..6dd6cf4 Binary files /dev/null and b/Java/zbparse/md_files/21.png differ diff --git a/Java/zbparse/md_files/22.png b/Java/zbparse/md_files/22.png new file mode 100644 index 0000000..84a71a6 Binary files /dev/null and b/Java/zbparse/md_files/22.png differ diff --git a/Java/zbparse/md_files/23.png b/Java/zbparse/md_files/23.png new file mode 100644 index 0000000..b1c2c5c Binary files /dev/null and b/Java/zbparse/md_files/23.png differ diff --git a/Java/zbparse/md_files/24.png b/Java/zbparse/md_files/24.png new file mode 100644 index 0000000..f45a8c0 Binary files /dev/null and b/Java/zbparse/md_files/24.png differ diff --git a/Java/zbparse/md_files/25.png b/Java/zbparse/md_files/25.png new file mode 100644 index 0000000..ef6b4c4 Binary files /dev/null and b/Java/zbparse/md_files/25.png differ diff --git a/Java/zbparse/md_files/26.png b/Java/zbparse/md_files/26.png new file mode 100644 index 0000000..0245e76 Binary files /dev/null and b/Java/zbparse/md_files/26.png differ diff --git a/Java/zbparse/md_files/27.png b/Java/zbparse/md_files/27.png new file mode 100644 index 0000000..6fd4694 Binary files /dev/null and b/Java/zbparse/md_files/27.png differ diff --git a/Java/zbparse/md_files/28.png b/Java/zbparse/md_files/28.png new file mode 100644 index 0000000..b40add6 Binary files /dev/null and b/Java/zbparse/md_files/28.png differ diff --git a/Java/zbparse/md_files/29.png b/Java/zbparse/md_files/29.png new file mode 100644 index 0000000..9d40b53 Binary files /dev/null and b/Java/zbparse/md_files/29.png differ diff --git a/Java/zbparse/md_files/3.png b/Java/zbparse/md_files/3.png new file mode 100644 index 0000000..23a6c6c Binary files /dev/null and b/Java/zbparse/md_files/3.png differ diff --git a/Java/zbparse/md_files/30.png b/Java/zbparse/md_files/30.png new file mode 100644 index 0000000..021d93e Binary files /dev/null and b/Java/zbparse/md_files/30.png differ diff --git a/Java/zbparse/md_files/31.png b/Java/zbparse/md_files/31.png new file mode 100644 index 0000000..9ba1d7d Binary files /dev/null and b/Java/zbparse/md_files/31.png differ diff --git a/Java/zbparse/md_files/32.png b/Java/zbparse/md_files/32.png new file mode 100644 index 0000000..a7c59a4 Binary files /dev/null and b/Java/zbparse/md_files/32.png differ diff --git a/Java/zbparse/md_files/33.png b/Java/zbparse/md_files/33.png new file mode 100644 index 0000000..30d9582 Binary files /dev/null and b/Java/zbparse/md_files/33.png differ diff --git a/Java/zbparse/md_files/34.png b/Java/zbparse/md_files/34.png new file mode 100644 index 0000000..c88aa62 Binary files /dev/null and b/Java/zbparse/md_files/34.png differ diff --git a/Java/zbparse/md_files/35.png b/Java/zbparse/md_files/35.png new file mode 100644 index 0000000..4a97aa7 Binary files /dev/null and b/Java/zbparse/md_files/35.png differ diff --git a/Java/zbparse/md_files/4.png b/Java/zbparse/md_files/4.png new file mode 100644 index 0000000..2e019f5 Binary files /dev/null and b/Java/zbparse/md_files/4.png differ diff --git a/Java/zbparse/md_files/5.png b/Java/zbparse/md_files/5.png new file mode 100644 index 0000000..2cfd811 Binary files /dev/null and b/Java/zbparse/md_files/5.png differ diff --git a/Java/zbparse/md_files/6.png b/Java/zbparse/md_files/6.png new file mode 100644 index 0000000..1fe34a5 Binary files /dev/null and b/Java/zbparse/md_files/6.png differ diff --git a/Java/zbparse/md_files/7.png b/Java/zbparse/md_files/7.png new file mode 100644 index 0000000..b309666 Binary files /dev/null and b/Java/zbparse/md_files/7.png differ diff --git a/Java/zbparse/md_files/8.png b/Java/zbparse/md_files/8.png new file mode 100644 index 0000000..a5289cf Binary files /dev/null and b/Java/zbparse/md_files/8.png differ diff --git a/Java/zbparse/md_files/9.png b/Java/zbparse/md_files/9.png new file mode 100644 index 0000000..039a875 Binary files /dev/null and b/Java/zbparse/md_files/9.png differ diff --git a/Java/zbparse/招标文件解析.md b/Java/zbparse/招标文件解析.md new file mode 100644 index 0000000..47c7470 --- /dev/null +++ b/Java/zbparse/招标文件解析.md @@ -0,0 +1,1233 @@ +产品官网:[智标领航 - 招投标AI解决方案](https://intellibid.cn/home) + +产品后台:https://intellibid.cn:9091/login?redirect=%2Findex + +项目地址:[zy123/zbparse - zbparse - 智标领航代码仓库](http://47.98.59.178:3000/zy123/zbparse) + +git clone地址:http://47.98.59.178:3000/zy123/zbparse.git + +选择develop分支,develop-xx 后面的xx越近越新。 + +正式环境:121.41.119.164:5000 + +测试环境:47.98.58.178:5000 + +大解析:指从招标文件解析入口进去,upload.py + +小解析:从投标文件生成入口进去,little_zbparse 和get_deviation,两个接口后端一起调 + +## 项目启动与维护: + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5tks-2.png) + +.env存放一些密钥(大模型、textin等),它是gitignore忽略了,因此在服务器上git pull项目的时候,这个文件不会更新(因为密钥比较重要),需要手动维护服务器相应位置的.env。 + +### **如何更新服务器上的版本:** + +#### 步骤 + +1. 进入项目文件夹 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5rfk-2.png) + +**注意:**需要确认.env是否存在在服务器,默认是隐藏的 +输入cat .env +如果不存在,在项目文件夹下sudo vim .env + +将密钥粘贴进去!!! + +2. git pull + +3. sudo docker-compose up --build -d 更新并重启 + +​ 或者 sudo docker-compose build 先构建镜像 + +​ sudo docker-compose up -d 等空间时再重启 + +4. sudo docker-compose logs flask_app --since 1h 查看最近1h的日志(如果重启后报错也能查看,推荐重启后都运行一下这个) + +requirements.txt一般无需变动,除非代码中使用了新的库,也要手动在该文件中添加包名及对应的版本 + + + +#### docker基础知识 + +**docker-compose:** + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5n10-2.png) + +本项目为**单服务项目**,只有flask_app(服务名) + +build context(`context: .`): +这是在构建镜像时提供给 Docker 的文件集,指明哪些文件可以被 Dockerfile 中的 `COPY` 或 `ADD` 指令使用。它是构建过程中的“资源包”。 + +对于多服务,build下就要针对不同的服务,指定所需的“资源包”和对应的Dockerfile + + + +**dockerfile:** + +![1](http://124.71.159.195:1000/i/2025/03/06/tz69oi-2.png) + +COPY . .(在 Dockerfile 中): +这条指令会将构建上下文中的所有内容复制到镜像中的当前工作目录(这里是 `/flask_project`)。 + +``` +docker exec -it zbparse-flask_app-1 sh +``` + +这个命令会直接进入到flask_project目录内部ls之后可以看到: + +``` +Dockerfile README.md docker-compose.yml flask_app md_files requirements.txt + +``` + +如果这个基础上再` cd / `会切换到这个容器的根目录,可以看到flask_project文件夹以及其他基础系统环境。如: + +``` +bin boot dev etc flask_project home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var + +``` + +**数据卷挂载:** + + volumes: +-/home/Z/zbparse_output_dev:/flask_project/flask_app/static/output *#* *额外的数据卷挂载* + +本地路径:容器内路径 都从根目录找起。 + + + +**完整的容器名** + +``` +<项目名>-<服务名>-<序号> +``` + +**项目名**:默认是当前目录的名称(这里是 `zbparse`),或者你在启动 Docker Compose 时通过 `-p` 参数指定的项目名称。 + +**服务名**:在 `docker-compose.yml` 文件中定义的服务名称(这里是 `flask_app`)。 + +**序号**:如果同一个服务启动了多个容器,会有数字序号来区分(这里是 `1`)。 + +docker-compose exec **flask_app** sh + +docker exec -it **zbparse-flask_app-1** sh + +这两个是等价的,因为docker-compose 会自动找到对应的完整容器名并执行命令。 + + + +**删除所有悬空镜像**(无容器引用的 `` 镜像) + +``` +docker image prune +``` + + + +### **如何本地启动本项目:** + +**Pycharm启动** + +1. requirements.txt里的环境要配好 + conda create -n zbparse python=3.8 + conda activate zbparse + pip install -r requirements.txt +2. .env环境配好 (一般不需要在电脑环境变量中额外配置了,但是要在Pycharm中**安装插件**,使得项目在**启动时**能将env中的环境变量**自动配置**到系统环境变量中!!!) +3. 点击下拉框,Edit configurations + + ![1](http://124.71.159.195:1000/i/2025/03/06/tz691k-2.png) + +​ 设置run_serve.py为启动脚本![1](http://124.71.159.195:1000/i/2025/03/06/tz5wy8-2.png) +​ 注意这里的working directory要设置到最外层文件夹,而不是flask_app!!! + + + +**命令行启动** + +1.编写ps1脚本 + +``` +# 切换到指定目录 +cd D:\PycharmProjects\zbparse + +# 激活 Conda 环境 +conda activate zbparse + +# 检查是否存在 .env 文件 +if (Test-Path .env) { + # 读取 .env 文件并设置环境变量 + Get-Content .env | ForEach-Object { + if ($_ -match '^\s*([^=]+)=(.*)') { + $name = $matches[1].Trim() + $value = $matches[2].Trim() + [System.Environment]::SetEnvironmentVariable($name, $value) + } + } +} else { + Write-Host ".env not find" +} + +# 设置 PYTHONPATH 环境变量 +$env:PYTHONPATH = "D:\flask_project" + +# 运行 Python 脚本 +python flask_app\run_serve.py +``` + +`$env:PYTHONPATH = "D:\flask_project"`,告诉 Python 去 D:\flask_project 查找模块,这样就能让 Python 找到你的 `flask_app` 包。 + + + +2.确保conda已添加到系统环境变量 + +- 打开 Anaconda Prompt,然后输入 `where conda` 来查看 conda 的路径。 + +- ``` + 打开系统环境变量Path,添加一条:C:\ProgramData\anaconda3\condabin + + 或者 CMD 中 set PATH=%PATH%;新添加的路径 + ``` + +- 重启终端可以刷新环境变量 + +3.如果你尚未在 PowerShell 中初始化 conda,可以在 Anaconda Prompt 中运行: + +``` +conda init powershell +``` + +4.进入到存放run.ps1文件的目录,在搜索栏中输入powershell + +5.默认情况下,PowerShell 可能会阻止运行脚本。你可以调整执行策略: + +``` +Set-ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + + + +6.运行脚本 + +``` +.\run.ps1 +``` + +**注意!!!** + +Windows 控制台存在QuickEdit 模式,在 QuickEdit 模式下,当你在终端窗口中点击(尤其是拖动或选中内容)时,控制台会进入文本选择状态,从而暂停正在运行的程序!! + +**禁用 QuickEdit 模式** + +- 在 PowerShell 窗口标题栏上点击右键,选择“属性”。 +- 在“选项”选项卡中,取消勾选“快速编辑模式”。 +- 点击“确定”,重启 PowerShell 窗口后再试。 + + + +### 模拟用户请求 + +postman打**post**请求测试: + +http://127.0.0.1:5000/upload + +body: + +{ + + "file_url":"xxxx", + + "zb_type":2 + +} +file_url如何获取:[OSS管理控制台](https://oss.console.aliyun.com/bucket/oss-cn-wuhan-lr/bid-assistance/object?path=test%2F) + +bid-assistance/test 里面找个文件的url,推荐'094定稿-湖北工业大学xxx' +注意这里的url地址有时效性,要经常重新获取新的url + + + +### 清理服务器上的文件夹 + +1. 编写shell文件,sudo vim clean_dir.sh + 命名为clean_dir.sh + +清理/home/Z/zbparse_output_dev下的output1这些二级目录下的c8d2140d-9e9a-4a49-9a30-b53ba565db56这种uuid的三级目录(只保留最近7天)。 + +``` +#!/bin/bash + +# 需要清理的 output 目录路径 +ROOT_DIR="/home/Z/zbparse_output_dev" + +# 检查目标目录是否存在 +if [ ! -d "$ROOT_DIR" ]; then + echo "目录 $ROOT_DIR 不存在!" + exit 1 +fi + +echo "开始清理 $ROOT_DIR 下超过 7 天的目录..." +echo "以下目录将被删除:" + +# -mindepth 2 表示从第二层目录开始查找,防止删除 output 下的直接子目录(如 output1、output2) +# -depth 采用深度优先遍历,确保先处理子目录再处理父目录 +find "$ROOT_DIR" -mindepth 2 -depth -type d -mtime +7 -print -exec rm -rf {} \; + +echo "清理完成。" + +``` + +2. 添加权限。 + +``` +sudo chmod +x ./clean_dir.sh +``` + +3. 执行 + +``` +sudo ./clean_dir.sh +``` + +4. 以 root 用户的身份编辑 crontab 文件,从而设置或修改系统定时任务(cron jobs)。每天零点10分清理 + +``` +sudo crontab -e +在里面添加: + +10 0 * * * /home/Z/clean_dir.sh +``` + +**目前测试服务器和正式服务器都写上了!无需变动** + + + +### 内存泄漏问题 + +#### 问题定位 + +**查看容器运行时占用的文件FD套接字FD等**(排查内存泄漏,长期运行这三个值不会很大) + +``` +[Z@iZbp13rxxvm0y7yz7l02hbZ zbparse]$ docker exec -it zbparse-flask_app-1 sh + +ls -l /proc/1/fd | awk ' +BEGIN { + file=0; socket=0; pipe=0; other=0 +} +{ + if(/socket:/) socket++ + else if(/pipe:/) pipe++ + else if(/\/|tmp/) file++ # 识别文件路径特征 + else other++ +} +END { + print "文件FD:", file + print "套接字FD:", socket + print "管道FD:", pipe + print "其他FD:", other +}' +``` + +**可以发现文件FD很大,基本上发送一个请求文件FD就加一,且不会衰减:** + +经排查,@validate_and_setup_logger注解会为每次请求都创建一个logger,需要在@app.teardown_request中获取与本次请求有关的logger并释放。 + +``` +def create_logger(app, subfolder): + """ + 创建一个唯一的 logger 和对应的输出文件夹。 + + 参数: + subfolder (str): 子文件夹名称,如 'output1', 'output2', 'output3' + """ + unique_id = str(uuid.uuid4()) + g.unique_id = unique_id + output_folder = os.path.join("flask_app", "static", "output", subfolder, unique_id) + os.makedirs(output_folder, exist_ok=True) + log_filename = "log.txt" + log_path = os.path.join(output_folder, log_filename) + logger = logging.getLogger(unique_id) + if not logger.handlers: + file_handler = logging.FileHandler(log_path) + file_formatter = CSTFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(logging.Formatter('%(message)s')) + logger.addHandler(stream_handler) + logger.setLevel(logging.INFO) + logger.propagate = False + g.logger = logger + g.output_folder = output_folder #输出文件夹路径 +``` + +handler:每当 logger 生成一条日志信息时,这条信息会被传递给所有关联的 handler,由 handler 决定如何输出这条日志。例如,`FileHandler` 会把日志写入文件,而 `StreamHandler` 会将日志输出到控制台。 + +`logger.setLevel(logging.INFO)` :它设置了 logger 的日志级别阈值。Logger **只会处理大于或等于 INFO 级别**的日志消息(例如 INFO、WARNING、ERROR、CRITICAL),而 DEBUG 级别的消息会被忽略。 + + + +**解决这个文件句柄问题后内存泄漏仍未解决,考虑分模块排查。** + +本项目结构大致是**1.**预处理(文件读取切分) **2.**并发调用5个函数分别调用大模型获取结果。 + +因此排查思路: + +先将**预处理模块单独拎出来**作为接口,上传文件测试。 + +文件一般几MB,首先会读到内存,再处理,必然会占用很多内存,且它是调用每个接口都会经历的环节(little_zbparse/upload等) + + + +**内存泄漏排查工具** + +pip install **memory_profiler** + +``` +from memory_profiler import memory_usage +import time +@profile +def my_function(): + a = [i for i in range(100000)] + time.sleep(1) # 模拟耗时操作 + b = {i: i*i for i in range(100000)} + time.sleep(1) + return a, b + +# 监控函数“运行前”和“运行后”的内存快照 +mem_before = memory_usage()[0] +result=my_function() +mem_after = memory_usage()[0] + print(f"Memory before: {mem_before} MiB, Memory after: {mem_after} MiB") +``` + +@profile注解加在函数上,可以逐行分析内存增减情况。 + +memory_usage()[0] 可以获取当前程序所占内存的**快照** + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5feb-2.png) + +产生的数据都存到result变量-》内存中,这是正常的,因此my_function没有内存泄漏问题。 +**但是** + +``` +@profile +def extract_text_by_page(file_path): + result = "" + with open(file_path, 'rb') as file: + reader =PdfReader(file) + num_pages = len(reader.pages) + # print(f"Total pages: {num_pages}") + for page_num in range(num_pages): + page = reader.pages[page_num] + text = page.extract_text() + return "" +``` + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5mah-2.png) + +可以发现尽管我返回"",内存仍然没有释放!因为就是读取pdf这块发生了内存泄漏! + + + +**tracemalloc** + +``` +def extract_text_by_page(file_path): + result = "" + with open(file_path, 'rb') as file: + reader =PdfReader(file) + num_pages = len(reader.pages) + # print(f"Total pages: {num_pages}") + for page_num in range(num_pages): + page = reader.pages[page_num] + text = page.extract_text() + return result + +# 开始跟踪内存分配 +tracemalloc.start() +# 捕捉函数调用前的内存快照 +snapshot_before = tracemalloc.take_snapshot() +# 调用函数 +file_path=r'C:\Users\Administrator\Desktop\fsdownload\00550cfc-fd33-469e-8272-9215291b175c\ztbfile.pdf' +result = extract_text_by_page(file_path) +# 捕捉函数调用后的内存快照 +snapshot_after = tracemalloc.take_snapshot() +# 比较两个快照,获取内存分配差异信息 +stats = snapshot_after.compare_to(snapshot_before, 'lineno') +print("[ Top 10 内存变化 ]") +for stat in stats[:10]: + print(stat) +# 停止内存分配跟踪 +tracemalloc.stop() +``` + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5rty-2.png) + +tracemalloc能更深入的分析,不仅是自己写的代码,**调用的库函数**产生的内存也能分析出来。在这个例子中就是PyPDF2中的各个函数占用了大部分内存。 + +**综上,定位到问题,就是读取PDF,使用PyPDF2库的地方** + + + +#### **如何解决:** + +1. 首先尝试用with open打开文件,代替直接使用 + +``` +reader =PdfReader(file_path) +``` + +能够确保文件正常关闭。但是没有效果。 + +2. 考虑为**每次请求开子进程**处理,有效**隔离内存泄漏**导致的资源占用,这样子进程运行结束后会释放资源。 + +3. 但是解析流程是流式/分段返回的,因此还需处理: + +**_child_target** 是一个“桥梁”: + +- 它在子进程内调用 `goods_bid_main(...)` (你的生成器) 并把每一次 `yield` 得到的数据放进队列。 +- 结束时放一个 `None` 表示没有更多数据。 + +**run_in_subprocess** 是主进程使用的接口,开启子进程: + +- 它启动子进程并实时 `get()` 队列数据,然后 `yield` 给外界调用者。 +- 当队列里读到 `None`,说明子进程运行完毕,就 `break` 循环并 `p.join()`。 + +**main_func**是真正执行的函数!!! + +``` +def _child_target(main_func, queue, output_folder, file_path, file_type, unique_id): + """ + 子进程中调用 `main_func`(它是一个生成器函数), + 将其 yield 出的数据逐条放进队列,最后放一个 None 表示结束。 + """ + try: + for data in main_func(output_folder, file_path, file_type, unique_id): + queue.put(data) + except Exception as e: + # 如果要把异常也传给父进程,以便父进程可感知 + queue.put(json.dumps({'error': str(e)}, ensure_ascii=False)) + finally: + queue.put(None) +def run_in_subprocess(main_func, output_folder, file_path, file_type, unique_id): + """ + 启动子进程调用 `main_func(...)`,并在父进程流式获取其输出(通过 Queue)。 + 子进程结束时,操作系统回收其内存;父进程则保持实时输出。 + """ + queue = multiprocessing.Queue() + p = multiprocessing.Process( + target=_child_target, + args=(main_func, queue, output_folder, file_path, file_type, unique_id) + ) + p.start() + + while True: + item = queue.get() # 阻塞等待子进程产出的数据 + if item is None: + break + yield item + + p.join() +``` + +如果开子线程,线程共享同一进程的内存空间,所以如果发生内存泄漏,泄漏的内存会累积在整个进程中,影响所有线程。 + +开子进程的缺点:多进程通常消耗的系统资源(如内存、启动开销)比多线程要大,因为每个进程都需要独立的资源和上下文切换开销。 + + + +**进程池** + +在判断上传的文件是否为招标文件时,需要快速准确地响应。因此既保证**内存不泄漏**,又**保证速度**的方案就是在项目启动时创建进程池。(因为**创建进程需要耗时2到3秒**!) + +如果是Waitress服务器启动,这里的进程池是全局共享的;但如果Gunicorn启动,每个请求分配一个worker进程,进程池是在worker里面共享的!!! + +``` +#创建app,启动时 +def create_app(): + # 创建全局日志记录器 + app = Flask(__name__) + app.process_pool = Pool(processes=10, maxtasksperchild=3) + app.global_logger = create_logger_main('model_log') # 全局日志记录器 + +#调用时 +pool = current_app.process_pool # 使用全局的进程池 +def judge_zbfile_exec_sub(file_path): + result = pool.apply( + judge_zbfile_exec, # 你的实际执行函数 + args=(file_path,) + ) + return result +``` + +但是存在一个问题:**第一次发送请求执行时间较慢!** + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5abr-2.png) + +可以发现实际执行只需7.7s,但是接口实际耗时10.23秒,主要是因**懒加载或按需初始化**:有些模块或资源在子进程启动时并不会马上加载,而是在子进程首次真正执行任务时才进行初始化。 + +**解决思路:提前热身(warm up)进程池** + +在应用启动后、还没正式接受请求之前,可以提交一个简单的“空任务”或非常小的任务给进程池,让子进程先**完成相关的初始化**。这种“预热”方式能在正式请求到来之前就完成大部分初始化,减少首次请求的延迟。 + +**还可以快速验证服务是否正常启动** + +``` +def warmup_request(): + # 等待服务器完全启动,例如等待 1-2 秒 + time.sleep(5) + try: + url = "http://127.0.0.1:5000/judge_zbfile" + #url必须为永久地址,完成热启动,创建进程池 + payload = {"file_url": "xxx"} # 根据实际情况设置 file_url + headers = {"Content-Type": "application/json"} + response = requests.post(url, json=payload, headers=headers) + print(f"Warm-up 请求发送成功,状态码:{response.status_code}") + except Exception as e: + print(f"Warm-up 请求出错:{e}") +``` + +threading.Thread(target=warmup_request, daemon=True).start() + + + +## flask_app结构介绍 + +1 + + + +### 项目中做限制的地方 + +#### **账号、服务器分流** + +服务器分流:目前linux服务器和windows服务器主要是硬件上的分流(文件切分需要消耗CPU资源),大模型基底还是调用阿里,共用的tpm qpm。 + +账号分流:qianwen_plus下的 + +``` +api_keys = cycle([ + os.getenv("DASHSCOPE_API_KEY"), + # os.getenv("DASHSCOPE_API_KEY_BACKUP1"), + # os.getenv("DASHSCOPE_API_KEY_BACKUP2") +]) +api_keys_lock = threading.Lock() +def get_next_api_key(): + with api_keys_lock: + return next(api_keys) + +api_key = get_next_api_key() +``` + +只需轮流使用不同的api_key即可。目前没有启用。 + + + +#### **大模型的限制** + +general/llm下的doubao.py 和通义千问long_plus.py +**目前是linux和windows各部署一套,因此项目中的qps是对半的,即calls=?** + +1. 这是qianwen-long的限制(针对阿里qpm为1200,每秒就是20,又linux和windows服务器对半,就是10;TPM无上限) + +``` +@sleep_and_retry +@limits(calls=10, period=1) # 每秒最多调用10次 +def rate_limiter(): + pass # 这个函数本身不执行任何操作,只用于限流 +``` + +2. 这是qianwen-plus的限制(针对tpm为1000万,每个请求2万tokens,那么linux和windows总的qps为8时,8x60x2=960<1000。单个为4) + **经过2.11号测试,calls=4时最高TPM为800,因此把目前稳定版把calls设为5** + + **2.12,用turbo作为超限后的承载,目前把calls设为7** + +``` +@sleep_and_retry +@limits(calls=7, period=1) # 每秒最多调用7次 +def qianwen_plus(user_query, need_extra=False): + logger = logging.getLogger('model_log') # 通过日志名字获取记录器 +``` + +3. qianwen_turbo的限制(TPM为500万,由于它是plus后的手段,稳妥一点,qps设为6,两个服务器分流即calls=3) + +``` +@sleep_and_retry +@limits(calls=3, period=1) # 500万tpm,每秒最多调用6次,两个服务器分流就是3次 (plus超限后的保底手段,稳妥一点) +``` + +**重点!!**后续阿里扩容之后成倍修改这块**calls=?** + +如果不用linux和windows负载均衡,这里的calls也要乘2!! + + + +#### **接口的限制** + +1. start_up.py的def create_app()函数,限制了对每个接口同时100次请求。这里事实上不再限制了(因为100已经足够大了),默认限制做到大模型限制这块。 + +``` +app.connection_limiters['upload'] = ConnectionLimiter(max_connections=100) + app.connection_limiters['get_deviation'] = ConnectionLimiter(max_connections=100) + app.connection_limiters['default'] = ConnectionLimiter(max_connections=100) + app.connection_limiters['judge_zbfile'] = ConnectionLimiter(max_connections=100) +``` + +2. ConnectionLimiter.py以及每个接口上的装饰器,如 + + ``` + @require_connection_limit(timeout=1800) + + def zbparse(): + ``` + + 这里限制了每个接口内部执行的时间,暂时设置到了30分钟!(不包括排队时间)超时就是解析失败 + +#### **后端的限制:** + +目前后端发起招标请求,如果发送超过100(max_connections=100)个请求,我这边会排队后面的请求,这时后端的计时器会将这些请求也视作正在解析中,事实上它们还在排队等待中,这样会导致在极端情况下,新进的解析文件速度大于解析的速度,排队越来越长,后面的文件会因为等待时间过长而直接失败,而不是'解析失败'。 + +​ + +### general + +是公共函数存放的文件夹,llm下是各类大模型,读取文件下是docx pdf文件的读取以及文档清理clean_pdf,去页眉页脚页码 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5ftd-2.png) + +general下的llm下的清除file_id.py 需要**每周运行至少一次**,防止file_id数量超出(我这边对每次请求结束都有file_id记录并清理,向应该还没加) + +llm下的model_continue_query是'模型继续回答'脚本,应对超长文本模型一次无法输出完的情况,继续提问,拼接成完整的内容。 + + + +general下的file2markdown是textin 文件--》markdown + +general下的format_change是pdf-》docx 或doc/docx->pdf + +general下的merge_pdfs.py是拼接文件的:1.拼接招标公告+投标人须知 2.拼接评标细则章节+资格审查章节 + + + +**general中比较重要的!!!** + +**后处理:** + +general下的**post_processing**,解析后的后处理部分,包括extract_info、 资格审查、技术偏离 商务偏离 所需提交的证明材料,都在这块生成。 + +post_processing中的**inner_post_processing**专门提取*extracted_info* + +post_processing中的**process_functions_in_parallel**提取 + +资格审查、技术偏离、 商务偏离、 所需提交的证明材料 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5y6o-2.png) + +大解析upload用了post_processing完整版, + +little_zbparse.py、小解析main.py用了inner_post_processing + +get_deviation.py、偏离表数据解析main.py用了process_functions_in_parallel + + + +**截取pdf:** + +*截取pdf_main.py*是顶级函数, + +二级是*截取pdf货物标版*.py和*截取pdf工程标版.py* (非general下) + +三级是*截取pdf通用函数.py* + +如何判断截取位置是否正确?根据output文件夹中的切分情况(打开各个文件查看是否切分准确,目前的逻辑主要是按大章切分,即'招标公告'章节) + + + +**如果切分不准确,如何定位正则表达式?** + +首先判断当前是工程标解析还是货物标解析,即zb_type=1还是2 + +如果是2,那么是货物标解析,那么就是*截取pdf_main.py*调用*截取pdf货物标版*.py,如下图,selection=1代表截取'招标公告',那么如果招标公告没有切准,就在这块修改。这里可以发现get_notice是通用函数,即*截取pdf通用函数.py*中的get_notice函数,那么继续往内部跳转。 + +若开头没截准,就改begin_pattern,末尾没截准,就改end_pattern + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5pcy-2.png) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5mkp-2.png) + +另外:在*截取pdf货物标版*.py中,还有extract_pages_twice函数,即第一次没有切分到之后,会运行该函数,这边又有一套begin_pattern和end_pattern,即二次提取 + + + +**如何测试?** + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5ivt-2.png) + +输入pdf_path,和你要切分的序号,selection=1代表切公告,依次类推,可以看切出来的效果如何。 + + + +**无效标和废标公共代码** + +获取无效标与废标项的主要执行代码。对docx文件进行预处理=》正则=》temp.txt=》大模型筛选 +如果提的不全,可能是正则没涵盖到位,也可能是大模型提示词漏选了。 + +这里:如果段落中既被正则匹配,又被follow_up_keywords中的任意一个匹配,那么不会添加到temp中(即不会被大模型筛选),它会**直接添加**到最后的返回中! + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5jhb-2.png) + + + +**投标人须知正文条款提取成json文件** + +将截取到的ztbfile_tobidders_notice_part2.pdf ,即须知正文,转为clause1.json 文件,便于后续提取**开评定标流程**、**投标文件要求**、**重新招标、不再招标和终止招标** + +这块的主要逻辑就是匹配形如'一、总则'这样的大章节 + +然后匹配形如'1.1' '1.1.1'这样的序号,由于是按行读取pdf,一个序号后面的内容可能有好几行,因此遇到下一个序号(如'2.1')开头,之前的内容都视为上一个序号的。 + + + +### old_version + +都是废弃文件代码,未在正式、测试环境中使用的,不用管 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5r69-2.png) + + + +### routes + +是接口以及主要实现部分,一一对应 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5zb7-2.png) + +get_deviation对应偏离表数据解析main,获得偏离表数据 + +judge_zbfile对应判断是否是招标文件 + +little_zbparse对应小解析main,负责解析extract_info + +test_zbparse是测试接口,无对应 + +upload对应工程标解析和货物标解析,即大解析 + +**混淆澄清**:小解析可以指代一个过程,即从'投标文件生成'这个入口进去的解析,后端会同时调用little_zbparse和get_deviation。这个过程称为'小解析'。 + +但是little_zbparse也叫小解析,命名如此因为最初只需返回这些数据(extract_info),后续才陆续返回商务、技术偏离... + + + +utils是接口这块的公共功能函数。其中validate_and_setup_logger函数对不同的接口请求对应到不同的output文件夹,如upload->output1。后续增加接口也可直接在这里写映射关系。 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5syz-2.png) + +重点关注大解析:**upload.py**和**货物标解析main.py** + + + +### static + +存放解析的输出和提示词 + +其中output用gitignore了,git push不会推送这块内容。 + +各个文件夹(output1 output2..)对应不同的接口请求 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5k57-2.png) + + + +### test_case&testdir + +test_case是测试用例,是对一些函数的测试。好久没更新了 + +testdir是平时写代码的测试的地方 + +它们都不影响正式和测试环境的解析 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5hz1-2.png) + + + +### 工程标&货物标 + +是两个解析流程中不一样的地方(一样的都写在**general**中了) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5pu6-2.png) + +主要是货物标额外解析了采购要求(提取采购需求main+技术参数要求提取+商务服务其他要求提取) + + + +### 最后: + +ConnectionLimiter.py定义了接口超时时间->超时后断开与后端的连接 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5br8-2.png) + +logger_setup.py 为每个请求创建单独的log,每个log对应一个log.txt + +start_up.py是启动脚本,run_serve也是启动脚本,是对start_up.py的简单封装,目前dockerfile定义的直接使用run_serve启动 + + + +## 持续关注 + +``` + yield sse_format(tech_deviation_response) + yield sse_format(tech_deviation_star_response) + yield sse_format(zigefuhe_deviation_response) + yield sse_format(shangwu_deviation_response) + yield sse_format(shangwu_star_deviation_response) + yield sse_format(proof_materials_response) +``` + +1. 工程标解析目前仍没有解析采购要求这一块,因此后处理返回的只有'资格审查'和''证明材料"和"extracted_info",没有''商务偏离''及'商务带星偏离',也没有'技术偏离'和'技术带星偏离',而货物标解析是完全版。 + +​ 其中''证明材料"和"extracted_info"是直接返给后端保存的 + +2. 大解析中返回了技术评分,后端接收后不仅显示给前端,还会返给向,用于生成技术偏离表 +3. 小解析时,get_deviation.py其实也可以返回技术评分,但是没有返回,因为没人和我对接,暂时注释了。 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5zfw-2.png) + + + +4.商务评议和技术评议偏离表,即评分细则的偏离表,暂时没做,但是**商务评分、技术评分**无论大解析还是小解析都解析了,稍微对该数据处理一下返回给后端就行。 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5y2n-2.png) + +这个是解析得来的结果,适合给前端展示,但是要生成商务技术评议偏离表的话,需要再调一次大模型,对该数据进行重新归纳,以字符串列表为佳。再传给后端。(未做) + + + +### 如何定位问题 + +1. 查看static下的output文件夹 (upload大解析对应output1) +2. docker-compose文件中规定了数据卷挂载的路径:- /home/Z/zbparse_output_dev:/flask_project/flask_app/static/output + 也就是说static/output映射到了服务器的Z/zbparse_output_dev文件夹 +3. 根据时间查找哪个子文件夹(uuid作为子文件名) +4. 查看是否有final_result.json文件,如果有,说明解析流程正常结束了,问题可能出在后端(a.后端接口请求超限30分钟 b.后处理存在解析数据的时候出错) + +​ 也可能出现在自身解析,可以查看子文件内的log.txt,查看日志。 + +5. 若解析正常(有final_result)但解析不准,可以根据以下定位: + +​ a.查看子文件夹下的文件切分是否准确,例如:如果评标办法不准确,那么查看ztbfile_evaluation_methon,是否正确切到了评分细则。如果切到了,那就改general/商务技术评分提取里的提示词;否则修改截取pdf那块关于'评标办法'的正则表达式。 + +​ b.总之是**先看切的准不准,再看提示词能否优化**,都要定位到对应的代码中! + + + +## 学习总结 + +### Flask + Waitress : + +Flask 和 Waitress 是两个不同层级的工具,在 Python Web 开发中扮演互补角色。它们的协作关系可以概括为:**Flask 负责构建 Web 应用逻辑,而 Waitress 作为生产级服务器承载 Flask 应用**。 + +``` +# Flask 开发服务器(仅用于开发) +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) + +# 使用 Waitress 启动(生产环境) +from waitress import serve +serve(app, host='0.0.0.0', port=8080) +``` + +**Waitress 的工作方式** + +- 作为 WSGI 服务器:Waitress 作为一个 WSGI 服务器,负责监听指定端口上的网络请求,并将请求传递给 WSGI 应用(如 Flask 应用)。 + +- 多线程处理:默认情况下,waitress 在**单个进程**内启用线程池。当请求到达时,waitress 会从线程池中分配一个线程来处理这个请求。由于 GIL 限制,同一时间只有一个线程在执行 Python 代码(只能使用一个核心,CPU利用率只能到100%)。 + +**Flask 与 waitress 的协同工作** + +- **WSGI 接口**:Flask 应用实现了 WSGI 接口。waitress 接收到请求后,会调用 Flask 应用对应的视图函数来处理请求,生成响应。 +- **请求处理流程** + - 请求进入 waitress + - waitress 分配一个线程并调用 Flask 应用 + - Flask 根据路由匹配并执行对应的处理函数 + - 处理函数返回响应,waitress 将响应发送给客户端 + +**Waitress 的典型使用场景** + +1. **跨平台部署**:尤其适合 Windows 环境(Gunicorn 等服务器不支持)。 +2. **简单配置**:无需复杂设置即可获得比开发服务器(Flask自带)更强的性能。 +3. **中小型应用**:对并发要求不极高的场景,Waitress 的轻量级特性优势明显。 + +**Waitress的不足与处理** + +由于 waitress 是在单进程下工作,所有线程共享进程内存,如果业务逻辑简单且无复杂资源共享问题,这种方式是足够的。 + +**引入子进程**:如果需要每个请求实现内存隔离或者绕过 GIL 来利用多核 CPU,有时会在 Flask 视图函数内部启动子进程来处理实际任务。 + +**直接采用多进程部署方案**:使用 Gunicorn 的多 worker 模式 + + + +### Gunicorn + +**Gunicorn 的工作方式** + +- **预启动 Worker 进程**。Gunicorn 启动时,会按照配置数量(例如 4 个 worker)创建多个 worker 进程。这些 worker 进程会一直运行,并监听同一个端口上的请求。不会针对每个请求单独创建新进程。 +- **共享 socket**:所有 worker 进程共享同一个监听 socket,当有请求到来时,操作系统会将请求分发给某个空闲的 worker。 + +推荐worker 数量 = (2 * CPU 核心数) + 1 + +**如何启动:** + +要使用异步 worker,你需要: + +``` +pip install gevent +``` + +启动 Gunicorn 时指定 worker 类型和数量,例如: + +``` +gunicorn -k gevent -w 4 --max-requests 100 flask_app.start_up:create_app --bind 0.0.0.0:5000 + +``` + +使用 `-k gevent`(或者 `-k eventlet`)就可以使用异步 worker,单个 worker 能够处理多个 I/O 密集型请求。 + +使用--max-requests 100 。每个 worker 在处理完 100 个请求后会自动重启,从而释放可能累积的内存。 + + + +### 本项目的执行流程: + +1. 调用CPU进行PDF文件的读取与切分,CPU密集型,耗时半分钟 +2. 针对切分之后的不同部分,分别调用大模型,得到回答,IO密集型,耗时2分钟。 + +解决方案: + +1.使用flask+waitress,waitress会为每个用户请求开新的线程处理,然后我的代码逻辑会在这个线程内**开子进程**来执行具体的代码,以绕过GIL限制,且正确释放内存资源。 + +**后续可以开一个共享的进程池代替为每个请求开子进程。以避免高并发下竞争多核导致的频繁CPU切换问题。 + +2.使用Gunicorn的异步worker,gunicorn为固定创建worker(进程),处理用户请求,一个异步 worker 可以同时处理多个用户请求,因为当一个请求在等待外部响应(例如调用大模型接口)时,worker 可以切换去处理其他请求。 + + + +### 全局解释器锁(GIL): + +Python(特别是 CPython 实现)中有一个叫做全局解释器锁(Global Interpreter Lock,简称 GIL)的机制,这个锁确保在任何时刻**只有一个线程**在执行 **Python 字节码。** + +这意味着,即使你启动了多个线程,它们在执行 Python 代码时实际上是串行执行的,而不是并行利用多核 CPU。 + +在 Java 中,多线程通常能充分利用多核,因为 **Java 的线程是真正的系统级线程**,不存在类似 CPython 中的 GIL 限制。 + +**影响**: + +- **CPU密集型任务**:由于 GIL 的存在,在 CPU 密集型任务中,多线程往往不能提高性能,因为同时只有一个线程在执行 Python 代码。 +- **I/O密集型任务**:如果任务主要等待 I/O(例如**网络**、**磁盘读写**),线程在等待时会释放 GIL,此时多线程可以提高程序的响应性和吞吐量。 + + + +**NumPy**能够在一定程度上绕过 Python 的 GIL 限制。许多 NumPy 的数值计算操作(如矩阵乘法、向量化运算等)是由高度优化的 C 或 Fortran 库(如 BLAS、LAPACK)实现的。这些库通常在执行计算密集型任务时会释放 GIL。**C 扩展模块的方式将 C 代码嵌入到 Python 中,从而利用底层 C 库的高性能优势** + + + +### 进程与线程 + +1、进程是操作系统分配任务的基本单位,进程是python中正在运行的程序;当我们打开了1个浏览器时就是开始了一个浏览器进程; +线程是进程中执行任务的基本单元(执行指令集),一个进程中至少有一个线程、当只有一个线程时,称为**主线程** +2、线程的创建和销毁耗费资源少,进程的创建和销毁耗费资源多;线程很容易创建,进程不容易创建 +3、线程的切换速度快,进程慢 +4、一个进程中有多个线程时:线程之间可以进行通信;一个进程中有多个子进程时,进程与进程之间不可以相互通信,如果需要通信时,就必须通过一个中间代理实现,Queue、Pipe。 +5、多进程可以利用多核cpu,**多线程不可以利用多核cpu** +6、一个新的线程很容易被创建,一个新的进程创建需要对父进程进行一次克隆 +7、多进程的主要目的是充分使用CPU的多核机制,**多线程的主要目的是充分利用某一个单核** +——————————————— + +**每个进程有自己的独立 GIL** + +**多线程适用于 I/O 密集型任务** + +**多进程适用于CPU密集型任务** + +**因此,多进程用于充分利用多核,进程内开多线程以充分利用单核。** + + + +#### 进程池 + +**multiprocessing.Pool库:**,通过 `maxtasksperchild` 指定每个子进程在退出前最多执行的任务数,这有助于防止某些任务中可能存在的内存泄漏问题 + +``` +pool =Pool(processes=10, maxtasksperchild=3) +``` + +**concurrent.futures.ProcessPoolExecutor**更高级、更统一,没有类似 `maxtasksperchild` 的参数,意味着进程在整个执行期内会一直存活,适合任务本身**比较稳定**的场景。 + +pool =ProcessPoolExecutor(max_workers=10) + +最好创建的进程数**等同于**CPU核心数,如果大于,且每个进程都是CPU密集型(高负债一直用到CPU),那么进程之间会竞争CPU,导致上下文切换增加,反而会降低性质。 + +设置的工作进程数接近 CPU 核心数,以便每个进程能**独占一个核**运行。 + + + +#### 进程、线程间通信 + +**线程间通信**: + +- 线程之间可以直接共享全局变量、对象或数据结构,不需要额外的序列化过程,但这也带来了同步的复杂性(如竞态条件)。 + +``` +import threading +num=0 +def work(): + global num + for i in range(1000000): + num+=1 + print('work',num) + + +def work1(): + global num + for i in range(1000000): + num+=1 + print('work1',num) + +if __name__ == '__main__': + t1=threading.Thread(target=work) + t2=threading.Thread(target=work1) + t1.start() + t2.start() + t1.join() + t2.join() + print('主线程执行结果',num) +``` + +运行结果: + +``` +work 1551626 + +work1 1615783 + +主线程执行结果 1615783 +``` + +这些数值都小于预期的 2000000,因为: + +即使存在 GIL,`num += 1` 这样的操作实际上**并不是原子**的。GIL 确保同一时刻只有一个线程执行 Python 字节码,但在执行 `num += 1` 时,实际上会发生下面几步操作: + +1. 从内存中读取 `num` 的当前值 +2. 对读取到的值进行加 1 操作 +3. 将新的值写回到内存 + +**由多个字节码组成!!!** + +因此会导致: + +线程 A 读取到 `num` 的值 + +切换到线程 B,线程 B 也读取同样的 `num` 值并进行加 1,然后写回 + +当线程 A 恢复时,它依然基于之前读取的旧值进行加 1,最后写回,从而覆盖了线程 B 的更新 + +**解决:** + +``` +from threading import Lock + +import threading +num=0 +def work(): + global num + for i in range(1000000): + with lock: + num+=1 + print('work',num) + +def work1(): + global num + for i in range(1000000): + with lock: + num+=1 + print('work1',num) + +if __name__ == '__main__': + lock=Lock() + t1=threading.Thread(target=work) + t2=threading.Thread(target=work1) + t1.start() + t2.start() + t1.join() + t2.join() + print('主线程执行结果',num) + +``` + + + +**进程间通信(IPC)**: + +- 进程之间默认不共享内存,因此如果需要传递数据,就必须使用专门的通信机制。 +- 在 Python 中,可以使用 `multiprocessing.Queue`、`multiprocessing.Pipe`、共享内存(如 `multiprocessing.Value` 和 `multiprocessing.Array`)等方式实现进程间通信。 + +``` +from multiprocessing import Process, Queue + +def worker(process_id, q): + # 每个进程将数据放入队列 + q.put(f"data_from_process_{process_id}") + print(f"Process {process_id} finished.") + +if __name__ == '__main__': + q = Queue() + processes = [] + for i in range(5): + p = Process(target=worker, args=(i, q)) + processes.append(p) + p.start() + + for p in processes: + p.join() + + # 从队列中收集数据 + results = [] + while not q.empty(): + results.append(q.get()) + + print("Collected data:", results) +``` + +- 当你在主进程中创建了一个 `Queue` 对象,然后将它作为参数传递给子进程时,子进程会获得一个能够与主进程通信的“句柄”。 + +- 子进程中的 `q.put(...)` 操作会将数据通过这个管道传送到主进程,而主进程可以通过 `q.get()` 来获取这些数据。 + +- 这种机制虽然看起来像是“共享”,但实际上是通过 IPC(进程间通信)实现的,而不是直接共享内存中的变量。 + + + +### 项目贡献 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5fvu-2.png) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5km4-2.png) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5kxd-2.png) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5crv-2.png) + +### 效果图 + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5trn-2.png) + +![](http://124.71.159.195:1000/i/2025/03/06/tz5z4v-2.gif) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5ymp-2.png) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz5uwp-2.png) + +![1](http://124.71.159.195:1000/i/2025/03/06/tz68dy-2.png) diff --git a/Java/力扣Hot 100题.md b/Java/力扣Hot 100题.md new file mode 100644 index 0000000..619f13e --- /dev/null +++ b/Java/力扣Hot 100题.md @@ -0,0 +1,887 @@ +## 力扣Hot 100题 + +### 杂项 + +- **最大值**:`Integer.MAX_VALUE` +- **最小值**:`Integer.MIN_VALUE` + + + +**数组集合比较** + +**`Arrays.equals(array1, array2)`** + +- 用于比较两个数组是否相等(内容相同)。 + +- 支持多种类型的数组(如 `int[]`、`char[]`、`Object[]` 等)。 + +- ``` + int[] arr1 = {1, 2, 3}; + int[] arr2 = {1, 2, 3}; + boolean isEqual = Arrays.equals(arr1, arr2); // true + ``` + + + +`Collections` 类本身没有直接提供类似 `Arrays.equals` 的方法来比较两个集合的内容是否相等。不过,Java 中的集合类(如 `List`、`Set`、`Map`)已经实现了 `equals` 方法 + +``` +List list1 = Arrays.asList(1, 2, 3); + List list2 = Arrays.asList(1, 2, 3); + List list3 = Arrays.asList(3, 2, 1); + System.out.println(list1.equals(list2)); // true + System.out.println(list1.equals(list3)); // false(顺序不同) +``` + + + +要实现接口自定义排序,必须实现 `Comparator` 接口的 `compare(T o1, T o2)` 方法。 + +`Comparator` 接口中定义的 `compare(T o1, T o2)` 方法返回**一个整数**(非布尔值!!),这个整数的正负意义如下: + +- 如果返回负数,说明 `o1` 排在 `o2`前面。 +- 如果返回零,说明 `o1` 等于 `o2`。 +- 如果返回正数,说明 `o1` 排在 `o2`后面。 + +``` +public class TestComparator { + // 定义一个升序排序的 Comparator + static Comparator ascComparator = new Comparator() { + @Override + public int compare(Integer a, Integer b) { + return a - b; // 如果 a < b, 则返回负数 + } + }; + + public static void main(String[] args) { + // 创建一个整数列表 + List numbers = new ArrayList<>(); + numbers.add(5); + numbers.add(3); + numbers.add(8); + numbers.add(1); + numbers.add(9); + numbers.add(2); + + // 使用 Collections.sort 进行排序,并指定 Comparator + Collections.sort(numbers, ascComparator); + + // 输出排序后的列表 + System.out.println("排序后的列表: " + numbers); + } +} +``` + +假设有两个参数a=3,b=5,那么返回负数,表示第一个参数a排在第二个参数b前面,因此是升序; + + + +**自定义比较器排序二维数组** 用Lambda表达式实现`Comparator接口` + +``` +import java.util.Arrays; + +public class IntervalSort { + public static void main(String[] args) { + int[][] intervals = { {1, 3}, {2, 6}, {8, 10}, {15, 18} }; + + // 自定义比较器,先比较第一个元素,如果相等再比较第二个元素 + Arrays.sort(intervals, (a, b) -> { + if (a[0] != b[0]) { + return a[0] - b[0]; + } else { + return a[1] - b[1]; + } + }); + + // 输出排序结果 + for (int[] interval : intervals) { + System.out.println(Arrays.toString(interval)); + } + } +} + +``` + + + +**逻辑比较** + +``` +boolean flag = false; + +if (!flag) { //! 是 Java 中的逻辑非运算符,只能用于对布尔值取反。 + System.out.println("flag 是 false"); +} + + +if (flag == false) { //更常用! + System.out.println("flag 是 false"); +} + +//java中没有 if(not flag) 这种写法! +``` + + + +### 常用数据结构 + +#### `String` + +子串:字符串中**连续的一段字符**。 + +子序列:字符串中**按顺序选取的一段字符**,可以不连续。 + +异位词:字母相同、字母频率相同、顺序不同,如`"listen"` 和 `"silent"` + + + +排序: + +需要String先转为char [] 数组,排序好之后再转为String类型!! + +``` +char[] charArray = str.toCharArray(); +Arrays.sort(charArray); +String sortedStr = new String(charArray); +``` + + + + + +取字符: + +- `charAt(int index)` 方法返回指定索引处的 `char` 值。 +- `char` 是基本数据类型,占用 2 个字节,表示一个 Unicode 字符。 +- HashSet set = new HashSet(); + +取子串: + +- `substring(int beginIndex, int endIndex)` 方法返回从 `beginIndex` 到 `endIndex - 1` 的子字符串。 +- 返回的是 `String` 类型,即使子字符串只有一个字符。 + + + +#### **`HashMap`** + +- 基于哈希表实现,查找、插入和删除的平均时间复杂度为 O(1)。 + +- 不保证元素的顺序。 + +- ``` + import java.util.HashMap; + import java.util.Map; + + public class HashMapExample { + public static void main(String[] args) { + // 创建 HashMap + Map map = new HashMap<>(); + + // 添加键值对 + map.put("apple", 10); + map.put("banana", 20); + map.put("orange", 30); + + // 获取值 + int appleCount = map.get("apple"); //如果获取不存在的元素,返回null + System.out.println("Apple count: " + appleCount); // 输出 10 + + // 遍历 HashMap + for (Map.Entry entry : map.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); + } + // 输出: + // apple: 10 + // banana: 20 + // orange: 30 + + // 检查是否包含某个键 + boolean containsBanana = map.containsKey("banana"); + System.out.println("Contains banana: " + containsBanana); // 输出 true + + // 删除键值对 + map.remove("orange"); //删除不存在的元素也不会报错 + System.out.println("After removal: " + map); // 输出 {apple=10, banana=20} + } + } + ``` + + + +#### **`ArrayList`** + +- 基于数组实现,支持动态扩展。 + +- 访问元素的时间复杂度为 O(1),在末尾插入和删除的时间复杂度为 O(1)。 + +- 在指定位置插入和删除O(n) `add(int index, E element)` `remove(int index)` + +- ``` + import java.util.ArrayList; + import java.util.List; + + public class ArrayListExample { + public static void main(String[] args) { + // 创建 ArrayList + List list = new ArrayList<>(); + + // 添加元素 + list.add(10); + list.add(20); + list.add(30); + + int size = list.size(); // 获取列表大小 + System.out.println("Size of list: " + size); // 输出 3 + + // 获取元素 + int firstElement = list.get(0); + System.out.println("First element: " + firstElement); // 输出 10 + + // 修改元素 + list.set(1, 25); // 将第二个元素改为 25 + System.out.println("After modification: " + list); // 输出 [10, 25, 30] + + // 遍历 ArrayList + for (int num : list) { + System.out.println(num); + } + // 输出: + // 10 + // 25 + // 30 + + // 删除元素 + list.remove(2); // 删除第三个元素 + System.out.println("After removal: " + list); // 输出 [10, 25] + } + } + ``` + + + +#### **`数组(Array)`** + +数组是一种固定长度的数据结构,用于存储相同类型的元素。数组的特点包括: + +- **固定长度**:数组的长度在创建时确定,无法动态扩展。 + +- **快速访问**:通过索引访问元素的时间复杂度为 O(1)。 + +- **连续内存**:数组的元素在内存中是连续存储的。 + +- ``` + public class ArrayExample { + public static void main(String[] args) { + // 创建数组 + int[] array = new int[5]; // 创建一个长度为 5 的整型数组 + + // 添加元素 + array[0] = 10; + array[1] = 20; + array[2] = 30; + array[3] = 40; + array[4] = 50; + + // 获取元素 + int firstElement = array[0]; + System.out.println("First element: " + firstElement); // 输出 10 + + // 修改元素 + array[1] = 25; // 将第二个元素改为 25 + System.out.println("After modification:"); + for (int num : array) { + System.out.println(num); + } + // 输出: + // 10 + // 25 + // 30 + // 40 + // 50 + + // 遍历数组 + System.out.println("Iterating through array:"); + for (int i = 0; i < array.length; i++) { + System.out.println("Index " + i + ": " + array[i]); + } + // 输出: + // Index 0: 10 + // Index 1: 25 + // Index 2: 30 + // Index 3: 40 + // Index 4: 50 + + // 删除元素(数组长度固定,无法直接删除,可以通过覆盖实现) + int indexToRemove = 2; // 要删除的元素的索引 + for (int i = indexToRemove; i < array.length - 1; i++) { + array[i] = array[i + 1]; // 将后面的元素向前移动 + } + array[array.length - 1] = 0; // 最后一个元素置为 0(或其他默认值) + System.out.println("After removal:"); + for (int num : array) { + System.out.println(num); + } + // 输出: + // 10 + // 25 + // 40 + // 50 + // 0 + + // 数组长度 + int length = array.length; + System.out.println("Array length: " + length); // 输出 5 + } + } + ``` + + + +#### **二维数组** + +``` +int rows = 3; +int cols = 3; +int[][] array = new int[rows][cols]; + +// 填充数据 +for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + array[i][j] = i * cols + j + 1; + } +} + +//创建并初始化 +int[][] array = { + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} +}; + + +// 遍历二维数组,不知道几行几列 +public void setZeroes(int[][] matrix) { + // 遍历每一行 + for (int i = 0; i < matrix.length; i++) { + // 遍历当前行的每一列 + for (int j = 0; j < matrix[i].length; j++) { + // 这里可以处理 matrix[i][j] 的元素 + System.out.print(matrix[i][j] + " "); + } + System.out.println(); // 换行,便于输出格式化 + } +} +``` + + + +``` +import java.util.ArrayList; +import java.util.List; + +int rows = 3; +int cols = 3; +List> list = new ArrayList<>(); + + +for (List row : list) { + for (int num : row) { + System.out.print(num + " "); + } + System.out.println(); // 换行 +} +for (int i = 0; i < list.size(); i++) { + List row = list.get(i); + for (int j = 0; j < row.size(); j++) { + System.out.print(row.get(j) + " "); + } + System.out.println(); // 换行 +} +``` + + + +**如果事先不知道数组的大小呢?** + +``` +List merged = new ArrayList<>(); + +merged.add(current); + +return merged.toArray(new int[merged.size()][]); +``` + + + + + +#### **`HashSet`** + +- 基于哈希表实现,查找、插入和删除的平均时间复杂度为 O(1)。 + +- 不保证元素的顺序!!因此不太用iterator迭代,而是用contains判断是否有xx元素。 + +- ``` + import java.util.HashSet; + import java.util.Set; + + public class HashSetExample { + public static void main(String[] args) { + // 创建 HashSet + Set set = new HashSet<>(); + + // 添加元素 + set.add(10); + set.add(20); + set.add(30); + set.add(10); // 重复元素,不会被添加 + + // 检查元素是否存在 + boolean contains20 = set.contains(20); + System.out.println("Contains 20: " + contains20); // 输出 true + + // 遍历 HashSet + for (int num : set) { + System.out.println(num); + } + // 输出: + // 20 + // 10 + // 30 + + // 删除元素 + set.remove(20); + System.out.println("After removal: " + set); // 输出 [10, 30] + } + } + ``` + + + + + + + +#### `PriorityQueue` + +- **基于优先堆(最小堆或最大堆)实现**,元素按优先级排序。 + +- **默认是最小堆**,即队首元素是最小的。 + +- **支持自定义排序规则**,通过 `Comparator` 实现。 + +- **常用操作的时间复杂度**: + + - 插入元素:`O(log n)` + - 删除队首元素:`O(log n)` + - 查看队首元素:`O(1)` + +- **常用方法** + + 1. **`add(E e)` / `offer(E e)`**: + - 将元素插入队列。 + - `add` 在队列满时会抛出异常,`offer` 返回 `false`。 + 2. **`remove()` / `poll()`**: + - 移除并返回队首元素。 + - `remove` 在队列为空时会抛出异常,`poll` 返回 `null`。 + 3. **`element()` / `peek()`**: + - 查看队首元素,但不移除。 + - `element` 在队列为空时会抛出异常,`peek` 返回 `null`。 + 4. **`size()`**: + - 返回队列中的元素数量。 + 5. **`isEmpty()`**: + - 检查队列是否为空。 + 6. **`clear()`**: + - 清空队列。 + +- ``` + import java.util.PriorityQueue; + import java.util.Comparator; + + public class PriorityQueueExample { + public static void main(String[] args) { + // 创建 PriorityQueue(默认是最小堆) + PriorityQueue minHeap = new PriorityQueue<>(); + + // 添加元素 + minHeap.add(10); + minHeap.add(20); + minHeap.add(30); + minHeap.add(5); + + // 查看队首元素 + System.out.println("队首元素: " + minHeap.peek()); // 输出 5 + + // 遍历 PriorityQueue(注意:遍历顺序不保证有序) + System.out.println("遍历 PriorityQueue:"); + for (int num : minHeap) { + System.out.println(num); + } + // 输出: + // 5 + // 10 + // 30 + // 20 + + // 移除队首元素 + System.out.println("移除队首元素: " + minHeap.poll()); // 输出 5 + + // 再次查看队首元素 + System.out.println("队首元素: " + minHeap.peek()); // 输出 10 + + // 创建最大堆(通过自定义 Comparator) + PriorityQueue maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); + maxHeap.add(10); + maxHeap.add(20); + maxHeap.add(30); + maxHeap.add(5); + + // 查看队首元素 + System.out.println("最大堆队首元素: " + maxHeap.peek()); // 输出 30 + + // 清空队列 + minHeap.clear(); + System.out.println("队列是否为空: " + minHeap.isEmpty()); // 输出 true + } + } + ``` + + + + +#### `Queue` + +队尾插入,队头取!`` + +``` +import java.util.Queue; +import java.util.LinkedList; + +public class QueueExample { + public static void main(String[] args) { + // 创建一个队列 + Queue queue = new LinkedList<>(); + + // 添加元素到队列中 + queue.add(10); // 使用 add() 方法添加元素 + queue.offer(20); // 使用 offer() 方法添加元素 + queue.add(30); + System.out.println("队列内容:" + queue); + + // 查看队头元素,不移除 + int head = queue.peek(); + System.out.println("队头元素(peek): " + head); + + // 移除队头元素 + int removed = queue.poll(); + System.out.println("移除的队头元素(poll): " + removed); + System.out.println("队列内容:" + queue); + + // 再次移除队头元素 + int removed2 = queue.remove(); + System.out.println("移除的队头元素(remove): " + removed2); + System.out.println("队列内容:" + queue); + } +} + +``` + + + +#### `Deque`(双端队列+栈) + +支持在队列的两端(头和尾)进行元素的插入和删除。这使得 Deque 既能作为队列(FIFO)又能作为栈(LIFO)使用。 + +建议在需要栈操作时使用 `Deque` 的实现 + + + +**栈** + +``` +Deque stack = new ArrayDeque<>(); +stack.push(1); // 入栈 +Integer top1=stack.peek() +Integer top = stack.pop(); // 出栈 +``` + + + +**双端队列** + +**在队头操作** + +- `addFirst(E e)`:在队头添加元素,如果操作失败会抛出异常。 +- `offerFirst(E e)`:在队头插入元素,返回 `true` 或 `false` 表示是否成功。 +- `peekFirst()`:查看队头元素,不移除;队列为空返回 `null`。 +- `removeFirst()`:移除并返回队头元素;队列为空会抛出异常。 +- `pollFirst()`:移除并返回队头元素;队列为空返回 `null`。 + +**在队尾操作** + +- `addLast(E e)`:在队尾添加元素,若失败会抛出异常。 +- `offerLast(E e)`:在队尾插入元素,返回 `true` 或 `false` 表示是否成功。 +- `peekLast()`:查看队尾元素,不移除;队列为空返回 `null`。 +- `removeLast()`:移除并返回队尾元素;队列为空会抛出异常。 +- `pollLast()`:移除并返回队尾元素;队列为空返回 `null`。 + + + +**添加元素**:调用 `add(e)` 或 `offer(e)` 时,实际上是调用 `addLast(e)` 或 `offerLast(e)`,即在**队尾**添加元素。 + +**删除或查看元素**:调用 `remove()` 或 `poll()` 时,则是调用 `removeFirst()` 或 `pollFirst()`,即在队头移除元素;同理,`element()` 或 `peek()` 则是查看队头元素。 + + + +``` +import java.util.Deque; +import java.util.LinkedList; + +public class DequeExample { + public static void main(String[] args) { + // 使用 LinkedList 实现双端队列 + Deque deque = new LinkedList<>(); + + // 在队列头部添加元素 + deque.addFirst(10); + // 在队列尾部添加元素 + deque.addLast(20); + // 在队列头部插入元素 + deque.offerFirst(5); + // 在队列尾部插入元素 + deque.offerLast(30); + + System.out.println("双端队列内容:" + deque); + + // 查看队头和队尾元素,不移除 + int first = deque.peekFirst(); + int last = deque.peekLast(); + System.out.println("队头元素:" + first); + System.out.println("队尾元素:" + last); + + // 从队头移除元素 + int removedFirst = deque.removeFirst(); + System.out.println("移除队头元素:" + removedFirst); + // 从队尾移除元素 + int removedLast = deque.removeLast(); + System.out.println("移除队尾元素:" + removedLast); + + System.out.println("双端队列最终内容:" + deque); + } +} + +``` + + + +#### `Iterator` + +- **`HashMap`、`HashSet`、`ArrayList` 和 `PriorityQueue`** 都实现了 `Iterable` 接口,支持 `iterator()` 方法。 + +`Iterator` 接口中包含以下主要方法: + +1. `hasNext()`:如果迭代器还有下一个元素,则返回 `true`,否则返回 `false`。 +2. `next()`:返回迭代器的下一个元素,并将迭代器移动到下一个位置。 +3. `remove()`:从迭代器当前位置删除元素。该方法是可选的,不是所有的迭代器都支持。 + +``` +import java.util.ArrayList; +import java.util.Iterator; + +public class Main { + public static void main(String[] args) { + // 创建一个 ArrayList 集合 + ArrayList list = new ArrayList<>(); + list.add(1); + list.add(2); + list.add(3); + + // 获取集合的迭代器 + Iterator iterator = list.iterator(); + + // 使用迭代器遍历集合并输出元素 + while (iterator.hasNext()) { + Integer element = iterator.next(); + System.out.println(element); + } + } +} +``` + + + +### 排序 + +排序时间复杂度:O(nlog(n)) + +求最大值:O(n) + +#### **数组排序** + +``` +import java.util.Arrays; + +public class ArraySortExample { + public static void main(String[] args) { + int[] numbers = {5, 2, 9, 1, 5, 6}; + Arrays.sort(numbers); // 对数组进行排序 + System.out.println(Arrays.toString(numbers)); // 输出 [1, 2, 5, 5, 6, 9] + } +} +``` + +#### 集合排序 + +``` +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ListSortExample { + public static void main(String[] args) { + List numbers = new ArrayList<>(); + numbers.add(5); + numbers.add(2); + numbers.add(9); + numbers.add(1); + numbers.add(5); + numbers.add(6); + + Collections.sort(numbers); // 对 List 进行排序 + System.out.println(numbers); // 输出 [1, 2, 5, 5, 6, 9] + } +} +``` + +#### **自定义排序** + +``` +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class Person { + String name; + int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return name + " (" + age + ")"; + } +} + +public class ComparatorSortExample { + public static void main(String[] args) { + List people = new ArrayList<>(); + people.add(new Person("Alice", 25)); + people.add(new Person("Bob", 20)); + people.add(new Person("Charlie", 30)); + + // 使用 Comparator 按姓名排序 + Collections.sort(people, new Comparator() { + @Override + public int compare(Person p1, Person p2) { + return p1.name.compareTo(p2.name); // 按姓名升序排序 + } + }); + + System.out.println(people); // 输出 [Alice (25), Bob (20), Charlie (30)] + } +} +``` + + + +### 题型 + +#### 哈希 + +**问题分析**: + +- 确定是否需要快速查找或存储数据。 +- 判断是否需要统计元素频率或检查元素是否存在。 + +**适用场景** + +1. **快速查找**: + - 当需要频繁查找元素时,哈希表可以提供 O(1) 的平均时间复杂度。 +2. **统计频率**: + - 统计元素出现次数时,哈希表是常用工具。 +3. **去重**: + - 需要去除重复元素时,`HashSet` 可以有效实现。 + +#### 双指针 + +1. 问题分析: + + - 确定问题是否涉及数组或链表的遍历。 + - 判断是否需要通过两个指针的协作来缩小搜索范围或比较元素。 + +2. 选择双指针类型: + + - **同向双指针**:两个指针从同一侧开始移动,通常用于**滑动窗口**或链表问题。 + - **对向双指针**:两个指针从两端向中间移动,通常用于有序数组或回文问题。重点是考虑**移动哪个指针**可能优化结果!!! + - **快慢指针**:两个指针以不同速度移动,通常用于链表中的环检测或中点查找。 + +3. 适用场景 + + **有序数组的两数之和**: + + - 在对向双指针的帮助下,可以在 O(n) 时间内找到两个数,使它们的和等于目标值。 + + **滑动窗口**: + + - 用于解决**子数组或子字符串**问题,如同向双指针可以在 O(n) 时间内找到满足条件的最短或最长子数组。 + + **链表中的环检测**: + + - 快慢指针可以用于检测链表中是否存在环,并找到环的起点。 + + **回文问题**: + + - 对向双指针可以用于判断字符串或数组是否是回文。 + + **合并有序数组或链表**: + + - 双指针可以用于合并两个有序数组或链表,时间复杂度为 O(n)。 + + + +### 前缀和 + +1. **前缀和的定义** + 定义前缀和 `preSum[i]` 为数组 `nums` 从索引 0 到 i 的元素和,即 + $$ + \text{preSum}[i] = \sum_{j=0}^{i} \text{nums}[j] + $$ + +2. **子数组和的关系** + 对于任意子数组 `nums[i+1..j]`(其中 `0 ≤ i < j < n`),其和可以表示为 + $$ + \text{sum}(i+1,j) = \text{preSum}[j] - \text{preSum}[i] + $$ + 当这个子数组的和等于 k 时,有 + $$ + \text{preSum}[j] - \text{preSum}[i] = k + $$ + 即 + $$ + \text{preSum}[i] = \text{preSum}[j] - k + $$ + $\text{preSum}[j] - k$表示 "以当前位置结尾的子数组和为k" + +3. **利用哈希表存储前缀和** + 我们可以使用一个哈希表 `prefix` 来存储每个**前缀和**出现的**次数**。 + + - 初始时,`prefix[0] = 1`,表示前缀和为 0 出现一次(对应空前缀)。 + - 遍历数组,每计算一个新的前缀和 `preSum`,就查看 `preSum - k` 是否在哈希表中。如果存在,则说明之前有一个前缀和等于 `preSum - k`,那么从该位置后一个位置到**当前索引**的子数组和为 k,累加其出现的次数。 + +4. **时间复杂度** + 该方法只需要遍历数组一次,时间复杂度为 O(n)。 \ No newline at end of file diff --git a/Java/安卓开发.md b/Java/安卓开发.md new file mode 100644 index 0000000..88ce9aa --- /dev/null +++ b/Java/安卓开发.md @@ -0,0 +1,52 @@ +# 安卓开发 + +## 导入功能模块心得 + +最近想在我的书城中开发一下阅读器的功能,难度颇高,因此在github上找到了一个封装了阅读器功能的项目,仅需获得文件本地存储地址,调用其提供的函数即可进行阅读。 + +**但是**,github介绍的使用方法并不总是有效,比如我就经常无法正确添加依赖 +![image-20240503200121198](D:/folder/test/output/9a28a61a-bb35-4a7a-9f09-db4c300212f1.png) + + + +因此,我将其项目代码拷贝到本地,手动集成。 + +![image-20240503200221147](D:/folder/test/output/3a17e26d-cc39-4823-802b-92f7d5f6e3ba.png) + +依据项目结构,可以发现app是主项目,hwtxtreaderlib是功能模块,根据是这张图: + +![image-20240503200343665](D:/folder/test/output/70cd971c-21ec-4914-9fe7-1f5aac7b85b2.png) + +build.gradle(:app)中引入了hwtxtreaderlib的依赖,而app只是个demo测试模块,相当于演示了如果在自己的项目中引用hwtxtreaderlib。因此,手动步骤如下: + +- 将hwtxtreaderlib复制到自己的项目文件夹中 + + ![image-20240503200651488](D:/folder/test/output/e05bc95d-b8e4-466f-8d1f-ddf21c9afe10.png) + +- 在app的build.gradle中,添加依赖 + +``` +implementation project(':hwtxtreaderlib') +``` + +- 在settings.gradle中,设置项目包括的模块 + + ``` + include ':app', ':hwtxtreaderlib' + ``` + +- syn now! 同步一下,然后android studio中项目结构变成如下图 + +![image-20240503201012590](D:/folder/test/output/3efab097-d5fd-411c-8641-2ef0977eded6.png) + +- build没报错基本就稳了,然后就运行试试 + +![image-20240503201211104](D:/folder/test/output/98ab47e6-d406-4c5c-84c1-4470bd803ef2.png) + +这里可能AndroidManifest.xml报错,需要查看原项目中app模块如何编写的,做些适当的修改!我这里卡了很久. + + + +**非常重要!!!**有时候github项目会将项目的详细信息写在wiki中!!! + +![image-20240503213919450](D:/folder/test/output/bbcc458c-16c2-455f-8853-441379e5807f.png) \ No newline at end of file diff --git a/Java/微服务.md b/Java/微服务.md new file mode 100644 index 0000000..dc0d012 --- /dev/null +++ b/Java/微服务.md @@ -0,0 +1,406 @@ +# 微服务 + +## Mybatis-Plus + +### 快速开始 + +**1.引入依赖** + +```XML + + com.baomidou + mybatis-plus-boot-starter + 3.5.3.1 + +``` + +由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter。 + +**2.定义mapper** + +修改mp-demo中的`com.itheima.mp.mapper`包下的`UserMapper`接口,让其继承`BaseMapper`: + +``` +public interface UserMapper extends BaseMapper { +} +``` + + + +MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢? + +**约定大于配置** + +**泛型中的User**就是与数据库对应的PO. + +MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下: + +- MybatisPlus会把PO实体的**类名**驼峰转下划线作为**表名** +- MybatisPlus会把PO实体的所有**变量名**驼峰转下划线作为表的**字段名**,并根据变量类型推断字段类型 +- MybatisPlus会把名为**id**的字段作为**主键** + +但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。 + +**3.常见注解** + +**@TableName** + +- 描述:表名注解,标识实体类对应的表 +- 使用位置:实体类 + +```Java +@TableName("user") +public class User { + private Long id; + private String name; +} +``` + +**@TableId** + +- 描述:主键注解,标识实体类中的主键字段 +- 使用位置:实体类的主键字段 + +`TableId`注解支持两个属性: + +| **属性** | **类型** | **必须指定** | **默认值** | **描述** | +| :------- | :------- | :----------- | :---------- | :----------- | +| value | String | 否 | "" | 主键字段名 | +| type | Enum | 否 | IdType.NONE | 指定主键类型 | + +``` +@TableName("user") +public class User { + @TableId(value="id",type=IdType.AUTO) + private Long id; + private String name; +} +``` + +必须指定type=IdType.AUTO,默认是雪花算法算出一个随机的id(插入操作时) + + + +**@TableField** + +一般情况下我们并不需要给字段添加`@TableField`注解,一些特殊情况除外: + +- 成员变量名与数据库字段名不一致 +- 成员变量是以`isXXX`命名,按照`JavaBean`的规范,`MybatisPlus`识别字段时会把`is`去除,这就导致与数据库不符。 +- 成员变量名与数据库一致,但是与数据库的**关键字(如order)**冲突。使用`@TableField`注解给字段名添加转义字符:```` + +支持的其它属性如下: + +exist:默认为true,表示是数据库字段,若 + +``` +@TableField(exist=false) +private String address; +``` + +将自动跳过address的增删查改,因为它不被视为字段。 + + + +```Java +@TableName("user") +public class User { + @TableId + private Long id; + private String name; + private Integer age; + @TableField("isMarried") + private Boolean isMarried; + @TableField("`order`") + private String order; +} +``` + +### 常见配置 + +大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如: + +- 实体类的别名扫描包 +- 全局id类型 + +要改也就改这两个即可 + +```YAML +mybatis-plus: + type-aliases-package: com.itheima.mp.domain.po + global-config: + db-config: + id-type: auto # 全局id类型为自增长 +``` + + + + + +### 核心功能 + +#### 条件构造器 + +除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以`id`作为`where`条件以外,还支持更加复杂的`where`条件。 + +`Wrapper`就是条件构造的抽象类,其下有很多默认实现,继承关系如图: + +![image-20240813112049624](D:/folder/test/output/a235a515-c19a-4d52-93f0-99f7bfe7b01a.png) + +![image-20240813134824946](D:/folder/test/output/f1fba482-f473-4d29-9cc2-2a2f3f492f25.png) + +**QueryWrapper** + +``` +/**查询出名字中带o的,存款大于等于1000元的人的id,username,info,balance + * SELECT id,username,info,balance + * FROM user + * WHERE username LIKE ? AND balance >=? + */ +@Test +void testQueryWrapper(){ + QueryWrapper wrapper =new QueryWrapper() + .select("id","username","info","balance") + .like("username","o") + .ge("balance",1000); + //查询 + List users=userMapper.selectList(wrapper); + users.forEach(System.out::println); +} +``` + + + +``` +//更新用户名为jack的用户的余额为2000 +@Test +void testUpdateByQueryWrapper() { + // 1.构建查询条件 where name = "Jack" + QueryWrapper wrapper = new QueryWrapper().eq("username", "Jack"); + // 2.更新数据,user中非null字段都会作为set语句 + User user = new User(); + user.setBalance(2000); + userMapper.update(user, wrapper); +} +``` + +**UpdateWrapper** + +基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例如:更新id为`1,2,4`的用户的余额,扣200,对应的SQL应该是: + +```Java +UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4) +``` + +``` +@Test +void testUpdateWrapper() { + List ids = List.of(1L, 2L, 4L); + // 1.生成SQL + UpdateWrapper wrapper = new UpdateWrapper() + .setSql("balance = balance - 200") // SET balance = balance - 200 + .in("id", ids); // WHERE id in (1, 2, 4) + // 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据, + // 而是基于UpdateWrapper中的setSQL来更新 + userMapper.update(null, wrapper); +} +``` + +**LambdaQueryWrapper** + +无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串`魔法值`。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢? + +其中一种办法是基于变量的`gettter`方法结合反射技术。因此我们只要将条件对应的字段的`getter`方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的`方法引用`和`Lambda`表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个: + +- LambdaQueryWrapper +- LambdaUpdateWrapper + +``` +@Test +void testLambdaQueryWrapper() { + // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .select(User::getId, User::getUsername, User::getInfo, User::getBalance) + .like(User::getUsername, "o") + .ge(User::getBalance, 1000); + // 2.查询 + List users = userMapper.selectList(wrapper); + users.forEach(System.out::println); +} +``` + + + +**总之,推荐使用LambdaQueryWrapper,若要使用set,才用LambdaUpdateWrapper。普通的QueryWrapper用得少** + + + +#### 自定义sql + +可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL + +1.先在业务层利用wrapper创建条件,传递参数 + +``` +@Test +void testCustomWrapper() { + // 1.准备自定义查询条件 + List ids = List.of(1L, 2L, 4L); + QueryWrapper wrapper = new QueryWrapper().in("id", ids); + + // 2.调用mapper的自定义方法,直接传递Wrapper + userMapper.deductBalanceByIds(200, wrapper); +} +``` + +2. 自定义mapper层把wrapper和其他业务参数传进去,自定义sql语句书写sql的前半部分,后面拼接。 + +``` +package com.itheima.mp.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.itheima.mp.domain.po.User; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; +import org.apache.ibatis.annotations.Param; + +public interface UserMapper extends BaseMapper { + @Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}") + void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper wrapper); +} +``` + +这里wrapper前面必须写@Param("ew") + +${ew.customSqlSegment}可以自动拼接前面写的条件语句 + + + +#### Mapper层常用方法 + +**查询:** + +selectById:根据主键 ID 查询单条记录。 + +selectBatchIds:根据主键 ID 批量查询记录。 + +selectOne:根据指定条件查询单条记录。 + +``` +QueryWrapper queryWrapper = new QueryWrapper<>(); +queryWrapper.eq("username", "alice"); +User user = userMapper.selectOne(queryWrapper); + +``` + +selectList:根据指定条件查询多条记录。 + +``` +QueryWrapper queryWrapper = new QueryWrapper<>(); +queryWrapper.ge("age", 18); +List users = userMapper.selectList(queryWrapper); + +``` + + + +**插入:** + +insert:插入一条记录。 + +``` +User user = new User(); +user.setUsername("alice"); +user.setAge(20); +int rows = userMapper.insert(user); + +``` + +**更新** + +updateById:根据主键 ID 更新记录。 + +``` +User user = new User(); +user.setId(1L); +user.setAge(25); +int rows = userMapper.updateById(user); + +``` + +update:根据指定条件更新记录。 + +``` +UpdateWrapper updateWrapper = new UpdateWrapper<>(); +updateWrapper.eq("username", "alice"); + +User user = new User(); +user.setAge(30); + +int rows = userMapper.update(user, updateWrapper); + +``` + +**删除操作** + +deleteById:根据主键 ID 删除记录。 + +deleteBatchIds:根据主键 ID 批量删除记录。 + +delete:根据指定条件删除记录。 + +``` +QueryWrapper queryWrapper = new QueryWrapper<>(); +queryWrapper.eq("username", "alice"); + +int rows = userMapper.delete(queryWrapper); + +``` + +#### IService + +![image-20240815092311650](D:/folder/test/output/8332f7ce-cdc8-4085-bd18-f479ef4ca2c2.png) + +![image-20240815092324887](D:/folder/test/output/f3fe1679-bc67-4ee5-9956-df3134e6a009.png) + +![image-20240815092338012](D:/folder/test/output/1da75d45-f5d6-4131-ad64-4bc70664c92d.png) + +![image-20240815092352179](D:/folder/test/output/ea9464f6-4df5-492b-941c-cb3fc42bbddc.png) + +![image-20240815092420201](D:/folder/test/output/6c18537e-381a-43ec-99b2-a6b78e4bcdc2.png) + +![image-20240815092604848](D:/folder/test/output/5fdfd44c-a756-4453-92f5-20f8d5bd868a.png) + +由于`Service`中经常需要定义与业务有关的自定义方法,因此我们不能直接使用`IService`,而是自定义`Service`接口,然后继承`IService`以拓展方法。同时,让自定义的`Service实现类`继承`ServiceImpl`,这样就不用自己实现`IService`中的接口了。 + +首先,定义`IUserService`,继承`IService`: + +``` +package com.itheima.mp.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.itheima.mp.domain.po.User; + +public interface IUserService extends IService { + // 拓展自定义方法 +} +``` + +然后,编写`UserServiceImpl`类,继承`ServiceImpl`,实现`UserService`: + +``` +package com.itheima.mp.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.itheima.mp.domain.po.User; +import com.itheima.mp.domain.po.service.IUserService; +import com.itheima.mp.mapper.UserMapper; +import org.springframework.stereotype.Service; + +@Service +public class UserServiceImpl extends ServiceImpl + implements IUserService { +} +``` + diff --git a/Java/苍穹外卖.md b/Java/苍穹外卖.md new file mode 100644 index 0000000..979eba2 --- /dev/null +++ b/Java/苍穹外卖.md @@ -0,0 +1,1151 @@ +# 苍穹外卖 + +## 开发环境搭建 + +### 后端项目结构 + +![image-20240327104750952](D:/folder/test/output/ab5d4e98-dd5c-48a6-999f-4c2897268111.png) + +分析sky-pojo模块的每个包的作用: + +| **名称** | **说明** | +| -------- | ------------------------------------------------------------ | +| Entity | 实体,通常和数据库中的表对应 | +| DTO | 数据传输对象,通常用于程序中各层之间传递数据(接收从web来的数据) | +| VO | 视图对象,为前端展示数据提供的对象(响应给web) | +| POJO | 普通Java对象,只有属性和对应的getter和setter | + +### 数据库设计文档 + +| 序号 | 数据表名 | 中文名称 | +| ---- | ------------- | -------------- | +| 1 | employee | 员工表 | +| 2 | category | 分类表 | +| 3 | dish | 菜品表 | +| 4 | dish_flavor | 菜品口味表 | +| 5 | setmeal | 套餐表 | +| 6 | setmeal_dish | 套餐菜品关系表 | +| 7 | user | 用户表 | +| 8 | address_book | 地址表 | +| 9 | shopping_cart | 购物车表 | +| 10 | orders | 订单表 | +| 11 | order_detail | 订单明细表 | + +###  @TableName("user")public class User {    @TableId    private Long id;    private String name;    private Integer age;    @TableField("isMarried")    private Boolean isMarried;    @TableField("`order`")    private String order;}Java + +**1.nginx 反向代理的好处:** + +- 提高访问速度 + + 因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。 + +- 进行负载均衡 + + 所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。 + +- 保证后端服务安全 + + 因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。 + +``` +server{ + listen 80; + server_name localhost; + + location /api/{ + proxy_pass http://localhost:8080/admin/; #反向代理 + } +} +``` + +当在访问http://localhost/api/employee/login,nginx接收到请求后转到http://localhost:8080/admin/,故最终的请求地址为http://localhost:8080/admin/employee/login + +**2.负载均衡配置**(有两个后端服务器) + +``` +upstream webservers{ + server 192.168.100.128:8080; + server 192.168.100.129:8080; +} +server{ + listen 80; + server_name localhost; + + location /api/{ + proxy_pass http://webservers/admin;#负载均衡 + } +} +``` + + + +### Swagger + +1. 使得前后端分离开发更加方便,有利于团队协作 + +2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担 + +3. 功能测试 + +**使用:** + +1. 导入 knife4j 的maven坐标 + +在pom.xml中添加依赖 + +```xml + + com.github.xiaoymin + knife4j-spring-boot-starter + +``` + +2. 在配置类中加入 knife4j 相关配置 + +WebMvcConfiguration.java + +```java +/** + * 通过knife4j生成接口文档 + * @return +*/ + @Bean + public Docket docket() { + ApiInfo apiInfo = new ApiInfoBuilder() + .title("苍穹外卖项目接口文档") + .version("2.0") + .description("苍穹外卖项目接口文档") + .build(); + Docket docket = new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo) + .select() + .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) + .paths(PathSelectors.any()) + .build(); + return docket; + } +``` + +3. 设置静态资源映射,否则接口文档页面无法访问 + +WebMvcConfiguration.java + +```java +/** + * 设置静态资源映射 + * @param registry +*/ +protected void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); + registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); +} +``` + + + +**常用注解** + +通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下: + +| **注解** | **说明** | +| ----------------- | ---------------------------------------------------------- | +| @Api | 用在类上,例如Controller,表示对类的说明 | +| @ApiModel | 用在类上,例如entity、DTO、VO | +| @ApiModelProperty | 用在属性上,描述属性信息 | +| @ApiOperation | 用在**方法**上,例如Controller的方法,说明方法的用途、作用 | + +EmployeeLoginDTO.java + +``` +@Data +@ApiModel(description = "员工登录时传递的数据模型") +public class EmployeeLoginDTO implements Serializable { + + @ApiModelProperty("用户名") + private String username; + + @ApiModelProperty("密码") + private String password; + +} +``` + +![image-20240327170247852](D:/folder/test/output/8ffded91-cd61-4373-a904-1279353cd3ef.png) + +## 开发 + +### 加密算法 + +存放在数据表中的密码不能以明文存储,需对前端传来的密码进行加密。 + +spring security中提供了一个加密类BCryptPasswordEncoder。 + +它采用[哈希算法](https://so.csdn.net/so/search?q=哈希算法&spm=1001.2101.3001.7020) SHA-256 +随机盐+密钥对密码进行加密。加密算法是一种**可逆**的算法,而哈希算法是一种**不可逆**的算法。 + +因为有随机盐的存在,所以相同的明文密码经过加密后的密码是**不一样**的,盐在加密的密码中是有记录的,所以需要对比的时候,springSecurity是可以从中获取到盐的 + + + +- 添加依赖 + +``` + + org.springframework.boot + spring-boot-starter-security + +``` + +- 添加配置 + +``` +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().permitAll() // 允许所有请求 + .and() + .csrf().disable(); // 禁用CSRF保护 + } + @Bean + public BCryptPasswordEncoder encoder(){ + return new BCryptPasswordEncoder(); + } +} +``` + +- 使用 + +``` +@Autowired +private BCryptPasswordEncoder bCryptPasswordEncoder; + +// 加密 +String encodedPassword=bCryptPasswordEncoder.encode(PasswordConstant.DEFAULT_PASSWORD); +employee.setPassword(encodedPassword); + +// 比较 +bCryptPasswordEncoder.matches(明文,密文); +``` + + + +### 新增员工的两个问题 + +**问题1:** + +录入的用户名已存,抛出的异常后没有处理。 + +法一:每次新增员工前查询一遍数据库,保证无重复username再插入。 + +法二:插入后系统报“Duplicate entry”再处理。 + +**推荐法二,因为发生异常的概率是很小的,每次新增前查询一遍数据库不划算。** + + + +**问题2:** + +如何获得当前登录的管理员id==》拦截器中解析的token中的id怎么传入controller里? + +方法:ThreadLocal + +ThreadLocal为**每个线程**提供**单独**一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。 + +**每次请求代表一个线程**!!!注:请求可以先经过拦截器,再经过controller=>service=>mapper,都是在一个线程里。 + +``` +public class BaseContext { + + public static ThreadLocal threadLocal = new ThreadLocal<>(); + + public static void setCurrentId(Long id) { + threadLocal.set(id); + } + + public static Long getCurrentId() { + return threadLocal.get(); + } + + public static void removeCurrentId() { + threadLocal.remove(); + } + +} +``` + +实现方式:登录的时候BaseContext.setCurrentId(id); + +要用的时候直接BaseContext.getCurrentId(); + + + +### SpringMVC的消息转换器(处理日期) + +**1). 方式一** + +在属性上加上注解,对日期进行格式化 + +image-20221112103501581 + +但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。 + +**2). 方式二(推荐 )** + +在**WebMvcConfiguration**中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理 + +作用: + +1. **请求数据转换(反序列化)**:当服务器接收到一个HTTP请求时,消息转换器将请求体中的数据(如JSON、XML等格式)转换成控制器(Controller)方法参数所期望的Java对象。这个过程称为反序列化。 +2. **响应数据转换(序列化)**:当控制器处理完业务逻辑后,需要将结果数据返回给客户端。消息转换器此时将Java对象序列化为客户端可识别的格式(如JSON、XML等),并包装在HTTP响应体中发送。 + +```java + /** + * 扩展Spring MVC框架的消息转化器 + * @param converters + */ + protected void extendMessageConverters(List> converters) { + log.info("扩展消息转换器..."); + //创建一个消息转换器对象 + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据 + converter.setObjectMapper(new JacksonObjectMapper()); + //将自己的消息转化器加入容器中 + converters.add(0,converter); + } +``` + + + +JacksonObjectMapper()文件: + +``` +public class JacksonObjectMapper extends ObjectMapper { + + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; + public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; +// public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; + public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; + + public JacksonObjectMapper() { + super(); + //收到未知属性时不报异常 + this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + + //反序列化时,属性不存在的兼容处理 + this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + SimpleModule simpleModule = new SimpleModule() + .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) + .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) + .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) + .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) + .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) + .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); + + //注册功能模块 例如,可以添加自定义序列化器和反序列化器 + this.registerModule(simpleModule); + } +} +``` + + + +### 构造实体对象的两种方法 + +```java +public void startOrStop(Integer status, Long id) { + //法一 + Employee employee = Employee.builder() + .status(status) + .id(id) + .build(); + //法二 + //Employee employee=new Employee(); + //employee.setStatus(status); + //employee.setId(id); +} +``` + + + +还有一种:把源对象source的属性值赋给目标**对象**target中与源**对象**source的中有着同属性名的属性 + +``` +BeanUtils.copyProperties(source,target); +``` + + + +### 修改员工信息(复用update方法) + +代码能复用尽量复用!在mapper类里定义一个**通用的update**接口,即mybatis操作数据库时修改员工信息都调用这个接口。**启用/禁用员工**可能只要修改status,**修改员工**可能大面积修改属性,在**mapper**类中定义一个通用的update方法,但是**controller层和service层**的函数命名可以不一样,以区分两种业务。 + + + +在 EmployeeMapper 接口中声明 update 方法: + +```java + /** + * 根据主键动态修改属性 + * @param employee + */ + void update(Employee employee); +``` + +在 EmployeeMapper.xml 中编写SQL: + +```sql + + update employee + + name = #{name}, + username = #{username}, + password = #{password}, + phone = #{phone}, + sex = #{sex}, + id_Number = #{idNumber}, + update_Time = #{updateTime}, + update_User = #{updateUser}, + status = #{status}, + + where id = #{id} + +``` + + + +### 公共字段自动填充——AOP编程 + +在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下: + +| **序号** | **字段名** | **含义** | **数据类型** | **操作类型** | +| -------- | ----------- | -------- | ------------ | -------------- | +| 1 | create_time | 创建时间 | datetime | insert | +| 2 | create_user | 创建人id | bigint | insert | +| 3 | update_time | 修改时间 | datetime | insert、update | +| 4 | update_user | 修改人id | bigint | insert、update | + + + +**实现步骤:** + +1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法 + +2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值 + +3). 在 Mapper 的方法上加入 AutoFill 注解 + + + +若要实现上述步骤,需掌握以下知识(之前课程内容都学过) + +**技术点:**枚举、注解、AOP、反射 + + + +## Java中操作Redis + +### 环境搭建 + +进入到sky-server模块 + +**1). 导入Spring Data Redis的maven坐标(已完成)** + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +**2). 配置Redis数据源** + +在application-dev.yml中添加 + +```yaml +sky: + redis: + host: localhost + port: 6379 + password: 123456 + database: 10 +``` + +**解释说明:** + +database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。 + +可以通过修改Redis配置文件来指定数据库的数量。 + +在application.yml中添加读取application-dev.yml中的相关Redis配置 + +```yaml +spring: + profiles: + active: dev + redis: + host: ${sky.redis.host} + port: ${sky.redis.port} + password: ${sky.redis.password} + database: ${sky.redis.database} +``` + +**3). 编写配置类,创建RedisTemplate对象** + +```java +package com.sky.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@Slf4j +public class RedisConfiguration { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ + log.info("开始创建redis模板对象..."); + RedisTemplate redisTemplate = new RedisTemplate(); + //设置redis的连接工厂对象 连接工厂负责创建与 Redis 服务器的连接 + redisTemplate.setConnectionFactory(redisConnectionFactory); + //设置redis key的序列化器 这意味着所有通过这个RedisTemplate实例存储的键都将被转换为字符串格式存储在Redis中 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + return redisTemplate; + } +} +``` + +**解释说明:** + +当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为 + +JdkSerializationRedisSerializer,**导致我们存到Redis中后的数据和原始数据有差别,故设置为** + +**StringRedisSerializer序列化器**。 + + + + + +### 功能测试 + +**通过RedisTemplate对象操作Redis** + +在test下新建测试类 + +**字符串测试** + +```java +@SpringBootTest +public class SpringDataRedisTest { + @Autowired + private RedisTemplate redisTemplate; + @Test + public void testRedisTemplate(){ + System.out.println(redisTemplate); + } + @Test + public void testString(){ + //set get setex setnx + redisTemplate.opsForValue().set("city","北京"); + String city= (String) redisTemplate.opsForValue().get("city"); + System.out.println(city); + redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES); //设置code的值为1234,过期时间3min + redisTemplate.opsForValue().setIfAbsent("lock","1"); //如果不存在该key则创建 + redisTemplate.opsForValue().setIfAbsent("lock","2"); + } +} +``` + +**哈希测试** + +``` +/** + * 操作哈希类型的数据 + */ + @Test + public void testHash(){ + //hset hget hdel hkeys hvals + HashOperations hashOperations = redisTemplate.opsForHash(); + + hashOperations.put("100","name","tom"); + hashOperations.put("100","age","20"); + + String name = (String) hashOperations.get("100", "name"); + System.out.println(name); + + Set keys = hashOperations.keys("100"); + System.out.println(keys); + + List values = hashOperations.values("100"); + System.out.println(values); + + hashOperations.delete("100","age"); + } +``` + +**get获得的是Object类型,keys获得的是set类型,values获得的是List** + + + +**3). 操作列表类型数据** + +```java + /** + * 操作列表类型的数据 + */ + @Test + public void testList(){ + //lpush lrange rpop llen + ListOperations listOperations = redisTemplate.opsForList(); + + listOperations.leftPushAll("mylist","a","b","c"); + listOperations.leftPush("mylist","d"); + + List mylist = listOperations.range("mylist", 0, -1); + System.out.println(mylist); + + listOperations.rightPop("mylist"); + + Long size = listOperations.size("mylist"); + System.out.println(size); + } +``` + + + +**4). 操作集合类型数据** + +```java + /** + * 操作集合类型的数据 + */ + @Test + public void testSet(){ + //sadd smembers scard sinter sunion srem + SetOperations setOperations = redisTemplate.opsForSet(); + + setOperations.add("set1","a","b","c","d"); + setOperations.add("set2","a","b","x","y"); + + Set members = setOperations.members("set1"); + System.out.println(members); + + Long size = setOperations.size("set1"); + System.out.println(size); + + Set intersect = setOperations.intersect("set1", "set2"); + System.out.println(intersect); + + Set union = setOperations.union("set1", "set2"); + System.out.println(union); + + setOperations.remove("set1","a","b"); + } +``` + + + +**5). 操作有序集合类型数据** + +```java + /** + * 操作有序集合类型的数据 + */ + @Test + public void testZset(){ + //zadd zrange zincrby zrem + ZSetOperations zSetOperations = redisTemplate.opsForZSet(); + + zSetOperations.add("zset1","a",10); + zSetOperations.add("zset1","b",12); + zSetOperations.add("zset1","c",9); + + Set zset1 = zSetOperations.range("zset1", 0, -1); + System.out.println(zset1); + + zSetOperations.incrementScore("zset1","c",10); + + zSetOperations.remove("zset1","a","b"); + } +``` + +**6). 通用命令操作** + + + +- `*` 匹配零个或多个字符。 +- `?` 匹配任何单个字符。 +- `[abc]` 匹配方括号内的任一字符(本例中为 'a'、'b' 或 'c')。 +- `[^abc]` 或 `[!abc]` 匹配任何不在方括号中的单个字符。 + +```java + /** + * 通用命令操作 + */ + @Test + public void testCommon(){ + //keys exists type del + Set keys = redisTemplate.keys("*"); + System.out.println(keys); + + Boolean name = redisTemplate.hasKey("name"); + Boolean set1 = redisTemplate.hasKey("set1"); + + for (Object key : keys) { + DataType type = redisTemplate.type(key); + System.out.println(type.name()); + } + + redisTemplate.delete("mylist"); + } +``` + + + +## HttpClient + +**HttpClient作用:** + +- 在Java程序中发送HTTP请求 +- 接收响应数据 + +**HttpClient发送请求步骤:** + +- 创建HttpClient对象 +- 创建Http请求对象 +- 调用HttpClient的execute方法发送请求 + + + +``` +public static String doGet(String url,Map paramMap){ + // 创建Httpclient对象 + CloseableHttpClient httpClient = HttpClients.createDefault(); + + String result = ""; + CloseableHttpResponse response = null; + + try{ + URIBuilder builder = new URIBuilder(url); + if(paramMap != null){ + for (String key : paramMap.keySet()) { + builder.addParameter(key,paramMap.get(key)); //将传入的参数 `paramMap` 中的每一个键值对转换为 URL 的查询字符串形式 + } + } + URI uri = builder.build(); + + //创建GET请求 + HttpGet httpGet = new HttpGet(uri); + + //发送请求 + response = httpClient.execute(httpGet); + + //判断响应状态 + if(response.getStatusLine().getStatusCode() == 200){ + result = EntityUtils.toString(response.getEntity(),"UTF-8"); + } + }catch (Exception e){ + e.printStackTrace(); + }finally { + try { + response.close(); + httpClient.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return result; + } +``` + + + +## 微信小程序 + +![image-20221204211800753](D:/folder/test/output/aec52692-0d27-46b2-b972-bfe3f6a70210.png) + + + +## 缓存功能 + +用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。 + +![image-20221208180228667](D:/folder/test/output/a13f6659-4d2c-45a8-8222-0c5bd8f5a487.png) + + + +### 实现思路 + +通过Redis来缓存菜品数据,减少数据库查询操作。 + +![image-20221208180818572](D:/folder/test/output/40b791dc-fbfe-4aa5-831b-2872c02a5090.png) + + + + + +### 经典缓存实现代码 + +**缓存逻辑分析:** + +- 每个分类下的菜品保存一份缓存数据 +- 数据库中菜品数据有**变更时清理缓存数据** + +``` +@Autowired + private RedisTemplate redisTemplate; + /** + * 根据分类id查询菜品 + * + * @param categoryId + * @return + */ + @GetMapping("/list") + @ApiOperation("根据分类id查询菜品") + public Result> list(Long categoryId) { + + //构造redis中的key,规则:dish_分类id + String key = "dish_" + categoryId; + + //查询redis中是否存在菜品数据 + List list = (List) redisTemplate.opsForValue().get(key); + if(list != null && list.size() > 0){ + //如果存在,直接返回,无须查询数据库 + return Result.success(list); + } + //////////////////////////////////////////////////////// + Dish dish = new Dish(); + dish.setCategoryId(categoryId); + dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 + + //如果不存在,查询数据库,将查询到的数据放入redis中 + list = dishService.listWithFlavor(dish); + //////////////////////////////////////////////////////// + redisTemplate.opsForValue().set(key, list); + + return Result.success(list); + } +``` + +为了保证**数据库**和**Redis**中的数据保持一致,修改**管理端接口 DishController** 的相关方法,加入清理缓存逻辑。 + +需要改造的方法: + +- 新增菜品 +- 修改菜品 +- 批量删除菜品 +- 起售、停售菜品 + +清理缓冲方法: + +``` + private void cleanCache(String pattern){ + Set keys = redisTemplate.keys(pattern); + redisTemplate.delete(keys); + } +``` + + + +### Spring Cache框架实现缓存 + +Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。 + +``` + + org.springframework.boot + spring-boot-starter-cache 2.7.3 + +``` + + + +在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个: + +| **注解** | **说明** | +| -------------- | ------------------------------------------------------------ | +| @EnableCaching | 开启缓存注解功能,通常加在**启动类**上 | +| @Cacheable | 在**方法**执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据(**取**);如果没有缓存数据,调用方法并将方法返回值**放**到缓存中 | +| @CachePut | 将方法的返回值**放**到缓存中 | +| @CacheEvict | 将一条或多条数据从缓存中**删除** | + +**@CachePut 说明:** + +​ 作用: 将方法返回值,放入缓存 + +​ value: 缓存的名称, 每个缓存名称下面可以有很多key + +​ key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法 + + + +`value` 和 `cacheNames` 属性在用法上是等效的。它们都用来指定缓存区的名称 + +在Redis中并没有直接的“缓存名”概念,而是通过键(key)来访问数据。Spring Cache通过`cacheNames`属性来模拟不同的“缓存区”,实际上这是通过将这些名称作为键的一部分来实现的。例如,如果你有一个缓存名为 `userCache`,那么所有相关的缓存条目的键可能以 `"userCache::"` 开头。 + + + +``` + @PostMapping + @CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1 + public User save(@RequestBody User user){ + userMapper.insert(user); + return user; + } +``` + +**说明:**key的写法如下 + +#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ; + +#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ; + + + +**@Cacheable 说明:** + +​ 作用: 在方法执行前,spring先查看缓存中是否有**指定的key的**数据,如果有数据,则直接返回缓存数据,不执行后续sql操作;若没有数据,调用方法并将方法返回值放到缓存中。 + +所以,@Cacheable(cacheNames = "userCache",key="#id")中的#id表示的是函数形参中的id,而不能是返回值中的user.id + +``` +@GetMapping + @Cacheable(cacheNames = "userCache",key="#id") + public User getById(Long id){ + User user = userMapper.getById(id); + return user; + } +``` + +**@CacheEvict 说明:** + +​ 作用: 清理指定缓存 + +``` +@DeleteMapping + @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据 + public void deleteById(Long id){ + userMapper.deleteById(id); + } + + @DeleteMapping("/delAll") + @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据 + public void deleteAll(){ + userMapper.deleteAll(); + } +``` + + + +## 微信支付 + +小程序支付 + +https://pay.weixin.qq.com/static/product/product_index.shtml + +![image-20221214223910840](D:/folder/test/output/30722ce7-3c6f-41a7-85b9-487d64ca7e47.png) + + + +**5.商户系统调用微信后台:** + +![image-20221214224409174](D:/folder/test/output/087fc1ca-67ab-4c9d-9730-91c30f1da899.png) + + + +**10.用户调起微信支付** + +![image-20221214224551220](D:/folder/test/output/523f0103-f8cd-40a0-ab7a-7658ddc1002d.png) + + + +### 内网穿透 + +微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。 + +目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。 + +**解决:**内网穿透。通过**cpolar软件**可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。 + +1)下载地址:https://dashboard.cpolar.com/get-started + + + +**2). cpolar指定authtoken** + +复制authtoken: + +![image-20240806133753849](D:/folder/test/output/bfa6cb35-3f24-4e8d-8399-d2f59cfd9f72.png) + +执行命令: + +注意,cd到cpolar.exe所在的目录打开cmd + +输入代码: + +``` +cpolar.exe authtoken ZmIwMmQzZDYtZDE2ZS00ZGVjLWE2MTUtOGQ0YTdhOWI2M2Q1 +``` + +3)获取临时域名 + +``` +cpolar.exe http 8080 +``` + +![image-20240806135141280](D:/folder/test/output/34b71597-dc25-4c1f-9c23-24c47348c748.png) + +这里的 https://52ac2ecb.r18.cpolar.top 就是与http://localhost:8080对应的临时域名。 + + + +## Spring Task + +**Spring Task** 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。 + +**定位:**定时任务框架 + +**作用:**定时自动执行某段Java代码 + +### 1.2 cron表达式 + +**cron表达式**其实就是一个字符串,通过cron表达式可以**定义任务触发的时间** + +**构成规则:**分为6或7个域,由空格分隔开,每个域代表一个含义 + +每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选) + +![image-20240807141614724](D:/folder/test/output/a975f870-f542-4eda-ad44-f2a66ecb6c21.png) + +cron表达式在线生成器:https://cron.qqe2.com/ + + + +### 1.3 入门案例 + +#### 1.3.1 Spring Task使用步骤 + +1). 导入maven坐标 spring-context(已存在) + +image-20221218193251182 + +2). 启动类添加注解 @EnableScheduling 开启任务调度 + +3). 自定义定时任务类 + + + +### 2.订单状态定时处理 + +#### 2.1 需求分析 + +用户下单后可能存在的情况: + +- 下单后未支付,订单一直处于**“待支付”**状态 +- 用户收货后管理端未点击完成按钮,订单一直处于**“派送中”**状态 + +对于上面两种情况需要通过**定时任务**来修改订单状态,具体逻辑为: + +- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消” +- 通过定时任务每天凌晨1点(打烊后)检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成” + + + +``` +@Component +@Slf4j +public class OrderTask { + /** + * 处理下单之后未15分组内支付的超时订单 + */ + @Autowired + private OrderMapper orderMapper; + + @Scheduled(cron = "0 * * * * ? ") + public void processTimeoutOrder(){ + log.info("定时处理支付超时订单:{}", LocalDateTime.now()); + LocalDateTime time = LocalDateTime.now().plusMinutes(-15); + + // select * from orders where status = 1 and order_time < 当前时间-15分钟 + List ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time); + if(ordersList != null && ordersList.size() > 0){ + ordersList.forEach(order -> { + order.setStatus(Orders.CANCELLED); + order.setCancelReason("支付超时,自动取消"); + order.setCancelTime(LocalDateTime.now()); + orderMapper.update(order); + }); + } + } + @Scheduled(cron = "0 0 1 * * ?") + public void processDeliveryOrder() { + log.info("处理派送中订单:{}", new Date()); + // select * from orders where status = 4 and order_time < 当前时间-1小时 + LocalDateTime time = LocalDateTime.now().plusMinutes(-60); + List ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time); + if (ordersList != null && ordersList.size() > 0) { + ordersList.forEach(order -> { + order.setStatus(Orders.COMPLETED); + orderMapper.update(order); + }); + } + } +} + +``` + + + + + +## Websocket + +WebSocket 是基于 TCP 的一种新的**网络协议**。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建**持久性**的连接, 并进行**双向**数据传输。 + +**HTTP协议和WebSocket协议对比:** + +- HTTP是**短连接** +- WebSocket是**长连接** +- HTTP通信是**单向**的,基于请求响应模式 +- WebSocket支持**双向**通信 +- HTTP和WebSocket底层都是TCP连接 + +### 入门案例 + +**实现步骤:** + +1). 直接使用websocket.html页面作为WebSocket客户端 + +2). 导入WebSocket的maven坐标 + +3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信(比较固定,建立连接、接收消息、关闭连接、发送消息) + +4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件 + +它通过Spring的 `ServerEndpointExporter` 将使用 `@ServerEndpoint` 注解的类自动注册为WebSocket端点。这样,当应用程序启动时,所有带有 `@ServerEndpoint` 注解的类就会被Spring容器自动扫描并注册为WebSocket服务器端点,使得它们能够接受和处理WebSocket连接。 + +5). 导入定时任务类WebSocketTask,定时向客户端推送数据 + + + +### 来单提醒 + +**设计思路:** + +- 通过WebSocket实现管理端页面和服务端保持长连接状态 +- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息 +- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报 +- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content + - type 为消息类型,1为来单提醒 2为客户催单 + - orderId 为订单id + - content 为消息内容 + diff --git a/Java/草稿.md b/Java/草稿.md new file mode 100644 index 0000000..d409ff0 --- /dev/null +++ b/Java/草稿.md @@ -0,0 +1,29 @@ +### 思路讲解 + +1. **前缀和的定义** + 定义前缀和 `preSum[i]` 为数组 `nums` 从索引 0 到 i 的元素和,即 + $$ + \text{preSum}[i] = \sum_{j=0}^{i} \text{nums}[j] + $$ + +2. **子数组和的关系** + 对于任意子数组 `nums[i+1..j]`(其中 `0 ≤ i < j < n`),其和可以表示为 + $$ + \text{sum}(i+1,j) = \text{preSum}[j] - \text{preSum}[i] + $$ + 当这个子数组的和等于 k 时,有 + $$ + \text{preSum}[j] - \text{preSum}[i] = k + $$ + 即 + $$ + \text{preSum}[i] = \text{preSum}[j] - k + $$ + +3. **利用哈希表存储前缀和** + 我们可以使用一个哈希表 `prefix` 来存储每个前缀和出现的次数。 + - 初始时,`prefix[0] = 1`,表示前缀和为 0 出现一次(对应空前缀)。 + - 遍历数组,每计算一个新的前缀和 `preSum`,就查看 `preSum - k` 是否在哈希表中。如果存在,则说明之前有一个前缀和等于 `preSum - k`,那么从该位置后一个位置到当前索引的子数组和为 k,累加其出现的次数。 + +4. **时间复杂度** + 该方法只需要遍历数组一次,时间复杂度为 O(n)。 \ No newline at end of file diff --git a/科研/PID控制器.md b/科研/PID控制器.md new file mode 100644 index 0000000..eab3cbf --- /dev/null +++ b/科研/PID控制器.md @@ -0,0 +1,122 @@ +# PID控制器 + +PID控制器是一种常用的反馈控制系统,广泛应用于工业控制系统和各种控制系统中,用来持续调整一个过程的控制输入,以减小系统当前位置和期望位置之间的误差。PID代表比例(Proportional)、积分(Integral)、微分(Derivative)。 + +## 控制系统概述 + +**开环控制系统** + +![image-20240324175333458](D:/folder/test/output/01450f0f-aa2c-4eef-8a9c-c5e607f13554.png) + +前馈控制系统尝试预先计算扰动对系统的影响,并在扰动影响系统输出之前调整输入以抵消它。 + + + +**闭环控制系统** + +控制器接收误差信号。该系统通过反馈回路来自动调节其输出 + +![image-20240324175424762](D:/folder/test/output/443925cc-f8c9-4ad0-8a22-75f1b4649a98.png) + +![image-20240324175627295](D:/folder/test/output/c6ade3a3-b97b-4fdb-b0b5-d092bc60079e.png) + +**复合控制系统** + +![image-20240324175704820](D:/folder/test/output/11202030-e7ac-4313-ab80-04768cc0be1c.png) + +## 连续与离散信号 + +![image-20240324180342885](D:/folder/test/output/5ee8ad6a-a04c-43d3-aea3-39c6eb75bb81.png) + +从连续信号到离散信号的转换过程涉及以下步骤: + +1. **采样**:在连续信号上每隔一定时间间隔取一个值。 +2. **量化**:将每个采样值映射到最接近的量化级上。 + +积分可以通过**求和**来近似,微分可以通过相邻样本之间的**差分**来近似。 + +## PID公式 + +![image-20240324181124423](D:/folder/test/output/08e86f7a-1690-44c3-a505-705847be3d8a.png) + +控制系统中的传感器会**连续**监测被控制对象的状态(例如,温度、压力、位置等),而PID控制器通过在固定的**采样间隔**收集输入信号,将其转换为**离散信号**,计算控制动作,然后输出到控制对象。离散PID控制的优势在于其灵活性和适应性,它可以轻松地与软件算法集成。 + +## 直观例子 + +![image-20240324182805939](D:/folder/test/output/0d4aaa48-bd9a-4c61-a96a-38f4bf00f2d0.png) + +![image-20240324184041843](D:/folder/test/output/90c2c452-4094-4a5a-b87a-432ecbc71935.png) + +**仅使用比例(P)控制无法消除稳态误差。**稳态误差是指当系统达到平衡状态时,控制系统的实际输出与期望输出之间的差异。 + +原因:当系统接近其期望点时,误差减小,进而控制器输出也减小。如果控制器输出减小到无法克服系统内部阻力(如摩擦力)或外部扰动的程度时,系统就无法进一步接近设定点,从而留下稳态误差。 + +![image-20240324184057164](D:/folder/test/output/fb14b105-9c1b-49c6-9112-115bd5dc6a9b.png) + +为了解决稳态误差问题,通常会在P控制基础上加入积分(I)控制。积分控制能够累积误差,即使是很小的误差,也会随时间积累,最终产生足够的控制作用以调整系统输出,直到误差为零。 + +![image-20240324184110766](D:/folder/test/output/941c8bde-fdbe-4c5a-966a-cc4c98625c58.png) + +微分控制在PID控制器中的作用主要是提高系统的瞬态响应和稳定性。 +$$ +{k}_{d}({e}_{i}-{e}_{i-1}) +$$ +它通过对误差信号的变化率(即误差的微分)进行响应,来预测系统未来的行为。如果误差在快速变化,微分项会产生一个相对较大的控制作用来尝试减缓这种变化。 + + + +## 相关控制知识 + +![image-20240325101541488](D:/folder/test/output/aa1c5e5d-ab7c-4f1c-9ec3-e33e5084b461.png) + +当系统启动时或者遇到大的扰动,会产生大的初始误差。若系统调整缓慢,积分项会在达到目标状态之前累积很大的值。这可导致控制器输出超出了实际的执行器(比如电机、阀门等)可以处理的范围。当这种情况发生时,即使误差减少,由于积分项累积的值太大,控制器的输出可能仍然处于饱和状态。 + +**积分限幅** + +积分限幅可防止积分项超过预设的上限和下限。 +$$ +{I}_{clamped}(t)=clamp({I}_{updated(t)},{I}_{max},{I}_{min}) +$$ + + +**积分分离** + +当误差超过某个预定阈值时,禁用积分作用,仅使用比例(P)和微分(D)控制来快速减小误差,避免因积分作用导致的控制器输出过度响应。 + +``` +if (abs(error) > threshold) { + // 积分作用被分离,即暂时禁用积分作用 + integral = 0; +} else { + // 正常积分累积 + integral += error * dt; +} + +``` + + + +PID控制器: + +```python + def update(self, current_value): + error = self.set_point - current_value + + # 实现积分分离逻辑 + if abs(error) < self.error_threshold: + self.integral += error * self.dt + # 应用积分限幅 + self.integral = max(min(self.integral, self.integral_limit), -self.integral_limit) + else: + # 误差过大时重置积分累积 + self.integral = 0 + + derivative = (error - self.prev_error) / self.dt + + # PID 输出 + output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative + + self.prev_error = error + return output +``` + diff --git a/科研/matlab.md b/科研/matlab.md new file mode 100644 index 0000000..718aaa7 --- /dev/null +++ b/科研/matlab.md @@ -0,0 +1,49 @@ +## matlab笔记 + +#### 命令行窗口 + +clc:清屏(命令行窗口) + +clear all:把命名的变量删掉,不是命令行窗口 + +#### 命名规则: + +变量命名以字母开头,不可以下划线,变量是区分字母大小写的 + +#### 脚本 + +%% xxx 注释(百分号+一个空格) + +% xxx 也是注释 + +s='a' '"aaaa",字符串 + +abs(s) 字符s的ascii码,为97 + +char(97), 输出'a' + +numtostr(65) ans='65',数字转字符串 + +length(str),字符串的长度 + +#### 矩阵 + +A=[1 2 3 ;4 5 6 ;7 8 9] 分号换行 + +B=A‘ ,矩阵转置 + +C=A(:) ,将矩阵拉成一列,按列存储,第一列拼接第二列拼接第三列 + +D=inv(A) 求逆矩阵 + +E=zeros(10,5,3) 生成10行5列3维0矩阵 + +![](D:/folder/test/output/b1c033f0-18b8-4daf-abc3-92d225719492.png) + +#### 元胞数组 + +​ A=cell(1,6),生成1行6列的小格子,每个小格子可以存放各种数据 + +eye(3),生成3x3的单位阵 + +​ A{2}=eye(3),matlab数组从1开始,不是0 \ No newline at end of file diff --git a/科研/交替方向乘子法(ADMM).md b/科研/交替方向乘子法(ADMM).md new file mode 100644 index 0000000..350b760 --- /dev/null +++ b/科研/交替方向乘子法(ADMM).md @@ -0,0 +1,235 @@ +# 交替方向乘子法(ADMM) + +**Alternating Direction Method of Multipliers (ADMM)** 是一种用于求解大规模优化问题的高效算法,结合了拉格朗日乘子法和分裂方法的优点。 + +--- + +## 基本概念 + +- **优化问题分解** + ADMM 的核心思想是将复杂优化问题分解为多个较简单的子问题,通过引入辅助变量将原问题转化为约束优化问题,使子问题独立求解。 + +- **拉格朗日乘子** + 利用拉格朗日乘子处理约束条件,构造增强拉格朗日函数,确保子问题求解时同时考虑原问题的约束信息。 + +- **交替更新** + 通过交替更新子问题的解和拉格朗日乘子,逐步逼近原问题的最优解。 + +--- + +## 算法流程 + +1. **问题分解** + 将原问题分解为两个子问题。假设原问题表示为: + $$\min_{x, z} f(x) + g(z) \quad \text{s.t.} \quad Ax + Bz = c$$ + 其中 $f$ 和 $g$ 是凸函数,$A$ 和 $B$ 为给定矩阵。 + +2. **构造增强拉格朗日函数** + 引入拉格朗日乘子 $y$,构造增强拉格朗日函数: + $$L_\rho(x, z, y) = f(x) + g(z) + y^T(Ax+Bz-c) + \frac{\rho}{2}\|Ax+Bz-c\|^2$$ + 其中 $\rho > 0$ 控制惩罚项的权重。 + +3. **交替更新** + - **更新 $x$**:固定 $z$ 和 $y$,求解 $\arg\min_x L_\rho(x, z, y)$。 + - **更新 $z$**:固定 $x$ 和 $y$,求解 $\arg\min_z L_\rho(x, z, y)$。 + - **更新乘子 $y$**:按梯度上升方式更新: + $$y := y + \rho(Ax + Bz - c)$$ + +4. **迭代求解** + 重复上述步骤,直到原始残差和对偶残差满足收敛条件(如 $\|Ax+Bz-c\| < \epsilon$)。 + + + +## 例子 + +下面给出一个简单的数值例子,展示 ADMM 在求解分解问题时的迭代过程。我们构造如下问题: + +$$ +\begin{aligned} +\min_{x, z}\quad & (x-1)^2 + (z-2)^2 \\ +\text{s.t.}\quad & x - z = 0. +\end{aligned} +$$ + +**注意**:由于约束要求 $x=z$,实际问题等价于 + +$$ +\min_{x} (x-1)^2 + (x-2)^2, +$$ + +其解析最优解为: + +$$ +2(x-1)+2(x-2)=4x-6=0\quad\Rightarrow\quad x=1.5, +$$ + +因此我们希望得到 $x=z=1.5$。 + + + +**构造 ADMM 框架** + +将问题写成 ADMM 标准形式: + +- 令 + $$ + f(x)=(x-1)^2,\quad g(z)=(z-2)^2, + $$ + +- 约束写为 + $$ + x-z=0, + $$ + 即令 $A=1$、$B=-1$、$c=0$。 + +增强拉格朗日函数为 + +$$ +L_\rho(x,z,y)=(x-1)^2+(z-2)^2+y(x-z)+\frac{\rho}{2}(x-z)^2, +$$ + +其中 $y$ 是拉格朗日乘子,$\rho>0$ 是惩罚参数。为简单起见,我们选取 $\rho=1$。 + + + +**ADMM 的更新公式** + +针对本问题可以推导出三个更新步骤: + +1. **更新 $x$:** + + 固定 $z$ 和 $y$,求解 + $$ + x^{k+1} = \arg\min_x\; (x-1)^2 + y^k(x-z^k)+\frac{1}{2}(x-z^k)^2. + $$ + 对 $x$ 求导并令其为零: + $$ + 2(x-1) + y^k + (x-z^k)=0 \quad\Rightarrow\quad (2+1)x = 2 + z^k - y^k, + $$ + 得到更新公式: + $$ + x^{k+1} = \frac{2+z^k-y^k}{3}. + $$ + +2. **更新 $z$:** + + 固定 $x$ 和 $y$,求解 + $$ + z^{k+1} = \arg\min_z\; (z-2)^2 - y^kz+\frac{1}{2}(x^{k+1}-z)^2. + $$ + 注意:由于 $y(x-z)$ 中关于 $z$ 的部分为 $-y^kz$(常数项 $y^kx$ 可忽略),求导得: + $$ + 2(z-2) - y^k - (x^{k+1}-z)=0 \quad\Rightarrow\quad (2+1)z = 4 + y^k + x^{k+1}, + $$ + 得到更新公式: + $$ + z^{k+1} = \frac{4+y^k+x^{k+1}}{3}. + $$ + +3. **更新 $y$:** + + 按梯度上升更新乘子: + $$ + y^{k+1} = y^k + \rho\,(x^{k+1}-z^{k+1}). + $$ + 这里 $\rho=1$,所以 + $$ + y^{k+1} = y^k + \bigl(x^{k+1}-z^{k+1}\bigr). + $$ + + + +**数值迭代示例** + +**第 1 次迭代:** + +- **更新 $x$:** + + $$ + x^1 = \frac{2+z^0-y^0}{3}=\frac{2+0-0}{3}=\frac{2}{3}\approx0.667. + $$ + +- **更新 $z$:** + + $$ + z^1 = \frac{4+y^0+x^1}{3}=\frac{4+0+0.667}{3}\approx\frac{4.667}{3}\approx1.556. + $$ + +- **更新 $y$:** + + $$ + y^1 = y^0+(x^1-z^1)=0+(0.667-1.556)\approx-0.889. + $$ + +--- + +**第 2 次迭代:** + +- **更新 $x$:** + + $$ + x^2 = \frac{2+z^1-y^1}{3}=\frac{2+1.556-(-0.889)}{3}=\frac{2+1.556+0.889}{3}\approx\frac{4.445}{3}\approx1.4817. + $$ + +- **更新 $z$:** + + $$ + z^2 = \frac{4+y^1+x^2}{3}=\frac{4+(-0.889)+1.4817}{3}=\frac{4-0.889+1.4817}{3}\approx\frac{4.5927}{3}\approx1.5309. + $$ + +- **更新 $y$:** + + $$ + y^2 = y^1+(x^2-z^2)\approx -0.889+(1.4817-1.5309)\approx -0.889-0.0492\approx -0.938. + $$ + +--- + +**第 3 次迭代:** + +- **更新 $x$:** + + $$ + x^3 = \frac{2+z^2-y^2}{3}=\frac{2+1.5309-(-0.938)}{3}=\frac{2+1.5309+0.938}{3}\approx\frac{4.4689}{3}\approx1.4896. + $$ + +- **更新 $z$:** + + $$ + z^3 = \frac{4+y^2+x^3}{3}=\frac{4+(-0.938)+1.4896}{3}\approx\frac{4.5516}{3}\approx1.5172. + $$ + +- **更新 $y$:** + + $$ + y^3 = y^2+(x^3-z^3)\approx -0.938+(1.4896-1.5172)\approx -0.938-0.0276\approx -0.9656. + $$ + +--- + +从迭代过程可以看出: + +- $x$ 和 $z$ 的值在不断调整,目标是使两者相等,从而满足约束。 +- 最终随着迭代次数增加,$x$ 和 $z$ 会收敛到约 1.5,同时乘子 $y$ 收敛到 $-1$(这与 KKT 条件相符)。 + + + +## 应用领域 + +- **大规模优化** + 在大数据、机器学习中利用并行计算加速求解。 +- **信号与图像处理** + 用于去噪、压缩感知等稀疏表示问题。 +- **分布式计算** + 在多节点协同场景下求解大规模问题。 + +--- + +## 优点与局限性 + +| **优点** | **局限性** | +| ------------------ | ---------------------- | +| 分布式计算能力 | 小规模问题可能收敛较慢 | +| 支持稀疏性和正则化 | 参数 $\rho$ 需精细调节 | +| 收敛性稳定 | — | + diff --git a/科研/传统图机器学习.md b/科研/传统图机器学习.md new file mode 100644 index 0000000..45e2e0b --- /dev/null +++ b/科研/传统图机器学习.md @@ -0,0 +1,434 @@ +## 传统图机器学习和特征工程 + +### 节点层面的特征工程 + +目的:**描述网络中节点的结构和位置** + +eg:输入某个节点的D维向量,输出该节点是某类的概率。 + +#### 节点度 (Node Degree) + +定义: +节点度是指一个节点直接连接到其他节点的边数。 + +- 无向图中: 节点的度即为与其相邻的节点数。 +- 有向图中: 通常区分为入度(有多少条边指向该节点)和出度(从该节点发出的边的数量)。 + +意义: +节点度直观反映了节点在网络中的直接连接能力。度高的节点通常在信息传播或资源分配中具有较大作用。 + +> 例如,在社交网络中,一个拥有大量好友的用户(高节点度)可能被视为“热门”或者“活跃”的社交人物。 + +#### 节点中心性 (Node Centrality) + +定义: +节点中心性是一类衡量节点在整个网络中“重要性”或“影响力”的指标。其核心思想是,不仅要看节点的直接连接数,还要看它在网络中的位置和在信息流动中的角色。 + +常见的指标: + +- *介数中心性 (Betweenness Centrality)*: 衡量节点位于其他节点间最短路径上的频率,反映其作为“桥梁”的作用。 +- *接近中心性 (Closeness Centrality)*: 衡量节点到网络中其他节点平均距离的倒数,距离越近中心性越高。 +- *特征向量中心性 (Eigenvector Centrality)*: 除了考虑连接数量外,还考虑邻居节点的“重要性”,连接到重要节点会提升自身的中心性。 + +**举例:** + +假设有 3 个节点,记为 $1, 2, 3$,它们构成了一条链: +$$ +1 \; \leftrightarrow \; 2 \; \leftrightarrow \; 3 +$$ +即只有边 $(1,2)$ 和 $(2,3)$,没有直接连接 $(1,3)$。 + +令邻接矩阵 $A$ 为: +$$ +A \;=\; +\begin{pmatrix} +0 & 1 & 0\\ +1 & 0 & 1\\ +0 & 1 & 0 +\end{pmatrix}. +$$ + + +**介数中心性(必经之地):** +$$ +\displaystyle +c_v \;=\; \sum_{s \neq t}\; \frac{\sigma_{st}(v)}{\sigma_{st}}, +$$ +其中 + +- $\sigma_{st}$ 表示从节点 $s$ 到节点 $t$ 的**所有最短路径数**; +- $\sigma_{st}(v)$ 表示这些最短路径当中**经过节点 $v$** 的条数; +- 求和一般只考虑 $s \neq t$ 且 $s \neq v \neq t$ 的情形,避免把端点本身算作中间节点。 + +换言之,**节点 $v$ 的介数中心性**就是它作为“中间节点”出现在多少对 $(s,t)$ 的最短路径上。 + +对 3 个节点 $\{1,2,3\}$,不同的 $(s,t)$ 有: + +1. $(s,t)=(1,2)$:最短路径为 $(1,2)$。 + - 路径上节点:1 → 2 + - 中间节点:**无** (1、2 是端点) +2. $(s,t)=(1,3)$:最短路径为 $(1,2,3)$。 + - 路径上节点:1 → 2 → 3 + - 中间节点:**2** +3. $(s,t)=(2,3)$:最短路径为 $(2,3)$。 + - 路径上节点:2 → 3 + - 中间节点:**无** (2、3 是端点) + +1. 节点 1 + + - 只会出现在路径 (1,2) 或 (1,3) 的端点位置;(2,3) 的最短路径不包含 1。 + + - 端点不计作中间节点,所以 $\sigma_{st}(1) = 0$ 对所有 $s\neq t\neq 1$。 + + - 因此 + $$ + c_1 = 0. + $$ + +2. 节点 2 + + - 在 (1,3) 的最短路径 (1,2,3) 中,2 是中间节点。此时 $\sigma_{1,3}(2) = 1$; + + - (1,2) 路径 (1,2) 中 2 是端点,(2,3) 路径 (2,3) 中 2 也是端点,因此不计入中间节点。 + + - 所以 + $$ + c_2 = \underbrace{\frac{\sigma_{1,3}(2)}{\sigma_{1,3}}}_{=1/1=1} \;=\; 1. + $$ + + + +**接近中心性(去哪儿都近):** +$$ +c_v \;=\; \frac{1}{\sum_{u \neq v} d(u,v)}, +$$ +其中 $d(u,v)$ 表示节点 $u$ 与节点 $v$ 的最短路径距离。 + +节点1: + +- 与节点 2 的距离:$d(1,2)=1$。 +- 与节点 3 的距离:$d(1,3)=2$(路径 1→2→3)。 +- 距离和:$\;1 + 2 = 3.$ +- 接近中心性:$\; c_1 = \tfrac{1}{3} \approx 0.333.$ + +其他两节点同理。 + + + +**特征向量中心性** + +特征向量中心性要求我们求解 +$$ +A\,\mathbf{x} = \lambda\,\mathbf{x}, +$$ +并选取对应**最大特征值** $\lambda$ 的特征向量 $\mathbf{x}$ 来代表每个节点的中心性。 + +**记 $\mathbf{x} = (x_1,\,x_2,\,x_3)^T$** +这里 $x_1$ 是节点 1 的中心性值,$x_2$ 是节点 2 的中心性值,$x_3$ 是节点 3 的中心性值 + +1. **方程 $A\,\mathbf{x} = \lambda\,\mathbf{x}$ 具体展开** + $$ + \begin{pmatrix} + 0 & 1 & 0\\ + 1 & 0 & 1\\ + 0 & 1 & 0 + \end{pmatrix} + \begin{pmatrix} + x_1\\ + x_2\\ + x_3 + \end{pmatrix} + \;=\; + \lambda + \begin{pmatrix} + x_1\\ + x_2\\ + x_3 + \end{pmatrix}. + $$ + 这意味着: + $$ + \begin{cases} + 1.\; x_2 = \lambda\, x_1, \\ + 2.\; x_1 + x_3 = \lambda\, x_2, \\ + 3.\; x_2 = \lambda\, x_3. + \end{cases} + $$ + +2. **求解最大特征值 $\lambda$ 及对应的 $\mathbf{x}$** + + - 通过特征多项式可知本矩阵的最大特征值为 $\sqrt{2}$。 + + - 最终(若需单位化)可以将向量归一化为 + $$ + \mathbf{x} = \frac{1}{2}\,\begin{pmatrix} 1 \\ \sqrt{2} \\ 1 \end{pmatrix}. + $$ + +3. **解释** + + - 节点 2 的得分最高($\tfrac{\sqrt{2}}{2}\approx 0.707$),因为它连接了节点 1 和 3,两边的贡献都能“传递”到它。 + - 节点 1 和 3 的得分相同且略低($\tfrac{1}{2}=0.5$),因为它们都只与节点 2 相连。 + + + +#### 聚类系数 (Clustering Coefficient) + +**定义:** +聚类系数描述一个节点的邻居之间彼此相连的紧密程度。 + +- 对于某个节点,其局部聚类系数计算为: + $$ + C = \frac{2 \times \text{实际邻居间的边数}}{k \times (k-1)} + $$ + 其中 $k$ 为该节点的度数。 + +**意义:** + +- **高聚类系数:** 表示节点的邻居往往彼此熟识,形成紧密的小团体。 +- **低聚类系数:** 则说明邻居之间联系较少,节点更多处于桥梁位置,可能连接不同的社群。 + +> 在很多网络中,聚类系数能揭示局部社区结构和节点的协同效应。 + + + +#### Graphlets + +**定义:** +Graphlets 是指网络中规模较小(通常由3至5个节点构成)的非同构子图。 + +- 通过统计一个节点参与的各种 graphlet 模式的数量,我们可以构造出该节点的 Graphlet Degree Vector (GDV)。 + +**意义:** + +- Graphlets 能捕捉节点在局部网络结构中的精细模式,比单纯的度数或聚类系数提供更丰富的信息。 +- 在很多应用中(如生物网络分析或社交网络挖掘),通过分析节点参与的 graphlet 模式,可以更好地理解节点的功能和在整个网络中的角色。 + +> Graphlets 被视为网络的“结构指纹”,有助于区分功能不同的节点。 + + + +### 连接层面的特征工程 + +目的:**通过已知连接补全未知连接** + +eg: AB之间有连接,BC之间有连接,那么AC之间是否可能有连接呢? + +法一:直接提取连接的特征,把连接变成D维向量(推荐)。 + +法二:把连接两端节点的D维向量拼接,即上一讲的节点特征拼接(不推荐,损失了连接本身的结构信息。) + + + +#### Distance-based Feature + +**核心思路:** 用两个节点之间的最短路径长度(或加权距离等)作为边的特征,衡量节点对的“接近”程度。 + + + +#### Local Neighborhood Overlap + +**核心思路:** 度量两个节点在其“一阶邻居”层面共享多少共同邻居,或者它们的邻居集合相似度如何。 + +1. **Common Neighbors** + $$ + \text{CN}(u,v) \;=\; \bigl|\,N(u)\,\cap\,N(v)\bigr|, + $$ + 其中 $N(u)$ 是节点 $u$ 的邻居集合,$\cap$ 表示交集。数值越大,表示两节点在局部网络中有更多共同邻居。 + +2. **Jaccard Coefficient** + $$ + \text{Jaccard}(u,v) \;=\; \frac{\bigl|\,N(u)\,\cap\,N(v)\bigr|}{\bigl|\,N(u)\,\cup\,N(v)\bigr|}. + $$ + 反映了两个节点邻居集合的交并比,越大则两者邻居越相似。 + +3. **Adamic-Adar** + $$ + \text{AA}(u,v) \;=\; \sum_{w \,\in\, N(u)\,\cap\,N(v)} \frac{1}{\log\,\bigl|N(w)\bigr|}. + $$ + 共同邻居数目较多、且这些邻居本身度数越小,贡献越大。常用于社交网络链接预测。(直观理解:如果AB都认识C,且C是个社牛,那么AB之间的友谊就不一定好) + + + +#### Global Neighborhood Overlap + +**核心思路:** 不只看“一阶邻居”,还考虑更大范围(如 2 步、3 步乃至更多跳数)上的共同可达节点,或更广泛的结构相似度。 + +**Katz 指标**:累加节点间所有长度的路径并衰减; + +**Random Walk**:基于随机游走来度量节点对的全局可达性; + +**Graph Embedding**:DeepWalk、node2vec等,都可将多跳结构信息编码到向量表示里,再用向量相似度当作边特征。 + + + +**真实情况如何编码边的特征?** + +在一个 **边的特征工程** 任务中,可以将 **Distance-based Feature**、**Local Neighborhood Overlap** 和 **Global Neighborhood Overlap** 等特征组合起来,形成一个完整的**特征向量**。 + +例如:对于每条边 $ (u,v) $,我们提取以下 6 种特征: + +1. **最短路径长度** $ d(u,v) $ (Distance-based Feature) +2. **共同邻居数** $ CN(u,v) $ (Local Neighborhood Overlap) +3. **Jaccard 系数** $ Jaccard(u,v) $ (Local Neighborhood Overlap) +4. **Adamic-Adar 指标** $ AA(u,v) $ (Local Neighborhood Overlap) +5. **Katz 指数** $ Katz(u,v) $ (Global Neighborhood Overlap) +6. **Random Walk 访问概率** $ RW(u,v) $ (Global Neighborhood Overlap) + +对于图中某条边 $ (A, B) $,它的特征向量可能是: +$$ +\mathbf{f}(A, B) = \big[ 2, 5, 0.42, 0.83, 0.31, 0.45 \big] +$$ + + +### 图层面的特征工程 + +目的:网络相似度、图分类(已知分子结构判断是否有xx特性) + +当我们要对**整张图**进行分类或比较(如图分类、图相似度计算等),需要将图转化为**可比较的向量或特征**。最朴素的想法是: + +​ **Bag-of-Node-Degrees**:统计每个节点的度,然后构建一个“度分布”或“度直方图”。 + +- 例如,对图 $G$,我们计算 $\phi(G) = [\,\text{count of deg}=1,\ \text{count of deg}=2,\ \dots\,]$。 +- 缺点:只关注了节点度,**无法区分很多结构不同、但度分布相同**的图。 + +为解决这个不足,人们提出了更精细的“Bag-of-*”方法,把节点周围的**更丰富结构**(子图、子树、图形)纳入统计,从而形成更有判别力的特征。 + +#### Graphlet Kernel + +- **Graphlets**:指小规模(如 3 节点、4 节点、5 节点)的**非同构子图**。 +- **做法**:枚举或随机采样网络中的所有小型子图(graphlets),并根据其类型计数出现频率。 + - 比如在 4 节点层面,有 6 种不同的非同构结构,就统计每种结构出现多少次。 +- **得到的特征**:一个“graphlet type”直方图,即 $\phi(G) = \big[\text{count}(\text{graphlet}_1), \dots, \text{count}(\text{graphlet}_k)\big]$。 +- **优点**:比单纯节点度更能捕捉网络的局部模式。 +- **缺点**:当图很大时,遍历或采样 graphlet 代价较高;仅依赖小子图也可能忽略更大范围结构。 + + + +#### Weisfeiler–Lehman Kernel + +- **Weisfeiler–Lehman (WL) 核**是一种**基于迭代标签传播**的方法,用于**图同构测试**和**图相似度**计算。 +- **核心思路**: + 1. **初始标签**:给每个节点一个初始标签(可能是节点的类型或颜色)。 + 2. **迭代更新**:在每一步迭代中,将节点自身标签与其邻居标签拼接后做哈希,得到新的标签。 + 3. **记录“标签多重集”**:每次迭代会产生新的节点标签集合,可视为“节点子树结构”的某种编码。 +- **Bag-of-Labels / Bag-of-Subtrees**: + - 在每一轮迭代后,统计**各类标签出现次数**,累加到特征向量中。 + - 相当于对**节点子树**或“局部邻域结构”做词袋统计。 +- **优点**:在保留更多结构信息的同时,计算复杂度相对可控。 +- **缺点**:仍然可能有一定的“同构测试”盲区;对于非常复杂的图,标签碰撞可能出现。 + +例子: + +假设我们有一个简单的无向图,包含 4 个节点和 4 条边,结构如下: + +``` +1 — 2 — 3 + | + 4 +``` + +**1. 初始化标签** + +首先,给每个节点一个初始标签。假设我们直接用节点的度数作为初始标签: + +初始标签如下: + +- 节点 1: `1` +- 节点 2: `3` +- 节点 3: `1` +- 节点 4: `1` + +**2. 第一次迭代** + +在第一次迭代中,每个节点的标签会更新为其自身标签和邻居标签的**拼接**,然后通过哈希函数生成新的标签。 + +- **节点 1**: + - 邻居是节点 2,标签为 `3`。 + - 拼接后的标签为 `(1, 3)`。 + - 假设哈希结果为 `A`。 + +- **节点 2**: + - 邻居是节点 1、3、4,标签分别为 `1`、`1`、`1`。 + - 拼接后的标签为 `(3, 1, 1, 1)`。 + - 假设哈希结果为 `B`。 + +- **节点 3**: + - 邻居是节点 2,标签为 `3`。 + - 拼接后的标签为 `(1, 3)`。 + - 假设哈希结果为 `A`。 + +- **节点 4**: + - 邻居是节点 2,标签为 `3`。 + - 拼接后的标签为 `(1, 3)`。 + - 假设哈希结果为 `A`。 + +第一次迭代后的标签如下: + +- 节点 1: `A` +- 节点 2: `B` +- 节点 3: `A` +- 节点 4: `A` + +**3. 第二次迭代** + +- **节点 1**: + - 邻居是节点 2,标签为 `B`。 + - 拼接后的标签为 `(A, B)`。 + - 假设哈希结果为 `C`。 + +- **节点 2**: + - 邻居是节点 1、3、4,标签分别为 `A`、`A`、`A`。 + - 拼接后的标签为 `(B, A, A, A)`。 + - 假设哈希结果为 `D`。 + +- **节点 3**: + - 邻居是节点 2,标签为 `B`。 + - 拼接后的标签为 `(A, B)`。 + - 假设哈希结果为 `C`。 + +- **节点 4**: + - 邻居是节点 2,标签为 `B`。 + - 拼接后的标签为 `(A, B)`。 + - 假设哈希结果为 `C`。 + +第二次迭代后的标签如下: + +- 节点 1: `C` +- 节点 2: `D` +- 节点 3: `C` +- 节点 4: `C` + +**4. 停止条件** + +通常,WL Kernel 会进行多次迭代,直到节点的标签不再变化(即收敛)。在这个例子中,假设我们只进行两次迭代。 + +**5.统计标签的多重集** + +在每次迭代后,统计图中所有节点的标签分布(即“标签多重集”),并将其作为图的特征。 + +- **初始标签多重集**: + - 标签 `1` 出现 3 次(节点 1、3、4)。 + - 标签 `3` 出现 1 次(节点 2)。 + +- **第一次迭代后的标签多重集**: + - 标签 `A` 出现 3 次(节点 1、3、4)。 + - 标签 `B` 出现 1 次(节点 2)。 + +- **第二次迭代后的标签多重集**: + - 标签 `C` 出现 3 次(节点 1、3、4)。 + - 标签 `D` 出现 1 次(节点 2)。 + +$$ +\phi(G) = [\text{count}(1), \text{count}(3), \text{count}(A), \text{count}(B), \text{count}(C), \text{count}(D)]. \\ +\phi(G) = [3, 1, 3, 1, 3, 1]. +$$ + +**直观理解** + +- 初始标签:只关注节点的度数。 +- 第一次迭代:关注节点的度数及其邻居的度数。 +- 第二次迭代:关注节点的度数、邻居的度数,以及邻居的邻居的度数。 +- 随着迭代的进行,WL Kernel 能够捕捉到越来越复杂的局部结构信息。 + + + diff --git a/科研/卡尔曼滤波.md b/科研/卡尔曼滤波.md new file mode 100644 index 0000000..c0ea840 --- /dev/null +++ b/科研/卡尔曼滤波.md @@ -0,0 +1,254 @@ +# 卡尔曼滤波 + +卡尔曼滤波(Kalman Filter)是一种用于线性动态系统状态估计的递归最优滤波算法,它在**噪声环境**下对系统状态进行估计,并常用于目标跟踪、导航和控制等领域。 + +卡尔曼滤波假设系统可以用状态空间模型描述,模型包括两个部分: + +- **状态转移模型**:描述系统状态如何从上一时刻转移到当前时刻。 +- **测量模型**:描述通过传感器获得的测量值与系统状态之间的关系。 + +这两个模型中均包含随机噪声,分别记为过程噪声和测量噪声。卡尔曼滤波的目标就是在已知这些噪声统计特性的前提下,利用当前和过去的测量值来对系统状态进行最优估计。 + +## 引入 + +![image-20240311130512387](D:/folder/test/output/75078ebe-6c65-409b-b8d9-f4163a37fbaf.png) + +![image-20240311130359589](D:/folder/test/output/13a65daf-5f59-4ee2-aac1-8feaa68dc6a5.png) + +![image-20240311130828892](D:/folder/test/output/62b05638-f749-4200-91d8-83ed332e9f28.png) + +## 公式 + +**状态转移模型** + +设系统的状态向量为 $\mathbf{x}_k$,控制输入为 $\mathbf{u}_k$,过程噪声为 $\mathbf{w}_k$(假设均值为0,协方差矩阵为 $\mathbf{Q}$),状态转移模型可写为: + +$$ +\mathbf{x}_k = \mathbf{A} \mathbf{x}_{k-1} + \mathbf{B} \mathbf{u}_{k-1} + \mathbf{w}_{k-1} +$$ + +其中: + +- $\mathbf{A}$ 是状态转移矩阵, +- $\mathbf{B}$ 是控制输入矩阵。 + +**测量模型** + +设测量向量为 $\mathbf{z}_k$,测量噪声为 $\mathbf{v}_k$(假设均值为0,协方差矩阵为 $\mathbf{R}$),测量模型为: + +$$ +\mathbf{z}_k = \mathbf{H} \mathbf{x}_k + \mathbf{v}_k +$$ + +其中: + +- $\mathbf{H}$ 是测量矩阵。 + + + +## 递归过程 + +卡尔曼滤波的递归过程主要分为两大步:**预测(Prediction)** 和 **更新(Update)**。 + +注意:$\hat{\mathbf{x}}_k^-$右上角的'-'符号是区分预测状态和更新后的状态。 + +### 预测步骤 + +1. **状态预测**: + + 利用系统的状态转移模型,将上一次的状态估计 $\hat{\mathbf{x}}_{k-1}$ 通过转移矩阵 $\mathbf{A}$(和控制输入 $\mathbf{B} \mathbf{u}_{k-1}$)预测到当前时刻的状态: + $$ + \hat{\mathbf{x}}_k^- = \mathbf{A} \hat{\mathbf{x}}_{k-1} + \mathbf{B} \mathbf{u}_{k-1} + $$ + 这里 $\hat{\mathbf{x}}_k^-$ 称为**先验状态估计**,它反映了系统在没有新测量数据情况下的预期状态。 + +2. **协方差预测**: + 同时,将上一次状态的不确定性(协方差矩阵 $\mathbf{P}_{k-1}$)传播到当前时刻,并加上过程噪声 $\mathbf{Q}$ 的影响: + $$ + \mathbf{P}_k^- = \mathbf{A} \mathbf{P}_{k-1} \mathbf{A}^\mathrm{T} + \mathbf{Q} + $$ + 这个预测协方差反映了预测状态的置信程度,不确定性通常会因过程噪声的加入而增大。 + +### 更新步骤 + +当时刻 $k$ 新的测量值 $\mathbf{z}_k$ 到达时,我们使用它来校正预测结果。 + +1. **卡尔曼增益的计算**: + 卡尔曼增益 $\mathbf{K}_k$ 衡量了预测的不确定性与测量不确定性之间的权衡。计算公式为: + $$ + \mathbf{K}_k = \mathbf{P}_k^- \mathbf{H}^\mathrm{T} \left(\mathbf{H} \mathbf{P}_k^- \mathbf{H}^\mathrm{T} + \mathbf{R}\right)^{-1} + $$ + 当预测的置信度较低($\mathbf{P}_k^-$较大)时,卡尔曼增益较大,说明更多地信任测量值;反之,则更多地依赖预测值。 + +2. **状态更新**: + 根据卡尔曼增益修正先验状态,将测量的偏差信息(即测量值与预测值之间的差异,也叫创新)加权融合: + $$ + \hat{\mathbf{x}}_k = \hat{\mathbf{x}}_k^- + \mathbf{K}_k \left(\mathbf{z}_k - \mathbf{H} \hat{\mathbf{x}}_k^- \right) + $$ + 这个更新后的状态 $\hat{\mathbf{x}}_k$ 就是当前时刻的**后验状态估计**,它综合了预测和测量两方面的信息。 + +3. **协方差更新**: + 更新后的协方差表示在新的测量信息下的不确定性: + $$ + \mathbf{P}_k = (\mathbf{I} - \mathbf{K}_k \mathbf{H}) \mathbf{P}_k^- + $$ + 一般来说,经过更新后,状态的不确定性会降低(即协方差矩阵的数值减小)。 + + + + + +# 扩展卡尔曼滤波 + +扩展卡尔曼滤波(Extended Kalman Filter,简称 EKF)是一种针对非线性系统状态估计问题的滤波方法。传统的卡尔曼滤波要求系统的状态转移和观测模型都是线性的,而在实际问题中,很多系统往往存在非线性特性。EKF 的核心思想就是对非线性模型进行局部线性化,然后在此基础上应用卡尔曼滤波的递归估计方法。 + +1. **非线性系统模型** + 假设系统的状态转移和观测模型为非线性的: + + - 状态转移模型: + $$ + \mathbf{x}_k = f(\mathbf{x}_{k-1}, \mathbf{u}_{k-1}) + \mathbf{w}_{k-1} + $$ + + - 观测模型: + $$ + \mathbf{z}_k = h(\mathbf{x}_k) + \mathbf{v}_k + $$ + 其中,$f(\cdot)$ 和 $h(\cdot)$ 为非线性函数,$\mathbf{w}_{k-1}$ 和 $\mathbf{v}_k$ 分别表示过程噪声和测量噪声(均假设为零均值高斯噪声)。 + +2. **线性化** + 为了使用卡尔曼滤波方法,扩展卡尔曼滤波需要对非线性函数进行局部线性化。具体做法是使用泰勒展开在当前状态估计附近进行一阶近似,计算函数的雅可比矩阵: + + - 状态转移函数 $f$ 的雅可比矩阵: + $$ + F_k = \left.\frac{\partial f}{\partial \mathbf{x}}\right|_{\mathbf{x}=\hat{\mathbf{x}}_{k-1}, \mathbf{u}=\mathbf{u}_{k-1}} + $$ + + - 观测函数 $h$ 的雅可比矩阵: + $$ + H_k = \left.\frac{\partial h}{\partial \mathbf{x}}\right|_{\mathbf{x}=\hat{\mathbf{x}}_k^-} + $$ + +3. **滤波过程** + 扩展卡尔曼滤波的递归过程与标准卡尔曼滤波类似,但在每一步都需要用雅可比矩阵替换原来的线性模型矩阵: + + - **预测步骤**: + + - 状态预测: + $$ + \hat{\mathbf{x}}_k^- = f(\hat{\mathbf{x}}_{k-1}, \mathbf{u}_{k-1}) + $$ + + - 协方差预测: + $$ + \mathbf{P}_k^- = F_k \mathbf{P}_{k-1} F_k^\mathrm{T} + \mathbf{Q} + $$ + 这里 $F_k$ 是在 $\hat{\mathbf{x}}_{k-1}$ 处计算得到的雅可比矩阵。 + + - **更新步骤**: + + - 计算卡尔曼增益: + $$ + \mathbf{K}_k = \mathbf{P}_k^- H_k^\mathrm{T} \left(H_k \mathbf{P}_k^- H_k^\mathrm{T} + \mathbf{R}\right)^{-1} + $$ + + - 状态更新: + $$ + \hat{\mathbf{x}}_k = \hat{\mathbf{x}}_k^- + \mathbf{K}_k \left(\mathbf{z}_k - h(\hat{\mathbf{x}}_k^-)\right) + $$ + + - 协方差更新: + $$ + \mathbf{P}_k = (\mathbf{I} - \mathbf{K}_k H_k) \mathbf{P}_k^- + $$ + +通过这样的线性化步骤,EKF 能够对非线性系统进行状态估计,虽然由于线性化近似可能带来一定误差,但在大多数情况下能达到较好的效果。 + + + + + +**雅各比矩阵定义** + +雅可比矩阵(Jacobian Matrix)是一个多变量函数各个分量对各个变量的偏导数组成的矩阵。它反映了在某一点处函数的局部线性化近似,也就是该函数在这一点的“导数”信息。在扩展卡尔曼滤波中,为了对非线性状态转移函数 $f(\mathbf{x}, \mathbf{u})$ 或观测函数 $h(\mathbf{x})$ 进行线性化,我们需要计算它们在当前估计点的雅可比矩阵。 + +**示例 1:状态转移函数的雅可比矩阵** + +假设系统的状态为 $\mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \end{bmatrix}$(例如,$x_1$ 表示位置,$x_2$ 表示速度),状态转移函数定义为: +$$ +f(\mathbf{x}) = +\begin{bmatrix} +f_1(x_1, x_2) \\ +f_2(x_1, x_2) +\end{bmatrix} += +\begin{bmatrix} +x_1 + x_2 + 0.1 x_1^2 \\ +x_2 + 0.05 x_1 +\end{bmatrix} +$$ +这里函数中的非线性项为 $0.1 x_1^2$ 和 $0.05 x_1$。 + +**求雅可比矩阵** + +雅可比矩阵 $F$ 是一个 $2 \times 2$ 矩阵,其中每个元素为: +$$ +F_{ij} = \frac{\partial f_i}{\partial x_j} +$$ + +计算各个偏导数: + +1. 对 $f_1(x_1, x_2) = x_1 + x_2 + 0.1 x_1^2$: + - $\frac{\partial f_1}{\partial x_1} = 1 + 0.2x_1$ + - $\frac{\partial f_1}{\partial x_2} = 1$ + +2. 对 $f_2(x_1, x_2) = x_2 + 0.05 x_1$: + - $\frac{\partial f_2}{\partial x_1} = 0.05$ + - $\frac{\partial f_2}{\partial x_2} = 1$ + +因此,雅可比矩阵为: +$$ +F = \begin{bmatrix} +1 + 0.2x_1 & 1 \\ +0.05 & 1 +\end{bmatrix} +$$ + +**示例 2:观测函数的雅可比矩阵** + +假设观测函数为: +$$ +h(\mathbf{x}) = +\begin{bmatrix} +h_1(x_1, x_2) \\ +h_2(x_1, x_2) +\end{bmatrix} += +\begin{bmatrix} +\sqrt{x_1} \\ +x_2 +\end{bmatrix} +$$ +这里假设传感器对位置进行非线性测量(取平方根),而速度直接测量。 + +**求雅可比矩阵** + +计算各个偏导数: + +1. 对 $h_1(x_1, x_2) = \sqrt{x_1}$: + - $\frac{\partial h_1}{\partial x_1} = \frac{1}{2\sqrt{x_1}}$ + - $\frac{\partial h_1}{\partial x_2} = 0$(因为 $h_1$ 与 $x_2$ 无关) + +2. 对 $h_2(x_1, x_2) = x_2$: + - $\frac{\partial h_2}{\partial x_1} = 0$ + - $\frac{\partial h_2}{\partial x_2} = 1$ + +因此,雅可比矩阵为: +$$ +H = \begin{bmatrix} +\frac{1}{2\sqrt{x_1}} & 0 \\ +0 & 1 +\end{bmatrix} +$$ + +### diff --git a/科研/图神经网络.md b/科研/图神经网络.md new file mode 100644 index 0000000..a97941d --- /dev/null +++ b/科研/图神经网络.md @@ -0,0 +1,188 @@ +## 图神经网络 + +图表示学习的本质是把节点映射成低维连续稠密的向量。这些向量通常被称为 **嵌入(Embedding)**,它们能够捕捉节点在图中的结构信息和属性信息,从而用于下游任务(如节点分类、链接预测、图分类等)。 + +- **低维**:将高维的原始数据(如邻接矩阵或节点特征)压缩为低维向量,减少计算和存储开销。 +- **连续**:将离散的节点或图结构映射为连续的向量空间,便于数学运算和捕捉相似性。 +- **稠密**:将稀疏的原始数据转换为稠密的向量,每个维度都包含有意义的信息。 + + + +### 对图数据进行深度学习的“朴素做法” + +把图的邻接矩阵和节点特征“直接拼接”成固定维度的输入,然后将其送入一个深度神经网络(全连接层)进行学习。 + +![image-20250316142412685](D:\folder\test\output\image-20250316142412685.png) + +这种做法面临重大问题,导致其**并不可行**: + +1. **$O(|V|^2)$ 参数量** ,参数量庞大 + +2. **无法适应不同大小的图** ,需要固定输入维度 + +3. **对节点顺序敏感** ,节点编号顺序一变,输入就完全变样,但其实图的拓扑并没变(仅节点编号/排列方式不同)。 + + ``` + A —— B + | | + D —— C + ``` + + *矩阵 1*(顺序 $[A,B,C,D]$): + $$ + M_1 = + \begin{pmatrix} + 0 & 1 & 0 & 1\\ + 1 & 0 & 1 & 0\\ + 0 & 1 & 0 & 1\\ + 1 & 0 & 1 & 0 + \end{pmatrix}. + $$ + *矩阵 2*(顺序 $[C,A,D,B]$): + $$ + M_2 = + \begin{pmatrix} + 0 & 0 & 1 & 1 \\ + 0 & 0 & 1 & 1 \\ + 1 & 1 & 0 & 0 \\ + 1 & 1 & 0 & 0 + \end{pmatrix}. + $$ + +​ 两个矩阵完全不同,但**它们对应的图是相同的**(只不过节点的顺序改了)。 + + + +### **邻居聚合** + +#### **计算图** + +在**图神经网络**里,通常每个节点$v$ 都有一个**局部计算图**,用来表示该节点在聚合信息时所需的所有邻居(及邻居的邻居……)的依赖关系。 + +- 直观理解 + - 以节点 $v$ 为根; + - 1-hop 邻居在第一层,2-hop 邻居在第二层…… + - 逐层展开直到一定深度(例如 k 层)。 + - 这样形成一棵“邻域树”或“展开图”,其中每个节点都需要从其子节点(邻居)获取特征进行聚合。 + + + +![image-20250316152729679](D:\folder\test\output\image-20250316152729679.png) + +![image-20250316152836156](D:\folder\test\output\image-20250316152836156.png) + +**例子** + +在图神经网络中,每一层的计算通常包括以下步骤: + +1. **聚合(Aggregation)**:将邻居节点的特征聚合起来(如求和、均值、最大值等)。 + +2. **变换(Transformation)**:将聚合后的特征通过一个神经网络(如 MLP)进行非线性变换。 + + + +``` + A + | + B + / \ + C D +``` + +假设每个节点的特征是一个二维向量: + +- 节点 $ A $ 的特征:$ h_A = [1.0, 0.5] $ +- 节点 $ B $ 的特征:$ h_B = [0.8, 1.2] $ +- 节点 $ C $ 的特征:$ h_C = [0.3, 0.7] $ +- 节点 $ D $ 的特征:$ h_D = [1.5, 0.9] $ + +**第 1 层更新:$A^{(0)} \to A^{(1)}$** + +1. **节点 $A$ 的 1-hop 邻居**:只有 $B$。 + +2. **聚合**(示例:自 + sum 邻居): + $$ + z_A^{(1)} \;=\; A^{(0)} + B^{(0)} + \;=\; [1.0,\,0.5] + [0.8,\,1.2] + \;=\; [1.8,\,1.7]. + $$ + +3. **MLP 变换**:用一个MLP映射 $z_A^{(1)}$ 到 2 维输出: + $$ + A^{(1)} \;=\; \mathrm{MLP_1}\bigl(z_A^{(1)}\bigr). + $$ + + - (数值略,可想象 $\mathrm{MLP}([1.8,1.7]) \approx [1.9,1.1]$ 之类。) + +**结果**:$A^{(1)}$ 包含了 **A** 的初始特征 + **B** 的初始特征信息。 + +--- + +**第 2 层更新:$A^{(1)} \to A^{(2)}$** + +为了让 **A** 获得 **2-hop** 范围($C, D$)的信息,需要**先**让 **B** 在第 1 层就吸收了 $C, D$ 的特征,从而 **B^{(1)}** 蕴含 $C, D$ 信息。然后 **A** 在第 2 层再从 **B^{(1)}** 聚合。 + +1. **节点 B 在第 1 层**(简要说明) + + - 邻居:$\{A,C,D\}$ + - 聚合:$z_B^{(1)} = B^{(0)} + A^{(0)} + C^{(0)} + D^{(0)}$ + - MLP 变换:$B^{(1)} = \mathrm{MLP}\bigl(z_B^{(1)}\bigr)$。 + - 此时 **B^{(1)}** 已经包含了 $C, D$ 的信息。 + +2. **节点 $A$ 的第 2 层聚合** + + - 邻居:$B$,但此时要用 **B^{(1)}**(它已吸收 C、D) + + - **聚合**: + $$ + z_A^{(2)} = A^{(1)} + B^{(1)}. + $$ + + - **MLP 变换**: + $$ + A^{(2)} = \mathrm{MLP_2}\bigl(z_A^{(2)}\bigr). + $$ + +**结果**:$A^{(2)}$ 就包含了 **2-hop** 范围的信息,因为 **B^{(1)}** 中有 $C, D$ 的贡献。 + + + +**GNN 的层数**就是**节点聚合邻居信息的迭代次数**,对应了“节点感受 k-hop 邻域”的深度。每层中,节点会用上一层的邻居表示进行聚合并经过可学习的变换,最终得到本层新的节点表示。 + +同一层里,**所有节点共享一组参数**(同一个 MLP 或线性变换) + + + +``` +public boolean hasCycle(ListNode head) { + // 如果链表为空或者只有一个节点,直接返回无环 + if (head == null || head.next == null) { + return false; + } + + // 用 originalHead 保存最初的头节点 + ListNode originalHead = head; + // 从 head.next 开始遍历,先把 head 与链表分离 + ListNode cur = head.next; + ListNode pre = head; + // 断开 head 和后面的连接 + head.next = null; + + while (cur != null) { + // 如果当前节点又指回了 originalHead,则说明出现环 + if (cur == originalHead) { + return true; + } + // 反转指针 + ListNode temp = cur.next; + cur.next = pre; + // 移动 pre 和 cur + pre = cur; + cur = temp; + } + + // 走到空指针,说明无环 + return false; + } +``` + diff --git a/科研/循环神经网络.md b/科研/循环神经网络.md new file mode 100644 index 0000000..65dcbf8 --- /dev/null +++ b/科研/循环神经网络.md @@ -0,0 +1,249 @@ +## 循环神经网络RNN + +循环神经网络(Recurrent Neural Network,简称RNN)是一类专门用于处理序列数据的神经网络模型。与传统的前馈神经网络不同,RNN具有“记忆”功能,能够捕捉数据序列中的时间依赖关系。 + +### 基本结构 + +RNN的核心在于它的循环结构,这个结构使得信息可以沿着时间步流动。一个典型的RNN单元在时间步 $t$ 接收输入向量 $x_t$ 和前一时刻的隐藏状态 $h_{t-1}$,然后计算当前时刻的隐藏状态 $h_t$。这种循环过程允许模型利用之前的状态信息来影响当前的预测。 + +### 隐藏状态的更新 + +隐藏状态更新通常通过如下公式实现: + +$$ +h_t = f(W_{xh} \cdot x_t + W_{hh} \cdot h_{t-1} + b_h) +$$ + +其中: + +- $h_t$ 表示时间步 $t$ 的隐藏状态(所有隐藏层神经元激活值的集合。)。 +- $x_t$ 是时间步 $t$ 的输入向量。 +- $W_{xh}$ 是输入到隐藏状态的权重矩阵。 +- $W_{hh}$ 是隐藏状态之间的递归**连接权重**矩阵。 +- $b_h$ 是偏置项。 +- $f$ 是激活函数,通常会选择非线性函数如tanh或ReLU,以引入非线性变换。 + +在这种更新过程中,当前的隐藏状态 $h_t$ 同时依赖于当前的输入 $x_t$ 和之前的隐藏状态 $h_{t-1}$,这使得RNN能够捕捉长时间序列中的上下文关系。 + +### 输出层 + +有时RNN还会在每个时间步产生输出,输出计算方式通常为: + +$$ +y_t = g(W_{hy} \cdot h_t + b_y) +$$ + +其中: + +- $y_t$ 是时间步 $t$ 的输出。 +- $W_{hy}$ 是隐藏状态到输出的权重矩阵。 +- $b_y$ 是输出层的偏置项。 +- $g$ 是输出层激活函数(例如softmax用于分类任务)。 + +### 困惑度 + +假设我们有一个测试序列,其中包含 3 个单词,模型对每个单词的预测概率分别为: + +- $P(w_1) = 0.5$ +- $P(w_2|w_1) = 0.2$ +- $P(w_3|w_1, w_2) = 0.1$ + +根据困惑度的公式: + +$$ +\text{Perplexity} = \exp\left(-\frac{1}{N} \sum_{i=1}^{N} \log P(w_i | \text{context})\right) +$$ + +当模型对每个单词都能百分之百预测(即概率为1),则平均交叉熵为0,困惑度为 $\exp(0)=1$。这表示模型没有任何不确定性,是理想状态。 + + + +我们这里 $N=3$。下面是具体的计算步骤: + +1. **计算每个单词的对数概率** + +$$ +\log P(w_1) = \log(0.5) \approx -0.6931 +$$ + +$$ +\log P(w_2|w_1) = \log(0.2) \approx -1.6094 +$$ + +$$ +\log P(w_3|w_1, w_2) = \log(0.1) \approx -2.3026 +$$ + +2. **求和并求平均** + +将这些对数值相加: + +$$ +\sum_{i=1}^{3} \log P(w_i|\text{context}) = -0.6931 - 1.6094 - 2.3026 \approx -4.6051 +$$ + +然后求平均: + +$$ +\text{平均对数概率} = \frac{-4.6051}{3} \approx -1.5350 +$$ + +3. **计算困惑度** + +取负值再求指数: + +$$ +\text{Perplexity} = \exp\left(1.5350\right) \approx 4.64 +$$ + + + +### 训练过程与挑战 + +整体训练流程可以总结为下面几个步骤,每个 epoch 都会重复这些步骤: + +1. **前向传播** + - 对于一个完整的句子(或者一个批次中的多个句子),模型按顺序处理所有时间步,生成每个时间步的输出。 + - 比如,对于句子“我 爱 编程”,模型会依次处理“我”、“爱”、“编程”,得到对应的输出(例如每个时间步预测下一个词的概率分布)。 +2. **计算损失** + - 将模型在所有时间步的输出与真实目标序列(也就是每个时间步的正确答案)进行比较,计算整体损失。 + - 损失通常是所有时间步损失的总和或平均值,例如均方误差或交叉熵损失。 +3. **反向传播(BPTT)** + - 对**整个句子**进行反向传播,即通过时间(Back Propagation Through Time,BPTT)计算所有时间步的梯度。 + - 这一步会利用链式法则,把整个序列中各个时间步的梯度累积起来,形成每个参数的总梯度。 +4. **参数更新** + - 使用优化器(如 Adam、SGD 等)根据计算得到的梯度更新模型参数。 +5. **重复整个过程** + - 以上步骤构成了一个训练迭代周期(一个 epoch),在一个 epoch 中,所有训练样本都会被送入模型进行训练。 + - 然后在下一个 epoch 中,再次重复整个流程,直到达到预设的 epoch 数或满足其他停止条件。 + + + +在训练过程中,RNN通过反向传播算法(具体为“反向传播通过时间”(BPTT))来更新参数。然而,由于梯度在长序列上传播时可能出现梯度消失或梯度爆炸问题,使得RNN在捕捉长程依赖关系时面临挑战。为此,后来发展出了如长短时记忆网络(LSTM)和门控循环单元(GRU)等改进模型,它们在结构上增加了门控机制,有效缓解了这一问题。 + + + + + +## 门控循环单元GRU + +GRU(Gated Recurrent Unit,门控循环单元)是一种常用的循环神经网络变种,旨在解决标准 RNN 中梯度消失或梯度爆炸的问题,同时比 LSTM 结构更简单。 + +### 基本结构 + +GRU 通过两个门(gate)来控制信息的流动: + +1. **更新门 $z_t$**: + 控制当前隐藏状态需要保留多少来自过去的信息以及引入多少新的信息。 +2. **重置门 $r_t$**: + 决定如何结合新输入和过去的记忆,尤其是在产生候选隐藏状态时。 + +另外,GRU 计算一个候选隐藏状态 $\tilde{h}_t$,并结合更新门 $z_t$ 的信息,更新最终的隐藏状态 $h_t$。 + +### 隐藏状态更新公式 + +对于每个时间步 $t$,GRU 的计算过程通常包括以下步骤: + +1. **更新门 $z_t$** + $$ + z_t = \sigma(W_{xz} x_t + W_{hz} h_{t-1} + b_z) + $$ + 其中: + + - $x_t$ 是当前时间步的输入; + - $h_{t-1}$ 是上一时刻的隐藏状态; + - $W_z$ 和 $U_z$ 是权重矩阵; + - $b_z$ 是偏置向量; + - $\sigma(\cdot)$ 是 sigmoid 函数,用于将输出限制在 $[0, 1]$ 区间。 + +2. **重置门 $r_t$** + $$ + r_t = \sigma(W_{xr} x_t + W_{hr} h_{t-1} + b_r) + $$ + 其中参数意义与更新门类似,重置门决定忘记多少过去的信息。 + +3. **候选隐藏状态 $\tilde{h}_t$** + $$ + \tilde{h}_t = \tanh(W_{xh} x_t + W_{hh} (r_t \odot h_{t-1}) + b_h) + $$ + 这里: + + - $r_t \odot h_{t-1}$ 表示**重置门** $r_t$ 和上一时刻隐藏状态的逐元素相乘(Hadamard 乘积),用以调制历史信息的影响; + - $\tanh(\cdot)$ 用来生成候选隐藏状态,将输出限制在 $[-1, 1]$。 + +4. **最终隐藏状态 $h_t$** + GRU 结合更新门和候选隐藏状态更新最终隐藏状态: + $$ + h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t. + $$ + 这表明更新门 $z_t$ 决定了新信息 $\tilde{h}_t$ 与旧信息 $h_{t-1}$ 的比例。 + + + +### 公式 + +GRU 更新公式如下: +$$ +\begin{aligned} +z_t &= \sigma(W_{xz} x_t + W_{hz} h_{t-1} + b_z), \\ +r_t &= \sigma(W_{xr} x_t + W_{hr} h_{t-1} + b_r), \\ +\tilde{h}_t &= \tanh(W_{xh} x_t + W_{hh}(r_t \odot h_{t-1}) + b_h), \\ +h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t. +\end{aligned} +$$ + + +## 长短时记忆网络LSTM + +LSTM 是一种常用的循环神经网络变种,专门为解决标准 RNN 中的梯度消失问题而设计。它通过引入额外的“记忆单元”和多个门控机制,有效地控制信息的保存、遗忘和输出,从而捕捉长距离的依赖关系。 + +--- + +### 基本结构 + +LSTM 的核心在于其“细胞状态”(cell state),这是一个贯穿整个序列传递的信息流,同时有三个主要的门(gate)来控制细胞状态的更新过程: + +1. **遗忘门 $f_t$** + 决定当前时间步需要遗忘多少之前的记忆信息。 + +2. **输入门 $i_t$** + 决定当前时间步有多少新的信息写入细胞状态。 + +3. **输出门 $o_t$** + 决定当前时间步从细胞状态中输出多少信息作为隐藏状态。 + +此外,还引入了一个候选细胞状态 $\tilde{c}_t$ 用于更新细胞状态。 + +--- + +### 隐藏状态更新公式 + +对于每个时间步 $t$,LSTM 的更新过程通常可以写为以下公式(所有权重矩阵用 $W$ 和 $U$ 表示,各门的偏置为 $b$): + +$$ +\begin{aligned} +\textbf{遗忘门:}\quad f_t &= \sigma\Big(W_{xf}\, x_t + W_{hf}\, h_{t-1} + b_f\Big), \\ +\textbf{输入门:}\quad i_t &= \sigma\Big(W_{xi}\, x_t + W_{hi}\, h_{t-1} + b_i\Big), \\ +\textbf{输出门:}\quad o_t &= \sigma\Big(W_{xo}\, x_t + W_{ho}\, h_{t-1} + b_o\Big), \\ +\\ +\textbf{候选细胞状态:}\quad \tilde{c}_t &= \tanh\Big(W_{xc}\, x_t + W_{hc}\, h_{t-1} + b_c\Big), \\ +\textbf{细胞状态更新:}\quad c_t &= f_t \odot c_{t-1} + i_t \odot \tilde{c}_t, \\ + +\textbf{隐藏状态:}\quad h_t &= o_t \odot \tanh(c_t). +\end{aligned} +$$ + + +### 直观理解 + +- **细胞状态 $c_t$**: + 细胞状态是贯穿整个序列的“记忆通道”,负责长期保存信息。它像一条传送带,在不同时间步中线性传递,避免信息被频繁修改,从而维持长期记忆。 +- **遗忘门 $f_t$**: + 用于丢弃上一时刻不再需要的信息。如果遗忘门输出接近 0,说明遗忘了大部分过去的信息;如果接近 1,则保留大部分信息。 + **类比**:若模型遇到新段落,遗忘门可能关闭(输出接近0),丢弃前一段的无关信息;若需要延续上下文(如故事主线),则保持开启(输出接近1)。 +- **输入门 $i_t$ 和候选细胞状态 $\tilde{c}_t$**: + 输入门控制有多少候选信息被写入细胞状态。候选细胞状态是基于当前输入和上一时刻隐藏状态生成的新信息。 + **类比**:阅读时遇到关键情节,输入门打开,将新信息写入长期记忆(如角色关系),同时候选状态 $\tilde{c}_t$提供新信息的候选内容。 +- **输出门 $o_t$**: + 控制从细胞状态中输出多少信息作为当前时间步的隐藏状态。隐藏状态 $h_t$ 通常用于后续计算(例如,生成输出、参与下一时刻计算)。 + **类比**:根据当前任务(如预测下一个词),输出门决定暴露细胞状态的哪部分(如只关注时间、地点等关键信息)。 \ No newline at end of file diff --git a/科研/数学基础.md b/科研/数学基础.md new file mode 100644 index 0000000..dfddc62 --- /dev/null +++ b/科研/数学基础.md @@ -0,0 +1,1529 @@ +# 数学基础 + +## 求解一阶非齐线性微分方程 + +考虑方程 +$$ +y' + y = x +$$ + +**第一步:求齐次方程的通解** + +先求对应的齐次方程 +$$ +y' + y = 0 +$$ +其解为 +$$ +y_h = Ce^{-x} +$$ +其中 $C$ 为任意常数。 + +**第二步:设特解形式** + +利用常数变易法,令特解取形式 +$$ +y_p = u(x) e^{-x} +$$ +其中 $u(x)$ 为待定函数。 + +**第三步:求导并代入原方程** + +计算 $y_p$ 的导数: +$$ +y_p' = u'(x)e^{-x} - u(x)e^{-x} +$$ +将 $y_p$ 和 $y_p'$ 代入原方程 $y' + y = x$: +$$ +\bigl[u'(x)e^{-x} - u(x)e^{-x}\bigr] + u(x)e^{-x} = u'(x)e^{-x} = x +$$ + +因此有: +$$ +u'(x) = x e^{x} +$$ + +**第四步:求 $u(x)$** + +对 $u'(x)$ 积分: +$$ +u(x) = \int x e^{x} dx +$$ +计算积分,可以用分部积分法:令 +$$ +\begin{cases} +u = x, \quad dv = e^x dx,\\[1mm] +du = dx, \quad v = e^x, +\end{cases} +$$ +得: +$$ +\int x e^x dx = x e^x - \int e^x dx = x e^x - e^x + C_1 = e^x (x-1) + C_1 +$$ +注意这里求得的常数 $C_1$可以忽略,因为它会与齐次解合并。故我们取 +$$ +u(x) = e^x (x-1) +$$ + +**第五步:构造特解并给出通解** + +将 $u(x)$ 带回特解形式: +$$ +y_p = u(x)e^{-x} = e^x (x-1) e^{-x} = x-1 +$$ + +因此,原方程的通解为齐次解与特解的和: +$$ +y = y_h + y_p = Ce^{-x} + (x-1) +$$ + + + +## 梯度下降 + +我们可以用一个简单的线性层作为例子,展示如何利用向量和矩阵计算梯度并更新参数。假设有一个全连接层,其计算公式为 + +$$ +y = W x + b +$$ + +其中 + +- $x \in \mathbb{R}^2$ 是输入向量 +- $W \in \mathbb{R}^{2\times2}$ 是权重矩阵 +- $b \in \mathbb{R}^2$ 是偏置向量 +- $y \in \mathbb{R}^2$ 是输出向量 + +我们使用均方误差(MSE)作为损失函数,定义为 + +$$ +L = \frac{1}{2} \|y - y_{\text{true}}\|^2 = \frac{1}{2} \sum_{i=1}^{2}(y_i - y_{\text{true}, i})^2 +$$ + +--- + +**设定具体数值** + +- 输入向量: + $$ + x = \begin{pmatrix} 1 \\ 2 \end{pmatrix} + $$ + +- 权重矩阵: + $$ + W = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} + $$ + +- 偏置向量: + $$ + b = \begin{pmatrix} 1 \\ 1 \end{pmatrix} + $$ + +- 真实输出: + $$ + y_{\text{true}} = \begin{pmatrix} 7 \\ 13 \end{pmatrix} + $$ + +--- + +#### **步骤 1:前向传播** + +计算输出 $y$: + +$$ +y = W x + b = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \begin{pmatrix} 1 \\ 2 \end{pmatrix} + \begin{pmatrix} 1 \\ 1 \end{pmatrix} +$$ + +首先计算矩阵乘法: + +$$ +W x = \begin{pmatrix} 1\cdot1 + 2\cdot2 \\ 3\cdot1 + 4\cdot2 \end{pmatrix} = \begin{pmatrix} 1+4 \\ 3+8 \end{pmatrix} = \begin{pmatrix} 5 \\ 11 \end{pmatrix} +$$ + +再加上偏置 $b$ 得到 + +$$ +y = \begin{pmatrix} 5+1 \\ 11+1 \end{pmatrix} = \begin{pmatrix} 6 \\ 12 \end{pmatrix} +$$ + +计算损失 $L$: + +$$ +L = \frac{1}{2} \left[(6-7)^2 + (12-13)^2\right] = \frac{1}{2} \left[(-1)^2 + (-1)^2\right] = \frac{1}{2} (1+1) = 1 +$$ + +--- + +#### **步骤 2:反向传播,计算梯度** + +首先,我们定义误差向量为 + +$$ +e = y - y_{\text{true}} = \begin{pmatrix} 6-7 \\ 12-13 \end{pmatrix} = \begin{pmatrix} -1 \\ -1 \end{pmatrix} +$$ + +由于损失函数 + +$$ +L = \frac{1}{2}\|y - y_{\text{true}}\|^2 +$$ + +对 $y$ 的偏导数为 + +$$ +\frac{\partial L}{\partial y} = y - y_{\text{true}} = e = \begin{pmatrix} -1 \\ -1 \end{pmatrix} +$$ + +接下来,我们利用链式法则将梯度传递到 $W$ 和 $b$。 + +--- + +**1. 梯度对 $W$ 的求导** + +对于输出层有 +$$ +y = W x + b +$$ + +每个元素 $y_i$ 对 $W_{ij}$ 的偏导数为 + +$$ +\frac{\partial y_i}{\partial W_{ij}} = x_j +$$ + +利用链式法则,损失对 $W_{ij}$ 的梯度为 + +$$ +\frac{\partial L}{\partial W_{ij}} = \frac{\partial L}{\partial y_i} \cdot \frac{\partial y_i}{\partial W_{ij}} = e_i \, x_j +$$ + +用矩阵形式写就是: + +$$ +\frac{\partial L}{\partial W} = e \cdot x^\top +$$ + +将数值代入: + +$$ +e = \begin{pmatrix} -1 \\ -1 \end{pmatrix}, \quad x^\top = \begin{pmatrix} 1 & 2 \end{pmatrix} +$$ + +所以, + +$$ +\frac{\partial L}{\partial W} = \begin{pmatrix} -1 \\ -1 \end{pmatrix} \begin{pmatrix} 1 & 2 \end{pmatrix} = \begin{pmatrix} -1\cdot1 & -1\cdot2 \\ -1\cdot1 & -1\cdot2 \end{pmatrix} = \begin{pmatrix} -1 & -2 \\ -1 & -2 \end{pmatrix} +$$ + +--- + +**2.梯度对 $b$ 的求导** + +由于 $y = W x + b$,且对 $b$ 的偏导数为 1, + +$$ +\frac{\partial L}{\partial b} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial b} = e \cdot 1 = e = \begin{pmatrix} -1 \\ -1 \end{pmatrix} +$$ + +--- + +#### **步骤 3:使用梯度下降更新参数** + +设定学习率 $\eta = 0.1$,更新公式为 + +$$ +W_{\text{new}} = W - \eta \frac{\partial L}{\partial W}, \quad b_{\text{new}} = b - \eta \frac{\partial L}{\partial b} +$$ + +--- + +**更新 $W$** +$$ +W_{\text{new}} = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} - 0.1 \cdot \begin{pmatrix} -1 & -2 \\ -1 & -2 \end{pmatrix} = \begin{pmatrix} 1 + 0.1 & 2 + 0.2 \\ 3 + 0.1 & 4 + 0.2 \end{pmatrix} = \begin{pmatrix} 1.1 & 2.2 \\ 3.1 & 4.2 \end{pmatrix} +$$ + +--- + +**更新 $b$** +$$ +b_{\text{new}} = \begin{pmatrix} 1 \\ 1 \end{pmatrix} - 0.1 \cdot \begin{pmatrix} -1 \\ -1 \end{pmatrix} = \begin{pmatrix} 1 + 0.1 \\ 1 + 0.1 \end{pmatrix} = \begin{pmatrix} 1.1 \\ 1.1 \end{pmatrix} +$$ + +--- + +#### **总结** + +在这个例子中,我们展示了如何用向量和矩阵的形式计算一个简单全连接层的前向传播、损失以及对参数 $W$ 和 $b$ 的梯度。关键步骤如下: + +1. **前向传播**:计算 $y = W x + b$ 得到输出,再计算损失 $L = \frac{1}{2}\|y - y_{\text{true}}\|^2$ +2. **反向传播**: + - 计算误差向量 $e = y - y_{\text{true}}$ + - 利用链式法则得出梯度: + $\frac{\partial L}{\partial W} = e \cdot x^\top$ + $\frac{\partial L}{\partial b} = e$ +3. **参数更新**:通过梯度下降将参数沿负梯度方向调整 + +这样,我们就得到了更新后的参数 $W_{\text{new}}$ 和 $b_{\text{new}}$。这种向量或矩阵形式的梯度计算方法在真实神经网络中是普遍应用的,能够有效处理高维数据和大规模参数。 + + + +## 范数 + +### **L2范数定义**: + +对于一个向量 $\mathbf{w} = [w_1, w_2, \dots, w_n]$,L2 范数定义为 +$$ +\|\mathbf{w}\|_2 = \sqrt{w_1^2 + w_2^2 + \dots + w_n^2} +$$ + +假设一个权重向量为 $\mathbf{w} = [3, -4]$,则 +$$ +\|\mathbf{w}\|_2 = \sqrt{3^2 + (-4)^2} = \sqrt{9+16} = \sqrt{25} = 5. +$$ + + +**用途**: + +- **正则化(L2正则化/权重衰减)**:在训练过程中,加入 L2 正则项有助于防止模型过拟合。正则化项通常是权重的 L2 范数的平方,例如 + $$ + \lambda \|\mathbf{w}\|_2^2 + $$ + 其中 $\lambda$ 是正则化系数。 + +- **梯度裁剪**:在 RNN 等深度网络中,通过计算梯度的 L2 范数来判断是否需要对梯度进行裁剪,从而防止梯度爆炸。 + + + +**具体例子**: + +假设我们有一个简单的线性回归模型,损失函数为均方误差(MSE): +$$ +L(\mathbf{w}) = \frac{1}{2N} \sum_{i=1}^N (y_i - \mathbf{w}^T \mathbf{x}_i)^2 +$$ +其中,$N$ 是样本数量,$y_i$ 是第 $i$ 个样本的真实值,$\mathbf{x}_i$ 是第 $i$ 个样本的特征向量,$\mathbf{w}$ 是权重向量。 + +加入 L2 正则项后,新的损失函数为: +$$ +L_{\text{reg}}(\mathbf{w}) = \frac{1}{2N} \sum_{i=1}^N (y_i - \mathbf{w}^T \mathbf{x}_i)^2 + \lambda \|\mathbf{w}\|_2^2 +$$ + +在训练过程中,优化算法会同时最小化原始损失函数和正则项,从而在拟合训练数据的同时,避免权重值过大。 + +**梯度更新** + +在梯度下降算法中,权重 $\mathbf{w}$ 的更新公式为: +$$ +\mathbf{w} \leftarrow \mathbf{w} - \eta \nabla L_{\text{reg}}(\mathbf{w}) +$$ +其中,$\eta$ 是学习率,$\nabla L_{\text{reg}}(\mathbf{w})$ 是损失函数关于 $\mathbf{w}$ 的梯度。 + +对于加入 L2 正则项的损失函数,梯度为: +$$ +\nabla L_{\text{reg}}(\mathbf{w}) = \nabla L(\mathbf{w}) + 2\lambda \mathbf{w} +$$ +因此,权重更新公式变为: +$$ +\mathbf{w} \leftarrow \mathbf{w} - \eta (\nabla L(\mathbf{w}) + 2\lambda \mathbf{w}) +$$ + +通过加入 L2 正则项,模型在训练过程中不仅会最小化原始损失函数,还会尽量减小权重的大小,从而避免过拟合。正则化系数 $\lambda$ 控制着正则化项的强度,较大的 $\lambda$ 会导致权重更小,模型更简单,但可能会欠拟合;较小的 $\lambda$ 则可能无法有效防止过拟合。因此,选择合适的 $\lambda$ 是使用 L2 正则化的关键。 + + + +### Frobenius 范数 + +对于一个矩阵 $A \in \mathbb{R}^{m \times n}$,其 Frobenius 范数定义为 + +$$ +\|A\|_F = \sqrt{\sum_{i=1}^{m}\sum_{j=1}^{n} a_{ij}^2} +$$ + +这个定义与向量 L2 范数类似,只不过是对矩阵中所有元素取平方和后再开平方。 + + + +如果矩阵 $A$ 的奇异值为 $\sigma_1, \sigma_2, \ldots, \sigma_n$,则: + +$$ +\|A\|_F = \sqrt{\sum_{i=1}^n \sigma_i^2} +$$ + +这使得 Frobenius 范数在低秩近似和矩阵分解(如 SVD)中非常有用。 + + + +**迹和 Frobenius 范数的关系**: +$$ +\|A\|_F^2 = \text{tr}(A^* A) +$$ +这表明 Frobenius 范数的平方就是 $A^* A$ 所有特征值之和。而 $A^* A$ 的特征值开方就是A的奇异值。 + + + +**权重为向量的情况** + +当模型的输出是标量时(如单变量线性回归或二分类逻辑回归): + +- **输入特征**:$\mathbf{x}_i \in \mathbb{R}^d$(向量) + +- **权重形状**:$\mathbf{w} \in \mathbb{R}^d$(向量) + +- **预测公式**: + $$ + \hat{y}_i = \mathbf{w}^\top \mathbf{x}_i + $$ + 其中 $\hat{y}_i$ 是标量输出。 + +--- + +**权重为矩阵的情况** + +当模型的输出是向量时(如多变量回归、神经网络全连接层): + +- **输入特征**:$\mathbf{x}_i \in \mathbb{R}^d$(向量) + +- **输出维度**:$\hat{\mathbf{y}}_i \in \mathbb{R}^m$(向量) + +- **权重形状**:$W \in \mathbb{R}^{m \times d}$(矩阵) + +- **预测公式**: + $$ + \hat{\mathbf{y}}_i = W \mathbf{x}_i + \mathbf{b} + $$ + 其中 $\mathbf{b} \in \mathbb{R}^m$ 是偏置向量。 + + + + + +## 方差等 + + + +**标准差** +$$ +\sigma =\sqrt{\frac{\textstyle\sum_{i=1}^{n}{({x}_{i}-\overline{x})}^{2}}{n}} +$$ + +**方差** +$$ +Var(X)=\mathrm{E}[{(X-\mu) }^{2}]= {\sigma}^{2} +$$ + +**性质** +$$ +Var(X)=\mathrm{E}({X}^{2})-{[\mathrm{E}(X)]}^{2} \\ +Var(kX)={k}^{2}Var(X) +$$ +![image-20240506121834075](D:\folder\test\output\57ac2ea3-9c03-4046-bb21-5385c54cc116-1741685912867-1.png) + + + +若X和Y是独立的随机变量 +$$ +Var(X+Y)=Var(X)+Var(Y) +$$ + +**协方差** + +![image-20240310120438207](D:\folder\test\output\f3557880-a768-40bf-b962-37cc4c466bbe-1741685912867-2.png) +$$ +\sum {p}_{i}({x}_{i}-{\mu }_{\mathrm{X}})({\mathcal{y}}_{i}-{\mu }_{Y}) +\\ Cov\begin{pmatrix}X,Y + +\end{pmatrix}=\mathrm{E}\begin{bmatrix}\begin{pmatrix}\mathrm{X}-{\mu }_{\mathrm{X}} + +\end{pmatrix} +\begin{pmatrix}Y-{\mu }_{Y} + +\end{pmatrix} +\end{bmatrix}\\ +Cov(X,Y)=E(XY)-E(X)E(Y) +$$ + +**性质:** + +**X Y表示随机变量** + +![image-20240315100802791](D:\folder\test\output\e61d28e3-8e04-4cd2-9255-337b03ac4cb9-1741685912867-3.png) + +**协方差矩阵** + +协方差矩阵计算了不同维度之间的协方差,它是一个对称矩阵 + +![image-20240310202835535](D:\folder\test\output\3bab5556-5ce3-4a63-8e55-24eb88e9aa55-1741685912867-5.png) + +**性质:** + +A为n阶矩阵,X为n维随机向量 +$$ +\text{cov}(AX, AX) = A\text{cov}(X, X)A^T +$$ +**推导:** + +![400000](D:\folder\test\output\71af28cc-5f04-484f-937c-235904e98e26-1741685912867-4.png) + +## 高斯分布 + +高斯分布的概率密度函数: +$$ +\mathcal{f}(\mathcal{x})=\frac{1}{\sqrt{2\pi }\sigma }\exp \begin{pmatrix}-\frac{{(x-u)}^{2}}{2{\sigma }^{2}} + +\end{pmatrix} +$$ +![image-20240310103741300](D:\folder\test\output\971de6cf-0e45-4abf-92dc-0a3e6731322e-1741685912867-6.png) + +- x 在 μ-σ 和 μ+σ 之间的样本数量占到整个样本数量的 68.2%; +- x 在 μ-2σ 和 μ+2σ 之间的样本数量占到整个样本数量的 95.4%; +- x 在 μ-3σ 和 μ+3σ 之间的样本数量占到整个样本数量的99.6%; + +## 数据融合 + +当前最优值=当前的先验估计值和观测值进行融合 + +我们通常会尝试最小化方差,以尽可能减小状态估计的不确定性,从而获得更可靠和准确的估计结果 + +![image-20240311132453071](D:\folder\test\output\c0786ed2-41b0-4e5d-b0b6-ad011689a2d4-1741685912867-7.png) + +## 拉普拉斯变换 + +![image-20240413112801149](D:\folder\test\output\19eeb887-807b-4f3f-9af8-dc102413eb3c-1741685912867-8.png) + +## 矩阵运算 + +### 特征值和特征向量 + +设矩阵: + +$$ +A = \begin{bmatrix} 2 & 1 \\ 0 & 3 \end{bmatrix} +$$ + +步骤 1:求特征值 + +构造特征方程: + +$$ +\det(A - \lambda I) = \det\begin{bmatrix} 2-\lambda & 1 \\ 0 & 3-\lambda \end{bmatrix} = (2-\lambda)(3-\lambda) - 0 = 0 +$$ + +解得: + +$$ +(2-\lambda)(3-\lambda) = 0 \quad \Longrightarrow \quad \lambda_1 = 2,\quad \lambda_2 = 3 +$$ + +步骤 2:求特征向量 + +- 对于 $\lambda_1 = 2$: + 解方程: + + $$ + (A - 2I)\mathbf{x} = \begin{bmatrix} 2-2 & 1 \\ 0 & 3-2 \end{bmatrix}\begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} 0 & 1 \\ 0 & 1 \end{bmatrix}\begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} x_2 \\ x_2 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \end{bmatrix} + $$ + + 从第一行 $x_2 = 0$。因此特征向量可以写成: + + $$ + \mathbf{v}_1 = \begin{bmatrix} 1 \\ 0 \end{bmatrix} \quad (\text{任意非零常数倍}) + $$ + +- 对于 $\lambda_2 = 3$: + 解方程: + + $$ + (A - 3I)\mathbf{x} = \begin{bmatrix} 2-3 & 1 \\ 0 & 3-3 \end{bmatrix}\begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} -1 & 1 \\ 0 & 0 \end{bmatrix}\begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} -x_1+x_2 \\ 0 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \end{bmatrix} + $$ + + 从第一行得 $-x_1 + x_2 = 0$ 或 $x_2 = x_1$。因此特征向量可以写成: + + $$ + \mathbf{v}_2 = \begin{bmatrix} 1 \\ 1 \end{bmatrix} \quad (\text{任意非零常数倍}) + $$ + + + +**设一个对角矩阵**: +$$ +D = \begin{bmatrix} d_1 & 0 \\ 0 & d_2 \end{bmatrix} +$$ + +$$ +\lambda_1 = d_1,\quad \lambda_2 = d_2 +$$ + +对角矩阵的特征方程为: + +$$ +\det(D - \lambda I) = (d_1 - \lambda)(d_2 - \lambda) = 0 +$$ + +因此特征值是: + +$$ +\lambda_1 = d_1,\quad \lambda_2 = d_2 +$$ + +- 对于 $\lambda_1 = d_1$,方程 $(D-d_1I)\mathbf{x}=\mathbf{0}$ 得到: + + $$ + \begin{bmatrix} 0 & 0 \\ 0 & d_2-d_1 \end{bmatrix}\begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} 0 \\ (d_2-d_1)x_2 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \end{bmatrix} + $$ + + 若 $d_1 \neq d_2$,则必须有 $x_2=0$,而 $x_1$ 可任意取非零值,因此特征向量为: + + $$ + \mathbf{v}_1 = \begin{bmatrix} 1 \\ 0 \end{bmatrix} + $$ + +- 对于 $\lambda_2 = d_2$,类似地解得: + + $$ + \mathbf{v}_2 = \begin{bmatrix} 0 \\ 1 \end{bmatrix} + $$ + + + +### 矩阵乘法 + +**全连接神经网络** + +![image-20250316145729703](D:\folder\test\output\image-20250316145729703.png) + +其中: + +- $a^{(0)}$ 是输入向量,表示当前**层**的输入。 +- $\mathbf{W}$ 是权重矩阵,表示输入向量到输出向量的线性变换。 +- $b$ 是偏置向量,用于调整输出。 +- $\sigma$ 是激活函数(如 ReLU、Sigmoid 等),用于引入非线性。 + +- **输入向量 $a^{(0)}$**: + $$ + a^{(0)} = \begin{pmatrix} + a_0^{(0)} \\ + a_1^{(0)} \\ + \vdots \\ + a_n^{(0)} + \end{pmatrix} + $$ + 这是一个 $n+1$ 维的列向量,表示输入特征。 + +- **权重矩阵 $\mathbf{W}$**: + $$ + \mathbf{W} = \begin{pmatrix} + w_{0,0} & w_{0,1} & \cdots & w_{0,n} \\ + w_{1,0} & w_{1,1} & \cdots & w_{1,n} \\ + \vdots & \vdots & \ddots & \vdots \\ + w_{k,0} & w_{k,1} & \cdots & w_{k,n} \\ + \end{pmatrix} + $$ + 这是一个 $k \times (n+1)$ 的矩阵,其中 $k$ 是输出向量的维度,$n+1$ 是输入向量的维度。 + +- **偏置向量 $b$**: + $$ + b = \begin{pmatrix} + b_0 \\ + b_1 \\ + \vdots \\ + b_k + \end{pmatrix} + $$ + 这是一个 $k$ 维的列向量,用于调整输出。 + + + + + +1. 在传统的连续时间 RNN 写法里,常见的是 + + $$ + \sum_{j} W_{ij} \, \sigma(x_j), + $$ + + 这代表对所有神经元 $j$ 的激活 $\sigma(x_j)$ 做加权求和,再求和到神经元 $i$。 + + 如果拆开来看,每个输出分量也都含一个求和 $\sum_{j}$: + + - 输出向量的第 1 个分量(记作第 1 行的结果): + + $$ + (W_r x)_1 = 0.3 \cdot x_1 + (-0.5) \cdot x_2 = 0.3 \cdot 2 + (-0.5) \cdot 1 = 0.6 - 0.5 = 0.1. + $$ + + - 输出向量的第 2 个分量(第 2 行的结果): + + $$ + (W_r x)_2 = 1.2 \cdot x_1 + 0.4 \cdot x_2 = 1.2 \cdot 2 + 0.4 \cdot 1 = 2.4 + 0.4 = 2.8. + $$ + + + +2. 在使用矩阵乘法时,你可以写成 + + $$ + y = W_r \, \sigma(x), + $$ + + 其中 $\sigma$ 表示对 $x$ 的各分量先做激活,接着用 $W_r$ 乘上去。这就是把“$\sum_j \dots$”用矩阵乘法隐藏了。 + +$$ +\begin{pmatrix} +0.3 & -0.5\\ +1.2 & \;\,0.4 +\end{pmatrix} +\begin{pmatrix} +2\\ +1 +\end{pmatrix} += +\begin{pmatrix} +0.3 \times 2 + (-0.5) \times 1\\[6pt] +1.2 \times 2 + 0.4 \times 1 +\end{pmatrix} += +\begin{pmatrix} +0.6 - 0.5\\ +2.4 + 0.4 +\end{pmatrix} += +\begin{pmatrix} +0.1\\ +2.8 +\end{pmatrix}. +$$ + + + + + + + +### 奇异值 + +**定义** + +对于一个 $m \times n$ 的矩阵 $A$,其奇异值是非负实数 $\sigma_1, \sigma_2, \ldots, \sigma_r$($r = \min(m, n)$),满足存在正交矩阵 $U$ 和 $V$,使得: + +$$ +A = U \Sigma V^T +$$ + +其中,$\Sigma$ 是对角矩阵,对角线上的元素即为奇异值。 + +--- + + + +**主要特点** + +1. **非负性**:奇异值总是非负的。 +2. 对角矩阵的奇异值是对角线元素的**绝对值**。 +3. **降序排列**:通常按从大到小排列,即 $\sigma_1 \geq \sigma_2 \geq \ldots \geq \sigma_r \geq 0$。 +4. **矩阵分解**:奇异值分解(SVD)将矩阵分解为三个矩阵的乘积,$U$ 和 $V$ 是正交矩阵,$\Sigma$ 是对角矩阵。 +5. **应用广泛**:奇异值在数据降维、噪声过滤、图像压缩等领域有广泛应用。 + +--- + +**计算** + +奇异值可以通过计算矩阵 $A^T A$ 或 $A A^T$ 的特征值的**平方根**得到。 + +**步骤 1:计算 $A^T A$** + +首先,我们计算矩阵 $A$ 的转置 $A^T$: + +$$ +A^T = \begin{pmatrix} 3 & 0 \\ 0 & -4 \end{pmatrix} +$$ + +然后,计算 $A^T A$: + +$$ +A^T A = \begin{pmatrix} 3 & 0 \\ 0 & -4 \end{pmatrix} \begin{pmatrix} 3 & 0 \\ 0 & -4 \end{pmatrix} = \begin{pmatrix} 9 & 0 \\ 0 & 16 \end{pmatrix} +$$ + +**步骤 2:计算 $A^T A$ 的特征值** + +接下来,我们计算 $A^T A$ 的特征值。特征值 $\lambda$ 满足以下特征方程: + +$$ +\det(A^T A - \lambda I) = 0 +$$ + +即: + +$$ +\det \begin{pmatrix} 9 - \lambda & 0 \\ 0 & 16 - \lambda \end{pmatrix} = (9 - \lambda)(16 - \lambda) = 0 +$$ + +解这个方程,我们得到两个特征值: + +$$ +\lambda_1 = 16, \quad \lambda_2 = 9 +$$ + +**步骤 3:计算奇异值** + +奇异值是特征值的平方根,因此我们计算: + +$$ +\sigma_1 = \sqrt{\lambda_1} = \sqrt{16} = 4 +$$ + +$$ +\sigma_2 = \sqrt{\lambda_2} = \sqrt{9} = 3 +$$ + +**结果** + +矩阵 $A$ 的奇异值为 **4** 和 **3**。 + + + +### 矩阵的迹 + +**迹的定义** + +对于一个 $n \times n$ 的矩阵 $B$,其迹(trace)定义为矩阵对角线元素之和: + +$$ +\text{tr}(B) = \sum_{i=1}^n B_{ii} +$$ + +**迹与特征值的关系** + +对于一个 $n \times n$ 的矩阵 $B$,其迹等于其特征值之和。即: + +$$ +\text{tr}(B) = \sum_{i=1}^n \lambda_i +$$ + +其中 $\lambda_1, \lambda_2, \ldots, \lambda_n$ 是矩阵 $B$ 的特征值。 + +**应用到 $A^* A$** + +对于矩阵 $A^* A$(如果 $A$ 是实矩阵,则 $A^* = A^T$),它是一个半正定矩阵,其特征值是非负实数。 + +$A^* A$ 的迹还与矩阵 $A$ 的 Frobenius 范数有直接关系。具体来说: + +$$ +\|A\|_F^2 = \text{tr}(A^* A) +$$ + +**迹的基本性质** + +迹是一个线性运算,即对于任意标量 $c_1, c_2$ 和矩阵 $A, B$,有: + +$$ +\text{tr}(c_1 A + c_2 B) = c_1 \text{tr}(A) + c_2 \text{tr}(B) +$$ + +对于任意矩阵 $A, B, C$,迹满足循环置换性质: + +$$ +\text{tr}(ABC) = \text{tr}(CAB) = \text{tr}(BCA) +$$ + +注意:迹的循环置换性**不**意味着 $\text{tr}(ABC) = \text{tr}(BAC)$,除非矩阵 $A, B, C$ 满足某些特殊条件(如对称性)。 + + + +### 酉矩阵 + +酉矩阵是一种复矩阵,其满足下面的条件:对于一个 $n \times n$ 的复矩阵 $U$,如果有 + +$$ +U^* U = U U^* = I, +$$ + +其中 $U^*$ 表示 $U$ 的共轭转置(先转置再取复共轭),而 $I$ 是 $n \times n$ 的单位矩阵,那么 $U$ 就被称为酉矩阵。简单来说,酉矩阵在复内积空间中保持内积不变,相当于在该空间中的“旋转”或“反射”。 + +如果矩阵的元素都是实数,那么 $U^*$ 就等于 $U^T$(转置),这时酉矩阵就退化为**正交矩阵**。 + + + +考虑二维旋转矩阵 + +$$ +U = \begin{bmatrix} +\cos\theta & -\sin\theta \\ +\sin\theta & \cos\theta +\end{bmatrix}. +$$ + +当 $\theta$ 为任意实数时,这个矩阵满足 + +$$ +U^T U = I, +$$ + +所以它是一个正交矩阵,同时也属于酉矩阵的范畴。 + +例如,当 $\theta = \frac{\pi}{4}$(45°)时, + +$$ +U = \begin{bmatrix} +\frac{\sqrt{2}}{2} & -\frac{\sqrt{2}}{2} \\ +\frac{\sqrt{2}}{2} & \frac{\sqrt{2}}{2} +\end{bmatrix}. +$$ + + + +### 对称非负矩阵分解 + +$$ +A≈HH^T +$$ + +**1. 问题回顾** + +给定一个**对称非负**矩阵 $$A\in\mathbb{R}^{n\times n}$$,我们希望找到一个**非负矩阵** $$H\in\mathbb{R}^{n\times k}$$ 使得 +$$ +A \approx HH^T. +$$ +为此,我们可以**最小化目标函数(损失函数)** +$$ +f(H)=\frac{1}{2}\|A-HH^T\|_F^2, +$$ +其中 $$\|\cdot\|_F$$ 表示 Frobenius 范数,定义为矩阵所有元素的平方和的平方根。 + +$\| A - H H^T \|_F^2$ 表示矩阵 $A - H H^T$ 的所有元素的平方和。 + +**2. 梯度下降方法** + +2.1 计算梯度 + +目标函数(损失函数)是 +$$ +f(H)=\frac{1}{2}\|A-HH^T\|_F^2. +$$ + +$$ +\|M\|_F^2 = \operatorname{trace}(M^T M), +$$ + +因此,目标函数可以写成: + +$$ +f(H)=\frac{1}{2}\operatorname{trace}\Bigl[\bigl(A-HH^T\bigr)^T\bigl(A-HH^T\bigr)\Bigr]. +$$ + +注意到 $$A$$ 和$$HH^T$$ 都是对称矩阵,可以简化为: + +$$ +f(H)=\frac{1}{2}\operatorname{trace}\Bigl[\bigl(A-HH^T\bigr)^2\Bigr]. +$$ + +展开后得到 + +$$ +f(H)=\frac{1}{2}\operatorname{trace}\Bigl[A^2 - 2AHH^T + (HH^T)^2\Bigr]. +$$ + +其中 $$\operatorname{trace}(A^2)$$ 与 $$H$$ 无关,可以看作常数,不影响梯度计算。 + +**计算** $\nabla_H \operatorname{trace}(-2 A H H^T)$ +$$ +\nabla_H \operatorname{trace}(-2 A H H^T) = -4 A H +$$ + +**计算** $\nabla_H \operatorname{trace}((H H^T)^2)$ +$$ +\nabla_H \operatorname{trace}((H H^T)^2) = 4 H H^T H +$$ + + + +将两部分梯度合并: + +$$ +\nabla_H f(H) = \frac{1}{2}(4 H H^T H - 4 A H )= 2(H H^T H - A H) +$$ + + + +2.2 梯度下降更新 + +设学习率为 $$\eta>0$$,则梯度下降的**基本更新公式为**: +$$ +H \leftarrow H - \eta\, \nabla_H f(H) = H - 2\eta\Bigl(HH^T H - A H\Bigr). +$$ + +由于我们要求 $$H$$ 中的元素保持非负,所以每次更新之后通常需要进行**投影**: +$$ +H_{ij} \leftarrow \max\{0,\,H_{ij}\}. +$$ + +这种方法称为**投影梯度下降**,保证每一步更新后 $$H$$ 满足非负约束。 + +**3. 举例说明** + +设对称非负矩阵: +$$ +A = \begin{bmatrix} 2 & 1 \\ 1 & 2 \end{bmatrix}, \quad k=1, \quad H \in \mathbb{R}^{2 \times 1} +$$ +初始化 $$H^{(0)} = \begin{bmatrix} 1 \\ 1 \end{bmatrix}$$,学习率 $$\eta = 0.01$$。 + + + +**迭代步骤**: + +1. **初始 \( H^{(0)} \):** + $$ + H^{(0)} = \begin{bmatrix} 1 \\ 1 \end{bmatrix}, \quad H^{(0)}(H^{(0)})^T = \begin{bmatrix} 1 & 1 \\ 1 & 1 \end{bmatrix}. + $$ + 目标函数值: + $$ + f(H^{(0)}) = \frac{1}{2} \left( (2-1)^2 + 2(1-1)^2 + (2-1)^2 \right) = 1. + $$ + +2. **计算梯度:** + $$ + HH^T H = \begin{bmatrix} 2 \\ 2 \end{bmatrix}, \quad AH = \begin{bmatrix} 3 \\ 3 \end{bmatrix}, + $$ + + $$ + \nabla_H f(H^{(0)}) = 2 \left( \begin{bmatrix} 2 \\ 2 \end{bmatrix} - \begin{bmatrix} 3 \\ 3 \end{bmatrix} \right) = \begin{bmatrix} -2 \\ -2 \end{bmatrix}. + $$ + +3. **更新 \( H \):** + $$ + H^{(1)} = H^{(0)} - 2 \cdot 0.01 \cdot \begin{bmatrix} -2 \\ -2 \end{bmatrix} = \begin{bmatrix} 1.04 \\ 1.04 \end{bmatrix}. + $$ + +4. **更新后目标函数:** + $$ + H^{(1)}(H^{(1)})^T = \begin{bmatrix} 1.0816 & 1.0816 \\ 1.0816 & 1.0816 \end{bmatrix}, + $$ + + $$ + f(H^{(1)}) = \frac{1}{2} \left( (2-1.0816)^2 + 2(1-1.0816)^2 + (2-1.0816)^2 \right) \approx 0.8464. + $$ + +一次迭代后目标函数值从 $1.0$ 下降至 $0.8464$ + + + +## 幂迭代 + +![image-20240428174019083](D:\folder\test\output\573a8976-b974-472c-942b-adb6ef434a60-1741845609725-1.png) + +![image-20240428173732285](D:\folder\test\output\76d0ae41-a948-45b1-931d-c10d7367941a-1741845609725-2.png) + +原理:每一次迭代都相当于将当前向量乘以 $A$ 后再归一化。由于矩阵 $A$ 作用下,初始向量中 $v_1$ 分量对应的系数**会按 $\lambda_1$ 的 $k$ 次幂**增长,而其他特征向量分量增长较慢(因为它们对应的特征值模较小),故随着迭代次数的增加,向量逐渐趋向于 $v_1$ 的方向。 + + + +## 拉普拉斯矩阵 + +### **拉普拉斯矩阵及其性质** + +对于一个无向图 \(G = (V, E)\),其拉普拉斯矩阵 \(L\) 通常定义为 +$$ +L = D - A, +$$ +其中: + +- \(D\) 是度矩阵,一个对角矩阵,其对角元 \($d_i$\) 为顶点 \(i\) 的度数; +- \(A\) 是邻接矩阵,反映了图中各顶点之间的连接关系。 + +示例: +考虑一个简单的无向图,该图包含三个顶点:1, 2, 3,以及两条边: - 边 (1, 2) - 边 (2, 3) + +**邻接矩阵 \(A\)** +$$ +A = \begin{pmatrix} +0 & 1 & 0 \\ +1 & 0 & 1 \\ +0 & 1 & 0 +\end{pmatrix}. +$$ + +**度矩阵 \(D\)** +$$ +D = \begin{pmatrix} +1 & 0 & 0 \\ +0 & 2 & 0 \\ +0 & 0 & 1 +\end{pmatrix}. +$$ + +**拉普拉斯矩阵 \(L\)** + +将上面两个矩阵相减得到 + +$$ +L = \begin{pmatrix} +1 & 0 & 0 \\ +0 & 2 & 0 \\ +0 & 0 & 1 +\end{pmatrix} +- +\begin{pmatrix} +0 & 1 & 0 \\ +1 & 0 & 1 \\ +0 & 1 & 0 +\end{pmatrix} += +\begin{pmatrix} +1 & -1 & 0 \\ +-1 & 2 & -1 \\ +0 & -1 & 1 +\end{pmatrix}. +$$ + + + +令常数向量 + +$$ +\mathbf{1} = \begin{pmatrix} 1 \\ 1 \\ 1 \end{pmatrix}, +$$ + +则有 + +$$ +L\mathbf{1} = \begin{pmatrix} +1 \cdot 1 + (-1) \cdot 1 + 0 \cdot 1 \\ +-1 \cdot 1 + 2 \cdot 1 + (-1) \cdot 1 \\ +0 \cdot 1 + (-1) \cdot 1 + 1 \cdot 1 +\end{pmatrix} += +\begin{pmatrix} +1 - 1 + 0 \\ +-1 + 2 - 1 \\ +0 - 1 + 1 +\end{pmatrix} += +\begin{pmatrix} +0 \\ +0 \\ +0 +\end{pmatrix}. +$$ + +这说明常数向量 \($\mathbf{1}$\) 是 \(L\) 的零空间中的一个向量,即零特征值对应的特征向量。 + +**主要性质** + +1. 对称性 + +由于对于无向图,邻接矩阵 \(A\) 是对称的,而度矩阵 \(D\) 本身也是对称的(因为它是对角矩阵),所以拉普拉斯矩阵 \(L\) 也是对称矩阵。 + +2. 正半定性 + +对于任意实向量 \(x\),都有: +$$ +x^T L x = \sum_{(i,j) \in E} (x_i - x_j)^2 \ge 0. +$$ +这说明 \(L\) 是正半定矩阵,即其所有特征值均非负。 + +3. 零特征值与连通分量 + +- 对于**任意图**,都有 + +$$ +L \mathbf{1} = \mathbf{0}, +$$ + +其中 $$\mathbf{1} = (1, 1, \ldots, 1)^T$$,因此 $$0$$ 一定是 $$L$$ 的一个特征值。 +因为拉普拉斯矩阵的定义为 $L = D - A$,其中每一行的元素之和为零,所以当向量所有分量都相等时,每一行的加权求和自然等于零。 + +- 更进一步,**零特征值的重数等于图的连通分量(独立的子图)个数**。也就是说,如果图 \(G\) 有 \(k\) 个连通分量,则 \(L\) 的零特征值重数为 \(k\)。 + +**简单证明思路** + +考虑图中每个连通分量,对于某个连通分量内的所有顶点,可以构造一个特征向量,使得在该连通分量中所有分量取相同常数,而在其他部分取零。由于该连通分量内部的任意两个顶点都是连通的,该特征向量满足 \(Lx = 0\)。这样,对于每个连通分量都可以构造出一个线性无关的零特征值特征向量,从而零特征值的重数至少为连通分量的数量;进一步证明可以证明重数不会超过这个数量。 + +4. **谱分解及应用** + +由于 \(L\) 是对称正半定矩阵,其可以进行谱分解: +$$ +L = U \Lambda U^T, +$$ +其中 \(U\) 是**正交矩阵**,\($\Lambda$) 是包含 \(L\) 所有非负特征值的**对角矩阵**。 +这一性质使得拉普拉斯矩阵在谱聚类、图分割等应用中非常有用。 + +总结 + +拉普拉斯矩阵 \(L = D - A\) 是描述图结构的重要工具,具有如下主要性质: + +- **对称性**:\(L\) 是对称矩阵; +- **正半定性**:任意向量 \(x\) 有 \(x^T L x \ge 0\); +- **零特征值**:\(L\) 总有零特征值,且其重数与图的连通分量个数相等; +- **谱分解**:\(L\) 可进行正交谱分解,广泛应用于图的聚类与分割等领域。 + +这些性质不仅在理论上非常重要,而且在图论和数据分析等实际问题中有广泛的应用。 + + + +### **平均拉普拉斯矩阵:** + +![image-20240428173204296](D:\folder\test\output\715f6bd7-2d6f-416c-b16c-e255100d56f3-1741845609725-3.png) + +### **归一化拉普拉斯矩阵** + +为了在某些应用中(例如谱聚类、图卷积网络等)获得更好的数值性质和归一化效果,我们可以构造 **对称归一化拉普拉斯矩阵**,记为 $$L_{sym}$$,定义为 + +$$ +L_{sym} = D^{-1/2} L D^{-1/2} = I - D^{-1/2} A D^{-1/2}, +$$ + +其中 + +- $$D^{-1/2}$$ 表示度矩阵的逆平方根, +- $$I$$ 为单位矩阵。 + +$$ +D = \begin{pmatrix} +4 & 0 & 0 \\ +0 & 9 & 0 \\ +0 & 0 & 16 +\end{pmatrix}. + +D^{-1/2} = \begin{pmatrix} +\frac{1}{2} & 0 & 0 \\ +0 & \frac{1}{3} & 0 \\ +0 & 0 & \frac{1}{4} +\end{pmatrix}. +$$ + +**主要特点** + +1. **归一化**: + 通过 $$D^{-1/2}$$ 的两侧预处理,将不同顶点的度数影响消除,使得矩阵在谱分解时能更好地反映图的结构。 + +2. **对称性**: + $$L_{sym}$$ 是对称矩阵,这意味着它可以进行正交谱分解,其特征值均为实数。 + +3. **谱性质**: + $$L_{sym}$$ 的特征值都位于区间 $$[0, 2]$$ 内。这一性质对于很多图论算法的稳定性和收敛性分析都非常重要。 + + + + + +## Fiedler向量 + +根据谱分解理论,$$L$$ 的特征值满足 +$$ +x 0 = \lambda_1 \le \lambda_2 \le \cdots \le \lambda_n. +$$ +其中,$$\lambda_1 = 0$$ 对应的特征向量通常为所有分量相同的常数向量。而 **Fiedler 向量** 就是对应于 $$\lambda_2$$ (第二小的特征值)的特征向量。 + +**图的谱划分** + +1. 构建图的拉普拉斯矩阵 + + - 根据给定的图结构,构建图的拉普拉斯矩阵 L。 + +2. 计算 Fiedler 向量 + + - 求解拉普拉斯矩阵 L 的第二小特征值对应的特征向量,即 Fiedler 向量。 + +3. 根据 Fiedler 向量进行图划分 + + - 将 Fiedler 向量的元素按大小**排序**。 + - 找到 Fiedler 向量元素值为 0 附近的分界点,将图划分为两个子图。 + + **Fiedler 向量在连接紧密的顶点上的取值往往比较接近** + $$ + Fiedler 向量 :xv = \begin{pmatrix}0.8 \\0.7 \\0.6 \\-0.5 \\-0.6 \\-0.7\end{pmatrix}. + $$ + + - 正值部分:对应顶点 1, 2, 3; + - 负值部分:对应顶点 4, 5, 6。 + + 经过这种划分后,通常会发现: + + - 子图内部:顶点之间的连接较为紧密(边较多), + - 子图之间:连接较弱(边较少或只有一两条边)。 + +4. 递归划分子图(可选) + + - 对划分得到的两个子图,分别递归应用上述步骤(1-3步),进一步将其划分为更小的子图。 + - 这样可以将原图层层划分为多个子图。 + +5. 确定最终聚类结果 + + - 根据上述划分过程得到的多个子图,就对应了图的最终聚类结果。 + - 每个子图内的节点被认为属于同一个聚类。 + + + +## 谱聚类 + +谱聚类的基本思想是通过图的特征向量将数据点映射到低维空间中,然后在这个低维空间中使用传统的聚类技术。 + +1.构造相似性图 + +- **数据表示**: + 给定数据点 $$\{x_1, x_2, \ldots, x_n\}$$。 + +- **相似性矩阵 $$W$$**: + 根据数据点之间的距离或相似性构造矩阵 $$W$$。常见方法包括: + + - **Gaussian 核函数**: + $$ + W_{ij} = \exp\Bigl(-\frac{\|x_i - x_j\|^2}{2\sigma^2}\Bigr), + $$ + 只有当 $$x_i$$ 与 $$x_j$$ 彼此接近时, $$W_{ij}$$ 才较大;衡量数据点之间的距离并将其映射为一个 [0, 1] 之间的相似性值。 + + 其中 $$\sigma$$ 为尺度参数,当 $$\sigma$$ 较小时,只有非常接近的数据点才会被认为是相似的 + + - **K近邻图**:仅连接每个点与其 $$k$$ 个最近邻之间的边,其余 $$W_{ij} = 0$$。 + +2.构造图拉普拉斯矩阵 + + - 对称归一化拉普拉斯矩阵 + - 未归一化的拉普拉斯矩阵 + +3.计算特征向量 + +​ 对选定的拉普拉斯矩阵(例如 $$L_{sym}$$)进行特征分解,求出前 $$k$$ 个最小特征值对应的特征向 量。 +​ 注意:对于未归一化的拉普拉斯矩阵,零特征值对应的特征向量通常是常数向量,所以在分 解时忽略这个解,选择第二小开始的 $$k$$ 个特征向量。 + +4.构造嵌入空间 + +- **形成矩阵 $$U$$**: + 将求得的 $$k$$ 个特征向量作为列组成矩阵 + $$ + U = \begin{pmatrix} + u_1(1) & u_2(1) & \cdots & u_k(1) \\ + u_1(2) & u_2(2) & \cdots & u_k(2) \\ + \vdots & \vdots & \ddots & \vdots \\ + u_1(n) & u_2(n) & \cdots & u_k(n) + \end{pmatrix}. + $$ + 其中,每一行对应原数据点在低维空间中的表示。 + 例: + +- **归一化(可选)**: + 对于对称归一化的情况,可以对 $$U$$ 的每一行做归一化处理,使得每一行变为单位向量,这一步有助于后续聚类的稳定性。 + +5.聚类 + +​ **使用 k-means 等传统聚类算法**: +​ 在低维嵌入空间中,每一行表示一个数据点的低维表示,然后对这些点进行聚类。 +​ 得到每个数据点对应的簇标签。 + + + +**谱聚类示例(6个数据点分成3类)** + +假设数据点为 +$$ +x_1=1,\quad x_2=2,\quad x_3=5,\quad x_4=6,\quad x_5=10,\quad x_6=11. +$$ +直观上我们希望将它们分为3类: + +- 类1:靠近 1、2 +- 类2:靠近 5、6 +- 类3:靠近 10、11 + +--- + +**1. 构造相似性矩阵 $$W$$** + +采用 **Gaussian 核函数** +$$ +W_{ij}=\exp\Bigl(-\frac{(x_i-x_j)^2}{2\sigma^2}\Bigr). +$$ + +取 $$\sigma=2$$(参数可调),则分母为 $$2\sigma^2=8$$。 + +计算部分相似性(近似值): + +- $$x_1,x_2: \; |1-2|^2=1,\quad W_{12}=\exp(-1/8)\approx0.8825.$$ +- $$x_1,x_3: \; |1-5|^2=16,\quad W_{13}=\exp(-16/8)=\exp(-2)\approx0.1353.$$ +- $$x_1,x_4: \; |1-6|^2=25,\quad W_{14}=\exp(-25/8)\approx0.0439.$$ +- $$x_1,x_5: \; |1-10|^2=81,\quad W_{15}=\exp(-81/8)\approx0.00004.$$ +- $$x_1,x_6: \; |1-11|^2=100,\quad W_{16}=\exp(-100/8)\approx0.00001.$$ + +- $$x_2,x_3: \; |2-5|^2=9,\quad W_{23}=\exp(-9/8)\approx0.3247.$$ +- $$x_2,x_4: \; |2-6|^2=16,\quad W_{24}=\exp(-16/8)=\exp(-2)\approx0.1353.$$ +- $$x_2,x_5: \; |2-10|^2=64,\quad W_{25}=\exp(-64/8)=\exp(-8)\approx0.000335.$$ +- $$x_2,x_6: \; |2-11|^2=81,\quad W_{26}=\exp(-81/8)\approx0.00004.$$ + +- $$x_3,x_4: \; |5-6|^2=1,\quad W_{34}=\exp(-1/8)\approx0.8825.$$ +- $$x_3,x_5: \; |5-10|^2=25,\quad W_{35}=\exp(-25/8)\approx0.0439.$$ +- $$x_3,x_6: \; |5-11|^2=36,\quad W_{36}=\exp(-36/8)=\exp(-4.5)\approx0.0111.$$ + +- $$x_4,x_5: \; |6-10|^2=16,\quad W_{45}=\exp(-16/8)=\exp(-2)\approx0.1353.$$ +- $$x_4,x_6: \; |6-11|^2=25,\quad W_{46}=\exp(-25/8)\approx0.0439.$$ + +- $$x_5,x_6: \; |10-11|^2=1,\quad W_{56}=\exp(-1/8)\approx0.8825.$$ + +由于 $$W$$ 是对称矩阵,对角元一般取 0(或1,根据需求),我们构造相似性矩阵 $$W$$ 为 + +$$ +W=\begin{pmatrix} +0 & 0.8825 & 0.1353 & 0.0439 & 0.00004 & 0.00001 \\ +0.8825 & 0 & 0.3247 & 0.1353 & 0.000335& 0.00004 \\ +0.1353 & 0.3247 & 0 & 0.8825 & 0.0439 & 0.0111 \\ +0.0439 & 0.1353 & 0.8825 & 0 & 0.1353 & 0.0439 \\ +0.00004& 0.000335&0.0439 & 0.1353 & 0 & 0.8825 \\ +0.00001& 0.00004 & 0.0111 & 0.0439 & 0.8825 & 0 +\end{pmatrix}. +$$ + +--- + +2. **构造度矩阵 $$D$$** + +$$ +D_{ii}=\sum_{j=1}^6 W_{ij}. +$$ + +近似计算: + +- 对于 $$x_1$$: + $$D_{11}\approx0.8825+0.1353+0.0439+0.00004+0.00001\approx1.0617.$$ +- 对于 $$x_2$$: + $$D_{22}\approx0.8825+0.3247+0.1353+0.000335+0.00004\approx1.3429.$$ +- 对于 $$x_3$$: + $$D_{33}\approx0.1353+0.3247+0.8825+0.0439+0.0111\approx1.3975.$$ +- 对于 $$x_4$$: + $$D_{44}\approx0.0439+0.1353+0.8825+0.1353+0.0439\approx1.241.$$ +- 对于 $$x_5$$: + $$D_{55}\approx0.00004+0.000335+0.0439+0.1353+0.8825\approx1.0617.$$ +- 对于 $$x_6$$: + $$D_{66}\approx0.00001+0.00004+0.0111+0.0439+0.8825\approx0.9375.$$ + +构造度矩阵: +$$ +D=\begin{pmatrix} +1.0617 & 0 & 0 & 0 & 0 & 0\\[0.5em] +0 & 1.3429 & 0 & 0 & 0 & 0\\[0.5em] +0 & 0 & 1.3975 & 0 & 0 & 0\\[0.5em] +0 & 0 & 0 & 1.2410 & 0 & 0\\[0.5em] +0 & 0 & 0 & 0 & 1.0617 & 0\\[0.5em] +0 & 0 & 0 & 0 & 0 & 0.9375 +\end{pmatrix}. +$$ + +--- + +**3. 构造拉普拉斯矩阵 $$L$$** + +未归一化拉普拉斯矩阵定义为 +$$ +L = D - W. +$$ + +例如,矩阵的第 1 行为: +$$ +L_{1\cdot}=(1.0617,\ -0.8825,\ -0.1353,\ -0.0439,\ -0.00004,\ -0.00001), +$$ +其它行类似。 + +--- + +**4. 特征分解与构造低维嵌入** + +为了分成 3 类,通常我们取图拉普拉斯矩阵(或归一化拉普拉斯矩阵)的前 $$k=3$$ 个最小特征值对应的特征向量。 +(注意:对于未归一化拉普拉斯矩阵,第一个特征值为 0,对应常数向量;但在归一化方法中,所有 3 个特征向量通常都有实际意义。) + +假设经过特征分解后,我们得到了三个特征向量 +$$ +u_1,\; u_2,\; u_3, +$$ +每个都是 6 维向量。将它们按列排列构成矩阵 +$$ +U=\begin{pmatrix} +u_1(1) & u_2(1) & u_3(1) \\[0.3em] +u_1(2) & u_2(2) & u_3(2) \\[0.3em] +u_1(3) & u_2(3) & u_3(3) \\[0.3em] +u_1(4) & u_2(4) & u_3(4) \\[0.3em] +u_1(5) & u_2(5) & u_3(5) \\[0.3em] +u_1(6) & u_2(6) & u_3(6) +\end{pmatrix}. +$$ + +每一行 $$i$$ 表示数据点 $$x_i$$ 在 **3 维低维嵌入空间中的表示**。 + +**假设得到的低维表示**(示例数值): + +- $$x_1: \; (0.9,\ 0.2,\ 0.1)$$ +- $$x_2: \; (0.8,\ 0.3,\ 0.2)$$ +- $$x_3: \; (-0.1,\ 0.8,\ 0.1)$$ +- $$x_4: \; (-0.2,\ 0.7,\ 0.0)$$ +- $$x_5: \; (0.1,\ -0.2,\ 0.9)$$ +- $$x_6: \; (0.0,\ -0.1,\ 1.0)$$ + +--- + +**5. 在低维空间上使用 k-means 聚类** + +利用 k-means 算法对 6 个数据点的 3 维向量进行聚类。 +在本例中,k-means 会尝试将点分为 3 类。 +根据上述低维表示,很容易看到: + +- 数据点 $$x_1$$ 和 $$x_2$$ 聚在一起; +- 数据点 $$x_3$$ 和 $$x_4$$ 聚在一起; +- 数据点 $$x_5$$ 和 $$x_6$$ 聚在一起。 + +最终得到的聚类结果: + +- 类1:$$\{x_1, x_2\}$$ +- 类2:$$\{x_3, x_4\}$$ +- 类3:$$\{x_5, x_6\}$$ + + + +## **谱分解** + +一个对称矩阵可以通过其特征值和特征向量进行分解。对于一个 $$n \times n$$ 的对称矩阵 $$A$$,其谱分解可以表示为: + +$$ +A = \sum_{i=1}^{n} \lambda_i x_i x_i^T +$$ + +其中,$$\lambda_i$$ 是矩阵 $$A$$ 的第 $$i$$ 个特征值,$$x_i$$ 是对应的特征向量。 + +**推导过程** + +1. **特征值和特征向量的定义** + 对于一个对称矩阵 $$A$$,其特征值和特征向量满足: + $$ + A x_i = \lambda_i x_i + $$ + + 其中,$$\lambda_i$$ 是特征值,$$x_i$$ 是对应的特征向量。 + +2. **谱分解** + 将这些特征向量组成一个正交矩阵 $$Q$$ + + $$A = Q \Lambda Q^T$$ + $$ + Q = \begin{bmatrix} x_1 & x_2 & \cdots & x_n \end{bmatrix}, + $$ + + $$ + Q \Lambda = \begin{bmatrix} \lambda_1 x_1 & \lambda_2 x_2 & \cdots & \lambda_n x_n \end{bmatrix}. + $$ + + $$ + Q \Lambda Q^T = \begin{bmatrix} \lambda_1 x_1 & \lambda_2 x_2 & \cdots & \lambda_n x_n \end{bmatrix} \begin{bmatrix} x_1^T \\ x_2^T \\ \vdots \\ x_n^T \end{bmatrix}. + $$ + + $$ + Q \Lambda Q^T = \lambda_1 x_1 x_1^T + \lambda_2 x_2 x_2^T + \cdots + \lambda_n x_n x_n^T. + $$ + + 可以写为 + $$ + A = \sum_{i=1}^{n} \lambda_i x_i x_i^T. + $$ + + + +3. **网络重构** + 在随机网络中,网络的邻接矩阵 $$A$$ 通常是对称的。利用预测算法得到的谱参数 $$\{\lambda_i, x_i\}$$ 后,就可以用以下公式重构网络矩阵: + $$ + A(G) = \sum_{i=1}^{n} \lambda_i x_i x_i^T + $$ + + + diff --git a/科研/李雅普诺夫稳定性.md b/科研/李雅普诺夫稳定性.md new file mode 100644 index 0000000..1143751 --- /dev/null +++ b/科研/李雅普诺夫稳定性.md @@ -0,0 +1,145 @@ +# 李雅普诺夫方法 + +判断系统是否能够在受到扰动后返回平衡状态或维持在稳定状态。 + +## 数学基础 + +**雅各比矩阵定义** + +雅可比矩阵(Jacobian matrix)是一个重要的数学概念,它在向量值函数的微分方面起着关键作用。雅可比矩阵描述了一个向量值函数的局部线性近似。 + +![image-20240413104858542](D:/folder/test/output/900d0a43-e441-4379-bbd3-9721d3c07871.png) + +理解:从n维实向量空间到m维实向量空间的函数f,假设输入为2维,用x,y表示,即二维平面上的一个点;输出为3维,每个点的位置由坐标f1(x,y),f2(x,y),f3(x,y)表示。 + + + +**求解雅各比矩阵:** + +![image-20240413105515972](D:/folder/test/output/3c3b1590-1333-419c-a6a1-154428aef5ee.png) + + + + + +**状态空间** + +![image-20240413122729169](D:/folder/test/output/862bdb0d-e02f-4dff-b388-0b889aace213.png) + + + +**特征值、特征向量的几何意义** + +![image-20240413162816588](D:/folder/test/output/99fca3f1-f84f-459f-897f-707cb2d5f906.png) + +矩阵A表示某线性变换 + + + +![image-20240413163625587](D:/folder/test/output/5f08a566-f942-4737-8b37-82934aabae85.png) + +image-20240413165536752 + +image-20240413165512450 + +为结论 + + + +## 稳定性的定义 + +![微信图片_20240413101222](D:/folder/test/output/6048c17b-a9b1-4750-87b0-a5b20f796d8f.png) + +![微信图片_20240413101229](D:/folder/test/output/9537b4ef-e38f-4f13-8de9-434ed71d5371.png) + + + +## 李雅普诺夫第一法(间接方法) + +通过分析线性系统的系数矩阵的特征值来判断系统的稳定性 + +![微信图片_20240413101333](D:/folder/test/output/81166264-ffad-4f9f-96bb-c667afe7b27d.png) + +![image-20240413110954014](D:/folder/test/output/6f93f4de-5e41-477e-b6d7-3a9c2ceee26e.png) + +**雅各比矩阵**使我们能够将非线性系统在平衡点附近的行为**近似为线性系统**。通过这种局部线性化,我们可以应用线性系统理论来研究非线性系统的稳定性。 + +特征值的实部决定了系统在这些点附近是趋向平衡点还是远离平衡点。 + +- 所有特征值的实部都小于零意味着系统是渐进稳定的; +- 任何特征值的实部大于零意味着系统在该点是不稳定的。 +- 如果所有特征值的实部都不大于零,并且存在实部正好为零的特征值,李一法失效。 + +**why特征值???** + +![微信图片_20240413162152](D:/folder/test/output/fd868772-de9b-45e8-93bb-181713c7523e.jpg) + +可以以对角矩阵为例,特征值为对角线上元素,设平衡点x1=0,x2=0; + +- **基变换**:将一个向量**左乘特征向量矩阵V**实际上是在将这个向量**从原始坐标系转换到以A的特征向量为基的新坐标系**。在新的坐标系中,原始向量的坐标表示由特征向量矩阵V 决定。 +- 原始坐标系:y1、y2, 新坐标系:x1、x2 + + + +eg: + +![微信图片_20240413102132](D:/folder/test/output/4d375e46-b596-4574-8c19-66949655bc59.png) + + + +## 希尔维斯特判据 + +![微信图片_20240413101523](D:/folder/test/output/8c8eed88-7fc6-4c53-9646-9b585a5ec96f.png) + +## 李雅普诺夫第二法(直接法) + +关键是构造一个李雅普诺夫函数V(x) + +![微信图片_20240413101545](D:/folder/test/output/1196db6a-4255-4730-9c64-b11e7acc5b49.png) + + + +eg:![微信图片_20240413101643](D:/folder/test/output/9bcf942f-89ec-4b40-9bbb-8cbbcc4ce422.png) + + + +当使用李雅普诺夫的第二方法分析系统稳定性时,直接找到一个合适的李雅普诺夫函数可能很困难。 + + + +### 线性定常连续系统 + +$$ +\dot{x} = Ax +$$ + +A为系统的状态矩阵,应用**李雅普诺夫方程**可构造李雅普诺夫函数。 + +![微信图片_20240413101732](D:/folder/test/output/fd7cb571-b715-4f37-8fb2-d7cf05bb09f3.png) + + + +eg: + +![微信图片_20240413101758](D:/folder/test/output/d423cf16-39b7-470f-b11e-3aa0526b43f0.png) + + + +### 非线性系统 + +$$ +\dot{x} = f(x) +$$ + +**克拉索夫斯基算法** + +![微信图片_20240413101854](D:/folder/test/output/b45a2b0b-803b-48d0-81ff-8a7e247da517.png) + + + +eg: + +![微信图片_20240413101950](D:/folder/test/output/89cbf829-8ff9-48b0-8840-282780b2ec41.png) + +![微信图片_20240413101959](D:/folder/test/output/9ff05b23-3f6e-4071-a85e-8566d0a66c8a.png) + diff --git a/科研/液态神经网络.md b/科研/液态神经网络.md new file mode 100644 index 0000000..426f393 --- /dev/null +++ b/科研/液态神经网络.md @@ -0,0 +1,567 @@ +## 液态神经网络 + +### 连续时间递归神经网络(CT-RNN) + +**举例说明** + +- 以下以第 $i$个隐藏神经元为例,给出一个典型的 **连续时间** 动力学方程(微分方程形式): + + $$ + \frac{d h_i(t)}{dt} + \;=\; + -\alpha \, h_i(t) + \;+\; + \sum_{j} W_{ij} \,\sigma\bigl(h_j(t)\bigr) + \;+\; + V_i\, x(t). + $$ + + - $\displaystyle h_i(t)$ + 表示第 \(i\) 个神经元的 **内部状态**(或称膜电位、液体状态等)。 + - $\displaystyle -\alpha\,h_i(t)$ + 表示自然衰减项,$\alpha>0$ 是衰减系数。 + - $\displaystyle \sum_{j} W_{ij}\,\sigma\bigl(h_j(t)\bigr)$ + 表示对第 $i$ 个输出神经元,计算所有输入神经元$j$的加权和。 + - $\displaystyle \sigma(\cdot)$ 是一个非线性激活函数,例如 $\tanh$、ReLU 等; + - $\displaystyle W_{ij}$ 是从神经元 \(j\) 到神经元 \(i\) 的 **连接权重**; + - 这里的求和 $\sum_{j}$意味着 **第 $i$ 个神经元** 会「收集」当前层所有神经元(含自己)的输出信号。 + - $\displaystyle V_i\, x(t)$ + 外部输入 $x(t)$ 对神经元 $i$ 的直接驱动作用。 + + + + 因此,这个公式表示:**第 $i$个隐藏神经元** 的状态变化率,依赖: + + 1. 自身的衰减; + 2. 其他神经元的输出(相互耦合); + 3. 来自上一层(或外部)的输入刺激。 + +------ + +**使用欧拉法 (Forward Euler) 离散近似** + +这是最简单、最直接的数值积分方法。给定一个小的时间步长$\Delta t$,将连续时间 $t$ 离散化为 $t_0,\, t_1,\, \dots$,其中 $t_{n+1} = t_n + \Delta t$。 +则第 $i$ 个神经元的状态 $h_i(t)$ 在离散时刻 $t_n$ 的值可以表示为 $h_i^{(n)}$,其中 $h_i^{(n)}$ 表示在时间 $t_n$ 时刻的状态。 + +微分方程: +$$ +\frac{d h_i(t)}{dt} += f_i\bigl(h_1(t), \dots, h_N(t), x(t)\bigr), +$$ + +在这里, +$$ +f_i(\mathbf{h}(t),\, x(t)) \;=\; +-\alpha\, h_i(t) +\;+\; +\sum_j W_{ij}\,\sigma\bigl(h_j(t)\bigr) +\;+\; +V_i\,x(t). +$$ +**欧拉更新公式**: +$$ +h_i^{(n+1)} +\;=\; +h_i^{(n)} +\;+\; +\Delta t \,\Bigl[ + f_i\bigl(\mathbf{h}^{(n)},\, x^{(n)}\bigr) +\Bigr], +$$ + +其中: + +1. $ \mathbf{h}^{(n)} = [h_1^{(n)}, \dots, h_N^{(n)}]^\top$ + 表示所有神经元在时刻 $t_n$ 的状态向量。 +2. $x^{(n)} $ + 表示输入信号在时刻$ t_n$的值(或小区间平均值)。 + +这可以并行对 **所有** $i$ 同时更新。 + +**优点**:简单易实现 +**缺点**:稳定性、精度较低,需要选小一些的$\Delta t$才能获得良好数值表现。 + + + +### 神经ODE的基本形式 + +神经ODE(Neural ODE)的状态 $x(t)$ 由以下微分方程定义: +$$ +\frac{dx(t)}{dt} = f(x(t), I(t), t, \theta) +$$ + +- 其中,$f$ 是一个由参数 $\theta$ 定义的神经网络,$I(t)$ 是输入,$t$ 是时间。 + +- 通过数值ODE求解器可以计算状态 $x(t)$,并通过**反向模式自动微分**(reverse-mode automatic differentiation)来训练网络。 + +- 使用**伴随敏感度 (adjoint) 方法** 来节省显存,但这会带来一定的数值不稳定与反向误差 + +### **连续时间递归神经网络(CT-RNN)的稳定性** + +$$ +\frac{dx(t)}{dt} = -\frac{x(t)}{\tau} + f(x(t), I(t), t, \theta) +$$ + +其中,$-\frac{x(t)}{\tau}$ 是一个阻尼项,帮助系统达到平衡状态,$\tau$ 是时间常数。 + +*$τ$* 越大,系统的响应越慢;*$τ$* 越小,系统的响应越快 + + + +### 小型生物(如线虫)的神经动力学模型 + +在生物学中,非脉冲神经元的电位动态可以通过以下线性微分方程描述: +$$ +\frac{d\mathbf{v}(t)}{dt} = -g_l \mathbf{v}(t) + \mathbf{S}(t) +$$ +其中: + +- $\mathbf{v}(t)$ 是神经元的电位。 +- $g_l$ 是**泄漏电导**(leakage conductance),表示神经元电位的自然衰减速度。 +- $\mathbf{S}(t)$ 是**突触输入**的总和,表示来自其他神经元的输入信号。 + +突触输入 $\mathbf{S}(t)$ 可以通过以下非线性函数近似: +$$ +\mathbf{S}(t) = f(\mathbf{v}(t), \mathbf{I}(t))(A - \mathbf{v}(t)) +$$ +其中: + +- $f(\mathbf{v}(t), \mathbf{I}(t))$ 是一个**非线性函数**(通常是 sigmoid 函数),表示突触前神经元的电位 $\mathbf{v}(t)$ 和外部输入 $\mathbf{I}(t)$ 对突触输入的影响。 +- $A$ 是一个**偏置项**,表示突触输入的最大值。(*$A$* 可以理解为突触输入的平衡电位。当神经元的电位 **$v(t)$**接近 *$A$* 时,突触输入*$S(t)$*会减小,从而防止电位无限增长。) + +**例子** + +为了具体化,我们设定以下参数: + +- **泄漏电导**:$g_l = 0.1$(表示电位以每秒 0.1 的速度自然衰减)。 + +- **突触输入的最大值**:$A = 1$。 + +- **非线性函数**:假设 $f(\mathbf{v}(t), \mathbf{I}(t))$ 是一个简单的 sigmoid 函数: + $$ + f(\mathbf{v}(t), \mathbf{I}(t)) = \frac{1}{1 + e^{-\mathbf{I}(t)}} + $$ + 其中,$\mathbf{I}(t)$ 是外部输入。 + + + +假设在 $t = 0$ 时,神经元的电位为: +$$ +\mathbf{v}(0) = 0.5 +$$ + +假设在 $t = 0$ 到 $t = 10$ 秒内,外部输入 $\mathbf{I}(t)$ 为: +$$ +\mathbf{I}(t) = 1 +$$ + +**计算突触输入** + +根据设定的非线性函数,突触输入为: +$$ +f(\mathbf{v}(t), \mathbf{I}(t)) = \frac{1}{1 + e^{-\mathbf{I}(t)}} = \frac{1}{1 + e^{-1}} \approx 0.731 +$$ + +这里为了简化,突触输入仅由外部驱动,不随自身电位变化。 + +因此,突触输入项为: +$$ +f(\mathbf{v}(t), \mathbf{I}(t))(A - \mathbf{v}(t)) = 0.731 \times (1 - \mathbf{v}(t)) +$$ + +**动态方程** + +将参数代入动态方程,得到: +$$ +\frac{d\mathbf{v}(t)}{dt} = -0.1 \mathbf{v}(t) + 0.731 (1 - \mathbf{v}(t)) +$$ + +**数值模拟** + +我们可以通过数值方法(如显示欧拉法)来模拟神经元的电位变化。假设时间步长 $\Delta t = 0.1$ 秒,初始电位 $\mathbf{v}(0) = 0.5$。 + +第一次迭代($t = 0$ 到 $t = 0.1$ 秒) + +- 计算电位变化率: + $$ + \frac{d\mathbf{v}(0)}{dt} = -0.1 \times 0.5 + 0.731 \times (1 - 0.5) = -0.05 + 0.3655 = 0.3155 + $$ + +- 更新电位: + $$ + \mathbf{v}(0.1) = \mathbf{v}(0) + \frac{d\mathbf{v}(0)}{dt} \times \Delta t = 0.5 + 0.3155 \times 0.1 = 0.53155 + $$ + +- 重复上述过程,直至t=10秒 + +由于泄漏电导和偏置项$A$的作用,电位的上升速度逐渐减慢,最终趋于稳定值。 + +**稳定状态** + +在稳定状态下,电位变化率为 0,即: +$$ +\frac{d\mathbf{v}(t)}{dt} = 0 +$$ +代入动态方程: +$$ +0 = -0.1 \mathbf{v}_{\text{stable}} + 0.731 (1 - \mathbf{v}_{\text{stable}}) +$$ +解得: +$$ +\mathbf{v}_{\text{stable}} = \frac{0.731}{0.1 + 0.731} \approx 0.88 +$$ + + + +### 液态时间常数网络(LTCs) + +$$ +\frac{dx(t)}{dt} = -\frac{x(t)}{\tau} + S(t) +$$ + +其中,$S(t)$ 是一个非线性项,定义为: +$$ +S(t) = f(x(t), I(t), t, \theta)(A - x(t)) +$$ +这里,$f$ 是一个神经网络,$A$ 是一个偏置项。 + +将 $S(t)$ 代入隐藏状态方程后,得到LTCs的动态方程: +$$ +\frac{dx(t)}{dt} = -\left[\frac{1}{\tau} + f(x(t), I(t), t, \theta)\right] x(t) + f(x(t), I(t), t, \theta) A +$$ +LTCs 的核心创新在于其**可变的时间常数** $\tau_{sys}$,它由以下公式定义: +$$ +\tau_{sys} = \frac{\tau}{1 + \tau f(x(t), I(t), t, \theta)} +$$ +这意味着时间常数 $\tau_{sys}$ 会根据输入 $I(t)$ 和隐藏状态 $x(t)$ 的变化而动态调整。从而在处理复杂时间序列数据时表现出更强的适应性和表达能力。 + +这个方程展示了LTCs的核心特性:**可变的时间常数**。 + + + +#### 显式欧拉 vs 隐式欧拉 + +| 方法 | 公式 | 特点 | +| ------------ | ---------------------------------------------------- | ------------------------------------------------------------ | +| **显式欧拉** | $x_{k+1} = x_k + \Delta t \cdot f(x_k, t_k)$ | 用当前时刻的导数计算下一步,**计算快但稳定性差**(步长受限) | +| **隐式欧拉** | $x_{k+1} = x_k + \Delta t \cdot f(x_{k+1}, t_{k+1})$ | 用未来时刻的导数计算下一步,**稳定性好但需解方程**(适合刚性系统) | + + + +#### **融合求解器** + +$$ +\frac{dx(t)}{dt} = -\left[\frac{1}{\tau} + f(x(t), I(t), t, \theta)\right] x(t) + f(x(t), I(t), t, \theta) A +$$ + +$$ +\frac{dx}{dt} = -\alpha(t)x(t) + \beta(t) \quad \text{其中}\ \alpha(t) = \frac{1}{\tau} + f, \ \beta(t) = f \odot A +$$ + +**应用隐式欧拉法离散化:** +$$ +x_{k+1} = x_k + \Delta t \cdot \left[ -\alpha_{k+1} x_{k+1} + \beta_{k+1} \right] +$$ +**关键点**:右侧的$\alpha_{k+1}$和$\beta_{k+1}$都依赖于未来状态$x_{k+1}$。 + +**显示近似非线性项:** + +论文假设非线性项$f$在时间步内近似不变(即$f_{k+1} \approx f_k$),从而: +$$ +\alpha_{k+1} \approx \alpha_k = \frac{1}{\tau} + f_k, \quad \beta_{k+1} \approx \beta_k = f_k \odot A +$$ + +代入后方程变为: +$$ +x_{k+1} = x_k + \Delta t \cdot \left[ -\left( \frac{1}{\tau} + f_k \right) x_{k+1} + f_k \odot A \right] +$$ + +**求解:** + +将含$x_{k+1}$的项移到左边: +$$ +x_{k+1} + \Delta t \left( \frac{1}{\tau} + f_k \right) x_{k+1} = x_k + \Delta t \cdot f_k \odot A +$$ + +提取公因子$x_{k+1}$: +$$ +x_{k+1} \left[ 1 + \Delta t \left( \frac{1}{\tau} + f_k \right) \right] = x_k + \Delta t \cdot f_k \odot A +$$ + +最终显式解: +$$ +x_{k+1} = \frac{x_k + \Delta t \cdot f_k \odot A}{1 + \Delta t \left( \frac{1}{\tau} + f_k \right)} +$$ + +- $x_k \in \mathbb{R}^N$ 是第 $k$ 个时间步的隐藏状态向量。 +- $I_k$ 是输入。 +- $f(\cdot)$ 是包含可学习权重的非线性映射,$f_k$ 表示在第 $k$ 步时刻对 $\bigl(x_k,I_k\bigr)$ 的运算结果。 + 可以假设 +- $\tau$ 是时间常数(若每个神经元各有一套,可以是一个向量 $\tau \in \mathbb{R}^N$)。 +- $A \in \mathbb{R}^N$ 是可学习的偏置向量。 +- $\odot$ 表示逐元素相乘。 + + + +#### 示例 + +**参数与初始数据设定** + +为便于演示,这里只做 **一次** 更新(从 $x_k$ 到 $x_{k+1}$),并给出具体数值。 + +- 隐藏层维度 $N=2$。 + +- 时间步长 $\Delta t = 1$(只是示例;实际中可更小或可自适应)。 + +- 初始隐藏状态和输入(随意设定): + $$ + x_k = \begin{bmatrix}0 \\[4pt] 1\end{bmatrix}, + \quad + I_k = 2. + $$ + +- 令时间常数 $\tau = \begin{bmatrix}1 \\[4pt] 1\end{bmatrix}$(即 2 维,都为 1)。 + +- 令 $A = \begin{bmatrix}2 \\[4pt] -1\end{bmatrix}$。 + +**非线性 $f$ 的定义** + +- 我们假设 + + $$ + f(x,I) \;=\; \mathrm{ReLU}\!\bigl(W_r\,x \;+\; W_i\,I \;+\; b\bigr), + $$ + + 其中 + + - $W_r$ 是隐藏层的“自连接”或“循环”权重,尺寸 $2\times 2$; + - $W_i$ 是输入到隐藏层的权重,尺寸 $2\times 1$; + - $b$ 是偏置向量(2 维); + - $\mathrm{ReLU}(z)$ 对每个分量做 $\max(z,0)$。 + + 这里举例设: + + $$ + W_r = + \begin{bmatrix} + 0.5 & -0.3\\ + 0.1 & \;\,0.2 + \end{bmatrix}, + \quad + W_i = + \begin{bmatrix} + 1\\ + 2 + \end{bmatrix}, + \quad + b = + \begin{bmatrix} + -1\\ + 0.5 + \end{bmatrix}. + $$ + + + +**计算 $f_k$** + +1. 先算 $W_r\,x_k$: + +$$ +W_r\,x_k += +\begin{bmatrix} +0.5 & -0.3\\ +0.1 & \;\,0.2 +\end{bmatrix} +\begin{bmatrix} +0\\[3pt] +1 +\end{bmatrix} += +\begin{bmatrix} +0.5 \times 0 \;+\; (-0.3)\times 1\\[5pt] +0.1 \times 0 \;+\; 0.2 \times 1 +\end{bmatrix} += +\begin{bmatrix} +-0.3\\[3pt] +0.2 +\end{bmatrix}. +$$ + +2. 再算 $W_i \, I_k$: + +$$ +W_i \, I_k += +\begin{bmatrix} +1\\ +2 +\end{bmatrix} +\cdot 2 += +\begin{bmatrix} +2\\ +4 +\end{bmatrix}. +$$ + +3. 加上偏置 $b$: + +$$ +\begin{bmatrix} +-0.3\\[3pt] +0.2 +\end{bmatrix} ++ +\begin{bmatrix} +2\\[3pt] +4 +\end{bmatrix} ++ +\begin{bmatrix} +-1\\[3pt] +0.5 +\end{bmatrix} += +\begin{bmatrix} +-0.3 + 2 \;-\; 1\\[3pt] +0.2 + 4 \;+\; 0.5 +\end{bmatrix} += +\begin{bmatrix} +0.7\\[3pt] +4.7 +\end{bmatrix}. +$$ + +4. 通过 $\mathrm{ReLU}$,得到 + +$$ +f_k += +\mathrm{ReLU}\!\Bigl(\begin{bmatrix}0.7\\[4pt]4.7\end{bmatrix}\Bigr) += +\begin{bmatrix}0.7\\[4pt]4.7\end{bmatrix}. +$$ + + + +**更新 $x_{k+1}$** +$$ +x_{k+1} += +\frac{ +x_k + \Delta t\,\bigl[f_k \odot A\bigr] +}{ +1 + \Delta t\,\Bigl(\frac{1}{\tau} + f_k\Bigr) +} +\quad\longrightarrow\quad +\text{都是逐元素算}. +$$ + +1. 先算分子: + - $f_k \odot A = [\,0.7 \times 2,\;\;4.7 \times(-1)\,] = [\,1.4,\;-4.7]$。 + - $x_k + \Delta t\,\bigl[f_k \odot A\bigr] = [\,0,\,1\,] + [\,1.4,\;-4.7\,] = [\,1.4,\;-3.7\,]$。 + +2. 分母也要逐元素: + +$$ +1 + \Delta t \Bigl(\frac{1}{\tau} + f_k\Bigr) += +1 + 1 \cdot +\bigl([\,1,\,1\,] + [\,0.7,\,4.7\,]\bigr) += +1 + [\,1.7,\,5.7\,] += +[\,2.7,\;\,6.7\,]. +$$ + +3. 逐元素相除: + +$$ +x_{k+1} += +\bigl[\,1.4,\;-3.7\bigr] +\;\Big/\; +\bigl[\,2.7,\;6.7\bigr] += +\Bigl[\;\frac{1.4}{2.7},\;\;\frac{-3.7}{6.7}\Bigr] +\approx +[\,0.5185,\;-0.5522\,]. +$$ + +因此,我们最终得到 + +$$ +x_{k+1} \approx [\,0.5185,\;-0.5522\,]. +$$ + + + + + +#### 训练方法 + +论文采用 **BPTT(通过时间反向传播)** 进行训练: + +1. **前向传播**: + 使用数值求解器(融合显式-隐式欧拉法)沿时间步迭代计算状态 $x(t)$,公式为: + $$ + x_{k+1} = \frac{x_k + \Delta t \cdot f_k \odot A}{1 + \Delta t \left( \frac{1}{\tau} + f_k \right)} + $$ + 其中 $f_k = f(x_k, I_k, t_k, \theta)$,所有中间状态 $\{x_0, x_1, ..., x_T\}$ 被缓存。 + +2. **反向传播**: + 从最终损失 $L$ 出发,沿时间步逆向计算梯度: + + - 通过链式法则逐层传递梯度 $\frac{\partial L}{\partial x_k}$; + - 更新参数 $\tau$, $A$, $\theta$ 的梯度:$\nabla_{\tau} L$, $\nabla_{A} L$, $\nabla_{\theta} L$; + - 显式利用缓存的中间状态,避免伴随方法的重积分误差。 + +3. **优势**: + + - 精度高:直接计算梯度,无近似误差累积; + - 稳定性强:适用于刚性(Stiff)动力学系统; + - 代价:内存复杂度为 $O(T)$($T$ 为时间步数),需权衡序列长度。 + + + +代码训练:python har.py --model ltc --size 32 --epochs 50 --log 1 + + + + + +#### **液态时间常数的直观作用** + +**对快/慢时间尺度的自适应**: +当网络检测到输入信号变化非常快或幅度很大时,可动态增大衰减、加速更新;反之信号较稳定时,则让衰减变小、记忆更久。 + +**增强模型的非线性表征能力**: +因为衰减系数也会因网络状态而变,所以整体微分方程更具表达力,理论上能更好地逼近复杂的非线性时变系统。 + + + + + +**优势** + +- **参数数量减少**:每个神经元本身通过内置的动态机制承担了更多的功能,网络在**捕捉时间依赖性**时不需要额外堆叠大量的隐藏层或者引入复杂的循环结构(LSTM、GRU)。这大大减少了模型参数数量,从而降低了计算资源和能耗。 +- **稀疏激活**:动态更新机制意味着并非所有神经元在每个时刻都需要全量参与计算,只有部分神经元在关键时刻激活处理,从而提升整体计算效率。 + + + +**应用场景** + +**无人机和自动驾驶** + +- 由于液态神经网络能够在新环境下实时适应,其在无人机导航和自动驾驶系统中表现出色。研究表明,即使在复杂、未见过的场景中,它也能做出精准决策,从而实现高效导航。 + +**金融和医疗预测** + +- 在处理连续的时间序列数据(如股票价格、气候数据或生命体征监控)时,液态神经网络能够捕捉细微的动态变化,帮助进行更准确的预测与预警。 + + + diff --git a/科研/草稿.md b/科研/草稿.md new file mode 100644 index 0000000..ceb1e0f --- /dev/null +++ b/科研/草稿.md @@ -0,0 +1,81 @@ +以下是转换为 Markdown 格式的内容,公式已用 `$` 或 `$$` 包裹: + +--- + +下面给出一个**最简示例**,只展示 **节点 $A$** 在两层 GNN 里的更新计算过程,并说明它是如何从 2-hop 范围(即节点 $C, D$)间接获得信息。 + +--- + +## 图结构与初始特征 + +``` + A + | + B + / \ + C D +``` + +- **边**:$A$–$B$,$B$–$C$,$B$–$D$ +- **初始特征 (Layer-0)**: + - $A^{(0)} = [1.0,\ 0.5]$ + - $B^{(0)} = [0.8,\ 1.2]$ + - $C^{(0)} = [0.3,\ 0.7]$ + - $D^{(0)} = [1.5,\ 0.9]$ + +--- + +**第 1 层更新:$A^{(0)} \to A^{(1)}$** + +1. **节点 $A$ 的 1-hop 邻居**:只有 $B$。 +2. **聚合**(示例:自 + sum 邻居): + $$ + z_A^{(1)} \;=\; A^{(0)} + B^{(0)} + \;=\; [1.0,\,0.5] + [0.8,\,1.2] + \;=\; [1.8,\,1.7]. + $$ +3. **MLP 变换**:用一个两层感知器或线性+激活函数映射 $z_A^{(1)}$ 到 2 维输出: + $$ + A^{(1)} \;=\; \mathrm{MLP}\bigl(z_A^{(1)}\bigr). + $$ + - (数值略,可想象 $\mathrm{MLP}([1.8,1.7]) \approx [1.9,1.1]$ 之类。) + +**结果**:$A^{(1)}$ 包含了 **A** 的初始特征 + **B** 的初始特征信息。 + +--- + +**第 2 层更新:$A^{(1)} \to A^{(2)}$** + +为了让 **A** 获得 **2-hop** 范围($C, D$)的信息,需要**先**让 **B** 在第 1 层就吸收了 $C, D$ 的特征,从而 **B^{(1)}** 蕴含 $C, D$ 信息。然后 **A** 在第 2 层再从 **B^{(1)}** 聚合。 + +1. **节点 B 在第 1 层**(简要说明) + + - 邻居:$\{A,C,D\}$ + - 聚合:$z_B^{(1)} = B^{(0)} + A^{(0)} + C^{(0)} + D^{(0)}$ + - MLP 变换:$B^{(1)} = \mathrm{MLP}\bigl(z_B^{(1)}\bigr)$。 + - 此时 **B^{(1)}** 已经包含了 $C, D$ 的信息。 + +2. **节点 $A$ 的第 2 层聚合** + - 邻居:$B$,但此时要用 **B^{(1)}**(它已吸收 C、D) + - **聚合**: + $$ + z_A^{(2)} = A^{(1)} + B^{(1)}. + $$ + - **MLP 变换**: + $$ + A^{(2)} = \mathrm{MLP}\bigl(z_A^{(2)}\bigr). + $$ + +**结果**:$A^{(2)}$ 就包含了 **2-hop** 范围的信息,因为 **B^{(1)}** 中有 $C, D$ 的贡献。 + +--- + +## 小结 + +- **第 1 层**:A 直接获取 B 的初始特征;B 同时获取 A、C、D 的初始特征。 +- **第 2 层**:A 获取 B 的**新表示**(B^{(1)}),该表示已带有 C、D 信息 → A 间接获得 2-hop 信息。 +- **每层**都进行“聚合 + MLP”**同步更新**,最终 $A^{(2)}$ 就是节点 A 的 2 层 GNN 输出。 + +--- + +如果需要进一步调整或补充,请告诉我! diff --git a/科研/郭款论文.md b/科研/郭款论文.md new file mode 100644 index 0000000..0208aa2 --- /dev/null +++ b/科研/郭款论文.md @@ -0,0 +1,599 @@ +## 郭款论文 + +### **整体逻辑** + +*网络拓扑的生成与模拟* + +- RW、RWP、RD模型:这几种随机移动模型用于模拟节点在动态网络中的移动情况,从而生成连续时间点上的网络快照(邻接矩阵)。 + +- ER模型:作为静态网络模型,它提供了对比基准,用于验证重构算法在无动态扰动情况下的表现。 + + + + 这些模型为实验提供了“真实”的网络拓扑数据(或称地面真值),即论文中构建和测试网络重构算法的基础数据来源。 + + + +*网络拓扑的预测* + +- 卡尔曼滤波:在动态网络中,由于网络拓扑会随时间变化,直接获取每个时刻的真实拓扑通常不现实。论文利用卡尔曼滤波等预测算法,根据之前时刻网络的特征谱参数(如特征值和特征向量)来预测未来时刻的谱参数,为减少**计算复杂度和降低噪声**影响,通常只预测前 K 个最重要的特征谱参数,因为这部分信息包含了网络大部分的全局结构特征。 + +*网络重构过程* + +- 基于对称非负矩阵分解的重构:利用预测得到的谱参数(卡尔曼滤波预测的特征值和特征向量),可以对网络邻接矩阵进行逆向重构。这一步可以看作是“从低维(特征)恢复高维(原始邻接矩阵)”的过程,但由于预测误差和噪声,得到的初步重构矩阵通常存在一定偏差。 + +*矢量量化算法的应用* + +- 矢量量化(如改进的模糊 C 均值聚类):为了消除初步重构矩阵中的小幅扰动和噪声,通过对矩阵元素进行聚类量化,将分散的数值映射到各自的“代表值”上,从而提高重构精度。这个步骤实际上起到了“降噪”和“修正预测误差”的作用,使得最终的重构网络拓扑更加准确。 + + + + + +# KAN不稳定/卡尔曼滤波 小波变换 + + + +# GNN + + + +### 矢量量化 + +矢量量化的基本思想是将输入数据点视为多维向量,并将其映射到一个码本(codebook)中的最接近的码字。码本是预先确定的一组离散的向量,通常通过无监督学习方法(如**K-means**)从大量训练数据中得到。在矢量量化中,输入数据点与码本中的码字之间的距离度量通常使用**欧氏距离**。通过选择最接近的码字作为量化结果,可以用较少的码字表示输入数据,从而实现数据的压缩。同一个码字能够代表多个相似的多维向量,从而实现了**多对一的映射**。 + + + +**为什么引入矢量量化理论?** + +在谱分解重构过程中,由于特征值和特征向量的预测存在误差,每个矩阵元素可能会略微偏离理想值。例如,对于一个理想的 $0/1$ 邻接矩阵,真实的连接应为 $1$,而不连接应为 $0$。由于预测误差,实际重构矩阵可能出现如下数值: + +$$ +\begin{bmatrix} +0 & 0.98 & 0.03 \\ +0.97 & 0 & 1.02 \\ +0.05 & 1.01 & 0 \\ +\end{bmatrix} +$$ + +这里,$0.98$, $0.97$, $1.02$, $1.01$ 这些数值本来应该都是 $1$,但由于误差略有偏离;同样,$0.03$ 和 $0.05$ 本应为 $0$。 + +**矢量量化**通过将多个离散的扰动值聚类映射到固定值(如0/1或权值),消除随机误差,提高重构精度。 + +理想的量化后的矩阵: + +$$ +\begin{bmatrix} +0 & 1 & 0 \\ +1 & 0 & 1 \\ +0 & 1 & 0 \\ +\end{bmatrix} +$$ + + + +#### **K-means矢量量化示例** + +假设我们有 4 个二维数据点,希望将它们分成 2 类,即构造一个包含 2 个码字的码本。整个过程类似于 k-means 聚类。 + +**1. 数据矩阵** + +设原始数据矩阵为 + +$$ +X=\begin{pmatrix} +1 & 2 \\ +1.2 & 2.1 \\ +3 & 4 \\ +2.9 & 3.8 +\end{pmatrix}. +$$ + +这 4 个数据点中,前两个点大致聚集在一起,后两个点大致在另一处。 + +2. **初始化码本** + +我们希望构造一个码本矩阵 $$C$$,初始时可以从数据中随机选择两个点作为码字。假设选择 $$x_1=(1,2)$$ 和 $$x_3=(3,4)$$,则初始码本为 + +$$ +C^{(0)}=\begin{pmatrix} +1 & 2 \\ +3 & 4 +\end{pmatrix}. +$$ + +其中,每一行代表一个码字。 + +3. **分配数据点到码字** + +对于每个数据点,计算它与各个码字之间的欧氏距离,并将其分配到距离最近的码字。下面计算各数据点到码字的距离(这里只计算距离的平方,便于比较): + +- **数据点 $$x_1=(1,2)$$:** + + - 到码字 1 $$(1,2)$$ 的距离平方:$$ (1-1)^2+(2-2)^2=0 $$ + - 到码字 2 $$(3,4)$$ 的距离平方:$$ (1-3)^2+(2-4)^2=4+4=8 $$ + + 分配:$$x_1$$ 属于码字 1。 + +- **数据点 $$x_2=(1.2,2.1)$$:** + + - 到码字 1 $$(1,2)$$ 的距离平方:$$ (1.2-1)^2+(2.1-2)^2=0.04+0.01=0.05 $$ + - 到码字 2 $$(3,4)$$ 的距离平方:$$ (1.2-3)^2+(2.1-4)^2\approx3.24+3.61=6.85 $$ + + 分配:$$x_2$$ 属于码字 1。 + +- **数据点 $$x_3=(3,4)$$:** + + - 到码字 1 $$(1,2)$$ 的距离平方:$$ (3-1)^2+(4-2)^2=4+4=8 $$ + - 到码字 2 $$(3,4)$$ 的距离平方:$$ 0 $$ + + 分配:$$x_3$$ 属于码字 2。 + +- **数据点 $$x_4=(2.9,3.8)$$:** + + - 到码字 1 $$(1,2)$$ 的距离平方:$$ (2.9-1)^2+(3.8-2)^2\approx3.61+3.24=6.85 $$ + - 到码字 2 $$(3,4)$$ 的距离平方:$$ (2.9-3)^2+(3.8-4)^2=0.01+0.04=0.05 $$ + + 分配:$$x_4$$ 属于码字 2。 + +可以将分配结果用指示矩阵 $$Z$$ 表示,每行对应一个数据点,每列对应一个码字,若数据点分配给该码字,则对应位置为 1,否则为 0。于是 + +$$ +Z=\begin{pmatrix} +1 & 0 \\ +1 & 0 \\ +0 & 1 \\ +0 & 1 +\end{pmatrix}. +$$ + +4. **更新码字** + +对于每个码字,计算分配给该码字的数据点的均值,更新码字。更新公式为 + +$$ +c_k = \frac{1}{|S_k|}\sum_{x\in S_k} x, +$$ + +其中 $$S_k$$ 表示被分配给第 $$k$$ 个码字的点的集合。 + +- 对于码字 1: + 数据点 $$x_1=(1,2)$$ 和 $$x_2=(1.2,2.1)$$, + 新码字为 + $$ + c_1^{(1)}=\left(\frac{1+1.2}{2},\frac{2+2.1}{2}\right) + = (1.1,\ 2.05). + $$ + +- 对于码字 2: + 数据点 $$x_3=(3,4)$$ 和 $$x_4=(2.9,3.8)$$, + 新码字为 + $$ + c_2^{(1)}=\left(\frac{3+2.9}{2},\frac{4+3.8}{2}\right) + = (2.95,\ 3.9). + $$ + +因此,更新后的码本矩阵为 + +$$ +C^{(1)}=\begin{pmatrix} +1.1 & 2.05 \\ +2.95 & 3.9 +\end{pmatrix}. +$$ + +5. **重复迭代** + +通常,矢量量化的过程会不断迭代“分配数据点”和“更新码字”的步骤,直到码本收敛或达到预设的迭代次数。上述过程就是一次完整的迭代。实际中可能需要多次迭代来使码本更好地逼近数据的分布。 + + + +**传统Kmeans的局限:** + +对初始簇中心敏感 + +- K-means 需要预先设定簇的数量 $K$并随机初始化簇中心,初始选择不佳可能导致算法收敛到局部最优解,而非全局最优。 + +对离群点敏感 + +- 算法使用均值来更新簇中心,离群点会显著影响均值的计算,从而使得簇中心偏离真实的“中心”位置,进而影响整体聚类效果。 + +硬性划分(硬聚类) + +- 每个数据点只能被分配到一个簇,无法反映数据点可能同时具有多个簇的隶属关系。对于边界数据来说,这种“一刀切”的划分可能导致信息丢失。 + +需要预先确定$K$ 值 + +- 算法必须事先指定簇的数量,但在许多实际问题中,合适的$K$ 值可能难以确定,并且对最终结果有较大影响。 + + + +#### **PAM算法** + +主要流程如下 : + +1. **初始化** + 通过谱分解方法获得重构后的网络邻接矩阵,并根据预先确定的簇数 K 初始化 K 个簇中心。 + +2. **分配阶段** + 计算矩阵中每个元素 \( $a_{ij} $\) 与所有簇中心之间的欧氏距离 \( $d_{ij}$ \)(即 \($\sqrt{(a_{ij} - C_k)^2}$\)),并将该元素分配给距离最近的簇。 + 时间复杂度:$O(nk)$ + +3. **更新簇中心** + + - 对于某个簇 $$C_k$$,我们先将该簇中所有的数据点(除去原簇中心)都作为候选中心,即构成集合 $$P$$。 + + - 对于集合 $$P$$ 中的每一个候选点 $$p$$,计算它与同一簇中其他所有点的距离总和: + + $$ + D(p) = \sum_{q \in C_k, \, q \neq p} d(p, q) + $$ + + 其中 $$d(p, q)$$ 通常用欧氏距离或其他合适的距离度量表示。 + + - 选取使 $$D(p)$$ 最小的那个候选点作为新的簇中心 $$C_k$$。 + + 时间复杂度分析:需要尝试的替换次数:$k \times (n-k)$​ + 每次替换需要对所有(n)个元素重新分配并计算代价,则该阶段在一次完整迭代中的最坏情况下复杂度为 + $$ + O\bigl(k \times (n - k) \times n\bigr)\,. + $$ + + 当 \(k\) 相对 \(n\) 不是特别大时,可近似视为 + $$ + O(n^2 k)\,. + $$ + + + +4. **迭代** + 重复“分配阶段”和“更新簇中心”两个步骤,直到簇中心值不再发生变化或元素的分配不再改变为止。 + +5. **量化处理** + 聚类完成后,对原矩阵中每个元素进行量化,即将其映射为其所属簇的中心值,得到误差被有效消除的量化重构矩阵。 + +6. **输出结果** + 最终输出经过量化处理后的矩阵,该矩阵的重构误差明显降低,尤其在**存在较大扰动或离群值**的情况下,PAM 算法可以较好地缓解这些因素带来的负面影响。 + +PAM 改进了离群点的问题,但它依然属于硬性聚类方法,且计算成本大 + + + +#### **FCM算法** + +FCM的目标是将矩阵中的元素聚类到K个簇中,每个元素通过**隶属度**(概率值)表示其属于各簇的程度。以下是算法的详细步骤: + +**1. 初始化** + +- 输入:待聚类的矩阵 \( A \)(例如邻接矩阵)。 +- 设置:簇数 \( K \)、模糊因子 \( m \)(通常取2)、收敛精度 \( $\epsilon$ \)、最大迭代次数。 +- 初始化:随机生成隶属度矩阵 \( U \) 和簇中心 \( C \)。 + +**2. 更新隶属度** + +对于每个元素 \( $a_{ij} $\),计算其属于簇 \( k \) 的隶属度 \($ \mu_k(a_{ij})$ \): +$$ +\mu_k(a_{ij}) = \frac{1}{\sum_{l=1}^{K} \left( \frac{\|a_{ij} - c_k\|}{\|a_{ij} - c_l\|} \right)^{2/(m-1)}} +$$ + +- \( \|$a_{ij} - c_k$\| \):元素 \($ a_{ij}$ \) 到簇中心 \( $c_k$ \) 的距离(通常用欧氏距离)。 + +- \( m \):模糊因子,控制聚类的模糊程度(\( m=2 \) 时,隶属度更平滑)。 + 时间复杂度: + + 计算单个隶属度需要$O(K)$ 的(遍历所有 \($l=1,\dots,K$\)\) + + 由于要计算对 $K$个簇的隶属度,单个数据点的隶属度更新是 $O(K^2)$。 + +**3. 更新簇中心** + +对于每个簇 \( k \),计算新的簇中心 \( $c_k$ \): +$$ +c_k = \frac{\sum_{i,j} [\mu_k(a_{ij})]^m a_{ij}}{\sum_{i,j} [\mu_k(a_{ij})]^m} +$$ + +- \($ [\mu_k(a_{ij})]^m $):隶属度的加权值,表示元素对簇的贡献。 + +**4. 判断收敛** + +- 计算簇中心的变化量 \( $\Delta C$ = \|$C_{\text{new}} - C_{\text{old}}$\| \)。 +- 如果 \( $\Delta C < \epsilon $\),则停止迭代;否则返回步骤2。 + +**5. 量化处理** + +- 根据隶属度将元素分配到概率最大的簇,并用簇中心值替换元素值。 + +--- + +**具体矩阵例子** + +假设有一个3×3的邻接矩阵 \( A \): +$$ +A = \begin{bmatrix} +0.1 & 0.9 & 0.3 \\ +0.8 & 0.2 & 0.7 \\ +0.4 & 0.6 & 0.5 \\ +\end{bmatrix} +$$ +目标是将矩阵元素聚类为2个簇(\( K=2 \)),分别表示“连接”(簇1)和“断开”(簇2)。 + +**步骤1:初始化** + +- 随机初始化簇中心: + $$ + c_1 = 0.2, \quad c_2 = 0.8 + $$ + +- 随机初始化隶属度矩阵 \( U \)(每行和为1): + $$ + U = \begin{bmatrix} + 0.6 & 0.4 \\ + 0.3 & 0.7 \\ + 0.5 & 0.5 \\ + 0.8 & 0.2 \\ + 0.4 & 0.6 \\ + 0.7 & 0.3 \\ + 0.9 & 0.1 \\ + 0.2 & 0.8 \\ + 0.5 & 0.5 \\ + \end{bmatrix} + $$ + +**步骤2:更新隶属度** + +以元素 \( $a_{11}$ = 0.1 \) 为例: + +- 计算到簇1的距离: + $$ + \|a_{11} - c_1\| = |0.1 - 0.2| = 0.1 + $$ + +- 计算到簇2的距离: + $$ + \|a_{11} - c_2\| = |0.1 - 0.8| = 0.7 + $$ + +- 计算隶属度: + $$ + \mu_1(a_{11}) = \frac{1}{\left( \frac{0.1}{0.1} \right)^2 + \left( \frac{0.1}{0.7} \right)^2} \approx 0.98 + $$ + + $$ + \mu_2(a_{11}) = 1 - \mu_1(a_{11}) \approx 0.02 + $$ + +**步骤3:更新簇中心** + +以簇1为例: + +- 计算加权和: + $$ + \sum_{i,j} [\mu_1(a_{ij})]^2 a_{ij} = (0.98)^2 \times 0.1 + (0.3)^2 \times 0.8 + \dots + $$ + +- 计算新的簇中心: + $$ + c_1 = \frac{\text{加权和}}{\sum_{i,j} [\mu_1(a_{ij})]^2} + $$ + +**步骤4:判断收敛** + +- 如果簇中心变化小于 \($ \epsilon $\),停止迭代。 + +**步骤5:量化处理** + +- 根据隶属度将元素分配到簇1或簇2,并用簇中心值替换元素值。 + + + +#### **初始簇中心选取优化方法** + +**优化过程**: + +1. 设邻接矩阵最大元素值为$p$,设定初始簇间距阈值$\theta = \frac{p}{K}$($K$为预设簇数) + +2. 初始化候选集合$S_0$包含所有矩阵元素 + +3. 迭代选择簇中心: + + - 计算当前候选集中元素间最小距离$d_{min}$ + + - 选择具有$d_{min}$的两个元素,计算中心点作为新簇中心$C_i$ + + - 根据阈值$\theta$划分集合: + $$ + S_1 = \{ x \in S_0 \ | \ \|x - C_i\| \leq \theta \} + $$ + + $$ + S_2 = S_0 \setminus S_1 + $$ + + - 将$S_2$作为新的候选集,重复直到选出$K$个簇中心 + +**数学约束**: +对于选定的簇中心需满足: +$$ +\forall i,j \in [1,K], \ \|C_i - C_j\| \geq \theta +$$ +其中$\theta = \frac{\max(A)}{K}$,$A$为邻接矩阵。 + + + +### 网络重构 + +对称非负矩阵分解(SNMF)来构造网络的低维嵌入表示,从而实现对高维网络邻接矩阵的精确重构。 +$$ +A \approx U U^T. +$$ + +只需计算 $U U^T$ 就能重构出 $A$ 的低秩近似版本。如果选择保留全部特征(即 $r = n$),则可以精确还原 $A$;如果只取部分($r < n$),那么重构结果就是 $A$ 的低秩近似。 + +#### 节点移动模型 + +**模拟随机网络中节点的移动方式**,进而间接模拟网络拓扑在不同时刻的变化。它们是用来在仿真环境下产生“网络结构随时间动态变化”的数据,从而让后续的网络重构或网络分析算法有一个逼近真实场景的测试环境。 + +**RW(Random Walk,随机游走)模型** + +- 基本概念:在 **随机游走(RW)** 模型中,节点在一个定义的区域内随即选择方向并开始移动。每当节点到达区域的边界时,它会反弹并继续前进。节点的移动时间是固定的,且每个移动步骤都是独立的。 +- 特点 + - 节点移动过程中,节点的行进方向是完全随机的。 + - 节点在到达区域边界时会选择一个新的角度进行反弹,并继续随机移动。 + - 节点的移动是不规则的,每次运动的时间或距离是固定的,直到达到边界。 +- 适用场景:适用于需要模拟节点在空间中随机漂移或无规律移动的场景,如某些类型的无人机网络或粒子运动模型。 + +**RD(Random Direction,随机方向)模型** + +- 基本概念:在 **随机方向(RD)** 模型中,节点每次选择一个随机的方向,并在该方向上移动直到达到区域的边界。到达边界后,节点会随机选择一个新的目标方向,并继续移动。 +- 特点 + - 节点在每个时间片段内选择一个**随机的方向**,而不是随机选择目标。 + - 节点在到达边界时会停留一段时间,然后选择一个新的随机方向。 + - 该模型中,节点的运动行为更有方向性,与RW模型相比有更强的运动规律性。 +- 适用场景:适用于需要模拟节点在特定方向上移动的情况,如某些类型的车载网络或定向通信网络。 + +**RWP(Random Waypoint,随机路点)模型** + +- 基本概念:在 **随机路点(RWP)** 模型中,节点选择一个**随机的位置**作为目标点,然后以固定速度沿直线移动到该位置。到达目标点后,节点选择一个新的随机目标位置,并再次以相同的速度移动。 +- 特点 + - 节点移动到随机选定的目标位置,并在到达后停留一段时间。 + - 该过程重复进行,每次都选择新的随机目标位置。 + - 节点在任意时刻都有一个目标位置,而不是随机选择一个方向。 +- 适用场景:适用于模拟节点有明确目标且周期性移动的网络场景,比如无线传感器网络和移动广告网络等。 + + +**参数** + +**节点数量**决定了在该活动半径内随机分布并移动的节点数量 + +**通信半径** 决定了网络中“谁能跟谁连边”,对 **拓扑连通性** 影响极大(如果两节点之间的距离小于等于通信半径,就认为它们之间存在连边); + +**活动半径** 决定了节点分布和移动范围,对 **网络整体的稀疏/稠密程度**、节点之间的平均距离都有重要影响。 + + + +#### 基于 SNMF 的网络嵌入 + +文献[60]提出一种基于**简化特征分解**和**整体旋转**的近似方法(reduced Eigenvalue Decomposition,rEVD): + +1. 先通过截断特征值/向量,构造一个 $B = X \Lambda^{1/2}$; +2. 不断“旋转” $B$(乘以酉矩阵 $Q$)并对负值做截断,从而让最终的 $U$ 既接近 $B$ 又满足非负性。 + 这相当于把一部分“逼近”工作用特征分解先做了,然后只用旋转矩阵 $Q$ 来调整局部负值,再配合 $\max(0,\cdot)$ 截断满足非负约束。 + + + +假设我们有一个 $2 \times 2$ 对称非负矩阵 + +$$ +A = \begin{pmatrix} 3 & 1 \\ 1 & 3 \end{pmatrix}. +$$ + +--- + +**1. 初始步骤:构造 $B$ 和 $Q$** + +首先对 $A$ 做谱分解,得到(已知结果): + +- 特征值:$\lambda_1=4, \lambda_2=2$ + +- 对应的单位特征向量: + $$ + x_1 = \begin{pmatrix} \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} \end{pmatrix}, \quad + x_2 = \begin{pmatrix} \frac{1}{\sqrt{2}} \\ -\frac{1}{\sqrt{2}} \end{pmatrix}. + $$ + +构造 +$$ +X = \begin{bmatrix} x_1 & x_2 \end{bmatrix} = \begin{pmatrix} \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}} \end{pmatrix}, \quad +\Lambda = \begin{pmatrix} 4 & 0 \\ 0 & 2 \end{pmatrix}. +$$ + +取 $r=2$(通常𝑟 ≪ $n$,这里仅作例子),定义 +$$ +B = X \Lambda^{\frac{1}{2}} \quad \text{其中} \quad \Lambda^{\frac{1}{2}} = \begin{pmatrix} 2 & 0 \\ 0 & \sqrt{2} \end{pmatrix}. +$$ + +因此, +$$ +B = X \Lambda^{\frac{1}{2}} = +\begin{pmatrix} \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} \\ \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}} \end{pmatrix} +\begin{pmatrix} 2 & 0 \\ 0 & \sqrt{2} \end{pmatrix} += \begin{pmatrix} \frac{2}{\sqrt{2}} & \frac{\sqrt{2}}{\sqrt{2}} \\ \frac{2}{\sqrt{2}} & -\frac{\sqrt{2}}{\sqrt{2}} \end{pmatrix} += \begin{pmatrix} \sqrt{2} & 1 \\ \sqrt{2} & -1 \end{pmatrix}. +$$ + +同时,初始令 $Q = I_2$($2 \times 2$ 单位矩阵)。 + +--- + +**2. 初始 $U$ 的计算** + +按照迭代初始公式: +$$ +U \approx \max\Bigl(0, B Q \Bigr). +$$ +由于 $Q = I_2$,初始 $U$ 就是 +$$ +U^{(0)} = \max\Bigl(0, B \Bigr) = \max\left(0, \begin{pmatrix} \sqrt{2} & 1 \\ \sqrt{2} & -1 \end{pmatrix}\right) += \begin{pmatrix} \sqrt{2} & 1 \\ \sqrt{2} & 0 \end{pmatrix}. +$$ + +--- + +**3. 更新 $Q$ 的步骤** + +接下来计算中间矩阵 +$$ +F = U^{(0)T} B. +$$ +计算 $U^{(0)T}$: +$$ +U^{(0)T} = \begin{pmatrix} \sqrt{2} & \sqrt{2} \\ 1 & 0 \end{pmatrix}. +$$ +因此, +$$ +F = U^{(0)T} B = \begin{pmatrix} \sqrt{2} & \sqrt{2} \\ 1 & 0 \end{pmatrix} \begin{pmatrix} \sqrt{2} & 1 \\ \sqrt{2} & -1 \end{pmatrix}= \begin{pmatrix} 4 & 0 \\ \sqrt{2} & 1 \end{pmatrix}. +$$ +接下来,对 $F$ 进行奇异值分解(SVD):设 +$$ +F = H \Sigma V^T. +$$ +(在实际数值计算中可以得到具体 $H, \Sigma, V$;此处我们只说明流程。) +然后按照更新公式,令 +$$ +Q \leftarrow V H^T. +$$ + +这一步的作用是“旋转” $B$ 的基,使得 $U$ 更贴近满足 $A \approx U U^T$ 的目标。 + +--- + +**4. 迭代更新 $U$** + +在更新了 $Q$ 后,再更新 $U$ 的公式仍为: +$$ +U \leftarrow \max\bigl(0, B Q \bigr). +$$ +也就是说,用新的 $Q$ 重新计算 $B Q$,再将负值截断为零。 + +然后再重复“计算 $F = U^T B$ → SVD 得到新 $Q$ → 更新 $U$”这一过程,直至满足收敛条件(例如 $\|U^T(U - B Q)\|_F^2 \le \varepsilon$)。 + +--- + +**5. 迭代结束后的意义** + +当迭代停止时,得到的 $U$ 满足 +$$ +A \approx U U^T. +$$ +此时 $U$(尺寸为 $2 \times 2$ 的矩阵)就是对 $A$ 进行低维嵌入得到的“特征表示”,它不仅满足非负性,而且通过内积 $U U^T$ 能近似重构出原矩阵 $A$。 + +**两种求对称非负矩阵分解的方法** + +1. **纯梯度下降** + - 优点:实现原理简单,对任意大小/形状的矩阵都能做。 + - 缺点:收敛速度有时较慢,且对初始值敏感。 + +2. **rEVD + 旋转截断** + - 优点:利用了特征分解,可以先一步把主要信息“压缩”进 $B$,后续只需解决“如何把负数修正掉”以及“如何微调逼近”。 + - 缺点:需要先做特征分解,适合于对称矩阵或低秩场景。 + diff --git a/科研/颜佳佳论文.md b/科研/颜佳佳论文.md new file mode 100644 index 0000000..a20c335 --- /dev/null +++ b/科研/颜佳佳论文.md @@ -0,0 +1,2 @@ +## 颜佳佳论文 +