跳到主要内容

TypeScript 为 JavaScript 带来了什么

· 阅读需 6 分钟
Jason Lee

TypeError 举例,TS 是如何解决的

当一项新技术摆在我这样的懒人前,有必要弄清学它的必要性,否则一定会半途而废。JavaScript 的过于灵活,一直以来为人诟病。它所带来的麻烦,主要体现在很多 TypeError 报错,直到运行时才会暴露出来。这一点在我的编程体验中最为深刻。以下是 chatgpt 对这个问题的理解和 typescript 能在多大程度上解决这类错误。

You:

JavaScript 中的 TypeError

ChatGPT:

在 JavaScript 中,TypeError 是一种常见的错误类型,当尝试对一个数据类型执行不适合的操作时会抛出。下面是一些常见的 TypeError 发生的情况:

  1. 访问未定义的属性
let obj;
console.log(obj.property); // TypeError: Cannot read property 'property' of undefined
  1. 调用非函数类型的变量
let nonFunction = 123;
nonFunction(); // TypeError: nonFunction is not a function
  1. 将值赋给只读属性
const obj = {};
Object.defineProperty(obj, "prop", {
value: 42,
writable: false,
});
obj.prop = 77; // TypeError: Cannot assign to read only property 'prop' of object
  1. 在基础数据类型上调用方法
null.someMethod(); // TypeError: Cannot read property 'someMethod' of null
  1. 使用new关键字调用不是构造函数的对象
let notAConstructor = {};
new notAConstructor(); // TypeError: notAConstructor is not a constructor

为了处理这些错误,可以使用 try...catch 语句来捕获和处理异常。例如:

try {
let obj;
console.log(obj.property);
} catch (e) {
if (e instanceof TypeError) {
console.error("TypeError caught:", e.message);
} else {
throw e;
}
}

通过合理的错误处理,可以使代码更加健壮,并能够更好地调试和维护。

You:

这些错误能通过把 js 改写为 typescript 来解决吗

ChatGPT:

将 JavaScript 转换为 TypeScript 可以预防许多类型相关的错误,因为 TypeScript 提供了静态类型检查和更严格的语法规则。然而,TypeScript 并不能完全消除所有运行时错误。让我们看看上述几种 TypeError 是如何在 TypeScript 中得到改进或预防的:

  1. 访问未定义的属性
let obj: { property?: string };
console.log(obj.property); // TypeScript 提示 obj 可能为 undefined

TypeScript 会警告你 obj 可能是 undefined,需要检查或使用类型保护。

  1. 调用非函数类型的变量
let nonFunction: any = 123;
(nonFunction as Function)(); // TypeScript 会警告类型不匹配

TypeScript 类型系统会阻止非函数类型的变量被调用。

  1. 将值赋给只读属性
interface MyObject {
readonly prop: number;
}
const obj: MyObject = { prop: 42 };
obj.prop = 77; // TypeScript 编译错误: Cannot assign to 'prop' because it is a read-only property.

TypeScript 通过 readonly 修饰符强制只读属性不可被重新赋值。

  1. 在基础数据类型上调用方法
let nullable: null | { someMethod: () => void } = null;
nullable.someMethod(); // TypeScript 提示 nullable 可能为 null

TypeScript 会提示 nullable 可能是 null,需要进行检查。

  1. 使用new关键字调用不是构造函数的对象
let notAConstructor: any = {};
new notAConstructor(); // TypeScript 编译错误: Cannot use 'new' with an expression whose type lacks a call or construct signature.

TypeScript 会阻止在非构造函数的对象上使用 new 关键字。

TypeScript 通过静态类型检查、接口、类型断言等机制,大大减少了因为类型错误而导致的运行时异常。尽管如此,TypeScript 不能完全避免所有错误,特别是那些涉及到动态数据和外部输入的错误,因此在编写 TypeScript 代码时仍需谨慎,并结合适当的错误处理机制。

TS 很强,但是还不够

TypeScript 类型检查的范围只局限在代码编译阶段,运行时是无能为力的。比如一次数据库查询期望返回一个数组格式的数据,但是因为某种原因变成了 undefined,那么后续代码用到这个数组的地方就会报错了。所以,涉及到动态数据的,应该要手动加类型检查,比如说

if (!Array.isArray(o)) {
throw new Error();
}

强类型?弱类型?

ts 是在 js 的基础上加上了类型,因此成了一门静态语言。但是,这种静态类型还要加一个"弱"的形容词。这是因为,ts 和 js 一样会执行隐式转换而不会报错。

console.log(1 + "1");
// 打印出字符串 '11'

上面这段代码,js/ts 都能执行成功。

nestjs入门

· 阅读需 11 分钟
Jason Lee

前言

软开团队之前做过的一个项目,用 nestjs 做的。我听到时很惊讶,部门负责人竟然这么敢用当时看起来还那么稚嫩的 nodejs 作为后端语言。让我们开始吧。

安装

类似于 vue2,也要安装一个脚手架。启动后,访问 localhost:3000,出现一个朴实无华的 Hello world,甚至没有用标题 html 标签:

16907906119941690790611215.png

nestjs 和 express 的关系

NestJS 和 Express 之间有密切的关系,可以将 NestJS 视为建立在 Express 之上的框架。NestJS 的 HTTP 模块和功能实际上是建立在 Express 的基础之上的,并且可以与 Express 的功能无缝交互。

具体来说,NestJS 在底层使用 Express 来处理 HTTP 请求和构建 Web 应用程序。这意味着,NestJS 的 HTTP 模块实际上是使用 Express 的路由和中间件系统来定义路由和处理请求。因此,你可以在 NestJS 应用程序中直接使用 Express 中的中间件,也可以使用 NestJS 提供的中间件。

虽然 NestJS 在底层使用了 Express,但它为开发者提供了一种更现代、模块化的方法来构建应用程序。NestJS 引入了一系列新的概念,如模块、控制器、提供者、依赖注入等,这些使得代码更易于组织、测试和维护。此外,NestJS 还支持多种其他功能,如 WebSocket、GraphQL 等,可以让你构建更强大的应用程序。

总的来说,NestJS 是建立在 Express 之上的,它保留了 Express 的灵活性和强大性能,同时为开发者提供了更高级的抽象和功能,使得构建复杂的服务器端应用程序更加容易和愉快。

项目结构

我是跟着油管的教程敲了一个基础小项目。下面是项目目录的截图。

16909392755291690939274607.png

nestjs 推荐通过脚手架工具来生成代码文件,这样的初始代码来自模板文件。所有可自动生成的文件如下:

16909397445231690939744129.png

这里和我熟悉的 django 框架做一个对比。

nestjsdjangonest-cli说明
moduleappnest generate module users模块
controllersurls.pynest generate controller /users/controllers/users路由
servicesviews.pynest g s /users/services/users视图,运行逻辑代码
pipesforms.pynest g pipe /users/pipes/ValidateCreateUser对表单或者传来的数据进行验证
middlewaremiddlewarenest g mi /users/middlewares/example中间件
guard权限装饰器nest g guard /users/guards/Auth权限控制

其它的,app.module.ts对模块进行注册,相当于 django 中 settings.py 中的 INSTALLED_APPS。

DTO(Data Transfer Object)

在 NestJS 中,DTO(Data Transfer Object)是一种用于数据传输的对象。DTO 主要用于在应用程序的不同层之间传输数据,通常在控制器(Controller)层和服务(Service)层之间进行数据传递。DTO 的目的是将数据从一个地方传输到另一个地方,并在传输过程中进行数据验证和转换。

DTO 在应用程序中扮演了重要的角色,特别是在处理 HTTP 请求和响应的过程中。它们可以用于将请求数据从控制器传递给服务,也可以用于将服务返回的数据转换为适合返回给客户端的格式。

通常,DTO 对象会在不同层之间定义,并在控制器和服务中使用。DTO 可以用于以下几个方面:

请求数据验证:在控制器中,DTO 可以用于验证客户端传递的请求数据是否符合预期的格式和规则。通过使用装饰器来定义 DTO 类的属性,NestJS 可以自动验证传入的数据,并根据定义的规则返回验证错误。

数据传输:在服务层和控制器之间传递数据。服务层可以接收 DTO 对象,执行相应的业务逻辑,然后返回 DTO 对象,控制器再将其转换为 HTTP 响应返回给客户端。

数据转换:DTO 可以用于将服务返回的数据转换为适合返回给客户端的格式。例如,将数据库查询的结果转换为 JSON 格式返回给客户端。

隐藏敏感信息:在某些情况下,DTO 可以用于隐藏敏感信息,确保客户端只能获得必要的数据而不会暴露不应该看到的信息。

在 NestJS 中,你可以使用 @nestjs/swagger 模块来自动生成 API 文档,并自动将 DTO 类中的装饰器用于验证请求数据。

下面是一个简单的例子,展示了在 NestJS 中定义一个 DTO 类的示例:

import { IsString, IsInt } from 'class-validator';

export class CreateUserDto {
@IsString()
readonly name: string;

@IsInt()
readonly age: number;

@IsString()
readonly email: string;
}

在上面的例子中,我们定义了一个 CreateUserDto 类,该类用于在控制器中接收创建用户的请求数据。使用 @IsString() 和 @IsInt() 装饰器来定义每个属性的验证规则。在控制器中,我们可以使用该 DTO 类来验证客户端传入的数据是否符合预期的规则。

两处类型验证的区别

由于请求传入参数和保存在数据库的参数可能不一致,所以需要两种验证。请求参数的验证时 放在 dtos 文件夹下,在 controller 中使用。而保存数据库的验证放在 utils 文件夹,在 service 层使用。

依赖注入

GPT:

依赖注入是一种编程的方式,用于管理代码中不同部分之间的依赖关系。在软件开发中,一个模块可能会需要使用另一个模块提供的功能或数据,而依赖注入就是一种让这种关系更加松散和可维护的方法。

举个例子,假设你在开发一个网站后端,使用了 NestJS 这个框架。你需要创建一些控制器来处理不同的请求,同时还需要连接数据库来存储和检索数据。这时就涉及到依赖关系:控制器依赖于数据库连接来获取数据。

在使用依赖注入之前,你可能会在每个控制器内部创建数据库连接,但这会导致代码重复和难以维护。而使用依赖注入,你可以这样做:

定义一个数据库连接的服务(或者称之为提供者):在 NestJS 中,你可以创建一个类,用于创建和管理数据库连接。

在控制器中声明依赖:在你的控制器类中,你可以声明需要使用的依赖,比如数据库连接。

在模块中设置依赖关系:在 NestJS 中,你需要将控制器和提供者(数据库连接)放入同一个模块中,并在模块的配置中告诉 NestJS 如何建立这些依赖关系。

NestJS 完成注入:当你启动应用程序时,NestJS 将根据你在模块中的配置,自动将数据库连接注入到控制器中。这意味着你不必自己管理控制器和连接之间的关系,NestJS 会帮你处理。

通过依赖注入,你可以更好地组织代码,降低模块之间的耦合度,使得代码更容易测试和维护。

下面是一个伪代码示例,演示如何在 NestJS 中使用依赖注入:

// 数据库连接提供者
@Injectable()
class DatabaseService {
connect() {
// 连接数据库的逻辑
}
}

// 控制器
@Controller()
class UserController {
constructor(private databaseService: DatabaseService) {}

getUser(id: number) {
const user = this.databaseService.fetchUser(id);
return user;
}
}

// 模块配置
@Module({
providers: [DatabaseService],
controllers: [UserController],
})
class UserModule {}

在这个示例中,DatabaseService 负责数据库连接,UserController 控制器依赖于 DatabaseService 来获取用户信息。在 UserModule 模块中,我们通过将 DatabaseService 提供者添加到 providers 列表中,将 UserController 控制器添加到 controllers 列表中,NestJS 将自动完成依赖注入。

简单来说,nestjs 的依赖注入,就类似于 django 中的 urls 和 views 相分离。

LINUX 基础操作(基于UBUNTU)

· 阅读需 8 分钟
Jason Lee

前言

跟随全栈学习路线之 linux 系统熟悉。由于 linux 的命令很多,且有些命令很长,很难记住,但是借助 gpt,无需强调记住命令,而是重点学习 linux 系统的工作原理,以及和 win 系统不同的操作逻辑。

如何定义“常见”的命令呢?我认为,看到那些习以为常的 windows 操作在 linux 中的实现,就可以认为是常见命令加以记忆。

directory

删除某一个文件夹时,如果文件夹下有文件,是不能直接删除的,得用rm -r <directory>. -r 表示递归的删除,即将所有存在的文件全部删除。

文件操作

因为 linux 内万物皆文件,所以下面的命令适用于 linux 内任何实体。

复制粘贴/移动

复制粘贴文件时,并不是像 win 下有分为 copy&paste 两个动作,而是一个命令完成:cp <src> <target>. 如果要将整个文件夹下的文件复制到另一个地方,要分两种情况,要么是所有文件的转移:cp -r . <target> 或者以文件夹的形式复制:cp -r <dirname> <target>.

mv 命令从字面上是移动(剪切粘贴),也可作为重命名文件的命令使用

linux 目录结构

win 的所有文件都存在 C 盘(假如不分区),而 linux 的根目录则是朴实无华的/,类似于 C 盘。用 xshell 等工具登录后的着陆目录可能是你的用户文件夹,比如/root

文件的增删改查

创建空文件用touch,但是好像没什么必要。绝大部分场景是创建文本文件,最好是vi

  1. 删:rm

  2. 改:vi

查有两个层面:一是查看文件属性,用ls -lh filename。二是查看文件内容。小文件直接用 vi,大文件(一般是日志),要么查看尾部几条tail -n 200 filename,要么用less进入文件,熟练运用space/b进行上下翻页。

vi 的详细命令本文后面会说。

system info

最主要的三个状态:cpu,内存和磁盘。

cpu 和内存

可以通过top或者htop(需要另外安装)

top 的输出:

htop 的输出:

htop 的信息更清晰一些。在进入 top 界面后,可以输入下列快捷键改变实时显示:

  • q:退出 top

  • P:按 CPU 使用率排序(默认)。

  • M:按内存使用率排序。

  • k:结束一个进程(输入进程 ID)。

磁盘

磁盘大概不必查看实时状态。命令为df -h.

用centos的截图代替了

package manager

windows 下所有的软件安装都是走可视化窗口,而 ubuntu 一般都是通过命令行。搜索软件:sudo apt search <name>, 安装:sudo apt install <name>, 卸载:sudo apt remove <name>

或者,也有的软件是通过下载压缩包,并解压后得到 deb 后缀的安装包,运行sudo dpkg -i <name>.

注意:软件安装都需要 sudo 的权限,因为安装过程都是对系统文件的修改。

text editor

值得说明的是,用 vscode 连接远程的,可以直接用图形页面编辑文本,也是优先选择。可选工具:vi, vim, nano 等。

~~nano 在我心中取代了 vim,因为它的操作逻辑更简单直观。~~Nano 不是自带的,使用还得安装;而大部分系统(ubuntu/centos)都自带 vi。所以,还是掌握 vi 为好。

vi 常用命令

  • 搜索文本

    • /pattern:向下搜索 pattern

    • ?pattern:向上搜索 pattern

    • n:重复上一次搜索(相同方向)。

    • N:反向重复上一次搜索。

  • 显示/隐藏行号: :set nu/set nonu

  • 跳转到某一行: :n

  • 翻页Ctrl + f向下翻页;Ctrl + b向上翻页;G移动到文件末尾;gg移动到文件开头。

打包压缩

Linux 的打包和压缩实际上是两个独立的过程,不同于 windows 的默认集中在一起完成。

  • 打包(Archive) : 仅将多个文件合并成一个文件,并不压缩大小,常用 tar 命令。

  • 压缩(Compression) : 通过算法减少文件大小,常用 gzipbzip2xz 命令。

  • 打包和压缩结合使用 通常情况下,打包和压缩结合使用可以将多个文件合并为一个文件,同时减少其总大小。在 Linux 中,最常用的组合是 tar 命令和 gzipbzip2 压缩工具的组合。

  • 打包并压缩 :

tar -cvzf archive.tar.gz file1.txt file2.txt  # 打包并使用 gzip 压缩
tar -cvjf archive.tar.bz2 file1.txt file2.txt # 打包并使用 bzip2 压缩
tar -cvJf archive.tar.xz file1.txt file2.txt # 打包并使用 xz 压缩
  • 解压并解包 :
tar -xvzf archive.tar.gz  # 解压并解包 gzip 压缩的 tar 文件
tar -xvjf archive.tar.bz2 # 解压并解包 bzip2 压缩的 tar 文件
tar -xvJf archive.tar.xz # 解压并解包 xz 压缩的 tar 文件

选项解释:

  • -c:创建新的打包文件(create)。

  • -v:显示打包过程的详细信息(verbose)。

  • -f:指定输出文件名(file)。

  • -t:查看打包文件的内容(list)。

  • -x:解包(extract)。

  • -z:使用 gzip 压缩。

  • -j:使用 bzip2 压缩。

  • -J:使用 xz 压缩。

软硬连接对标 windows 的快捷方式。区别如下:

比较项软连接(Symbolic Link)硬连接(Hard Link)
创建命令ln -s <目标> <链接名> ln <目标> <链接名>
是否可以跨文件系统
是否可以指向目录否(通常不能指向目录)
是否依赖目标路径是(依赖目标路径)否(与目标文件共享相同的 inode)
目标删除的影响目标被删除,链接失效(断链)目标被删除,硬链接仍可访问文件内容
是否共享 inode否(独立的 inode)是(共享相同的 inode)
用途用于创建目录快捷方式或跨分区引用用于在同一分区内创建文件的多个引用

快速部署深度学习模型:gradio初体验

· 阅读需 6 分钟
Jason Lee

前言

需求:将机器学习的模型变成网页应用,并部署到公网上用于访问。

前几个月,当时也有类似的需求,想将自己毕业论文中的一个图像处理模型部署到公网。当时考虑的是,用 django 或者 flask 结合 pytorch 在本地搭一个应用,实现图片进出预训练模型的逻辑,然后用内网穿透的工具,使得外部可以访问到我的主机。但是这就带来许多问题,比如当时找了好几个内网穿透工具,要么收费,要么不好用。而且,主机总会有关机的时候,那么关机后服务也就停止了。一时间迟迟无法入手,当时就暂时放弃了。前阵子听说了 gradio 这个好东西,一个 python 包,通过几行代码,就可以在本地起一个应用,不仅带有简洁的 UI,而且内置了内网穿透。当时觉得很困难的问题,竟然一下迎刃而解了。感叹现在深度学习社区发展的速度。

gradio 本地运行

下面以知乎上的 rgb2gray 为例,讲解 gradio 库的使用。新建一个虚拟环境,安装上 gradio 包:

pip install -Uq gradio ## -U表示安装最新版本的包,-q表示静默安装,避免命令行污染

新建一个 app.py:

import gradio as gr
import cv2

def to_black(image):
output = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return output

interface = gr.Interface(fn=to_black, inputs="image", outputs="image")
interface.launch(share=True)

运行 py 文件,gradio 在本地启动一个 HTTP 服务器,并通过内网穿透将该服务器暴露给公共互联网。局域网和公网链接都会输出在命令行终端。但是生成的公网链接会在 72 小时后过期,因此它是一个临时链接。如果要实现永久访问,需要将代码以及预训练模型(如有)上传到 huggingface space 上。

huggingface space

huggingface space 是一个类似 github 的地方,可以用来存 py 文件和预训练模型,并且在和 gradio 绑定后,可以实现免费的部署。如果是新电脑,运行gradio deploy,按指示添加 write token,绑定 huggingface 账号。也可以通过 git clone 命令,下载空间到本地再修改。新建 space 时的信息:

16886436128041688643612164.png

除了一个基础的 cpu 是免费的,其他都是按小时收费的。说实话还是蛮贵的,比如 T4 gpu,colab 和 kaggle 都是免费提供的,这里收 0.6 美刀/小时。当代码每次有更改时,空间会自动刷新并运行一次,可通过点击红框内的按钮查看进度:

16886441928151688644192174.png

成功之后,可以通过分享这个 space 的链接,来实现在线演示。

ps: 如何找到自己创建过的 space。要通过点击头像,进入个人主页。

访问速度对比

这里做一个简单的测试,本例的 rgb2gray 是一个非常简单的函数,cpu 都能瞬间完成,下面分别测试 gradio 在代码存在本地时通过 localhost 和内网穿透以及代码在 hf space 下的处理速度。图片尺寸为 1280*853,浏览器为 firefox。

方式时间(估计)使用场景
localhost0.4s本地演示给自己或者旁边的人看
内网穿透24s有外部访问的需求,且本地有 gpu,能覆盖网速带来的负面影响
hf space4s有外部访问或者长时间保持服务的需求,可以购买 gpu 时长

可以看出,内网穿透最慢,localhost 最快。也可以理解,内网穿透的网络传输方式最复杂,受网速波动影响大。

api

除了 gradio 提供的 UI,也可以自己设计 UI,并利用 gradio 提供的 api 接口,实现模型推理。但是,当我按照文档的代码运行时,遇到了报错:

ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。

现在暂时未解决。

结语

记录了 gradio 的初级用法,并给出了不同场景下的使用方案。有了 gradio,深度学习模型部署变得省事多了。

vue和django项目的开发/生产环境配置管理

· 阅读需 5 分钟
Jason Lee

前言

虽然教程作者提到了一些分开管理两种环境的做法,但是我这边没有形成完整可复制的做法。借助 chatgpt,将收获整理出来。

vuejs

在项目的根目录下,分别创建两个文件:.env.development and .env.production。最基础的设置是 api 地址:

# .env.development
VITE_API_URL=http://dev-api.example.com

# .env.production
VITE_API_URL=http://api.example.com

任何设置都有以 VITE_开头。为了区别使用配置,在vite.config.js中,有如下:

export default defineConfig({
// 开发环境使用 .env.development 文件
envDir: './',
build: {
// 生产环境使用 .env.production 文件
envFile: '.env.production',
},
});

django

相对于 vue, django 的设置就要复杂一些了。这里把 gpt 关于生产和开发环境的不同列出来,虽然有很多在这个项目没有实现,但是可以为以后更复杂的真实项目做参考。

在 Django 项目中,开发环境和生产环境通常需要进行不同的设置。下面是一些常见的设置差异:

调试模式(Debug Mode):

开发环境:设置 DEBUG = True,以便在出现错误时显示详细的错误信息。 生产环境:设置 DEBUG = False,以避免显示敏感信息和详细的错误信息。 数据库设置:

开发环境:通常使用本地数据库(如 SQLite)进行开发和测试。 生产环境:使用更稳定和可靠的数据库服务器(如 PostgreSQL、MySQL、Oracle)。 静态文件设置:

开发环境:通常将静态文件存储在本地,并通过 Django 的 staticfiles 应用程序来提供静态文件服务。 生产环境:可以使用 CDN(内容分发网络)来提供静态文件,以减轻服务器负载并提高速度。 日志设置:

开发环境:可以设置较详细的日志记录级别,以便更容易调试和跟踪问题。 生产环境:通常使用更严格的日志记录级别,并将日志信息定向到适当的日志文件,以便进行监视和故障排除。 密钥和敏感信息:

开发环境:可以在本地设置中直接存储密钥和敏感信息。 生产环境:建议使用环境变量或其他安全的方式来存储密钥和敏感信息,以保护生产环境的安全性。 域名设置:

开发环境:可以使用本地的开发服务器(如 localhost)进行开发和测试。 生产环境:设置正确的域名,并配置 Web 服务器(如 Nginx 或 Apache)以将请求转发到 Django 应用程序。 缓存设置:

开发环境:可以禁用缓存或使用简单的缓存配置,以方便开发和调试。 生产环境:使用更高效的缓存后端(如 Memcached 或 Redis)来提高性能。 这些只是一些常见的设置差异,实际上还可以根据项目需求和部署环境的要求进行更多的设置和调整。

本项目中,在根目录下创建了三个文件,settings_common.py, settings_dev.py, settings_prod.py。其中 common 是两种环境之间共享的通用配置,然后在 dev 和 prod 中分别引用 common:

from .settings_common import *

然后在 dev 和 prod 各自写一些配置(比如数据库等)。之后修改启动文件manage.py:

import os
import sys

if os.environ.get('DJANGO_ENV') == 'production':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings_prod')
else:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project.settings_dev')

def main():
# 启动逻辑...

if __name__ == '__main__':
main()

如果要部署,记得把环境变量添加上,即设置 DJANGO_ENV 环境变量为 'production'. 而且如果是 uwsgi 作为 django 的服务器的话,还有修改wsgi.py

总结

前端 vue 的环境区别很简单,后端 django 的稍复杂些。主要分三步走:编写配置文件->修改启动文件->部署设置环境变量。

django+nginx+mysql 三容器部署

· 阅读需 4 分钟
Jason Lee

前言

之前的部署直接采用默认的 sqlite3 作为数据库。因为 sqlite3 是文件式数据库,所以打造容器时,本地调试过程中的数据记录也被包含进去。另外,生产环境中,不可能使用 sqlite。因此,在之前的 django+nginx 的基础上,引入了 mysql 容器。

镜像设置

首先在根目录创建一个MySQL数据库,下面有 data.sql 和 Dockerfile。这里我们先单独启动一下 mydql 容器。这时候 mysql 容器里已经有 django 的数据库了(database)。这是因为 data.sql 已经提前创建好了数据库并设置为当前数据库:

CREATE DATABASE IF NOT EXISTS my_database;

USE my_database;

而 Dockerfile 的内容为:

FROM mysql:8

ENV MYSQL_ROOT_PASSWORD pass
COPY ./data.sql /docker-entrypoint-initdb.d/data.sql

最后一行使得 data.sql 在容器初始化时就被执行。这时候虽然有了数据库但是没有表格,需要去 django 项目下,执行python manage.py migrate来在库里面创建表格。运行docker exec -it <name> /bin/bash,进入容器 terminal,进入 mysql 命令行,show tables,可以得到:

16875289399621687528939092.png

说明,数据表格创建成功了。

django settings.py 变动

由于之前是默认的 sqlite3,配置非常简单,只需要指定数据库文件位置。在 django 的 settings.py 中,关于 mysql 数据库的设定为:

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'my_database',
'HOST': 'database', #compose中mysql容器的服务名称
'PORT': '3306',
'USER': 'root',
'PASSWORD': 'pass',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
},
}
}

docker-compose

由于我们是全 docker 运行,因此也要实验一下将 django 项目打包成 docker 容器后,能否也能与 mysql 容器通信且成功创建表格。这次用 compose 实现。

version: "3.9"
services:
nginx:
build: ./wey-frontend/
restart: always
ports:
- "8000:80"
volumes:
- web_static:/var/www/mysite/assets/static
- web_media:/var/www/mysite/assets/media
depends_on:
- django

django:
build: ./wey_backend/
restart: always
expose:
- 8000
command: >
sh -c "python manage.py collectstatic --noinput
&& python manage.py migrate
&& uwsgi --ini uwsgi.ini"
volumes:
- web_static:/var/www/mysite/assets/static
- web_media:/var/www/mysite/assets/media
depends_on:
- database

database:
build: ./MySQL/
ports:
- '3306:3306'
# environment:
# MYSQL_DATABASE: 'appdb'
# MYSQL_PASSWORD: 'root'
# MYSQL_ROOT_PASSWORD: 'root'
# volumes:
# - 'db:/var/lib/mysql'
# # command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci']

volumes:
web_static:
web_media:

总结

用 mysql 作为数据库,更符合实际生产场景。由于一开始 mysql 容器内没有数据库,因此需要在 Dockerfile 中创建自定义数据库并且使用。django 的 settings.py 中也要作相应调整。docker-compose 中,需要在创建 django 容器后,执行python manage.py migrate,来迁移数据库(即在自定义数据库中创建表来供上线后数据放置)。

wey: 基于vue+django的社交网站(0)

· 阅读需 3 分钟
Jason Lee

前言

这个项目我是跟随 youtube 上的博主视频教程做的。大概是从五月初开始的,持续了三个星期左右。这个项目对我来说有着非凡的意义,因为能将它完成说明自己具备了设计一个网站和部署上线的能力,而且用上了我目前最喜欢的技术栈:vuejs+django。写这篇博客时,它还处于一个比较难用的阶段,很多功能还没有添加,我打算在记录的过程中逐步添加这些功能。

项目 github 链接

工作流

写前端代码的编辑器为 vscode,插件组合为 Tailwind CSS IntelliSense+Volar。后端代码用 pycharm,python 的环境管理参照我之前写的博文。很不幸的是,自己创建项目时没有引入 git 进行代码管理。然后我现在已经加上了 git。

资源网站:

完成记录

  • day1: 将数据库更换为 mysql,并用 docker 部署运行。暂时删除了注册账户时发送邮件的功能。批量生成 post 和 comment 的伪数据。
  • day2: 实现了帖子和评论的分页。管理后台界面用了 django-admin-grappeli 插件进行美化(其实差不多)。修复了 edit profile 无法显示头像上传框的问题。登录逻辑由原来的双 token 改为单 token。
  • day3: 整理了生产环境和开发环境配置文件,并在生产环境(云主机+docker)调试上线。

我的python开发环境管理实践

· 阅读需 3 分钟
Jason Lee

前言

前段时间做项目,摸索出了一套比较稳定且适合自己的 python 环境管理方案,适用于 win10/11。

2024 年更新:这个节点上,主流有四种环境管理工具:venv(也就是本文推荐的),virtualenv(貌似在 windows 上不友好,比较繁琐),conda(辣鸡,不考虑),poetry(号称现代化的 python 环境管理工具,以后可以尝试)。当时写的 venv 的用法还是有些不对的地方,故修改一下。

venv 的使用

我在之前很长一段时间里用的是 anaconda 作为包管理器。但是一些缺点让我放弃了它:首先 anaconda 太庞大了,其次它的 conda 库经常找不到一些库或者网络速度极慢。后来我转到了 venv,venv 和 conda 一样,虚拟环境被包含在一个文件夹中,里面有一个脚本文件用于激活。

每个项目可以用 venv 创建自己独有的环境。为了统一和简介,依赖的文件夹名都叫做'venv'。打开 cmd,并切换到文件夹下,新建环境的命令如下:

python -m venv venv

这样就创建了一个文件夹,内部目录为:

16873086912601687308690642.png

其中,Lib 装了环境,Scripts 有激活和退出环境的脚本。以 powershell 为例,输入脚本路径,就激活了环境:

.\venv\Scripts\activate.ps1

退出:deactivate. 通过pip list,新环境预装的包为:

16873091022651687309101499.png

可以说什么都没有,具有很大的灵活性了,完全由用户掌控。

vscode 和 pycharm 选择创建好的 venv

如果是一些小脚本,单独运行的那种,我会选择 vscode;如果用到了 django 等创建一个项目,则用 pycharm。

首先说说 vscode。右下角选择 venv 解释器路径:

16873102812591687310281154.png

然后是 pycharm,同样是右下角,add new interpreter->existing->选择解释器位置:

16873105482641687310547564.png

这时候要新建一个 terminal,就会自动将环境挂载上。

逐步让Google收录本博客

· 阅读需 3 分钟
Jason Lee

前言

这个博客建立快满一年了,到目前为止积累了 77 篇文章,都是原创。建立之初以为半年之内 Google 会自动收录,没想到现在依旧未收录。之前有手动向 Google 提交过网址,但是好像是所有权验证问题失败了。这一次,我打算纪录下从开始提交到最终收录的全过程,顺便可以学到一点 seo 的知识。本文将持续更新。

第一天

登录 google search console,并验证了网站所有权(DNS 方式)后,有两个资源:一个是根域名 jasonleehere.com,一个是二级域名 blog.jasonleehere.com。然后向 Google 提交 sitemap(之前已经安装好 hexo-sitemap 插件并生成好 xml 文件)。想要查询 Google 收录的网址和数量,可以在“网页索引编制”中查看:

16871396076941687139607410.png

现在就等明天回来,看看网页是否被收录。

第二天

已收录,可通过在 google 搜索栏键入网址来找到:

16872193526401687219352555.png

16872195266361687219526444.png

前五个结果都是我的网站里的内容。但是,如果直接搜索 jasonleehere 或者 blog jasonleehere 是不会显示的,因为有其他来源的网页占据,一直到了第五页才有我。

这样就是成功了,后续就是看看 google search console 里的数据分析,看看有没有人通过 google 点进我的网站。

第三天

效果栏里开始有数据了。

16873078462601687307845321.png

第四天

非常幸运地,第四天就有了网页索引报告。可以看出,第一批收录了 10 个网页,其余大部分网页处在看见了但暂时未收录的状态。

16873931464181687393145504.png

针对csv文件类型的深度学习全流程记录

· 阅读需 7 分钟
Jason Lee

前言

就像我之前的post里讲的那样,输入形式或者说处理对象的多样化是ai从业者面对的挑战之一。这篇文章,我将尽力解决存储在csv中的数据,涉及到读取、预处理、写入等环节。

读取

pd.read_csv('path')

预处理

一个csv文件,里面的值会是什么样呢?一种是数值形式,一种是文本形式。对于数值形式,有哪些处理方法呢?

缺失值处理:检查数据中是否存在缺失值(NaN或空值),并根据实际情况进行处理。常见的方法包括删除带有缺失值的样本、用平均值或中位数填充缺失值,或使用插值方法进行填充。

特征缩放:对于数值型特征,进行特征缩放可以帮助模型更好地进行训练。常见的特征缩放方法包括标准化(将数据转换为均值为0,方差为1的分布)、归一化(将数据缩放到0和1之间的范围)等。

异常值处理:检测和处理异常值可以提高模型的鲁棒性。可以使用统计方法(如均值加减三倍标准差)或基于分布的方法来检测异常值,并根据实际情况进行修正或删除。

离散化/分箱处理:将连续型的数值特征转化为离散型特征,可以在一定程度上提高模型的效果。可以使用等频分箱、等宽分箱等方法将数值范围划分为多个离散的区间,并将数值映射到对应的区间。

特征选择:根据数据的实际情况,选择对目标变量有更强相关性的数值特征进行训练。可以使用相关性分析、特征重要性评估等方法进行特征选择。

数据标准化:对于某些具有明确量纲的特征,例如日期、时间等,可以进行数据标准化,将其转换为合适的数值表示,便于模型理解和处理。

对于非数值型数据,可以采取以下预处理措施:

标签编码:对于具有顺序关系的分类变量,可以使用标签编码将其转换为数值表示。例如,将"低"、"中"、"高"转换为0、1、2等。

独热编码:对于没有顺序关系的分类变量,可以使用独热编码将其转换为多个二进制特征表示。独热编码将每个类别转换为一个独立的特征,并且只有一个特征的值为1,其余特征的值都为0。这可以避免模型将无序的类别变量视为有序的。

词袋模型/文本向量化:对于文本数据,可以使用词袋模型或文本向量化方法将文本转换为数值表示。词袋模型将文本视为单词的集合,并计算每个单词在文本中的出现频率或使用TF-IDF等方法进行加权。文本向量化方法(如Word2Vec、GloVe等)可以将单词嵌入到低维向量空间中,保留了单词之间的语义关系。

序列编码:对于具有顺序关系的序列数据,例如时间序列或序列文本,可以使用序列编码方法将其转换为数值表示。常见的序列编码方法包括循环神经网络(RNN)和长短期记忆网络(LSTM)等。

类别合并/分组:对于具有大量类别但类别之间相似度较高的非数值型数据,可以考虑将类别进行合并或分组,减少特征维度的同时保留信息。

异常值处理:对于非数值型数据,也可能存在异常值,例如错误的标签或不一致的类别。可以检测和处理这些异常值,例如进行纠正或删除。

标准化

numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))

数据集的制作

预处理好数据后,将数据转化为tensor格式。要将目前的pd表格数据转为tensor,最简单的方法是torch.tensor():

train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)

这里不是采用继承torch.utils.Dataset类的写法,而是直接将train_features用一个函数制作数据集:

dataset = torch.utils.data.TensorDataset(*data)

最后包裹一层DataLoader。

搭建网络

对于网络,可以不用类的写法,而是实现nn.Sequential函数:

net = nn.Sequential(
nn.Linear(in_features, 512),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(512, 1),
)

这样,节省了不少代码。

损失和优化器

这里的损失采用了MSE函数,由于期望值较大(六位数),因此损失值较大,让我以为训练出错了。另外,如果用SGD,损失将为nan。改用Adam优化器才能正常训练。推测是因为SGD对学习率敏感,学习率不设置的很精确的话,训练直接失败。

完整流程

形成一种规范:代码开头,导入库后,跟着指定默认设备,和超参数。接着开始制作数据集,包括数据的预处理。然后定义网络结构、损失函数、优化器。然后是正式的训练代码。最后测试时,记得将model调至测试模式,使得dropout能正常工作。

代码地址

总结

之前写pytorch的时候,写法很死板。经过这一次练习,接触到了新的写法,比如dataset部分和网络部分。