[{"content":" 权限模型\nRBAC（Role-Based Access Control） ○ 按“角色→权限”授权，用户绑定角色，角色拥有权限。 ○ 例：财务角色可查看对账；运营角色可编辑商品。 ABAC（Attribute-Based Access Control） ○ 按“属性表达式”授权：主体（用户）属性、资源属性、动作、环境（时间/地点/设备）等组成策略。 ○ 例：用户部门=资源部门 且 当前时间在工作时段 才能读。 ACL（Access Control List） ○ 每个资源上维护“可访问主体列表及其权限”。 ○ 例：文档 A 允许 user_1 读写，允许 group_sales 只读。 核心差异 维度 RBAC ABAC ACL 授权粒度 角色级（中等） 条件级（最细，灵活） 资源级（细，但分散） 维护方式 维护角色和权限映射 维护策略表达式与属性字典 每个对象维护名单 动态场景 一般，需要建很多角色适配 强，天然支持时间/地点/级别等 一般，需逐对象改列表 可审计性 强，审计角色→权限清晰 中，需要记录策略判定依据 弱/中，分散在对象上 扩展到行/列级 需配合数据权限或 ABAC 原生支持（属性即行/列条件） 可做，但运维成本高 典型应用 企业后台、SaaS 多角色 安全/合规/多维条件场景 文件共享、按对象授权 优缺点对比 权限模型 适用场景 优点 缺点 RBAC（基于角色的访问控制） 角色稳定、权限边界清晰的后台系统；SaaS 的租户内角色管理 易理解、易审计、运维成本低；菜单与接口权限映射自然 面对细粒度、多条件权限需求时，容易出现角色爆炸（成百上千个角色） ABAC（基于属性的访问控制） 基于用户、资源、环境等多维条件进行权限判定；适用于租户、部门、属地等数据隔离，以及合规、零信任、行级/列级权限控制 灵活性高，单条策略可覆盖大量场景；天然支持动态上下文 策略设计与属性治理复杂；可解释性和排障难度较高；评估链路较长，通常需要缓存优化 ACL（访问控制列表） 针对特定对象的临时分享与协作，如文档、文件、相册、项目权限 符合“分享给谁”的直觉；支持极细粒度的对象级权限控制 对象数量较多时管理复杂；权限审计与回收成本高；难以表达批量授权策略 如何选择 只需角色分权（管理员/运营/财务等），且权限不复杂 → 选 RBAC。\n需要数据级别的隔离或动态约束（部门/地域/级别/时间/设备） → 选 ABAC，或 RBAC + ABAC 组合。\n需要按单个对象分享给具体人（文档/仪表盘/工单） → 选 ACL，或在 RBAC/ABAC 之上对特定资源叠加 ACL。\n多租户 SaaS 常见组合：\n租户边界/行级隔离：ABAC（tenant_id 必须相等等条件）\n租户内功能授权：RBAC（角色→权限）\n文档/报告临时共享：ACL\n简单示例 RBAC（权限点）\n权限命名：order:read、order:approve\n角色映射：manager → [order:read, order:approve]\n判断：user.roles 含 manager 且 action=order:approve → allow\nABAC（属性表达式）\nallow if user.dept == order.dept and user.level \u0026gt;= 3 and env.time in [09:00–18:00] and action == \u0026ldquo;read\u0026rdquo;\nACL（对象名单）\norder/123: { allow: [user:alice:read, group:ops:read, user:bob:write] }\n常见坑与规避 RBAC 角色爆炸：用角色层级/权限聚合，别把数据维度塞进角色；把数据条件交给 ABAC。 ABAC 属性失真：属性字典不统一、来源不可信；先做属性治理（来源、口径、更新、授权）。 ACL 权限漂移：对象多且多人维护，定期做“可见性审计/分享过期回收”。 ","date":"2026-08-08T00:00:00Z","permalink":"/p/rbac/","title":"权限模型设计"},{"content":"常用命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 docker update --restart=always 容器id\t# 设置自启动（随机器启动) docker ps docker ps -a\t# 查看全部容器 docker exec -it \u0026lt;container id\u0026gt; bash\t# 进入容器 docker logs --tail 50 --follow --timestamps\t\u0026lt;container id\u0026gt;\t# 查看容器日志 docker build -t game_community_be .\t# 构建镜像 docker-compose up -d --build --force-recreate\t# 构建和启动容器，--build重新构建镜像 docker cp 容器id:/app/test.txt .\t# 将容器中的test.ext文件拷贝到宿主机某个路径下 # 删除none镜像 docker images |grep none | awk \u0026#39;{print $3}\u0026#39;| xargs docker rmi # 临时测试 docker run -it --rm CONTAINER ID bash #重置上一次提交，并且不保留记录 git reset --hard HEAD~1 git push -f dockerfile 指令 描述 FROM 设定基础镜像 RUN 在镜像中运行命令 CMD 提供默认的可执行命令 LABEL 添加元数据到镜像 EXPOSE 指定要在容器运行时开放的端口 ENV 设置环境变量 ADD 从上下文目录中复制文件到镜像 COPY 与 ADD 类似，但更好，因为它更明确 ENTRYPOINT 为容器提供可执行文件 VOLUME 在容器中创建一个挂载点 USER 设定运行容器时的用户名或 UID WORKDIR 设定工作目录 ARG 定义在构建过程中要使用的变量 ONBUILD 定义在构建子镜像时要运行的指令 STOPSIGNAL 定义停止容器时要发送的系统调用信号 HEALTHCHECK 配置容器的健康检查 SHELL 设置默认的 shell docker-compose 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 version: \u0026#39;3\u0026#39; services: app: image: game_community_be restart: on-failure command: bash -c \u0026#34;gunicorn manage:app\u0026#34; env_file: .env.test ports:\t# 宿主机器端口:容器端口，浏览器访问9091端口即可映射到容器内5577端口 - \u0026#34;9091:5577\u0026#34;\tvolumes: # 宿主机目录:容器内目录 - /data/logs/game_community_be:/data/logs/game_community_be celery_worker: image: game_community_be restart: on-failure:3 env_file: .env.test command: celery -A make_celery.celery_app worker -l INFO depends_on: - app celery_beat: image: game_community_be restart: on-failure:3 env_file: .env.test command: celery -A make_celery.celery_app beat -l INFO depends_on: - app 本地构建推送到harbor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 在有外网的机器上拉取镜像 docker pull ghcr.io/astral-sh/uv:latest docker pull python:3.12-slim-bookworm # 2. 重新打标签 docker tag ghcr.io/astral-sh/uv:latest harbor.yourdomain.com/library/uv:latest docker tag python:3.12-slim-bookworm harbor.yourdomain.com/library/python:3.12-slim-bookworm # 3. 推送到内网 Harbor docker login harbor.yourdomain.com docker push harbor.yourdomain.com/library/uv:latest docker push harbor.yourdomain.com/library/python:3.12-slim-bookworm # 4. 修改 Dockerfile FROM harbor.yourdomain.com/library/python:3.12-slim-bookworm COPY --from=harbor.yourdomain.com/library/uv:latest /uv /uvx /bin/ 网络模式 flowchart TD subgraph HOST [Docker 宿主机] direction TB subgraph BridgeMode [Bridge 模式] B1[容器 A\neth0: 172.17.0.2] B2[容器 B\neth0: 172.17.0.3] B1 \u003c-.-\u003e B0[docker0 网桥\n172.17.0.1] B2 \u003c-.-\u003e B0 B0 \u003c-.-\u003e HostNet[宿主机网络栈] end subgraph HostMode [Host 模式] H1[容器 C\n共享宿主机网络栈] H1 \u003c-.-\u003e HostNet end subgraph NoneMode [None 模式] N1[容器 D\n仅 lo 回环接口] end subgraph ContainerMode [Container 模式] C1[容器 E\n共享网络栈] C2[容器 F\neth0: 172.17.0.4] C1 \u003c-.-\u003e C2 end end External((外部网络)) --\u003e HostNet 网络模式 核心特点 适用场景 **bridge** (默认) 通过 docker0 虚拟网桥为每个容器分配私有 IP，容器间可通过 IP 通信。外部访问需用 -p 进行端口映射。 单机运行多个需要相互隔离、但又要通信的普通容器。 **host** 容器直接共享宿主机的网络栈，没有独立的 IP。性能最高，无 NAT 开销。 对网络性能要求极高，且能容忍端口冲突和较低隔离性的场景。 **none** 容器完全隔离网络，仅有 lo 本地回环接口。 执行无需网络的高安全性或计算任务，如批处理、密码学计算。 **container** 新容器与一个已存在的容器共享网络命名空间（IP 和端口等）。 实现“边车（Sidecar）”模式，如主应用容器与日志收集、网络监控容器紧密耦合 更改镜像源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 配置 Docker registry mirror sudo mkdir -p /etc/docker sudo vim /etc/docker/daemon.json # 写入： { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://docker.m.daocloud.io\u0026#34;, \u0026#34;https://docker.imgdb.de\u0026#34;, \u0026#34;https://docker-0.unsee.tech\u0026#34;, \u0026#34;https://docker.hlmirror.com\u0026#34;, \u0026#34;https://docker.1ms.run\u0026#34;, \u0026#34;https://func.ink\u0026#34;, \u0026#34;https://lispy.org\u0026#34;, \u0026#34;https://docker.xiaogenban1993.com\u0026#34; ] } # 重启docker sudo systemctl daemon-reload sudo systemctl restart docker ","date":"2025-10-08T16:57:05+08:00","permalink":"/p/docker/","title":"Docker"},{"content":"验证项：\n针对旧版验证api：https://developer.apple.com/documentation/appstorereceipts/verifyreceipt 为了验证收据伪造等情况，需验证以下几点：\nreceipt.environment，区分订单环境（生产或沙盒），若返回21007表示沙盒环境，此时需用沙盒api再次验证https://sandbox.itunes.apple.com/verifyReceipt，方便客户端在生产环境进行测试而不用来回切换环境 receipt.status == 0，为0表示有效 order.product_id in product，系统中商品是否存在 receipt.product_id == order.product_id，收据返回的商品id是否和订单的商品id一致 receipt.bundle_id == product.bundle_id，不同bundle_id之间的product_id可能重复，若有多个包，系统可单独管理内购商品，此时订单中的product_id应该为product表的主键id receipt.transaction_id，检查商户id是否已存在于订单记录中，若存在表示已验签，可将 (pay_type, transaction_id) 作为联合唯一索引 receipt.cancellation_date，如果receipt中包含cancellation_date属性，说明交易被取消或退款 order.ctime \u0026lt; receipt.purchase_date_ms，正常情况应该是下单时间小于收据中的购买时间，前提是系统时间order.ctime准确无误 流程图：\nsequenceDiagram actor Client participant Server participant Apple Server participant MQ Client -\u003e\u003e Server: query product info Server -\u003e\u003e Client: return product Client -\u003e\u003e+ Server: request orderId Server -\u003e\u003e Server: create order Server -\u003e\u003e MQ: send delay message (check order state after 15 min) Server -\u003e\u003e- Client: return orderId Client -\u003e\u003e+ Apple Server: pay Apple Server -\u003e\u003e- Client: return receipt Client -\u003e\u003e+ Server: verify Server -\u003e\u003e Server: query order alt not exists ? Server -\u003e\u003e Client: return error else exists Server -\u003e\u003e- Apple Server: verify receipt Apple Server -\u003e\u003e Server: return result alt passed Server -\u003e\u003e Client: return success else else Server -\u003e\u003e Client: return failed end end MQ -\u003e\u003e+ Server: deliver message Server -\u003e\u003e Server: check order state alt unpaid Server -\u003e\u003e- Server: marked as canceled end ","date":"2024-11-28T00:00:00Z","permalink":"/p/apple-iap-receipt/","title":"Apple内购验签流程"},{"content":"问题1：job无法被worker正确接收 线上同一台机器部署了两套服务，分别启动了Celery应用，选取Rabbitmq作为Broker，其中两套服务共用同一个Broker。结果导致：服务B的任务被服务A的Celery received，于是可能会报错：\nReceived unregistered task of type \u0026lsquo;apps.tasks.scheduled_job.calc_post_score\u0026rsquo;\n消息队列 首先了解一下AMQP的几个核心定义：\nAMQP，即Advanced Message Queuing Protocol（高级消息队列协议）\n参考：celery文档\nmessage 消息，由headers和body组成，例如celery中消息格式：\n1 2 3 4 5 6 { \u0026#39;task\u0026#39;: \u0026#39;myapp.tasks.add\u0026#39;, \u0026#39;id\u0026#39;: \u0026#39;54086c5e-6193-4575-8308-dbab76798756\u0026#39;, \u0026#39;args\u0026#39;: [4, 4], \u0026#39;kwargs\u0026#39;: {} } producer 生产者，发送信息的客户端通常称为发布者或生产者\nconsumer 消费者，接收消息的实体\nbroker 消息中间件，负责将信息从生产者路由到消费者，可以是redis或rabbitmq（官方推荐）\nchannel 信道，建立在connection连接之上\nexchanges 交换机，负责接收生产者发送的消息，并将其路由到相应的队列。支持3种类型：Direct、Topic、Fanout，不是所有传输都支持这3种\nqueues 队列，存储消息的地方，消费者会从队列中获取消息\nrouting keys 路由键，exchange会根据路由键将消息路由到对应的队列\n任务发送接收流程\nAMQP通信流程\n--- config: theme: forest themeVariables: primaryColor: \"#ffa600\" --- graph LR subgraph Producer A[Producer1] B[Producer2] C[Producer3] end subgraph Channel CH1[Channel1] CH2[Channel2] CH3[Channel3] end subgraph Exchange E1[Exchange1] E2[Exchange2] end subgraph Queue Q1[Queue1] Q2[Queue2] Q3[Queue3] end subgraph Consumer X[Consumer1] Y[Consumer2] Z[Consumer3] end A --\u003e|send msg| CH1 B --\u003e|send msg| CH2 C --\u003e|send msg| CH3 CH1 --\u003e E1 CH2 --\u003e E1 CH3 --\u003e E2 E1 --\u003e|routingkey1| Q1 E1 --\u003e|routingkey2| Q2 E2 --\u003e|routingkey3| Q3 Q1 --\u003e|pick up| X Q2 --\u003e|pick up| Y Q3 --\u003e|pick up| Z解决方案 问题1原因：\nexchange无法将job路由到对应的队列，或者说因为没有指定队列，导致队列中的job共享而无法找到。\n解决方法就是初始化时指定队列名。\n以下展示了Flask中如何配置队列和交换机\n通过工厂函数初始化celery app，配置了queue之后，任务函数也必须指定一个队列名，或者设置一个task_default_queue默认队列，就不用在每个任务函数装饰器中使用 queue 参数指定队列了\n1 2 3 4 5 6 7 8 9 10 11 12 # 配置queues celery.conf.task_queues = ( Queue( \u0026#34;game_community_default\u0026#34;, Exchange(\u0026#34;game_community_default\u0026#34;), routing_key=\u0026#34;game_community_default\u0026#34;, ), ) celery.conf.task_default_queue = \u0026#34;game_community_default\u0026#34; celery.set_default() celery.autodiscover_tasks([\u0026#34;apps.tasks\u0026#34;]) 使用celery的注意点 task_acks_late\n设置为True表示任务在执行完成之后才确认，如果Worker进程突然退出或被kill掉也会立即确认；如果为False表示执行之前就确认（默认值）\ntask_reject_on_worker_lost\n设为False, worker进程崩掉之后将重新加入worker，默认值False，可能会造成循环执行\ntask_always_eager 设为True可用于本地同步调试，而不经过broker\n周期任务无法被正确识别？\n使用include指定任务模块（和import语义相同）\n使用beat_schedule参数指定周期任务字典配置，比如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026#34;\u0026#34;\u0026#34;手动导入任务，否则可能无法识别\u0026#34;\u0026#34;\u0026#34; INCLUDES = \u0026#34;apps.tasks.scheduled_job\u0026#34; BEAT_SCHEDULES = { # 更新文章热度值 \u0026#34;calc_post_score\u0026#34;: { \u0026#34;task\u0026#34;: \u0026#34;apps.tasks.scheduled_job.calc_post_score\u0026#34;, \u0026#34;schedule\u0026#34;: crontab(\u0026#34;0\u0026#34;, \u0026#34;1,7,13,19\u0026#34;), \u0026#34;args\u0026#34;: (), }, # 更新评论热度值 \u0026#34;calc_comment_score\u0026#34;: { \u0026#34;task\u0026#34;: \u0026#34;apps.tasks.scheduled_job.calc_comment_score\u0026#34;, \u0026#34;schedule\u0026#34;: crontab(\u0026#34;30\u0026#34;, \u0026#34;1,7,13,19\u0026#34;), \u0026#34;args\u0026#34;: (), }, } celery如何访问Flask App上下文\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def init_app(app: Flask, config) -\u0026gt; Celery: class FlaskTask(Task): def __call__(self, *args: object, **kwargs: object) -\u0026gt; object: with app.app_context(): return self.run(*args, **kwargs) celery = Celery(app.name, task_cls=FlaskTask) celery.task_cls = FlaskTask celery.config_from_object(config) # 配置queues celery.conf.task_queues = ( Queue( \u0026#34;game_community_default\u0026#34;, Exchange(\u0026#34;game_community_default\u0026#34;), routing_key=\u0026#34;game_community_default\u0026#34;, ), ) celery.conf.task_default_queue = \u0026#34;game_community_default\u0026#34; celery.set_default() celery.autodiscover_tasks([\u0026#34;apps.tasks\u0026#34;]) app.extensions[\u0026#34;celery\u0026#34;] = celery return celery 另外，使用 @celery_app.task 来装饰任务函数需要访问 celery_app 对象，而使用工厂模式则无法访问 celery_app对象，用@shared_task来替代。\n调用 celery_app.set_default() 使得@shared_task 装饰器可以访问任何current app，这与 Flask 的蓝图和应用程序上下文概念类似。\nTask states\n状态 说明 PENDING 任务已被添加到任务队列中，等待执行 STARTED 任务已开始执行 RETRY 任务已失败并且正在尝试重新执行 FAILURE 任务执行失败 SUCCESS 任务执行成功 REVOKED 任务已被撤销 自定义状态\n1 2 3 4 5 6 @app.task(bind=True) def upload_files(self, filenames): for i, file in enumerate(filenames): if not self.request.called_directly: self.update_state(state=\u0026#39;PROGRESS\u0026#39;, meta={\u0026#39;current\u0026#39;: i, \u0026#39;total\u0026#39;: len(filenames)}) 问题2：Apscheduler周期任务重复执行 多进程环境下，比如Gunicorn多个worker部署，Apscheduler周期任务会重复执行多次\n这是Apscheduler3的一个issue，不知道后续版本会不会优化\n解决方案 尝试过设置gunicorn worker数=1，无法解决 redis分布式锁，执行之前尝试获取锁，成功才执行，注意最后无论执行是否成功都要释放锁 如何设计一个Scheduled Job cron表达式\n属性 英文名 值 分 minute 0-59 时 hour 0-23 日 day_of_month 1-31 月 month_of_year 1-12 周 day_of_week 0-6 开始时间（可选） start_time 截止时间（可选） end_time 周期任务表 periodic_task\ncolumn type description name str 任务名 description str 备注 args json / jsonb 任务函数参数 last_run_time datetime 最后一次运行时间 trigger str contab触发参数 status int 执行状态 enabled int 任务启用 / 关闭 job_id str 任务唯一id result str / json 执行结果 log_path str 日志路径 ","date":"2024-02-01T00:00:00Z","permalink":"/p/celery/","title":"Celery"},{"content":"\n数据库设置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 连接 pgsql 并创建数据库 psql -h localhost -U username -W -c \u0026#34;CREATE DATABASE game_community_admin\u0026#34; # 连接某一个数据库 psql -h 172.16.6.41 -p 5432 -U postgres -d game_community_admin -W # 查询当前数据库连接数，datname：数据库名称 SELECT pid, usename, application_name, state, count(*) FROM pg_stat_activity WHERE datname = \u0026#39;game_community_admin\u0026#39;; # 释放当前数据库全部连接，才能删库 SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = \u0026#39;game_community_admin\u0026#39; AND pid\u0026lt;\u0026gt;pg_backend_pid(); \\l\t# 查看所有库 \\x\t# 扩展显示 \\c \u0026lt;database\u0026gt;\t# 切换数据库 \\q\t# 退出 sql 命令行 \\dt\t# 查看此数据库下所有表 \\d \u0026lt;tablename\u0026gt;\t# 查看表结构 数据库设计规范 主键设计 自增主键 uuid ulid 雪花算法 根据业务规则自定义生成，比如：userID + timestamp + randomNumber 雪花算法由 64bit 数字组成\n1-bit 不用于生成 ID（符号位） 41-bit 当前时间戳（毫秒）- 指定时间的差值，可以表示 1 x 2^41 / (1000 x 3600 x 24 x 365) = 69 年的时间 10-bit 可以分别表示 1 x 2^10 = 1024 台机器节点，范围 [0,1023]；可以拆分为 5 位数据中心 id + 5 位工作节点 id 12-bit 表示 1ms 内自动递增的序列号，1 x 2^12 = 4096 个，范围 [0,4095]。单机 1ms 可以生成 4096 个不重复的 ID ulid 和 uuid 的对比\nuuid ulid 128bit 128bit 36 个字符 26 个字符（前 10 个字符为时间戳，后 16 个为随机数） 随机数（v4） 按词典排序，但不保证同一毫秒内有序 无特殊字符（url 安全） pgsql 主键：\n1 2 3 4 5 6 7 CREATE TABLE ROLE ( -- GENERATED BY DEFAULT AS IDENTITY(START 20000000) 指定起始值 id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, ); \u0026#34;id\u0026#34; serial PRIMARY KEY 索引 索引命名规范\n命名 含义 uk_\u0026lt;table\u0026gt;_\u0026lt;column\u0026gt; 唯一索引 uc_\u0026lt;table\u0026gt;_\u0026lt;column1\u0026gt;_\u0026lt;column2\u0026gt; 联合唯一索引 ix_\u0026lt;table\u0026gt;_\u0026lt;column\u0026gt; 普通单列索引 ix_\u0026lt;table\u0026gt;_\u0026lt;column1\u0026gt;_\u0026lt;column2\u0026gt; 联合索引 回表\nusers 包含以下列：id（主键）、name、age。\n创建了一个以 name 列为索引的非聚集索引，现在执行查询 SELECT name, age FROM users WHERE name = 'John'。\n由于 age 不在索引中，查询时需要先通过 name 查询出主键 id，然后在主键 id 的索引树中找到 age 的数据。\n如何解决回表？\n聚集索引 1 SELECT name, age FROM users WHERE id = 1; 覆盖索引 1 2 3 4 5 # 创建覆盖索引 CREATE INDEX idx_name_age ON students (name, age); # 查询语句，利用覆盖索引 SELECT name, age FROM users WHERE name = \u0026#39;John\u0026#39;; 逻辑删除 如何解决唯一性约束和 is_delete 冲突的问题？\n在存在唯一索引的表中添加一个 delete_id 字段，默认为 -1，删除此条记录时，将 is_delete 设为 true，同时 delete_id 设为当前行的主键 id。\n假如要求 username 字段唯一，则设置 username 和 delete_id 为联合唯一索引。\n1 2 3 4 5 6 7 8 9 class User(BaseModel): __tablename__ = \u0026#39;user\u0026#39; username = db.Column(db.string(64), nullable=False, index=True) org_id = db.Column(db.SMALLINT, nullable=False, comment=\u0026#39;组织id\u0026#39;) delete_id = db.Column(db.INTEGER, nullable=False, default=0) __table_args__ = ( db.UniqueConstraint(\u0026#39;username\u0026#39;, \u0026#39;delete_id\u0026#39;, name=\u0026#39;_username_delete_id_uc\u0026#39;), ) json 和 jsonb 区别：\njson 写入快，读取慢 jsonb 写入慢，读取快（jsonb 以二进制形式存储已解析好的数据） JSONB 类型字段进行索引时，建议使用 GIN 索引。因为 GIN 索引适用于全文搜索和值匹配，可以更快地查询到符合条件的数据。而 BTree 索引只适用于比较操作和值匹配，对 JSONB 类型字段的查询效率可能会较低。\n多对多 sqlalchemy 语法\n1 2 3 4 5 6 7 8 9 10 11 12 user_roles = db.Table(\u0026#39;sys_user_role\u0026#39;, db.Column(\u0026#39;id\u0026#39;, db.Integer, primary_key=True), db.Column(\u0026#39;user_id\u0026#39;, db.Integer, nullable=False), db.Column(\u0026#39;role_id\u0026#39;, db.Integer, nullable=False) ) def set_user_role(user, args): stmt = user_roles.insert().values(**args).returning(user_roles) ret = db.session.execute(stmt) ret = ret.mappings().one_or_none() db.session.commit() 如何保证幂等性 乐观锁 —— 表中增加一个 version 字段，每次处理完业务后更新 version 唯一索引 —— 防止 post 插入脏数据 数据库操作 查询 条件写在 on 和 where 的区别\n对于 inner join：结果无区别 对于 left outer join：返回左表全部数据，右表若不满足 on 中的条件，返回 null right outer join 同理 查询 json 字段类型中的某个属性\n1 2 3 4 5 6 7 8 - 查询表 arguments 字段中的 name 属性 SELECT arguments-\u0026gt;\u0026gt;\u0026#39;name\u0026#39; FROM table1; - 查询 arguments 字段中的 name 属性，其中 arguments 值为数组嵌套字典格式，如 [{}, {}] select * from table1 where exists (select 1 from jsonb_array_elements(arguments) as nested_data where nested_data-\u0026gt;\u0026gt;\u0026#39;name\u0026#39; like \u0026#39;%原游戏%\u0026#39;); 深分页\n查询的页数过大时会出现深分页问题，比如查询 limit 10000 offset 20，则需要查出前 100020 条然后切片截取后 20 条。\n解决办法：\n1 2 3 4 5 SELECT title, content FROM post a JOIN (SELECT id FROM post ORDER BY title LIMIT 10000, 20) b ON a.id = b.id; 判断区间是否重叠\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SELECT * FROM fx_sharing_cp WHERE daterange(start_dt, end_dt, \u0026#39;[]\u0026#39;) \u0026amp;\u0026amp; daterange(\u0026#39;2023-12-21\u0026#39;, \u0026#39;2023-12-22\u0026#39;, \u0026#39;[]\u0026#39;); -- 为了保证高并发场景下数据不一致的问题，读和写最好用一条 sql 语句实现 WITH overlap_check AS ( SELECT * FROM fx_sharing_cp WHERE daterange(start_dt, end_dt, \u0026#39;[]\u0026#39;) \u0026amp;\u0026amp; daterange(\u0026#39;2023-12-21\u0026#39;, \u0026#39;2023-12-22\u0026#39;, \u0026#39;[]\u0026#39;) ) INSERT INTO fx_sharing_cp (org_id, game_id, pattern, start_dt, end_dt, create_by) SELECT 1, 1, 1, \u0026#39;2023-12-21\u0026#39;, \u0026#39;2023-12-22\u0026#39;, 0 WHERE (SELECT COUNT(*) FROM overlap_check) = 0 分组聚合\nGROUPING SETS 按不同维度汇总\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 SELECT tj_date, game_id, chl_id, plat, SUM(role_numb) AS role_numb, SUM(reg_numb) AS reg_numb FROM business_day WHERE game_id = 2 GROUP BY GROUPING SETS ( (tj_date, game_id), (tj_date, game_id, chl_id), (tj_date, game_id, chl_id, plat) ) 更新 插入性能从低到高依次为：\nexecutemany \u0026lt; execute_batch \u0026lt; prepare+execute_batch \u0026lt; execute_values\n更新性能从低到高依次为：\nexecutemany \u0026lt; execute_values \u0026lt; execute_batch \u0026lt; prepare+execute_batch\n删除性能从低到高依次为：\nexecutemany \u0026lt; execute_batch \u0026lt; execute_values \u0026lt; prepare+execute_batch\n作者：xiangrumei https://www.bilibili.com/read/cv26399649/ 出处：bilibili\n批量更新 使用临时表的方式：\n1 2 3 4 5 6 7 8 UPDATE_POST_SCORE = \u0026#34;\u0026#34;\u0026#34; UPDATE post_community SET score = tmp.score FROM (VALUES (%s, %s)) AS tmp(id, score) WHERE post_community.post_id=tmp.id; \u0026#34;\u0026#34;\u0026#34; from psycopg2.extras import execute_batch execute_batch(cursor, UPDATE_POST_SCORE, result) 如果数据量大，不能一次将数据全部加载进内存，使用 itersize 或 fetchmany 分批次读取，前提是使用命名游标 named cursor。\nstackoverflow 命名游标的使用分析\n注意：使用命名游标情况下，如果使用 fetchmany，即使设置了 itersize，itersize 也不会生效。\n1 2 3 4 5 6 7 8 9 10 11 12 13 cursor = self.conn.connection.cursor(name=\u0026#34;named_cursor\u0026#34;) cursor.itersize = 5000 cursor.execute(SELECT_POST_COUNTS) data = cursor.fetchmany(20000) now = DT.now_time() while data: result = map(lambda x: ( x[0], (x[1] + 2 * x[2] + 3 * x[3]) / (6 * ((round((now - x[-1].replace(tzinfo=now.tzinfo)).total_seconds() / 3600 + 2)) ** 1.8)) ), data) execute_batch(self.cursor, UPDATE_POST_SCORE, result) data = cursor.fetchmany(20000) 删除 1 2 3 4 5 6 - 清空表数据并重置主键 id TRUNCATE TABLE post_community RESTART IDENTITY; - 删除列后重建表，把仍保留的列重新紧凑写入新文件，然后替换旧文件，清除物理数据 VACUUM FULL qbank_inline_label_repair_backups; 分区 分区模式：\nrange —— 基于连续范围分区 list —— 基于离散值分区 1 2 3 4 5 6 7 8 9 10 11 12 -- 让 rtime 分区 CREATE TABLE IF NOT EXISTS \u0026#34;event_log\u0026#34; ( event VARCHAR(50), rtime INT NOT NULL DEFAULT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::INT, distinct_id VARCHAR NOT NULL, user_id INT DEFAULT -1, args json ) PARTITION BY range (rtime); -- 不属于任何分区的数据将会插入到默认表中 CREATE TABLE event_log_default PARTITION OF event_log DEFAULT; 创建分区表不建议使用触发器（会降低性能），因此使用脚本定时任务创建，比如当月 25 号创建下个月的分区表，具体创建逻辑据业务而定。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from dateutil.relativedelta import relativedelta def part_table_by_month(sub_table): \u0026#34;\u0026#34;\u0026#34;按月分区表\u0026#34;\u0026#34;\u0026#34; now = DT.cur_dt() next = now + relativedelta(months=1) # 下一个月 next_next = next + relativedelta(months=1) # 下下个月 part_name = f\u0026#34;{sub_table}_{next.year}{next.month:02d}\u0026#34; start = DT.str2ts(f\u0026#34;{next.year}-{next.month}-01\u0026#34;, \u0026#34;%Y-%m-%d\u0026#34;) end = DT.str2ts(f\u0026#34;{next_next.year}-{next_next.month}-01\u0026#34;, \u0026#34;%Y-%m-%d\u0026#34;) - 1 try: part_ret = part_table(sub_table, start, end) logger.info(f\u0026#34;创建分区表成功: {part_ret}\u0026#34;) except Exception as e: logger.critical(f\u0026#34;创建分区表失败: {part_name}, 失败原因：{str(e)}\u0026#34;) def part_table(sub_table: str, start, end) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34; 按月创建分区表（当月触发，创建下个月的） :param end: 分区结束范围 :param start: 分区起始范围 :param sub_table: 主表名 :return: 分区表名 \u0026#34;\u0026#34;\u0026#34; sql_command = f\u0026#34;\u0026#34;\u0026#34;CREATE TABLE IF NOT EXISTS {sub_table} PARTITION OF {sub_table} FOR VALUES FROM (:start) TO (:end);\u0026#34;\u0026#34;\u0026#34; try: db.session.execute(text(sql_command), {\u0026#34;start\u0026#34;: start, \u0026#34;end\u0026#34;: end}) db.session.commit() return sub_table except Exception as e: db.session.rollback() raise e 窗口函数 函数 说明 row_number() 依次编号 rank() 跳过并列编号：1 1 1 4 dense_rank() 不跳过并列行：1 1 1 2 统计表的大小 PostgreSQL 提供了几个函数来查看表的大小，它们的区别在于统计的范围：\n函数 统计范围 说明 pg_relation_size('表名') 仅表的数据 不包括索引、TOAST 数据等。 pg_table_size('表名') 表的数据 + TOAST 包括表数据、TOAST 数据、空闲空间映射和可见性映射，但不包括索引。 pg_total_relation_size('表名') 表的数据 + 索引 + TOAST 这是最全面的统计，包含了表本身及其所有索引和 TOAST 数据的总大小。 pg_indexes_size('表名') 仅索引 返回与表关联的所有索引的总大小。 注意：上述函数中的 '表名' 参数格式为 '模式名.表名'，例如 'public.users'。为了便于阅读，这些函数常与 pg_size_pretty() 函数连用，将字节数转换为 kB、MB、GB 等人类易读的格式。\n常用查询示例\n查询特定表的总大小（包含数据和所有索引） 1 SELECT pg_size_pretty(pg_total_relation_size(\u0026#39;你的模式名.你的表名\u0026#39;)) AS 总大小; 查询特定表的数据大小（不包含索引） 1 SELECT pg_size_pretty(pg_relation_size(\u0026#39;你的模式名.你的表名\u0026#39;)) AS 数据大小; 查询数据库中所有表的大小，并按从大到小排序 1 2 3 4 5 6 7 8 9 10 SELECT schemaname AS 模式, tablename AS 表名, pg_size_pretty(pg_total_relation_size(schemaname||\u0026#39;.\u0026#39;||tablename)) AS 总大小 FROM pg_tables WHERE schemaname NOT IN (\u0026#39;information_schema\u0026#39;, \u0026#39;pg_catalog\u0026#39;) ORDER BY pg_total_relation_size(schemaname||\u0026#39;.\u0026#39;||tablename) DESC; 统计数据库的大小 统计数据库大小主要使用 pg_database_size() 函数。\n函数 说明 pg_database_size('数据库名') 返回指定数据库的总大小。 常用查询示例\n查询特定数据库的大小 1 SELECT pg_size_pretty(pg_database_size(\u0026#39;你的数据库名\u0026#39;)) AS 数据库大小; 查询 PostgreSQL 服务器上所有数据库的大小，并按从大到小排序 1 2 3 4 5 6 7 SELECT datname AS 数据库名, pg_size_pretty(pg_database_size(datname)) AS 大小 FROM pg_database ORDER BY pg_database_size(datname) DESC; 补充：使用 \\l+ 和 \\dt+ 命令 如果你通过 psql 命令行工具连接数据库，也可以使用以下两个快捷命令：\n\\l+：列出所有数据库，并附带它们的大小、表空间和描述等信息。 \\dt+ 表名：显示指定表的详细信息，其中就包括大小。 数据库备份 1 2 3 4 5 6 7 8 # 备份数据库，排除`community_log`和 `users`表，仅导出数据不包括结构 pg_dump -h \u0026lt;host\u0026gt; -p \u0026lt;port\u0026gt; -U \u0026lt;user\u0026gt; -d \u0026lt;database\u0026gt; -F p --data-only -T community_log -T users \u0026gt; /data/backup.sql # 导出某张表 pg_dump -h host -p port -U username -s -t tablename dbname \u0026gt; struct.sql # 目标数据库上执行 sql 脚本 psql -h \u0026lt;host\u0026gt; -p \u0026lt;port\u0026gt; -U \u0026lt;user\u0026gt; -d \u0026lt;database\u0026gt; -W -f /data/backup.sql Redis 配置 1 2 3 4 5 6 7 # 生成 10 位 base64 编码的密码并以标准字符集输出 openssl rand 10 | openssl base64 -A vim /etc/redis.conf, 取消 requirepass 行的注释，修改后面的值 sudo systemctl restart redis # 开启过期 key 事件监听（会有一定的额外消耗） config set notify-keyspace-events Ex 持久化策略 以下是 Redis 的两种持久化策略及其默认配置的对比表格：\n持久化策略 RDB (Redis Database) AOF (Append Only File) 原理 定时生成内存快照（二进制文件） 记录所有写操作命令（文本追加日志） 触发方式 手动触发 / 按配置的时间间隔自动触发 实时记录（可配置同步频率：每秒/每次写入） 文件格式 紧凑的二进制文件（.rdb） 可读的文本文件（.aof） 恢复速度 快（直接加载快照） 慢（需重放所有命令） 数据安全性 可能丢失最后一次快照后的数据 更高（取决于同步频率，最多丢失 1 秒数据） 文件体积 小（仅最终数据状态） 大（持续增长，需定期重写优化） 性能影响 生成快照时可能阻塞主线程 写入日志对性能影响较小（但同步频繁时会降低吞吐量） 默认启用 是（Redis 默认持久化方式） 否（需手动配置） 关键说明 默认策略：Redis 默认启用 RDB 持久化，会在以下条件满足时自动生成快照（可通过 save 配置修改）： 1 2 3 save 900 1 # 900秒（15分钟）内至少1个key变化 save 300 10 # 300秒（5分钟）内至少10个key变化 save 60 10000 # 60秒内至少10000个key变化 混合持久化（Redis 4.0+）：可同时启用 RDB 和 AOF（aof-use-rdb-preamble yes），结合两者优势：\nAOF 文件前半部分是 RDB 格式的快照，后半部分是增量命令。 如何选择：\nRDB 适合备份、灾难恢复（快速加载）。 AOF 适合需要高数据安全性的场景（如金融交易）。 配置示例 1 2 3 4 5 6 # 启用 RDB（默认已启用） save 900 1 # 启用 AOF appendonly yes appendfsync everysec # 每秒同步一次（平衡性能与安全） 通过 INFO PERSISTENCE 命令可查看当前持久化状态。\n如何保证数据一致性？ 非强一致性场景：先更新数据库，再删除缓存（加分布式锁保证线程安全）\n强一致性场景：延迟双删\n先删缓存，然后更新数据库，延迟删除缓存 先更新数据库，删缓存，延迟删缓存 第 2 种更安全。\n休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒\n异步缓存写入：先更新缓存，再异步更新数据库（适用于动态数据且一致性要求不高的场景，如点赞、浏览等）\nRedis 分布式锁 redlock 算法\nhttps://redis.io/docs/manual/patterns/distributed-locks/#the-redlock-algorithm\n以毫秒为单位获取当前时间 T1。 尝试在所有 N 个实例中依次获取锁，在所有实例中使用相同的键名和随机值，并且会设置一个比锁的有效时间小的超时时间。例如，如果自动释放时间为 10 秒，那么超时时间可以在 5-50 毫秒之间。这样可以防止客户端在尝试与宕机的 Redis 节点通信时长时间处于阻塞状态：如果某个实例不可用，我们应尽快尝试与下一个实例通信。 客户端获取当前时间戳 T2。如果客户端能在大多数实例（至少 3 个）中获取锁，且 T2 - T1 \u0026lt; 锁的有效时间时，才认为获取了锁。 如果锁已被获取，则其有效时间被认为是初始有效时间减去步骤 3 计算出的已用时间。 如果客户机因某种原因未能获取锁（要么无法锁定 N/2+1 个实例，要么有效时间为负），它将尝试解锁所有实例（甚至是它认为无法锁定的实例）。 如何续期？\n额外启动一个守护线程定时去轮巡当前锁是否已释放。\n缓存击穿 \u0026amp; 缓存血崩 缓存击穿：单个热点 key 失效 + 并发访问，导致这些请求全部涌入数据库中\n缓存穿透：缓存和 db 都没有数据 + 并发访问\n缓存雪崩：批量 key 失效 + 并发访问，导致大量请求涌入数据库\n解决缓存击穿\n热点数据设置热度时间窗口，时间窗口内，延长缓存时间 多级缓存 设置较长的过期时间 解决缓存穿透\n使用布隆过滤器判断元素是否存在，不存在则直接返回 空对象缓存：不存在的数据存储为空对象缓存 延迟双判：查询请求穿透到 db 时，先在 db 查询，db 也没有，则将空结果缓存，设置一个较短的过期时间 缓存预热 限流 解决缓存雪崩\n多级缓存 缓存预热 key 设置随机过期时间 应用 HyperLogLog 统计 UV HyperLogLog (HLL) 是一种基数估计算法，用于统计一个集合中不重复元素的个数。\n我们先来看一个简单的例子：假设有一个篮子，里面装满了彩色的球，每个球上都有一个不同的数字。现在我们想知道篮子里有多少种不同的数字，但是我们不希望一个个球拿出来去重，因为球可能有很多甚至无限个。\n具体原理如下：\n创建一个定长的位数组，里面的每个位都初始化为 0。 对于集合中的每个元素，通过哈希函数将其映射为一个二进制字符串，并取这个字符串中特定的一段作为索引。 在位数组对应的索引位置上，记录该位置出现的最大前导零的长度。 根据位数组中最大前导零的长度，估算出集合中不重复元素的个数。 HyperLogLog 的核心思想是利用哈希函数的随机性和最大前导零的长度分布来估计不重复元素的个数。当位数组中的某个位置记录的最大前导零长度比较大时，说明这个位置对应的哈希值较小的元素较多，因此可以推测集合中不重复元素的个数也相对较多。\n由于使用了哈希函数和概率统计，HyperLogLog 的估计结果可能会有一定的误差，但在实际应用中，这个误差通常是可接受的。\nHyperLogLog 是一种通过概率统计估计集合中不重复元素个数的算法，它以极小的内存开销来实现高效的基数估计。\n模拟：http://content.research.neustar.biz/blog/hll.html\n命令\nPFADD PFADD key [element [element ...]] PFCOUNT PFCOUNT key [element [element ...]] PFMERGE PFMERGE key [element [element ...]] incr 文章浏览量计数 hincrby、incrby\nbitmap 位图 setbit key offset value\n最大支持 512mb = 2^32 位\n账号封禁 一个封禁能力对应一个 bitmap，比如 banned:login，用户 id 作为偏移量。\n封禁登录：setbit banned:login 1000018 1\n解禁登录：setbit banned:login 1000018 0\n判断是否被封禁：getbit banned:login 1000018\n如果用户 id 较大，可对 uid 哈希计算，或按一定规则处理，比如数据库中 uid 从 1000000 自增，那么偏移量可设为 uid - 1000000，其中偏移量是从 0 开始的。\nscan 扫描匹配的所有键 相比较于 keys 命令阻塞式命令，大 key 会存在问题；scan 命令是一个基于游标的迭代器，每次迭代 count 返回一个游标继续下一次迭代，不过会存在重复值，需要去重。\n1 2 3 4 5 6 7 def scan_uk(pattern, count=None): \u0026#34;\u0026#34;\u0026#34;使用 scan command 匹配 keys 并去重\u0026#34;\u0026#34;\u0026#34; uq_keys = set() for key in client.scan_iter(match=pattern, count=count): if key not in uq_keys: uq_keys.add(key) yield key 其他 实现数据表格拖拽排序 1. 全量更新 —— 添加一个 sort 字段，表示序号\n拖拽节点：最简单的方式是直接更新全部节点新的 sort 值（一般前端框架可以获取到更新后的序号），也可以只传这两个节点 id，服务端去计算 sort，更新这两个节点之间的记录的 sort。如果是前往后拖，则 sort = sort - 1，否则 sort = sort + 1 删除节点：其后的节点全部前移一位 新增节点：新节点的 parent_id = max(sort) + 1 适用场景：数据量小，拖拽操作不频繁 2. 单链表（邻接表） —— 添加一个 parent_id 字段，指向其前驱节点\n拖拽节点：需要交换这两个节点对应的 parent_id 值以及其前驱和后继节点的 parent_id 值 删除节点：更新其直接后继节点的 parent_id 值 新增节点：如果是尾插法，parent_id = 链表最后一条记录的 id；如果是头插法 parent_id = null 适用场景：数据量中等，频繁拖拽，不适用于分页场景（因为获取父子节点关系必须按序遍历全部数据） 3. 双链表 —— 添加 pre 和 next 字段\n交换两个节点：考虑以下几种情况\n情况一：cur 在前，dest 在后\n节点相邻\n1 2 3 4 5 6 7 8 9 10 11 # 更新当前对象的前驱节点的后继 cur.pre.next = dest # 更新目标对象的后继节点的前驱 dest.next.pre = cur # 更新目标对象前驱及后继 dest.pre = cur.pre dest.next = cur # 更新当前对象前驱及后继 cur.pre = dest cur.next = dest.next 节点不相邻\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 更新当前对象的前驱节点的后继 cur.pre.next = dest # 更新目标对象的后继节点的前驱 dest.next.pre = cur # 更新当前对象的后继节点的前驱 cur.next.pre = dest # 更新目标对象的前驱节点的后继 dest.pre.next = cur # 更新目标对象前驱及后继 dest.pre = cur.pre dest.next = cur.next # 更新当前对象前驱及后继 cur.pre = dest.pre cur.next = dest.next 情况二：cur 在后，dest 在前\n节点相邻\n1 2 3 4 5 6 7 cur.next.pre = dest dest.pre.next = cur dest.pre = cur dest.next = cur.next cur.pre = dest.pre cur.next = dest 节点不相邻\n1 2 3 4 5 6 7 8 9 cur.next.pre = dest dest.pre.next = cur cur.pre.next = dest dest.next.pre = cur dest.pre = cur.pre dest.next = cur.next cur.pre = dest cur.next = dest.next 如何判断双链表中任意两个节点的先后关系？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def find_order(self, n1: QueryT, n2: QueryT, filters: list = None): \u0026#34;\u0026#34;\u0026#34; 比较两个节点的顺序 :param n1: :param n2: :param filters: :return: 1 n1在前 -1 n1在后 None 节点不满足链表关系 \u0026#34;\u0026#34;\u0026#34; if filters is None: filters = [] data = self.model.query.filter(*filters).all() nodes = {self.get_cur(item): item for item in data} cur = n1 while cur: if self.get_cur(cur) == self.get_cur(n2): return 1 cur = nodes.get(self.get_next(cur)) cur = n2 while cur: if self.get_cur(cur) == self.get_cur(n1): return -1 cur = nodes.get(self.get_next(cur)) 使用 pgsql 递归查询：\n1 2 3 4 5 6 7 8 9 10 WITH RECURSIVE cte AS ( SELECT * FROM avatar_management WHERE pre_id IS NULL and community_id=8 -- 找到链表的起始节点，即 pre_id 为空的节点 UNION SELECT am.* FROM avatar_management am JOIN cte ON am.pre_id = cte.id -- 通过递归连接找到下一个节点 where am.community_id=8 ) SELECT * FROM cte; 4. 闭包表 —— 需要建立额外一张表 TreePath，使用空间换时间策略\n包含 3 个字段，（ancestor，descendant）设为联合主键，其中每一个字段都是指向 target_id 的外键，level 用来表示层级深度：\nancestor descendant level（可选） 分页查询 Redis zset 分页 场景举例：评论分页（按热度排序）\n由于热度值是动态变化的，且不能暴露给客户端，传统分页必然不行，zset 可以很好解决这个问题。stamp 是数据快照标识（比如当前时间戳），可保证在分页过程中的数据完整性和一致性，每次发布新评论或删除评论都是操作的最新数据快照缓存，有效期设为 6h 或 8h。\n查询参数：\nlast —— 前一次查询返回的游标，即下次查询的起始索引 limit —— 分页大小 stamp —— 数据快照标识（比如当前时间戳） 缓存 key 设计：comment:${资源类型}_${资源id}_${排序方式}_${数据版本标识}\nkey 存在才可操作，并且需要设置一个合适的 ttl，集合元素个数最好不要超过 5000。\n相关命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 ZREVRANGE 按分数值递减返回指定区间内的成员，相同 score 的 member 按字典序的逆序排列 \u0026gt;\u0026gt; ZREVRANGE key start stop [WITHSCORES] 向集合中插入元素，评论 id 作为 member，热度值作为 score \u0026gt;\u0026gt; ZADD key score1 member1 score2 member2 ... 删除元素 \u0026gt;\u0026gt; ZREM key member 查询 \u0026gt;\u0026gt; zrevrange key last last + limit - 1 获取总数 \u0026gt;\u0026gt; zcard key 1 2 3 4 # 获取最新 key max(keys, key=lambda x: int(x.split(\u0026#34;_\u0026#34;)[-1])) last = last + limit if last + limit \u0026lt; total else None 注意：缓存只保存评论 id，且只有当缓存 key 存在才插入\n游客模式 首次使用 app 时服务端分配一个 uid，status 设为 -1 表示游客。\n使用未占用的手机号或三方账号登录时，uid 不变，修改状态为 1，数据不合并 使用已占用的手机号或三方账户登录时，uid 更换为新登录账号的 uid（相当于切换账号），数据不合并 ","date":"2023-07-11T00:00:00Z","permalink":"/p/database/","title":"数据库"},{"content":" 经典git工作流 简化git工作流（基于环境） 功能开发/修复完成后合并到develop，测试通过后基于master创建发布分支release，release合并开发/修复分支后，部署上线， 验收通过并封版后合到master，看需要来打tag\n开发/缺陷分支只能合并稳定分支例如可以合并master分支，不可merge develop分支，作用是防止环境 污染 develop分支只能用于测试环境发布，不可直接合并到release分支，正确流程是在release分支合并开 发/缺陷分支 常用命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 # 初始化git配置 git config --global user.name xxx git config --global user.email xxx # 查看git配置 git config -l # 查看git用户名 邮箱 git config user.name git config user.email # 修改用户名 git config --global user.name {用户名} git remote -v 显示所有远程仓库 git remote prune origin 删除本地有但在远程库已经不存在的分支 git clone -b 克隆分支名 地址 git remote add origin git@github.com:XXXXX/demo.git 新增远程仓库 git remote set-url origin git@github.com:XXXXX/demo.git 修改远程仓库 git remote set-url --add origin git@github.com:XXXXX/demo.git 添加多个远程仓库 git stash 不提交工作区内容 git stash pop 恢复工作区 git fetch 拉取远程分支 git checkout -b develop origin/develop git reset --hard \u0026lt;commit ID号\u0026gt; 回滚 git push -f origin master 推送到远程分支 git diff 查看工作区和暂存区的区别 git diff –cached 查看暂存区和版本库之间的区别 git diff HEAD 查看工作区和版本库之间的区别 git status 查看当前的工作状态 git branch -a 查看本地所有分支 git branch -r 查看远程所有分支 git branch -r --contains v0.10.2\t查看远程tag属于哪一个分支 git push --set-upstream origin online_help_20191024 推分支 git push origin --delete dev20181018 删除远程分支 git branch -d dev20181018 删除本地分支 git cherry-pick 285c9477a759abdee94099e25e75e39e8c77d46b 合并指定版本 git tag -d tag-name\t#删除本地tag git push origin --delete tag-name\t#删除远程tag 分支操作 创建一个新分支 1 2 git branch \u0026lt;new_branch_name\u0026gt; git checkout \u0026lt;new_branch_name\u0026gt; 基于当前分支创建新分支 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 从main分支创建develop分支并切换到develop git pull origin main （可选） git checkout -b develop main # 发布develop分支，使本地分支与远程develop关联 git push -u origin develop # 发布到main分支 git checkout main # 切换到main分支 git merge --no-ff develop\t# 对develop分支进行合并 --no-ff 禁止使用快速合并模式，会生成一个新的提交，可以回退到main的上一次提交状态， 如果不加此参数的话，只能回到develop的上一次提交 从develop分支拉出一个新功能分支 1 2 3 4 5 6 7 8 9 git pull origin develop （可选） git checkout -b feature_v1.0 develop git push -u origin feature_v1.0 # 完成后，合并到develop分支 git pull origin develop git checkout develop git merge --no-ff feature_v1.0\t# 从feature分支合并到develop分支 git branch -d feature_v1.0\t# 删除feature分支，可选 从develop拉出一个release分支 1 2 3 4 5 6 7 8 9 10 11 12 13 git pull origin develop\t#可选，获取最新版本 git checkout -b release_v1.0 develop # 完成release，合并到main分支和develop分支，在main打上tag标记 git checkout main git merge --no-ff release_v1.0 git tag release1.0 master git push origin v0.10.3\t推送v0.10.3分支 git push --tags\t推送本地所有tags #合并到develop git checkout develop git merge --no-ff release_v1.0 拉取dev分支到当前本地分支 1 2 3 先切换到本地分支v1.0 git pull origin dev:v1.0 本地项目和远程仓库关联 1 2 3 4 5 6 7 8 9 git init git remote add origin \u0026lt;remote url\u0026gt; git fetch origin main\tgit branch --set-upstream-to=origin/main main git add . git commit -m \u0026#34;xxx\u0026#34; git push 若远程分支不存在，git fetch origin main命令替换为git push -u origin main 回退 git revert revert仅仅是撤销指定commit的修改，并不影响后续的commit，但所撤销的commit被后续的commit修改了同一地方则会产生冲突；\ngit reset 彻底回退到指定的commit版本，其后的commit会被删除 git log找到commit id，然后git reset \u0026lt;commit_id\u0026gt;，然后git push -f\n删除某次提交 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # 1. 找到要删除的那个 commit 的**前一个** commit hash（比如 B） git log --oneline # 2. 启动交互式 rebase git rebase -i \u0026lt;commit-B-hash\u0026gt; # 或者用相对位置，比如删除倒数第 3 个 git rebase -i HEAD~3 #把要删除的那行改成 drop 或直接删掉这一行： drop c123456 commit C pick d234567 commit D pick e345678 commit E # 保存退出，Git 会自动应用。 # 然后可能会出现：CONFLICT (content): Merge conflict in README.md error: could not apply 6cbbb27... feat: # 需要解决冲突 1. 查看冲突文件 git status # 2. 手动编辑冲突文件（删掉 \u0026lt;\u0026lt;\u0026lt; === \u0026gt;\u0026gt;\u0026gt; 标记） vim README.md # 3. 标记为已解决 git add README.md # 4. 继续 rebase git rebase --continue # 5. 如果又遇到冲突，重复 2-4 #3. 强制推送到远程 git push origin -f 开发流程 项目负责人从master的基线check out，初始化dev分支 开发者从dev分支check out，建立本地个人开发分支feature*\u0026lt;/font\u0026gt; 开发者完成功能开发后，commit个人feature分支，并push至远程个人feature分支 开发者在gitlab上提交个人的代码和并请求至dev分支 代码审查人负责代码审查，合并合理代码 代码提测时，开发负责人提交dev分支到test的合并请求 项目负责人合并dev分支至test分支 版本测试完成后，开发负责人提交test分支至master分支的合并请求 项目负责人合并代码至master 项目负责人以当前代码为基线，在master分支上tag当前版本号 1 2 3 git tag -a v1.0.0 -m \u0026#34;Release v1.0.0\u0026#34; # 或 git tag v1.0.0 git push origin v1.0.0 紧急修复工作流 1 2 3 4 5 6 7 8 git checkout master git checkout -b hotfix/1.2.1 # gerrit切出hotfix/1.2.1分支 # 修复完成 # 合并到master git push origin HEAD:refs/for/master # 合并到develop git push origin HEAD:refs/for/develop 版本发布工作流 1 2 3 4 5 6 7 8 9 10 11 12 # 获取预先规划的版本号，并根据实际更新内容修改版本号 # 确保规划的问题修复和功能开发均已合并到develop # gerrit上创建release/1.2.0分支 # 全量测试并生成固件 # 软件 # 修 bug、验证 git push origin HEAD:refs/for/master git tag -a v1.0.0 -m \u0026#34;Release v1.0.0\u0026#34; git checkout develop git merge release/1.2.0 git push origin HEAD:refs/for/develop # 删除release/1.2.0分支 主分支同步到功能分支 如果是仅个人的功能分支，推荐用rebase保持干净历史，多人开发的功能分支用merge\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 更新development git checkout development git pull origin development # 切回功能分支 git checkout feature-branch git rebase development # 如果有冲突 # 解决冲突后 git add . git rebase --continue # 强制推送 git push --force-with-lease 分支类型 开发分支: feat/xxx(功能)_20200804(创建日期)开发分支基于master创建，开发完成合并到测试develop分支 测试分支：develop 只用来发布测试环境 发布分支：release/vx.x.x(版本号)_20200804(创建日期)，基于master创建，验收完成，进行封版后合并回master 缺陷分支：hotfix/xxx(功能)_20200804(创建日期)合并流程和开发分支一致，区别在于，hotfix在测试环境测试通过后直接合到最新的release分支验收\n提交规范 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 commit 规范： feat: ⼀个新特性 fix: 修了⼀个 Bug docs: 更新了⽂档（⽐如改了 Readme） style: 代码的样式美化，不涉及到功能修改（⽐如改了缩进，css修改不算） refactor: ⼀些代码结构上优化，既不是新特性也不是修 Bug（⽐如函数改个名字） perf: 优化了性能的代码改动 test: 新增或者修改已有的测试代码 chore: 跟仓库主要业务⽆关的构建/⼯程依赖/⼯具等功能改动（⽐如新增⼀个⽂档⽣成⼯具） 操作规范（技巧）： 在代码push 前先 pull 遇到代码冲突时，最先考虑取最近commit的版本，若想撤销本次拉取 执行git revert --abort 正在开发发现分支错了，执行 git stash，切换分支后，执行git stash pop 只想合并其他分支的某条commit 记录 执行 git cherry-pick commitId 开发分支新建必须基于干净版本，合并到稳定版本前必须是验收通过的版本 开发分支不能合并其他正在开发或者非干净分支 提交类型 含义 feat 新功能（feature） fix 修复 bug perf 性能优化 refactor 重构代码 docs 文档相关 style 样式相关 test 测试相关 chore 构建过程或辅助工具的变动 生成密钥对 1 2 3 4 5 6 7 8 9 10 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; # 拷贝 ## mac pbcopy \u0026lt; ~/.ssh/id_ed25519.pub ## linux ## win cat ~/.ssh/id_ed25519.pub | clip GitLab CI/CD 项目根目录下新建.gitlab-ci.yml文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 variables: PIP_CACHE_DIR: \u0026#34;$CI_PROJECT_DIR/.cache/pip\u0026#34; # 缓存配置，加快项目的构建速度，减少重复下载 cache: key: $CI_JOB_NAME-$CI_COMMIT_REF_SLUG\t# job名称-提交的分支名称 paths: - ${PIP_CACHE_DIR}\t# 需要被缓存的文件或文件夹的路径 # 定义pipeline stages: - lint - test # 定义了2个job lint_job: stage: lint\t# 属于哪个stage image: python:3.7\t# 需要的docker镜像 before_script:\t# job开始之前执行的脚本 - pip install flake8 -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com script:\t# 执行脚本 - flake8 app test_job: stage: test image: python:3.7 services:\t# 启动名为 postgres 的服务 - postgres variables: POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_USER: postgres POSTGRES_PASS: postgres POSTGRES_DB: oa_performance_test POSTGRES_PASSWORD: postgres before_script: - pip install -r requirements-test.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com script: - echo \u0026#34;Test started.\u0026#34; # - coverage run --source app -m pytest tests # - coverage report - echo \u0026#34;Test finished.\u0026#34; 常见问题 fatal: refusing to merge unrelated histories git merge main \u0026ndash;allow-unrelated-histories\npull时提示：your local changes will be overwritten by merge\u0026hellip; 先git stash changes，然后pull更新，然后git stash pop\n忽略已跟踪的某个文件或目录 git rm \u0026ndash;cached /path/file 或 git rm -r \u0026ndash;cached logs/\n","date":"2023-04-12T00:00:00Z","permalink":"/p/git-notes/","title":"Git Notes"},{"content":"Anconda环境 jupyter后台运行：\nnohup jupyter notebook \u0026ndash;allow-root \u0026gt; jupyter.log 2\u0026gt;\u0026amp;1 \u0026amp;\nconda常用命令（windows环境）：\nconda create -n ENV_NAME python=x.x 创建环境 conda activate ENV_NAME 切换环境 conda env remove -n ENV_NAME 删除环境 conda env list 查看所有环境 conda deactivate 退出当前环境 conda config \u0026ndash;show 查看conda配置 主题设置\n字号12 cell屏幕宽占比90% -T显示工具栏\njt -t oceans16 -T -f fira -fs 12 -cellw 90%\n基础 深浅拷贝 都会生成一个新对象，深拷贝的新对象地址和源对象不同 若为嵌套可变类型，浅拷贝只会拷贝最外层，里面为引用；深拷贝拷贝全新对象 若为嵌套不可变，深浅拷贝只会拷贝最外层，里面为引用 迭代器、生成器 迭代器和可迭代对象的区别？\n​\t迭代器实现了__iter__ 和__next__方法，可迭代对象实现了__iter__方法。迭代器一定是可迭代对象\nyield from + 可迭代对象，实现对可迭代对象的再一次yield\n闭包 外部函数中定义了一个内部函数，返回了内部函数的引用，内部函数引用了外部函数的变量\n自由变量：\n在当前作用域中引用了但未定义的变量，而是在外部作用域中定义的，如果要访问外部作用域中的非全局变量，需用关键字nonlocal定义\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # 因访问了外部作用域中的非全局变量，会报错：UnboundLocalError: local variable \u0026#39;a\u0026#39; referenced before assignment def outer(): a = 0 def inner(y): a += y return a return inner # 使用nonlocal定义自由变量 def outer(): a = 0 def inner(y): nonlocal a a += y return a return inner func = outer() func(1) func(2) \u0026gt;\u0026gt;\u0026gt; output: 3 # 如果外部变量是可变对象，内部函数可以直接通过引用修改该对象的内容，这种行为并不需要额外的 global 或 nonlocal 关键字 def outer(): a = [] def inner(y): a.append(y) return a return inner func = outer() func(1) func(2) \u0026gt;\u0026gt;\u0026gt; output: [1, 2] 闭包的作用：\n封装和隐藏数据：通过闭包，内嵌函数可以访问外部函数的变量，而外部环境无法直接修改这些变量，从而实现了封装，保护数据不被外部直接访问。 延迟执行：闭包常用于回调函数或延迟执行的场景，确保函数在某个时刻可以访问到当时的变量状态。 维持状态：闭包允许我们在函数中“记住”状态，比如计数器、缓存等。 类与对象 1 2 3 4 5 6 7 8 9 class student(): # 类变量，可在所有实例共享 age = 20 name = \u0026#39;qiuwen\u0026#39; def __init__(self,age,name): # 实例变量，每个实例独有的 self.age = age self.name = name 继承多态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Student: def __init__(self, id, name, age, sex): self.id = id self.name = name self.age = age self.sex = sex def test(self): print(\u0026#34;name:{} age:{} sex:{}\u0026#34;.format(self.name, self.age,self.sex)) class BoyStudent(Student): def test(self): super().test() # super：子类重写父类方法后还需调用父类方法 print(\u0026#39;男学生方法\u0026#39;) class GirlStudent(Student): def __init__(self, id, name, age, sex, grade): super().__init__(id, name, age, sex) self.grade = grade def test(self): print(\u0026#39;女学生方法\u0026#39;+ self.grade) s = Student(1, \u0026#39;yuqiuwen\u0026#39;, \u0026#39;20\u0026#39;, \u0026#39;male\u0026#39;) s.test() bs = BoyStudent(1, \u0026#39;yuqiuwen\u0026#39;, \u0026#39;20\u0026#39;, \u0026#39;male\u0026#39;) bs.test() gs = GirlStudent(2, \u0026#39;zhongxin\u0026#39;, \u0026#39;18\u0026#39;, \u0026#39;female\u0026#39;,\u0026#39;3\u0026#39;) gs.test() 多态：一个类有不同的子类，不同的子类实例化后的对象调用相同的接口有有不同的行为\nwith实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class MyClass: def __init__(self, filename, mode=\u0026#39;r\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;): self.filename = filename self.mode = mode self.encoding = encoding def __enter__(self): self.file = open(self.filename, mode=self.mode, encoding=self.encoding) return self.file def __exit__(self, exc_type, exc_val, exc_tb): self.file.close() with MyClass(\u0026#39;test.txt\u0026#39;, \u0026#39;w\u0026#39;, encoding=\u0026#39;utf-8\u0026#39;) as f: f.write(\u0026#34;test msg\u0026#34;) 魔法方法 cls是类本身，self是实例化后的对象\n1 2 3 4 5 6 7 8 9 10 11 12 __new__(cls)：构造方法，创建实例时首先调用的方法 __init__(self)：初始化方法，new方法返回对象后进行属性初始化 __del__(self)：析构方法，实例化对象被彻底销毁时调用 __len__(self)：被len()调用时的操作 __bool__(self)：被bool()调用时的行为 __str__(self)：被str()调用时的操作，将值转化为适于人阅读的形式 __repr__(self)：repr()调用时的操作，将值转化为供解释器读取的形式 __getattr__(self,name)：获取一个不存在的属性时的行为 __setattr__(self,name,value)：当一个属性被设置时的行为 __getattribute__(self,name)：当该类的属性被访问时首先调用的方法，可作为属性访问拦截器 __setitem__(self, key, value)：同setattr方法类似，区别在于如果将对象当作字典操作，设置键值对时会触发该方法 __call__(self)：允许将类实例当做函数调用 内置属性 1 2 3 4 5 __dict__：包含一个字典，由类的数据属性组成 __doc__：类的文档字符串 __name__：类名 __module__：类定义所在的模块 __bases__：类的所有父类构成元素 元类（Metaclass） 如何理解元类\nPython 元类详解 - Mr-Yang` - 博客园 (cnblogs.com)\nPython进阶灵魂拷问，元类是什么，它有什么用？ - ludongguoa - 博客园 (cnblogs.com)\n可以通过继承自type来定义一个元类，用于控制类的行为和属性，比如实例化时新增方法或属性\n元类实例化 ——\u0026gt;自定义class ——\u0026gt;自定义类实例化 ——\u0026gt;实例\n抽象类 如何理解抽象类\n如果类是基于现实对象抽象而来，那么抽象类就是基于类抽象而来\n和普通类的区别\n抽象类不能被实例化，只能被继承 类中只能有抽象方法，没有具体功能实现 子类必须实现抽象方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import abc class All_file(metaclass=abc.ABCMeta): all_type=\u0026#39;file\u0026#39; @abc.abstractmethod #定义抽象方法，无需实现功能 def read(self): \u0026#39;子类必须定义读功能\u0026#39; pass @abc.abstractmethod #定义抽象方法，无需实现功能 def write(self): \u0026#39;子类必须定义写功能\u0026#39; pass class Txt(All_file): #子类继承抽象类，但是必须定义read和write方法 def read(self): print(\u0026#39;文本数据的读取方法\u0026#39;) def write(self): print(\u0026#39;文本数据的读取方法\u0026#39;) class Sata(All_file): #子类继承抽象类，但是必须定义read和write方法 def read(self): print(\u0026#39;硬盘数据的读取方法\u0026#39;) def write(self): print(\u0026#39;硬盘数据的读取方法\u0026#39;) 垃圾回收机制 引用计数\n增加引用计数：对象被创建或者将其引用赋值，引用计数就被设置为1\n对象被创建 另外的别名被创建 被作为参数传递给函数 成为容器对象的第一个元素 减少引用计数：当对象的引用被销毁时，引用计数就会减少\n本地引用离开了作用范围 对象的别名被显示销毁 对象的一个别名被赋值给其他对象 对象被从一个窗口对象中移除 窗口对象本身被销毁 标记-清除\n解决循环引用的问题。如a=1,b=2,执行a=b时将进行标记清除\n分代回收\n基于引用计数的回收机制，每次回收内存，都需要把所有对象的引用计数都遍历一遍，这是非常消耗时间的，于是引入了分代回收来提高回收效率，分代回收采用的是用“空间换时间”的策略。在历经多次扫描的情况下，都没有被回收的变量，gc机制就会认为，该变量是常用变量，gc对其扫描的频率会降低\nsocket 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from socket import * \u0026#34;\u0026#34;\u0026#34;客户端\u0026#34;\u0026#34;\u0026#34; IP = \u0026#39;127.0.0.1\u0026#39; SERVER_PORT = 8006 BUFLEN = 1024 dataSocket = socket(AF_INET, SOCK_STREAM) # 连接服务端 dataSocket.connect((IP, SERVER_PORT)) while True: toSend = input(\u0026#39;\u0026gt;\u0026gt; \u0026#39;) if toSend == \u0026#39;\u0026#39;: break dataSocket.send(toSend.encode()) recved = dataSocket.recv(BUFLEN) if not recved: break print(recved.decode()) dataSocket.close() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 from socket import * \u0026#34;\u0026#34;\u0026#34; 服务端 \u0026#34;\u0026#34;\u0026#34; IP = \u0026#39;127.0.0.1\u0026#39; PORT = 8006 BUFLEN = 1024 # 一次从socket缓冲区最多读入512个字节数据 \u0026#34;\u0026#34;\u0026#34; 实例化一个socket对象 AF_INET表示该socket网络层使用ip协议 SOCK_STREAM 表示socket传输层使用tcp协议 \u0026#34;\u0026#34;\u0026#34; listenSocket = socket(AF_INET, SOCK_STREAM) # socket绑定地址和端口 listenSocket.bind((IP, PORT)) # 使socket处于监听状态，等待客户端的连接请求 # 5表示最多接受5个等待连接的客户端 listenSocket.listen(5) print(f\u0026#34;服务端启动成功，在{PORT}端口等待客户端连接...\u0026#34;) dataSocket, addr = listenSocket.accept() print(\u0026#34;接收一个客户端连接：\u0026#34;, addr) while True: # 读取对方发送的信息，最多读取BUFLEN字节 recved = dataSocket.recv(BUFLEN) # 返回空字节，表示对方关闭了连接 if not recved: break info = recved.decode() print(f\u0026#34;收到对方信息：{info}\u0026#34;) dataSocket.send(f\u0026#34;服务端接收到了信息：{info}\u0026#34;.encode()) dataSocket.close() listenSocket.close() 装饰器 有参装饰 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def timeit(max_time): def inner(f): def wrapper(*args, **kw): start = time.perf_counter() res = f(*args, **kw) end = time.perf_counter() execute_time = end - start if execute_time \u0026gt; max_time: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function exceeds max_time\u0026#34;\u0026#34;\u0026#34;) else: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function ran for {execute_time}\u0026#34;\u0026#34;\u0026#34;) return res return wrapper return inner @timeit(max_time=1) def func(*args, **kw): time.sleep(2) func(1) \u0026#34;\u0026#34;\u0026#34;相当于执行func = timeit(1)(func)，即先执行timeit(1)返回一个inner函数的引用，然后将func传给inner函数，返回wrapper保存到func对象\u0026#34;\u0026#34;\u0026#34; 无参装饰 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def timeit(f): # 这里使用*args和**kwargs使得函数可传递变长参数 def wrapper(*args, **kwargs): start = time.perf_counter() f(*args, **kw) end = time.perf_counter() execute_time = end - start if execute_time \u0026gt; 2: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function exceeds max_time\u0026#34;\u0026#34;\u0026#34;) else: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function ran for {execute_time}\u0026#34;\u0026#34;\u0026#34;) return wrapper @timeit def func(*args, **kw): time.sleep(2) \u0026#34;\u0026#34;\u0026#34;相当于执行func = timeit(func)\u0026#34;\u0026#34;\u0026#34; 类装饰器 无参\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Timer: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): start = time.perf_counter() ret = self.func(*args, **kwargs) end = time.perf_counter() execute_time = end - start if execute_time \u0026gt; 2: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function exceeds max_time\u0026#34;\u0026#34;\u0026#34;) else: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function ran for {execute_time}\u0026#34;\u0026#34;\u0026#34;) return ret @Timer def func(*args, **kw): time.sleep(2) \u0026#34;\u0026#34;\u0026#34;相当于执行func=Timer(func)\u0026#34;\u0026#34;\u0026#34; 有参\n​\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Timer: def __init__(self, max_time): self.max_time = max_time def __call__(self, func): def wrapper(*args, **kwargs): start = time.perf_counter() ret = func(*args, **kwargs) end = time.perf_counter() execute_time = end - start if execute_time \u0026gt; 2: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function exceeds max_time\u0026#34;\u0026#34;\u0026#34;) else: print(f\u0026#34;\u0026#34;\u0026#34;\u0026#34;func\u0026#34; function ran for {execute_time}\u0026#34;\u0026#34;\u0026#34;) return ret return wrapper @Timer(max_time=2) def func(*args, **kw): time.sleep(2) \u0026#34;\u0026#34;\u0026#34;相当于执行func=Timer(max_time=2)(func)\u0026#34;\u0026#34;\u0026#34; GIL 全局解释器锁，遇到IO操作时，强制释放GIL锁，使得任何时刻仅有一个线程在执行，可以解决多线程之间数据完整性和状态同步问题，简化对共享资源的管理\n并发编程 进程是资源分配的单位，线程是操作系统调度的基本单元 进程的创建、销毁和切换较线程开销更大。因为系统运行时会为每个进程分配独立的内存空间，而线程不会分配，线程之间共享同一进程下的资源 一个进程中至少有一个线程 协程存在于某一个线程中，可以控制该线程中的几个函数交替执行（io阻塞时进行切换） 多线程 守护线程 所谓“守护线程”就是在主线程结束的时候，不值得再保留的执行线程。这里的不值得保留指的是守护线程会在其他非守护线程全部运行结束之后被销毁，它守护的是当前进程内所有的非守护线程。简单的说，守护线程会跟随主线程一起挂掉，而主线程的生命周期就是一个进程的生命周期。如果不理解，我们可以看一段简单的代码。\n将名为daemon的参数设置为True，这两个线程就会变成守护线程，那么在其他线程结束时，即便有死循环，两个守护线程也会挂掉，不会再继续执行下去，代码如下所示。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import time from threading import Thread def display(content): while True: print(content, end=\u0026#39;\u0026#39;, flush=True) time.sleep(0.1) def main(): Thread(target=display, args=(\u0026#39;Ping\u0026#39;, ), daemon=True).start() Thread(target=display, args=(\u0026#39;Pong\u0026#39;, ), daemon=True).start() time.sleep(5) if __name__ == \u0026#39;__main__\u0026#39;: main() 上面的代码，我们在主线程中添加了一行time.sleep(5)让主线程休眠5秒，在这个过程中，输出Ping和Pong的守护线程会持续运转，直到主线程在5秒后结束，这两个守护线程也被销毁，不再继续运行。\n锁 RLock：重入锁，可以被同一个线程多次acquire，调用相同次数的release才可完全释放锁；在线程A中创建则只能在线程A中释放\nLock：同一线程中只能被acquire一次；可以在线程A中创建，线程B中释放\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import time from concurrent.futures import ThreadPoolExecutor from threading import RLock class Account(object): \u0026#34;\u0026#34;\u0026#34;银行账户\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.balance = 0.0 self.lock = RLock() def deposit(self, money): # 通过上下文语法获得锁和释放锁 with self.lock: new_balance = self.balance + money time.sleep(0.01) self.balance = new_balance def main(): \u0026#34;\u0026#34;\u0026#34;主函数\u0026#34;\u0026#34;\u0026#34; account = Account() with ThreadPoolExecutor(max_workers=16) as pool: for _ in range(100): pool.submit(account.deposit, 1) print(account.balance) if __name__ == \u0026#39;__main__\u0026#39;: main() 打印0与奇偶数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class ZeroEvenOdd: def __init__(self, n): self.n = n self.lock_zero = threading.Lock() self.lock_even = threading.Lock() self.lock_odd = threading.Lock() # 初始锁住奇偶，保证先输出0 self.lock_even.acquire() self.lock_odd.acquire() def zero(self) -\u0026gt; None: for i in range(1, self.n + 1): self.lock_zero.acquire() print(0, end=\u0026#34;\u0026#34;) # 上次输出偶数，则此次释放奇数锁，使下次输出奇数 if i \u0026amp; 1 == 0: self.lock_even.release() else: self.lock_odd.release() # 偶数 def even(self) -\u0026gt; None: for i in range(2, self.n+1, 2): self.lock_even.acquire() print(i, end=\u0026#34;\u0026#34;) self.lock_zero.release() # 奇数 def odd(self) -\u0026gt; None: for i in range(1, self.n + 1, 2): self.lock_odd.acquire() print(i, end=\u0026#34;\u0026#34;) self.lock_zero.release() 死锁\n线程A获取了1号锁，等待释放2号锁；线程B获取了2号锁，等待释放1号锁\n队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 \u0026#34;\u0026#34;\u0026#34;启用两个线程实现交替打印foobar\u0026#34;\u0026#34;\u0026#34; class FooBar: def __init__(self, n): self.n = n self.queue_bar = Queue() self.queue_foo = Queue() self.queue_foo.put(\u0026#39;\u0026#39;)\t# 初始foo队列放入一个元素保证首先输出foo def foo(self): for i in range(self.n): self.queue_foo.get()\t# 默认阻塞，队列为空时会一直阻塞 print(\u0026#34;foo\u0026#34;, end=\u0026#34;\u0026#34;) self.queue_bar.put(\u0026#39;\u0026#39;)\t# 默认会阻塞直至队列有空位 def bar(self): for i in range(self.n): self.queue_bar.get() print(\u0026#34;bar\u0026#34;, end=\u0026#34;\u0026#34;) self.queue_foo.put(\u0026#39;\u0026#39;) if __name__ == \u0026#34;__main__\u0026#34;: n = int(input()) c = FooBar(n) foo = threading.Thread(target=c.foo, name=\u0026#39;thread_foo\u0026#39;) bar = threading.Thread(target=c.bar, name=\u0026#39;thread_bar\u0026#39;) foo.start() bar.start() 线程池 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from threading import RLock, Lock from concurrent.futures import ThreadPoolExecutor, as_completed class FooBar: def __init__(self, n): self.n = n self.queue_bar = Queue() self.queue_foo = Queue() self.queue_foo.put(\u0026#39;\u0026#39;) def foo(self): for i in range(self.n): # 默认阻塞，队列为空时会一直 self.queue_foo.get() time.sleep(0.3) print(\u0026#34;foo\u0026#34;, end=\u0026#34;\u0026#34;) self.queue_bar.put(\u0026#39;\u0026#39;) return \u0026#34;print foo has completed\u0026#34; def bar(self): for i in range(self.n): # 队列为空时会阻塞 self.queue_bar.get() time.sleep(0.3) print(\u0026#34;bar\u0026#34;, end=\u0026#34;\u0026#34;) self.queue_foo.put(\u0026#39;\u0026#39;) return \u0026#34;print bar has completed\u0026#34; def print_threading(): n = int(input()) c = FooBar(n) target = [c.foo, c.bar] futures = [] with ThreadPoolExecutor(max_workers=2) as pool: futures = [pool.submit(i) for i in target] # 获取返回结果，as_completed不用等待所有任务完成即可返回结果 for f in as_completed(futures): data = f.result() print(data) \u0026#34;\u0026#34;\u0026#34; timeit库的用法 stmt:传入要测试的代码或函数 setip:运行时的变量、模块等，多行语句用;隔开 number:运行次数，默认100万次 \u0026#34;\u0026#34;\u0026#34; execute_time = timeit.timeit(stmt=\u0026#39;print_threading()\u0026#39;, setup=\u0026#39;from __main__ import print_threading\u0026#39;, number=1,) print(execute_time) 以下是Python中进程间通信（IPC）的6种主要方式及其核心区别分析，结合代码示例和性能对比：\n进程通信 管道（Pipe） 特点\n单向通信（需两根管道实现双向） 基于内核缓冲区（默认大小64KB） 只能用于父子进程 示例代码\n1 2 3 4 5 6 7 8 9 10 11 12 from multiprocessing import Pipe, Process def worker(conn): conn.send(\u0026#34;子进程消息\u0026#34;) print(\u0026#34;Worker收到:\u0026#34;, conn.recv()) parent_conn, child_conn = Pipe() p = Process(target=worker, args=(child_conn,)) p.start() print(\u0026#34;主进程收到:\u0026#34;, parent_conn.recv()) # 阻塞接收 parent_conn.send(\u0026#34;主进程回复\u0026#34;) p.join() 性能\n传输速度：~500MB/s（本地测试） 适用场景：少量数据、父子进程通信 队列（Queue） 特点\n线程/进程安全 先进先出（FIFO） 底层使用Pipe+锁实现 示例代码\n1 2 3 4 5 6 7 8 9 10 11 12 from multiprocessing import Queue, Process def worker(q): q.put([42, None, \u0026#39;hello\u0026#39;]) print(\u0026#34;Worker收到:\u0026#34;, q.get()) q = Queue() p = Process(target=worker, args=(q,)) p.start() print(\u0026#34;主进程收到:\u0026#34;, q.get()) # 阻塞获取 q.put(\u0026#34;主进程消息\u0026#34;) p.join() 性能对比\n操作 10万次耗时 单进程操作 0.8s 多进程操作 1.5s 共享内存（Shared Memory） 特点\n最快IPC方式 需要处理同步问题 支持基础类型和数组 示例代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from multiprocessing import Process, Value, Array def worker(n, a): n.value = 3.1415926 a[0] = 888 num = Value(\u0026#39;d\u0026#39;, 0.0) # \u0026#39;d\u0026#39;表示double arr = Array(\u0026#39;i\u0026#39;, range(10)) # \u0026#39;i\u0026#39;表示int p = Process(target=worker, args=(num, arr)) p.start() p.join() print(num.value) # 输出: 3.1415926 print(arr[:]) # 输出: [888, 1, 2, ...] 数据类型映射\n类型码 C类型 Python类型 \u0026lsquo;c\u0026rsquo; char str \u0026lsquo;i\u0026rsquo; int int \u0026lsquo;f\u0026rsquo; float float 信号量（Semaphore） 特点\n控制资源访问数量 不传输实际数据 跨进程同步 示例代码\n1 2 3 4 5 6 7 8 9 10 11 12 from multiprocessing import Semaphore, Process import time def worker(sem, i): with sem: print(f\u0026#34;进程{i}进入临界区\u0026#34;) time.sleep(2) sem = Semaphore(3) # 允许3个进程同时访问 procs = [Process(target=worker, args=(sem, i)) for i in range(5)] for p in procs: p.start() for p in procs: p.join() Socket通信 特点\n可跨网络通信 支持多机分布式 协议可定制 本地IPC示例\n1 2 3 4 5 6 7 8 9 10 11 12 # server.py import socket sock = socket.socket(socket.AF_UNIX) sock.bind(\u0026#39;/tmp/ipc_socket\u0026#39;) sock.listen(1) conn, _ = sock.accept() print(conn.recv(1024)) # client.py sock = socket.socket(socket.AF_UNIX) sock.connect(\u0026#39;/tmp/ipc_socket\u0026#39;) sock.send(b\u0026#34;Hello via Unix Socket\u0026#34;) 信号（Signal） 特点\n异步事件通知 有限信号类型 不适用数据交换 示例代码\n1 2 3 4 5 6 7 8 9 10 11 import signal from multiprocessing import Process import os def handler(signum, frame): print(\u0026#34;收到信号:\u0026#34;, signum) signal.signal(signal.SIGUSR1, handler) p = Process(target=lambda: os.kill(os.getppid(), signal.SIGUSR1)) p.start() p.join() 综合对比 方式 传输数据 速度 复杂度 适用场景 管道 支持 快 低 父子进程简单通信 队列 支持 中 中 多生产者-消费者模型 共享内存 支持 极快 高 大数据量高速交换 信号量 不支持 - 中 资源访问控制 Socket 支持 慢 高 跨网络/分布式通信 信号 不支持 即时 高 紧急事件通知 选型建议 需要高性能数据共享 → 共享内存 + 信号量同步 简单任务分发 → 队列 跨机器通信 → Socket 进程控制 → 信号 父子进程通信 → 管道 性能优化技巧 共享内存减少数据拷贝：\n1 2 3 # 使用RawArray避免锁开销 from multiprocessing import RawArray data = RawArray(\u0026#39;d\u0026#39;, 1000000) # 100万个double 批量传输替代频繁小数据：\n1 2 3 4 5 6 # 差: 频繁发送小消息 for i in range(1000): queue.put(i) # 优: 批量发送 queue.put(list(range(1000))) 使用multiprocessing.connection模块提升管道性能：\n1 2 from multiprocessing.connection import Pipe high_speed_pipe = Pipe(duplex=True, rnonblock=True) 根据实际场景选择最适合的IPC方式，可显著提升Python多进程程序的效率。\n协程 单线程完成多任务，多个任务按照一定顺序交替执行\nawait后面接可等待对象：croutine\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import asyncio async def task1(x): print(\u0026#34;task1开始\u0026#34;) # 执行到await时任务被挂起，跳到下一个任务 await asyncio.sleep(1) print(\u0026#34;task1完成\u0026#34;) return x ** 2 async def task2(y): print(\u0026#34;task2开始\u0026#34;) await asyncio.sleep(2) print(\u0026#34;task2完成\u0026#34;) return y ** 2 async def main(): # 协程对象，该对象不能直接运行 coroutine1 = task1(2) coroutine2 = task2(3) await asyncio.gather(coroutine1, coroutine2) # python3.7版本及之后实现协程 asyncio.run(main()) # python3.7之前 # # 事件循环对象 # loop = asyncio.get_event_loop() # # 将协程对象注册到事件循环中，并执行 # ret = loop.run_until_complete(coroutine1) # print(ret) 如何选择？ CPU Bound =\u0026gt; Multi Processing I/O Bound, Fast I/O, Limited Number of Connections =\u0026gt; Multi Threading I/O Bound, Slow I/O, Many connections =\u0026gt; Asyncio 常用库 内置库 zipfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import os from zipfile import ZipFile def make_zip(target, output): \u0026#34;\u0026#34;\u0026#34; 压缩文件夹或单个文件 :param target: 要打包的文件路径. /xx/xx/file :param output: 输出路径. /xx/xx/file.zip :return: \u0026#34;\u0026#34;\u0026#34; if not os.path.exists(target): raise FileNotFoundError with ZipFile(output, \u0026#39;w\u0026#39;) as zf: if os.path.isfile(target): # 打包单个文件 zf.write(target, os.path.relpath(target, start=os.path.dirname(target))) else: # 打包目录 for parent, dirnames, filenames in os.walk(target): for filename in filenames: filepath = os.path.join(parent, filename) relpath = os.path.relpath(filepath, start=os.path.dirname(target)) zf.write(filepath, relpath) def unzip(target, output): \u0026#34;\u0026#34;\u0026#34; 解压文件夹 :param target: 压缩文件路径 :param output: 输出路径 :return: \u0026#34;\u0026#34;\u0026#34; if not os.path.exists(output): os.makedirs(output) with ZipFile(target, mode=\u0026#39;r\u0026#39;) as zf: zf.extractall(output) # make_zip(\u0026#34;E:/uploads/files\u0026#34;, \u0026#34;E:/uploads/files.zip\u0026#34;) unzip(\u0026#34;E:/uploads/files.zip\u0026#34;, \u0026#34;E:/uploads/output\u0026#34;) 三方库 Tenacity 错误重试 https://tenacity.readthedocs.io/en/latest/\npathlib2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 path1 = r\u0026#34;E:\\Projects\\PythonProjects\\flask\\test.py\u0026#34; path2 = Path(r\u0026#34;E:\\Projects\\PythonProjects\\flask\\test.py\u0026#34;) # 当前目录 print(Path.cwd()) print(os.getcwd()) # 家目录 print(Path.home()) # 绝对路径 print(os.path.abspath(\u0026#34;目标绝对路径\u0026#34;)) print(os.path.realpath(\u0026#34;返回软链接的源地址\u0026#34;)) print(path2.resolve()) print(os.path.isabs(path1)) print(path2.is_absolute()) # 相对路径 Path(\u0026#34;/etc/passwd\u0026#34;).relative_to(\u0026#34;/etc\u0026#34;) # passwd os.path.relpath(\u0026#34;/etc/passwd\u0026#34;, start=os.curdir) # 文件名 print(os.path.basename(path1)) print(path2.name) # 目录名 print(os.path.dirname(path1)) print(path2.parent) # 判断文件是否存在 print(os.path.isfile(path1)) print(path2.is_file()) # 判断目录是否存在 print(os.path.isdir(path1)) print(path2.is_dir()) # 判断路径是否存在 print(os.path.exists(path1)) print(path2.exists()) # 路径拼接 print(os.path.join(\u0026#34;path1\u0026#34;, \u0026#34;path2\u0026#34;))\t# 若path2也为绝对路径，则返回path2的路径 print(path2.joinpath(path3)) print(Path(path2, \u0026#39;path3\u0026#39;, \u0026#39;file.py\u0026#39;)) print(path2/\u0026#39;file.py\u0026#39;) # 路径分割 print(os.path.split(\u0026#34;/a/b/c.py.go\u0026#34;))\t# (\u0026#39;/a/b\u0026#39;, \u0026#39;c.py\u0026#39;) print(os.path.splitext(\u0026#34;/a/b/c.py.go\u0026#34;))\t# (\u0026#39;/a/b/c.py\u0026#39;, \u0026#39;.go\u0026#39;) print(Path(\u0026#34;/a/b/c.py.go\u0026#34;).suffix)\t# [\u0026#39;.go\u0026#39;] print(Path(\u0026#34;/a/b/c.py.go\u0026#34;).suffixes)\t# [\u0026#39;.py\u0026#39;, \u0026#39;.go\u0026#39;] print(Path(\u0026#34;/a/b/c.py.go\u0026#34;).stem)\t# c.py # 路径比较 os.path.commonpath([\u0026#39;/usr/lib\u0026#39;, \u0026#39;/usr/local/lib\u0026#39;]) # \u0026#39;/usr\u0026#39; os.path.commonprefix([\u0026#39;/usr/lib\u0026#39;, \u0026#39;/usr/local/lib\u0026#39;]) # \u0026#39;/usr/l\u0026#39; # 替换用户家目录,~会被替换为系统当前的用户名 os.path.expanduser(\u0026#34;~/test.py\u0026#34;) # /{user}/test.py Path(\u0026#34;~/test.py\u0026#34;).expanduser() # /{user}/test.py # 替换windows反斜杠 path2.as_posix() # 路径模式匹配，若pattern是绝对路径，path也必须为绝对路径 path2.match(\u0026#39;/*.py\u0026#39;) # 创建 os.mkdir(\u0026#34;创建目录\u0026#34;) os.makedirs(\u0026#34;支持多级创建\u0026#34;, exist_ok=True) # exist_ok=True 存在就不创建 Path(\u0026#34;path\u0026#34;).mkdir(exist_ok=True, parents=True) # parents=True 多级创建 Path(\u0026#34;在指定路径创建文件\u0026#34;).touch() # 删除，删除的目录必须是空目录，否则会报错 os.remove(\u0026#34;只能是删除文件\u0026#34;) os.rmdir(\u0026#34;删除目录\u0026#34;) os.removedirs(\u0026#34;可以删除多级目录\u0026#34;) Path(\u0026#34;删除目录\u0026#34;).rmdir() Path(\u0026#34;删除文件，若路径是目录，则删除目录\u0026#34;).unlink() # 重命名 os.rename(\u0026#34;旧文件名\u0026#34;, \u0026#34;新文件名\u0026#34;) os.renames(\u0026#34;旧路径\u0026#34;, \u0026#34;新路径\u0026#34;) # rename的扩展，还可以重命名路径（目录名） Path(\u0026#34;旧路径\u0026#34;).rename(\u0026#34;新路径\u0026#34;) # 只能重命名文件，无法重命名路径 Path(\u0026#34;c:/Downloads/pathlib.tar.gz\u0026#34;).with_name(\u0026#34;setup.py\u0026#34;) # c:/Downloads/setup.py Path(\u0026#34;c:/Downloads/pathlib.tar.gz\u0026#34;).with_suffix(\u0026#34;.bz2\u0026#34;) # c:/Downloads/pathlib.tar.bz2 Path(\u0026#34;返回文件信息\u0026#34;).stat() with path2.open() as f: ... # 遍历目录 for root, dirs, files in os.walk(\u0026#34;深度遍历\u0026#34;, followlinks=False): # root-目录路径，dirs-当前路径下的所有目录名，files当前目录下的所有文件名 for filename in files: file_path = os.path.join(root, filename) yield file_path files = [path for path in os.listdir(\u0026#34;只遍历一级的目录和文件\u0026#34;)] files = [path for path in Path(\u0026#34;返回生成器, 只遍历一级目录和文件\u0026#34;).iterdir()] files = [path for path in Path(\u0026#34;返回生成器, 只遍历一级目录和文件，可正则过滤\u0026#34;).glob(\u0026#39;*.txt\u0026#39;)] files = [path for path in Path(\u0026#34;返回生成器, 深度遍历，[!.]会排除以点开头的文件\u0026#34;).rglob(\u0026#39;[!.]*\u0026#39;)] 设计模式 单例模式 装饰器形式\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from functools import wraps from threading import RLock def singleton(cls): \u0026#34;\u0026#34;\u0026#34;线程安全的单例装饰器\u0026#34;\u0026#34;\u0026#34; instances = {} lock = RLock() # 使用wraps可保证不会修改被装饰函数的结构，如name等 @wraps(cls) def wrapper(*args, **kwargs): \u0026#34;\u0026#34;\u0026#34; 判断是否已有实例,如无,则新建一个实例并返回; 先做不带锁的判断，再做带锁的检查，因为如果对象已经创建就没必要加锁可直接返回；第二个if是因为多个线程抢占锁，若第一个线程创建了实例，那么下一个线程进入就需要再判断一次 instance = {\u0026lt;class \u0026#39;__main__.Single\u0026#39;\u0026gt;: \u0026lt;__main__.Single object at 0x000001A96F26FE20\u0026gt;} \u0026#34;\u0026#34;\u0026#34; if cls not in instances: with lock: if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return wrapper 单例类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Singleton: instance = None lock = threading.RLock() def __init__(self, *args, **kwargs): pass def __new__(cls, *args, **kwargs): # 返回空对象 if not cls.instance: with cls.lock: if not cls.instance: cls.instance = object.__new__(cls) return cls.instance xpath xPath 用法总结整理_疯狂哈丘的博客-CSDN博客_xpath语法\n符号 含义 / 从当前节点选取直接子节点 // 从当前节点选取子孙节点 . 选取当前节点 .. 选取当前节点的父节点 @ 选取属性 //*[contains(@class,\u0026lsquo;item\u0026rsquo;)] 提取所有class包含名为item的 //*[starts-with(@class,\u0026lsquo;ul\u0026rsquo;)] 提取所有class开头为ul的 在获得目标节点同兄弟节点后\n./following-sibling::兄弟标签名[1]　下面一个兄弟节点\n./following-sibling::*[N]　下面N个性的节点\n./preceding-sibling::兄弟标签名[1]　上面一个兄弟节点\n./following-sibling::*[N] 上面N个性的节点\nDjango 1 2 3 4 创建项目 django-admin startproject keep_healthy 创建app\tpython manage.py startapp diabetes 后台启动 nohup python3 manage.py runserver 0.0.0.0:8082\u0026gt; /dev/null 2\u0026gt; /dev/null \u0026amp; nohup flask run -h 0.0.0.0 -p 5000 \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 \u0026amp; wsgi: 同步\nasgi：wsgi+异步+websocket\n部署相关\nrunserver：开发调试时使用（单进程）\nwsgi：Web服务器网关接口，用于web服务器与python应用程序交互\nuwsgi：一种二进制线路协议，用于uWSGI服务器和其他网络服务器的通信\nuWSGI：实现了uwsgi和WSGI两种协议的Web服务器（默认单进程单线程）\nasgi: 基于wsgi接口规范之上的异步服务器网关接口规范\nflowchart LR A((Browser))\u003c--\u003e|http request|B(Nginx) subgraph Web Server B\u003c--\u003e|uwsgi|C(uWSGI) end C\u003c--\u003e|wsgi|E(Django/Flask) subgraph Web Applicantion E end 生成requirements.txt依赖\npipreqs ./ --encoding=utf8\nscrapy爬虫 项目创建 创建项目scrapy startproject tutorial 执行：scrapy crawl WeiboSpider\n中间件 开启中间件settings.py\n1 2 3 #SPIDER_MIDDLEWARES = { # \u0026#39;weibo_spider.middlewares.WeiboSpiderSpiderMiddleware\u0026#39;: 543, #} ITEM_PIPELINE 作用：清理html数据、验证去重、存储到数据库\n反爬 User-Agent代理反爬（可通过fake-useragent库应对） 验证码（暴力破解、机器学习建模、selenium模拟） 数据加密（css偏移、字体加密、js跳转） 字体反爬 脚本开发 打包成可执行文件（推荐使用虚拟环境，连同模块一起打包，使得脚本能在任何机器上执行而不用再安装环境）：\npyinstaller -F 脚本文件路径\n正则表达式 只能输入汉字、英文、数字和下划线\t^[\\w\\u4e00-\\u9fa5]+$ 匹配词中的字符 1 2 3 4 5 6 s1 = \u0026#39;bar ber bir bor bur\u0026#39; s2 = \u0026#39;abecdfghijklmnopqrstuvwxyz\u0026#39; regex = [\u0026#34;b[ieo]r\u0026#34;,\u0026#34;b[^ieo]r\u0026#34;,\u0026#34;[e-o]\u0026#34;] re.findall(regex[0],string=s1) # [\u0026#39;ber\u0026#39;, \u0026#39;bir\u0026#39;, \u0026#39;bor\u0026#39;] re.findall(regex[1],string=s1) # [\u0026#39;bar\u0026#39;, \u0026#39;bur\u0026#39;] re.findall(regex[2],string=s2) # 匹配字符范围，数字同理 量词 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 * 不出现、出现一次或多次（贪婪匹配） s = \u0026#39;br ber beer beeer\u0026#39; re.findall(\u0026#34;be*r\u0026#34;, string=s) + 出现一次或多次 re.findall(\u0026#34;be+r\u0026#34;, string=s) ? 可选字符(懒惰匹配) re.findall(\u0026#34;be?r\u0026#34;, string=s) # [\u0026#39;br\u0026#39;, \u0026#39;ber\u0026#39;] {} 出现次数 re.findall(\u0026#34;be{1}r\u0026#34;, string=s) # 出现一次 re.findall(\u0026#34;be{1,}r\u0026#34;, string=s) # 至少出现一次 re.findall(\u0026#34;be{1,3}r\u0026#34;, string=s) # 出现1~3次 组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 () s=\u0026#34;ha-ha,haa-haa\u0026#34; re.findall(r\u0026#34;(ha)-\\1,(haa)-\\2\u0026#34;,string=s) # \\1引用分组 re.findall(r\u0026#34;(?:ha)-ha,(haa)-\\1\u0026#34;,string=s) # (?:)非引用分组 | 或 s = \u0026#39;cat Cat rat\u0026#39; re.findall(r\u0026#34;(C|c)at|rat\u0026#34;, string=s) ^ $行首 行尾 s = \u0026#39;1. 3 eggs, beaten3\u0026#39; re.findall(\u0026#39;^[0-9]\u0026#39;, string=s) re.findall(\u0026#39;[0-9]$\u0026#39;, string=s) 字符 \\w 字母、数字、下划线 \\w 除字母、数字和下划线之外的字符 \\d 数字 \\D 除数字之外的字符 \\s 仅匹配空白字符 \\S 匹配除空白符之外的字符 零宽断言 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 s = \u0026#39;Date: 4 Aug 3PM\u0026#39; 正向先行断言: (?=) #只匹配后面有PM的数值 re.findall(\u0026#39;\\d+(?=PM)\u0026#39;,string=s) 负向先行断言: (?!) # 数字右边不是PM re.findall(\u0026#39;\\d+(?!PM)\u0026#39;,string=s) 正向后行断言: (?\u0026lt;=) # 数字左边为$ s=\u0026#39;Product Code: 1064 Price: $5\u0026#39; re.findall(\u0026#34;(?\u0026lt;=\\$)\\d+\u0026#34;,string=s) 负向后行断言 # 数字左边不包含$ re.findall(\u0026#34;(?\u0026lt;!\\$)\\d+\u0026#34;,string=s) 标志 1 2 3 4 5 6 7 8 9 s = \u0026#39;cAt cat cat\u0026#39; regex = re.compile(r\u0026#39;cat\u0026#39;, re.IGNORECASE) re.findall(regex, string=s) s = \u0026#34;42 11.1 -2.0 rng 24\u0026#34; re.findall(\u0026#34;(?:-?\\d+\\.?\\d+\\s+)+(?=rng)\u0026#34;, s) ?: 意思是非捕获分组，可以取消分组功能，若有\u0026#39;()\u0026#39;,findall默认会捕获括号分组，search默认捕获所有分组， 案例 1 2 3 4 5 6 7 8 9 10 import re \u0026#34;\u0026#34;\u0026#34;match ip\u0026#34;\u0026#34;\u0026#34; # 如47.98.97.198 1. 匹配0-255，包括0-199和200-255：([0-1]?\\d{1,2})|(2((5[0-5])|([0-4]\\d))) 2. ‘.XXX’匹配三次：(\\.(([0-1]?\\d{1,2})|(2((5[0-5])|([0-4]\\d))))){3} reg = re.compile(\u0026#34;^(([0-1]?\\d{1,2})|(2((5[0-5])|([0-4]\\d))))(\\.(([0-1]?\\d{1,2})|(2((5[0-5])|([0-4]\\d))))){3}$\u0026#34;) vscode编辑器正则替换\n1 2 3 4 5 6 7 8 9 10 11 12 字符串：==先变换成洛必达法则适用形式== 保留双等号中间的字符串内容，替换为： :::tip 先变换成洛必达法则适用形式 ::: 查找：(==)(.*)(==) 替换：:::tip\\n\\n $2 \\n\\n::: $2表示第二个变量 数据库 MySQL 增删改查 1 2 3 4 5 6 7 # 插入 INSERT INTO 表名 (列名) VALUES (,,,) # 删除 DELETE FROM 表名 WHERE 条件 TRUNCATE TABLE 表名 # 更新 UPDATE 表名 SET 列名1=值1,列名2=值2 WHERE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #DISTINCT返回唯一不同的值 SELECT DISTINCT 列名称 FROM 表名称 #ORDER BY排序（DESC降序，ASC升序） SELECT Company, OrderNumber FROM Orders ORDER BY Company DESC, OrderNumber ASC # IFNULL如果第一个参数 为 NULL，则返回第二个参数的备用值 IFNULL( (SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 2 OFFSET 1),\t#返回从第三行开始，共1行数据 NULL) ) #join内连接，和inner join效果一样 SELECT a.NAME AS Employee FROM Employee AS a JOIN Employee AS b ON a.ManagerId = b.Id AND a.Salary \u0026gt; b.Salary # Dense_RANK() over对成绩排名，RANK() over排名时若有并列将占用下一位的位置 select Score,Dense_RANK() over (order by Score desc) as `Rank` from Scores #顺序：where——group by——having，注意group by紧跟在where最后一个限制条件后面，不能被夹在where限制条件之间。使用了having必须使用group by，但是使用group by 不一定使用having #IF(condition, value_if_true, value_if_false) #回购率：某一个时间窗口内消费的用户，在下一个时间窗口仍旧消费的占比 #复购率：在某时间窗口内重复消费用户（消费两次及以上的用户）在总消费用户中占比 索引 数据库两大神器【索引和锁】 - 掘金 (juejin.cn)\nB树 B+索引 Mysql索引分类：\n唯一索引 主键索引 联合索引 说明：\n主键不能为null，唯一索引可以为null；主键可以做为外键，唯一索引不行 经常用到where条件的字段，应该建立索引，如果where条件中是OR关系，加索引不起作用 经常用到连接的列上创建索引 创建联合索引应当把最常用的条件字段放在最左（最左前缀原则），如（age,name,sex）相当于创建了（age）（age，name）（age,name,sex）三个索引 索引不会包含有NULL值的列 最左前缀原则如果遇到范围查询(\u0026gt;、\u0026lt;、between、like左匹配)等就不能进一步匹配了\n为什么能增加查询速度？\n索引底层是一棵B+树（叶节点存放数据，其余节点存放索引。所有叶子节点之间都有一个链指针），通过二分查找，找到对应目录的数据；如果不用索引，就要遍历双向链表\n为什么降低增删改速度？\nB+树是一棵平衡树（左右子树高度差的绝对值 \u0026lt;= 1），增删改会破坏原有树的结构，要重新调整平衡\nB树和B+树的区别？\nb+树叶节点存储数据，内部节点存储索引。查询效率较b树更稳定，因为每次查询都是从根节点到叶节点，而b树在内部某个节点查询到匹配值时就停止查找了。\n同时b+树支持范围查询，叶节点之间通过指针连接，形成一个有序链表，通过遍历叶节点即可遍历整棵树\n哈希索引 直接根据哈希值定位\n不支持最左匹配原则 在有大量重复键值的情况下，存在哈希碰撞可能 不支持范围查询 如何处理哈希冲突？\n开放定址 线性探测。冲突发生时，顺序查看下一单元 平方探测 再散列 拉链法 把所有同义词存放在一个线性链表中\n事务特性 原子性：事务不可分割，要么全部成功，要么全部失败 一致性：事务执行前后数据库保持一致性状态，若执行失败需要回滚 隔离性：多个事务之间互不干扰 持久性：事务开始就不会终止，不受其他因素影响 隔离级别 READ UNCOMMITED：读未提交，其他事务只要修改了数据，即使未提交，本事务也能看到修改后的数据值 READ COMMITED：读已提交，其他事务提交了对数据的修改后，本事务就能读取到修改后的数据值 REPEATABLE READ：可重复读，无论其他事务是否修改并提交了数据，在这个事务中看到的数据值始终不受其他事务影响 Serializable：串行化，一个事务一个事务的执行 锁 按属性分：\n共享锁（读锁）：读的时候其他事务不支持修改操作 排他锁（写锁）：在对数据进行修改的时候不允许其他事务读或写 按粒度分：\n行锁（InnoDB）：开销大，加锁慢，不易冲突 表锁（InnoDB、MyISAM）：开销小，加锁快，易冲突 乐观锁：读某个字段，处理完业务逻辑开始更新之前，需要查看字段值是否变化，如果不一样则拒绝更新\n悲观锁：数据库层面加锁，select语句之后加上for update\n行锁表锁都是悲观锁；MyISAM不支持事务\n死锁解决：\n以固定的顺序访问表和行。比如对两个job批量更新的情形，简单方法是对id列表先排序，后执行，这样就避免了交叉等待锁的情形；将两个事务的sql顺序调整为一致，也能避免死锁。 大事务拆小。大事务更倾向于死锁，如果业务允许，将大事务拆小。 在同一个事务中，尽可能做到一次锁定所需要的所有资源，减少死锁概率。 降低隔离级别。如果业务允许，将隔离级别调低也是较好的选择，比如将隔离级别从RR调整为RC，可以避免掉很多因为gap锁造成的死锁。 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁，死锁的概率大大增大。 执行流程 视图 触发器 游标 触发器：trigger，是指事先为某张表绑定一段代码，当表中的某些内容发生改变（增、删、改）的时候，系统会自动触发代码并执行。\n游标：类似一个指针，可对结果集进行处理，它总是指向某一行来对其进行操作\n查询优化 分库分表\n水平\n库和表的数据、结构都一样，无交集\n垂直\n分库：库结构、数据不一样，所有库的并集为全量数据 分表：表结构、数据不一样，至少有一列交集用于关联数据，所有表的并集为全量数据 字段避免null,可以使用0或其他数字代替null\n单表字段不要太多\n加索引\nSQL语句优化：limit、避免select *、小表 join 大表\n问题排查 1 2 3 4 5 6 7 8 9 10 11 12 13 查看当前Mysql所有的进程,找到最耗时的进程 show processlist; 查看Mysql的最大缓存 show global variables like \u0026#34;global max_allowed_packet\u0026#34; 查看当前正在进行的事务 select * from information_schema.INNODB_TRX 查看当前Mysql的连接数 show status like \u0026#39;thread%\u0026#39; top查看服务器的CPU占用率 设计规范 密码字段 账户密码存储设计\n动态盐+非固定加密算法\nsalt + cryption_algorithm + value\n（每次注册产生的盐值+注册日期）+ 加密算法 + 加密后的字符串\n时间字段 最好使用datetime，而不是timestamp（存在性能问题）\n配置指定时区，而不是使用操作系统的时区，配置文件中设置参数time_zone=\u0026rsquo;+08:00\u0026rsquo;\n每张业务核心表都增加一个datetime类型的last_modify_date字段，并设置修改自动更新机制\n范式准则 1NF：字段具有原子性，不能再分 2NF：每行必须被唯一区分，每张表使用BIGINT类型作为主键 3NF：每列都和主键列直接相关，不存在传递依赖 主键设计 自增主键\n核心业务表推荐使用UUID或业务自定义主键，非核心业务表可以用自增键做主键，原因如下：\n8.0版本之前自增存在回溯问题 自增值在服务器端产生，存在并发性能问题 自增值做主键，只能在当前实例中保证唯一，不能保证全局唯一 公开数据值，容易引发安全问题 MGR（mysql group replication）可能引起的性能问题 分布式架构设计问题 UUID主键设计\nUUID全局唯一\n海量并发推荐使用UUID（分布式数据库）\n1 2 3 4 5 6 7 8 create table User ( id BINARY(16) NOT NULL, uuid CHAR(36) AS (BIN_TO_UUID(id)), PRIMARY KEY(id) ); # 插入 insert into User Values (UUID_TO_BIN(UUID(), True),......) MongoDB 1 2 3 # 删除集合中所有数据 use \u0026lt;database\u0026gt; db.\u0026lt;collection\u0026gt;.deleteMany({}) 计算机网络 http状态码 状态码 含义 举例 1xx 信息性状态码 2xx 成功状态码 200请求成功 3xx 重定向 301永久重定向（url不可更改），302暂时重定向 4xx 客户端错误 403访问权限问题，404资源没找到 5xx 服务器错误 500服务器请求错误，502服务器网关错误，504网关超时， OSI七层模型 名称 作用 举例 应用层 访问网络服务的接口 HTTP、FTP(20/21)、smtp(25)、Telnet（23）、DNS（53） 表示层 提供数据格式转换服务 url加密、口令加密、图片编码解码 会话层 在通信双方之间建立、管理和终止会话 服务器验证登录 传输层 提供可靠的端到端的报文传输和差错控制 TCP、UDP 网络层 定义数据逻辑链路，转发数据 路由器、多层交换机、防火墙 数据链路层 建立数据链路连接 网卡、网桥、二层交换机 物理层 提供原始比特流的传输的通路 网线、集线器 URI和URL的区别：\nURI：统一资源标识符。由命名机制(如com、gov等)、ip、资源路径组成\nURL：统一资源定位符。由协议（http、https）、ip、端口号、资源路径、参数、片段组成\nURL相当于继承URI，是URI的扩展\n常见加密算法 对称（加密与解密密钥相同） DES、AES 非对称（加密与解密密钥不同，公钥和私钥加解密） RSA 散列（不需要密钥） SHA-1 MD-5（不可逆、密码值固定不安全） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import hashlib m = hashlib.md5() m.update(str.encode(\u0026#34;utf8\u0026#34;)) print(m.hexdigest()) sha1 = hashlib.sha1() data = \u0026#39;helloword\u0026#39; sha1.update(data.encode(\u0026#39;utf-8\u0026#39;)) sha1_data = sha1.hexdigest() print(sha1_data) def get_rsa(string): #1-1 实例化加密对象 (pubkey,privkey)=rsa.newkeys(1024) #1-2 公钥加密1 pwd1=rsa.encrypt(string,pubkey) print(\u0026#39;加密后结果1为：\u0026#39;,pwd1.hex()) #1-3 私钥解密1 depwd1=rsa.decrypt(pwd1,privkey) print(\u0026#39;解密后的结果1为：\u0026#39;,depwd1.decode()) get_rsa( b\u0026#34;15863274538\u0026#34;) Docker 镜像层可读，容器层可写\n常用命令 1 2 3 systemctl start/stop/restart docker systemctl status docker\t查看docker运行状态 systemctl enable docker\t开机启动 Command Description docker images 查看所有镜像 docker search 查找镜像 docker search \u0026ndash;limit 5 rabbitmq 查找前5个 docker pull rabbitmq 下载镜像（默认最新版本） docker pull redis:6.0.8 指定版本号下载 docker rmi 镜像id 删除镜像 docker system df 查看镜像/容器/数据卷占用空间 docker ps -a 查看所有容器 docker ps -f status=exited 查看停止的容器 docker run -it \u0026ndash;name=容器名称 镜像名称:TAG /bin/bash 以交互式创建容器 docker run -it -p 192.168.1.100:8080:80 将容器的TCP80端口映射到IP地址为192.168.1.100宿主机的8080端口 docker run -d 容器名称 以守护式创建容器 docker start/restart/stop/kill 容器id 启动/重启/停止/强制停止容器 docker rm 容器id 删除容器 docker inspect 容器id 查看容器内部细节 docker top 容器id 查看当前容器内的进程信息 docker exec -it 容器id /bin/bash 以交互式重新进入容器（exit退出后不会停止） docker attach 容器id /bin/bash 进入容器（exit退出后容器停止） docker cp 容器id:容器内路径 目标机器路径 拷贝容器文件到主机上 docker export 容器id \u0026gt; 文件名.tar 导出容器 cat 文件名.tar | docker import - 镜像用户/镜像名：版本号 导入容器 docker commit -m=\u0026ldquo;提交信息\u0026rdquo; -a=\u0026ldquo;作者\u0026rdquo; 容器id 目标镜像名:TAG 提交容器 docker build -t 镜像名:TAG 构建新镜像 DockerFile 用途：用来构建docker镜像的文本文件\n每条保留字指令都必须为大写字母且后面要跟随至少一个参数 指令按照从上到下顺序执行 每条指令都会创建一个新的镜像层并提交 常用保留字 Word Description FROM 基于已存在的某个镜像作为基础镜像 MAINTAINER 镜像维护者的姓名、邮箱 RUN 容器build时执行的命令 EXPOSE 当前容器对外暴露出的端口 WORKDIR 进入容器后的默认目录 USER 指定镜像执行的用户，默认root ENV MY_PATH /home/yuqiuwen 设置环境变量 VOLUME 容器数据卷 ADD 将宿主机目录下的文件拷贝到镜像，并处理URL自动解压 COPY 拷贝文件和目录到新的镜像层中 CMD 容器启动后要执行的操作（会被docker run之后添加的额外参数覆盖） ENTRYPOINT 同CMD，但不会被docker run的参数覆盖 Git 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 git branch 查看本地所有分支 git status 查看当前状态 git commit 提交 git branch -a 查看所有的分支 git branch -r 查看远程所有分支 git commit -am \u0026#34;init\u0026#34; 提交并且加注释 git remote add origin git@192.168.1.119:ndshow git push origin master 将文件给推到服务器上 git remote show origin 显示远程库origin里的资源 git push origin master:develop git push origin master:hb-dev 将本地库与服务器上的库进行关联 git checkout --track origin/dev 切换到远程dev分支 git branch -D master develop 删除本地库develop git checkout -b dev 建立一个新的本地分支dev git merge origin/dev 将分支dev与当前分支进行合并 git checkout dev 切换到本地dev分支 git remote show 查看远程库 git add . git rm 文件名(包括路径) 从git中删除指定文件 git clone git://github.com/schacon/grit.git 从服务器上将代码给拉下来 git config --list 看所有用户 git ls-files 看已经被提交的 git rm [file name] 删除一个文件 git commit -a 提交当前repos的所有的改变 git add [file name] 添加一个文件到git index git commit -v 当你用－v参数的时候可以看commit的差异 git commit -m \u0026#34;This is the message describing the commit\u0026#34; 添加commit信息 git commit -a -a是代表add，把所有的change加到git index里然后再commit git commit -a -v 一般提交命令 git log 看你commit的日志 git diff 查看尚未暂存的更新 git rm a.a 移除文件(从暂存区和工作区中删除) git rm --cached a.a 移除文件(只从暂存区中删除) git commit -m \u0026#34;remove\u0026#34; 移除文件(从Git中删除) git rm -f a.a 强行移除修改后文件(从暂存区和工作区中删除) git diff --cached 或 $ git diff --staged 查看尚未提交的更新 git stash push 将文件给push到一个临时空间中 git stash pop 将文件从临时空间pop下来 ","date":"2023-01-07T00:00:00Z","permalink":"/p/python-notes/","title":"Python \u0026 后端"},{"content":"协程 单线程完成多任务，多个任务按照一定顺序交替执行。\nawait后面为可等待对象，包含三类：\ncoroutine: 协程函数的返回对象\nTask: 任务，对协程进一步封装，其中包含了任务的各种状态\nFuture：最底层的对象，表示一个异步操作的最终结果\n如何创建任务？\n需要在另一个协程函数中创建，可以使用以下两种方式:\ncreate_task: 参数对象必须是coroutine\nensure_future: coroutine或future皆可\n最后通过asyncio.get_event_loop()将协程对象注册到事件循环中，并执行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import asyncio async def task1(x): print(\u0026#34;task1开始\u0026#34;) # 执行到await时任务被挂起，跳到下一个任务 await asyncio.sleep(1) print(\u0026#34;task1完成\u0026#34;) return x ** 2 async def task2(y): print(\u0026#34;task2开始\u0026#34;) await asyncio.sleep(2) print(\u0026#34;task2完成\u0026#34;) return y ** 2 async def main(): # 协程对象，该对象不能直接运行 coroutine1 = task1(2) coroutine2 = task2(3) await asyncio.gather(coroutine1, coroutine2) # python3.7版本及之后实现协程 asyncio.run(main()) # python3.7之前 # # 事件循环对象 # loop = asyncio.get_event_loop() # # 将协程对象注册到事件循环中，并执行 # ret = loop.run_until_complete(coroutine1) # print(ret) 案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 \u0026#34;\u0026#34;\u0026#34; In order to solve the problem that github cannot be accessed, by replacing ip in the hosts file \u0026#34;\u0026#34;\u0026#34; import ctypes import os, sys import traceback import aiohttp import asyncio from fake_useragent import UserAgent from lxml import etree from functools import partial import shutil ua = UserAgent(use_external_data=True) ip_dict = dict() def build_headers(): headers = { \u0026#39;use_agent\u0026#39;: ua.random } return headers async def fetch_latest_ip(url): async with aiohttp.ClientSession(headers=build_headers()) as session: async with session.get(url) as response: result = await response.text() html = etree.HTML(result) ip = html.xpath(\u0026#34;//th[text()=\u0026#39;IPv4 Addresses\u0026#39;]/following-sibling::td//text()\u0026#34;) print(f\u0026#34;正在获取ip: {url}\\n解析完成: {ip}\u0026#34;) return ip async def main(): base_url = \u0026#39;https://www.ipaddress.com/site/\u0026#39; targets = ( \u0026#39;github.com\u0026#39;, \u0026#39;assets-cdn.github.com\u0026#39;, \u0026#39;github.global.ssl.fastly.net\u0026#39; ) tasks = [] for url in targets: task = asyncio.ensure_future(fetch_latest_ip(base_url + url)) # 回调 task.add_done_callback(partial(parse, url)) tasks.append(task) await asyncio.gather(*tasks) def parse(url, task): ip_dict[url] = task.result() def save(): host = r\u0026#39;C:/Windows/System32/drivers/etc/hosts\u0026#39; temp = r\u0026#39;./temp\u0026#39; try: if not os.path.exists(host): print(\u0026#34;hosts文件不存在\u0026#34;) return False with open(host, mode=\u0026#39;r\u0026#39;) as f1, open(temp, mode=\u0026#39;w\u0026#39;) as f2: lines = f1.readlines() for line in reversed(lines): if \u0026#39;github.\u0026#39; in line: lines.remove(line) for name in ip_dict: for ip in ip_dict[name]: lines.append(f\u0026#34;{ip} {name}\\n\u0026#34;) # 提取一行中的ip和name # ip, name = re.split(r\u0026#34;\\s+\u0026#34;, line) f2.writelines(lines) os.remove(host) shutil.copy(temp, host) os.system(\u0026#34;ipconfig/flushdns\u0026#34;) print(\u0026#34;DNS刷新成功\u0026#34;) return True except Exception: traceback.print_exc() return False if __name__ == \u0026#39;__main__\u0026#39;: try: is_admin = ctypes.windll.shell32.IsUserAnAdmin() if not is_admin: # 解决无权访问的问题，重新以管理员权限运行 ctypes.windll.shell32.ShellExecuteW(None, \u0026#34;runas\u0026#34;, sys.executable, __file__, None, 1) # 这里如果使用asyncio.run(main())会报错，不知道原因 loop = asyncio.get_event_loop() loop.run_until_complete(main()) ret = save() if ret: print(\u0026#34;Succeeded!!\u0026#34;) else: print(\u0026#34;Failed!!\u0026#34;) except Exception as e: traceback.print_exc() print(\u0026#34;Failed!!\u0026#34;) finally: input(\u0026#34;\\nPlease press Enter to exit!\u0026#34;) 脚本打包成可执行文件 推荐使用虚拟环境，连同模块一起打包，使得脚本能在任何机器上执行而不用再安装环境\n安装pipenv\n1 2 3 4 5 6 7 8 # 创建虚拟环境 pipenv --python 3.8 # 查看依赖包 pipenv list # 打包 pyinstaller -F 脚本文件路径 ","date":"2023-01-07T00:00:00Z","permalink":"/p/python-asyncio/","title":"Python Asyncio"},{"content":"算法 查找 二分查找 1 2 3 4 5 6 7 8 9 10 11 12 13 def binsearch(arr, target): low, high = 0, len(arr)-1 while low \u0026lt;= high: mid = (low + high) // 2 if target == arr[mid]: return mid elif target \u0026gt; arr[mid]: low = mid + 1 else: high = mid - 1 return -1 排序 冒泡 1 2 3 4 5 6 7 8 9 10 \u0026#34;\u0026#34;\u0026#34; 冒泡排序:每趟能确定一个元素的最终位置 \u0026#34;\u0026#34;\u0026#34; def bubble_sort(data): n = len(data) for i in range(n-1): for j in range(n-i-1): if data[j] \u0026gt; data[j+1]: data[j], data[j+1] = data[j+1], data[j] print(data) 快排 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def quick_sort(arr): # 用栈保存区间左右边界，右边界先入栈 stack = [len(arr)-1, 0] while stack: low, high = stack.pop(), stack.pop() p = partition(arr, low, high) # 基准元素的左区间存在则入栈 if p \u0026gt; low: stack.append(p - 1) stack.append(low) # 基准元素的右区间存在则入栈 if high \u0026gt; p: stack.append(high) stack.append(p + 1) return arr def partition(arr, low, high): # 选取第一个元素作为基准元素 p = arr[low] while low \u0026lt; high: # 比基准元素小的移到前面 while low \u0026lt; high and arr[high] \u0026gt;= p: high -= 1 arr[low] = arr[high] # 比基准元素大的移到后面 while low \u0026lt; high and arr[low] \u0026lt;= p: low += 1 arr[high] = arr[low] # 将基准元素放到最终的位置 arr[low] = p return low 选择 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026#34;\u0026#34;\u0026#34; 选择排序：每趟（i）从后面的n-i+1个待排元素选择1个最小的作为最终第i个元素的值 \u0026#34;\u0026#34;\u0026#34; def select_sort(arr): n = len(arr) for i in range(n-1): min = i for j in range(i+1, n): if arr[j] \u0026lt; arr[min]: min = j # 若第一个数字不是最小值，则交换位置 if min != i: arr[i], arr[min] = arr[min], arr[i] return arr 归并 ==思路==：\n针对上图序列A（3,9）和序列B（1,4），指针i指向3，j指向1 1 \u0026lt; 3，1添加到temp，同时指针j后移 3 \u0026lt; 4，3添加到temp，同时指针i后移 4 \u0026lt; 9，4添加到temp，同时指针j后移，此时j=8 \u0026gt; 右边界r=7,跳出循环 此时i=5,j=8,mid=5,l=4,r=7，将剩下的序列A中的元素添加到temp列表中 将temp中的所有元素赋给原序列arr对应位置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 def merge(arr, l, r, mid): temp = [] i, j = l, mid + 1 while i \u0026lt;= mid and j \u0026lt;= r: if arr[i] \u0026lt; arr[j]: temp.append(arr[i]) i += 1 else: temp.append(arr[j]) j += 1 print(\u0026#39;-----------\u0026#39;) print(f\u0026#39;i:{i} j:{j} mid:{mid} l:{l} r:{r}\u0026#39;) # 此时其中一序列（如1,3,4）已经全部按顺序添加到temp中，另一序列还剩9,只需将剩下的添加到temp while i \u0026lt;= mid: temp.append(arr[i]) i += 1 while j \u0026lt;= r: temp.append(arr[j]) j += 1 print(temp) # 此时子序列已排好序，直接将temp中排好序的元素赋给arr对应位置 arr[l: r + 1] = temp print(arr) def merge_sort(arr, l, r): if l \u0026lt; r: mid = (l+r) // 2 merge_sort(arr, l, mid) merge_sort(arr, mid+1, r) merge(arr, l, r, mid) return arr 基础 查找第一个不重复的字符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def first_char(arr): d = {} \u0026#34;\u0026#34;\u0026#34;记录每个字符的出现次数\u0026#34;\u0026#34;\u0026#34; for i, v in enumerate(arr): if v in d: d[v] += 1 else: d[v] = 1 print(d) \u0026#34;\u0026#34;\u0026#34;再次遍历返回次数为1的元素\u0026#34;\u0026#34;\u0026#34; for i, v in enumerate(arr): if d[v] == 1: return v print(first_char([\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;a\u0026#39;])) 动态规划 01背包问题 从n个物品中选取一些物品（物品下标为i，背包容量为j），使得容量为s的背包包含的物品总价值最大。\ngraph LR A[情形]--\u003e装不下--\u003e前i个和前i-1个物品的总价值最佳组合相同; A--\u003eB[装得下]; B--\u003e|不装|前i个和前i-1个物品的总价值最佳组合一样; B--\u003e|装|总价值等于前i-1个物品的价值+当前物品的价值; graph LR A([\"dp[i][j] =\"])--\u003e|装不下|D([\"dp[i-1][j]\"]); A--\u003e|装得下|C([\"max( dp[i-1][j-w[i]] + v[i], dp[i-1][j] )\"]); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 使用滚动数组优化： # dp[j]表示容量为j的背包所装的最大价值 dp[j] = max(dp[j], dp[j-w[i]]+v[i]) def backpack(): # 使用滚动数组dp[j]取代二维数组，表示容量为j的背包所装的最大价值 w = [2, 3, 4, 6, 5] # 重量 v = [4, 5, 3, 3, 6] # 价值 s = 8 # 最大承受重量 n = len(w) # 物品个数 dp = [0] * (s+1) obj_list = [] dp1 = [[0]*(s+1) for i in range(n+1)] # 二维数组方法 for i in range(1, n+1): for j in range(s+1): if j \u0026gt;= w[i-1]: dp1[i][j] = max(dp1[i-1][j], dp1[i-1][j-w[i-1]] + v[i-1]) else: dp1[i][j] = dp1[i-1][j] print(dp1[-1]) # 一维滚动数组 for i in range(n): for j in range(s, w[i]-1, -1):\t# 倒序遍历背包 dp[j] = max(dp[j], dp[j-w[i]] + v[i]) return dp 礼物最大价值 在一个 m*n 的棋盘的每一格都放有一个礼物，每个礼物都有一定的价值（价值大于 0）。你可以从棋盘的左上角开始拿格子里的礼物，并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值，请计算你最多能拿到多少价值的礼物？\n链接：https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof\n1 2 3 4 5 6 7 8 9 10 11 12 13 def maxValue(grid: List[List[int]]) -\u0026gt; int: width = len(grid[0]) height = len(grid) dp = [[0]*(width+1) for _ in range(height+1)] # 注意遍历顺序需要和i,j对应 for i in range(1, height+1): for j in range(1, width+1): dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1] return dp[height][width] maxValue([[1,2,5],[3,2,1]]) 滑动窗口 给你一个整数数组 nums，有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值 。 链接：https://leetcode.cn/problems/sliding-window-maximum\n输入：nums = [1,3,-1,-3,5,3,6,7], k = 3 输出：[3,3,5,5,6,7] 解释： 滑动窗口的位置 最大值\n[1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def maxSlidingWindow(nums: List[int], k: int) -\u0026gt; List[int]: # 双端队列 q = collections.deque res = [] for i in range(len(nums)): # 单调递减栈 while q and nums[i] \u0026gt;= nums[q[-1]]: q.pop() q.append(i) # # 当队头元素超出窗口边界时，弹出队头元素 while q[0] \u0026lt;= i-k: q.popleft() # 从第二个窗口开始之后都要弹出队头元素，第一个窗口右边界k-1 if i\u0026gt;=k-1: res.append(nums[q[0]]) return res 数组和树形数据转换 1 2 3 4 5 6 data = [ {\u0026#39;id\u0026#39;: 1, \u0026#39;pid\u0026#39;: 0}, {\u0026#39;id\u0026#39;: 2, \u0026#39;pid\u0026#39;: 1}, {\u0026#39;id\u0026#39;: 7, \u0026#39;pid\u0026#39;: 2}, {\u0026#39;id\u0026#39;: 8, \u0026#39;pid\u0026#39;: 6} ] 扁平数组树形化\n1 2 3 4 5 6 7 8 9 def list2Tree(data, parent=None): output = [] for node in data: if node[\u0026#34;pid\u0026#34;] != parent: continue children = list2Tree(data, node[\u0026#34;id\u0026#34;]) node.update(children=children) output.append(node) return output 树形数据扁平化\n1 2 3 4 5 6 7 def flat_tree(data): ret = [] for item in data: ret.append({\u0026#34;id\u0026#34;: item[\u0026#34;id\u0026#34;], \u0026#34;pid\u0026#34;: item[\u0026#34;pid\u0026#34;]}) if item.get(\u0026#34;children\u0026#34;): ret.extend(flat_tree(item[\u0026#34;children\u0026#34;])) return ret 最大数字组合 有几个任意位数的正整数，求能组成的最大数字\n思路：\n若 a+b \u0026gt; b+a，则a应该在前，否则a在后\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from functools import cmp_to_key def max_number(nums): # 自定义比较函数 def compare(x, y): # 将两个字符串拼接起来进行比较 return int(y + x) - int(x + y) # 将正整数转换为字符串 nums = [str(num) for num in nums] # 使用自定义的比较函数对字符串进行排序 nums.sort(key=cmp_to_key(compare)) # 将排序后的字符串连接起来得到最大数 result = \u0026#39;\u0026#39;.join(nums) return result MySQL 基础 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 1 #DISTINCT返回唯一不同的值 SELECT DISTINCT 列名称 FROM 表名称 2#ORDER BY排序（DESC降序，ASC升序） SELECT Company, OrderNumber FROM Orders ORDER BY Company DESC, OrderNumber ASC 3# IFNULL如果第一个参数 为 NULL，则返回第二个参数的备用值 IFNULL( (SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 2 OFFSET 1),\t#返回从第三行开始，共1行数据 NULL) ) 4 #join内连接，和inner join效果一样 SELECT a.NAME AS Employee FROM Employee AS a JOIN Employee AS b ON a.ManagerId = b.Id AND a.Salary \u0026gt; b.Salary 5# Dense_RANK() over对成绩排名，RANK() over排名时若有并列将占用下一位的位置 select Score,Dense_RANK() over (order by Score desc) as `Rank` from Scores 6 #顺序：where——group by——having，注意group by紧跟在where最后一个限制条件后面，不能被夹在where限制条件之间。使用了having必须使用group by，但是使用group by 不一定使用having 7 #IF(condition, value_if_true, value_if_false) 8#回购率：某一个时间窗口内消费的用户，在下一个时间窗口仍旧消费的占比 #复购率：在某时间窗口内重复消费用户（消费两次及以上的用户）在总消费用户中占比 次日留存率 #查看用户在某天刷题后第二天还会再来刷题的平均概率。\n1 2 3 4 5 6 7 8 #先对用户去重，然后自连接，q1显示所有，q2显示留存 select count(q2.device_id)/count(q1.device_id) as avg_ret from (select distinct device_id, date from question_practice_detail) q1 left join (select distinct device_id, date from question_practice_detail) q2 on q1.device_id=q2.device_id and q2.date=date_add(q1.date,interval 1 day) ","date":"2022-11-16T00:00:00Z","permalink":"/p/leetcode/","title":"LeetCode"},{"content":"面向对象设计原则 开放封闭原则\n尽量不修改代码前提下进行扩展\n里氏替换原则\n尽量不重写父类方法，若要重写应满足以下要求：\n子类可以实现父类的抽象方法，但不能覆盖父类的非抽象方法\n子类可以增加自己特有的方法\n当子类的方法重载父类的方法时，方法的形参要比父类方法的输入参数更宽松\n当子类的方法实现父类的抽象方法时，方法的返回值应比父类更严格\n依赖倒置原则\n高层模块不应该依赖底层模块，二者都应该依赖抽象。 抽象不应该依赖细节，细节应该依赖抽象。 依赖倒置的中心思想是面向接口编程。 接口隔离原则\n不要在一个接口里面放很多的方法，这样会显得这个类很臃肿\n客户端不应依赖它不需要的接口 类间的依赖关系应该建立在最小的接口上 单一职责原则\n一个类只负责一项职责\n创建型模式 工厂方法模式 优点：\n每一个具体产品对应一个工厂类，不需要修改工厂类代码 隐藏对象创建的内部细节 缺点：\n每增加一个产品类，就必须增加一个工厂类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class GreekLocalizer: \u0026#34;\u0026#34;\u0026#34;A simple localizer a la gettext\u0026#34;\u0026#34;\u0026#34; def __init__(self) -\u0026gt; None: self.translations = {\u0026#34;dog\u0026#34;: \u0026#34;σκύλος\u0026#34;, \u0026#34;cat\u0026#34;: \u0026#34;γάτα\u0026#34;} def localize(self, msg: str) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;We\u0026#39;ll punt if we don\u0026#39;t have a translation\u0026#34;\u0026#34;\u0026#34; return self.translations.get(msg, msg) class EnglishLocalizer: \u0026#34;\u0026#34;\u0026#34;Simply echoes the message\u0026#34;\u0026#34;\u0026#34; def localize(self, msg: str) -\u0026gt; str: return msg def get_localizer(language: str = \u0026#34;English\u0026#34;) -\u0026gt; object: \u0026#34;\u0026#34;\u0026#34;Factory\u0026#34;\u0026#34;\u0026#34; localizers = { \u0026#34;English\u0026#34;: EnglishLocalizer, \u0026#34;Greek\u0026#34;: GreekLocalizer, } return localizers[language]() def main(): e, g = get_localizer(language=\u0026#34;English\u0026#34;), get_localizer(language=\u0026#34;Greek\u0026#34;) for msg in \u0026#34;dog parrot cat bear\u0026#34;.split(): print(e.localize(msg), g.localize(msg)) 抽象工厂模式 提供一个创建一系列相关或者相互依赖产品的接口\n优点：\n除工厂方法模式的优点之外，还有利于约束产品族\n缺点：\n扩展新的产品很麻烦\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 from typing import Type from abc import abstractmethod, ABCMeta class Pet(metaclass=ABCMeta): def __init__(self, name: str) -\u0026gt; None: self.name = name @abstractmethod def speak(self) -\u0026gt; None: pass @abstractmethod def __str__(self) -\u0026gt; str: pass class Dog(Pet): def speak(self) -\u0026gt; None: print(f\u0026#34;\u0026lt;{self.name}\u0026gt;汪汪~\u0026#34;) def __str__(self) -\u0026gt; str: return f\u0026#34;Dog\u0026lt;{self.name}\u0026gt;\u0026#34; class Cat(Pet): def speak(self) -\u0026gt; None: print(f\u0026#34;\u0026lt;{self.name}\u0026gt;喵喵~\u0026#34;) def __str__(self) -\u0026gt; str: return f\u0026#34;Cat\u0026lt;{self.name}\u0026gt;\u0026#34; class PetShop: def __init__(self, animal_factory: Type[Pet]) -\u0026gt; None: self.pet_factory = animal_factory def buy_pet(self, name: str) -\u0026gt; Pet: pet = self.pet_factory(name) print(f\u0026#34;Here is your lovely {pet}\u0026#34;) return pet def main() -\u0026gt; None: cat_shop = PetShop(Cat) dog_shop = PetShop(Dog) pet1 = cat_shop.buy_pet(\u0026#34;布偶\u0026#34;) pet1.speak() pet2 = dog_shop.buy_pet(\u0026#34;阿拉斯加\u0026#34;) pet2.speak() 创建者模式 将一个复杂对象的创建于它的表示分离，使得同样的构建过程可以创建不同的表示\n优点：\n将构造代码与表示代码分开，可以精细控制构建过程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 from abc import abstractmethod, ABCMeta # 抽象建造者 class Building: def __init__(self) -\u0026gt; None: self.build_floor() self.build_size() @abstractmethod def build_floor(self): pass @abstractmethod def build_size(self): pass def __repr__(self) -\u0026gt; str: return \u0026#34;Floor: {0.floor} | Size: {0.size}\u0026#34;.format(self) # 具体建造者 class House(Building): def build_floor(self) -\u0026gt; None: self.floor = \u0026#34;One\u0026#34; def build_size(self) -\u0026gt; None: self.size = \u0026#34;Big\u0026#34; # 具体建造者 class Flat(Building): def build_floor(self) -\u0026gt; None: self.floor = \u0026#34;More than One\u0026#34; def build_size(self) -\u0026gt; None: self.size = \u0026#34;Small\u0026#34; # In some very complex cases, it might be desirable to pull out the building # logic into another function (or a method on another class), rather than being # in the base class \u0026#39;__init__\u0026#39;. (This leaves you in the strange situation where # a concrete class does not have a useful constructor) class ComplexBuilding: def __repr__(self) -\u0026gt; str: return \u0026#34;Floor: {0.floor} | Size: {0.size}\u0026#34;.format(self) class ComplexHouse(ComplexBuilding): def build_floor(self) -\u0026gt; None: self.floor = \u0026#34;One\u0026#34; def build_size(self) -\u0026gt; None: self.size = \u0026#34;Big and fancy\u0026#34; # 指挥者 def construct_building(cls) -\u0026gt; Building: building = cls() building.build_floor() building.build_size() return building def main(): house = House() flat = Flat() print(house) print(flat) # Using an external constructor function: complex_house = construct_building(ComplexHouse) print(complex_house) # output Floor: One | Size: Big Floor: More than One | Size: Small Floor: One | Size: Big and fancy 原型模式 单例模式 保证一个类只有一个实例\n结构型模式 适配器模式 桥模式 组合模式 装饰模式 外观模式 享元模式 代理模式 行为型模式 解释器模式 责任链模式 命令模式 迭代器模式 中介者模式 备忘录模式 观察者模式 状态模式 策略模式 访问者模式 模板方法模式 ","date":"2022-11-06T00:00:00Z","permalink":"/p/design-patterns/","title":"设计模式"},{"content":"由于工作中经常用到这两个库，不过基本上用的fabric来实现环境部署、预检、数据上传下发等操作。因此，本文重点介绍Fabric的使用（fabric2版本）。\nparamiko paramiko是使用SSHv2协议的三方库，提供了客户端和服务端的功能。\nWelcome to Paramiko’s documentation! — Paramiko documentation\n安装 1 pip install paramiko paramiko中的几个重要组件：\nSSHClient：ssh服务器会话的高级封装，封装了Transport, Channel, and SFTPClient\nSFTPClient：基于一个已连通的Transport打开一个sftp会话，可实现对文件的操作（上传下载等）\nChannel：一种ssh传输的安全通道，类似socket\nTransport：一种协商加密的会话，会创建tunnels通道流，称为channels。多个channels可以在单个会话中多路复用\nSSHClient 连接 通过密钥连接，有两种方式： 实例化SSHClient 创建一个Transport加密通道 通过密码连接，有两种方式： 实例化SSHClient 创建一个Transport加密通道 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026#34;\u0026#34;\u0026#34;密钥连接(通过Transport连接)\u0026#34;\u0026#34;\u0026#34; private = paramiko.RSAKey.from_private_key_file(\u0026#39;/xxx/xxx\u0026#39;) transport = paramiko.Transport((host, port)) transport.connect(username=user, pkey=private) \u0026#34;\u0026#34;\u0026#34;密钥连接(通过SSHClient连接)\u0026#34;\u0026#34;\u0026#34; private = paramiko.RSAKey.from_private_key_file(\u0026#39;/xxx/xxx\u0026#39;) ssh_client = paramiko.SSHClient() ssh_client.connect(hostname=host,port=22,username=user,pkey=private) \u0026#34;\u0026#34;\u0026#34;密码连接(通过Transport连接)\u0026#34;\u0026#34;\u0026#34; transport = paramiko.Transport((host, port)) transport.connect(username=user, password=password) \u0026#34;\u0026#34;\u0026#34;密码连接(通过SSHClient连接)\u0026#34;\u0026#34;\u0026#34; ssh_client = paramiko.SSHClient() ssh_client.connect(hostname=host,port=22,username=user,password=password) 执行SSH命令 通过exec_command()方法打开一个channel在服务器上执行命令，相当于我们使用ssh客户端工具执行。\n若要执行多个命令可以通过 \u0026amp;\u0026amp;连接，或者for循环调用exec_command()，这将会打开多个channel。\n1 stdin, stdout, stderr = ssh_client.exec_command(\u0026#34;rm -i test1.txt \u0026amp;\u0026amp; cd /home\u0026#34;) 执行完成后会一次性返回stdout和stderr，同时可以通过stdout.channel.recv_exit_status()获取执行结果，该方法是一个阻塞方法，如果想实时获取日志输出可以封装一下，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def exec_cmd(ssh_client, commands, f): \u0026#34;\u0026#34;\u0026#34; :param commands: 待执行命令列表 :param f: 文件对象 :return: 执行状态 \u0026#34;\u0026#34;\u0026#34; status = [] for cmd in commands: stdin, stdout, stderr = ssh_client.exec_command(cmd) f.write(f\u0026#34;start run command: {cmd}\\n\u0026#34;) while True: err, out = stderr.readline().strip(), stdout.readline().strip() if not (out or err): break if out: f.write(out + \u0026#34;\\n\u0026#34;) if err: f.write(err + \u0026#34;\\n\u0026#34;) status.append(stdout.channel.recv_exit_status()) return status 关闭连接 ssh_client.close()\ntransport.close()\nSFTPClient 创建SFTP客户端通道 1 2 3 4 5 6 7 8 9 10 transport = paramiko.Transport((host, port)) transport.connect(username=user, password=password) # 基于已连通的transport创建一个SFTP客户端channel # sftp = paramiko.SFTPClient.from_transport(transport) sftp = transport.open_sftp_client() # 或者通过SSHClient创建 ssh_client = paramiko.SSHClient() ssh_client._transport = transport sftp = ssh_client.open_sftp() 常用操作 listdir(path)：遍历远程路径下的文件目录（只能遍历一级目录和文件）\nlistdir_attr(path)：遍历远程路径下的文件目录，并且可以获取每个文件的详细属性\nremove(path)：删除文件\nrmdir(path)：删除目录\nmkdir(path)：创建目录\nstat(path)：获取文件属性信息（若为软链接文件，获取的是指向的源文件）\nlstat(path)：获取文件属性信息（若为软链接文件，获取的是自身文件）\nget(remotepath, localpath)：下载\nput(localpath, remotepath)：上传\n检查远程文件是否存在\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def normpath(path): \u0026#34;\u0026#34;\u0026#34; 由于windows和linux操作系统不同，路径格式会出现不统一的情况，反斜杠不处理的话会出现很多问题 替换windows路径中的\\ :param path: :return: \u0026#34;\u0026#34;\u0026#34; if isinstance(path, Path): path = str(path) return path.replace(\u0026#39;\\\\\u0026#39;, \u0026#39;/\u0026#39;) def check_remote_path(remote_path, is_mkdir=False): \u0026#34;\u0026#34;\u0026#34; 判断目标机器路径是否存在 :param remote_path: :param is_mkdir: 若为True，则会创建此路径 :return: \u0026#34;\u0026#34;\u0026#34; remote_path = normpath(remote_path) try: sftp.lstat(remote_path) return True except FileNotFoundError: if is_mkdir: sftp.mkdir(remote_path) else: return False 判断远程路径是否为目录\n1 2 3 4 5 6 7 8 def remote_path_isdir(remote_path): \u0026#34;\u0026#34;\u0026#34; 检查一个远程路径是否为目录 :param remote_path: :return: \u0026#34;\u0026#34;\u0026#34; attr = sftp.lstat(normpath(remote_path)) return stat.S_ISDIR(attr.st_mode) 上传\n注意：远程目录必须存在，否则会报错\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 def get(remote_path, local_path): \u0026#34;\u0026#34;\u0026#34; 下载文件或目录 :param remote_path: 目标机器路径（注意路径反斜杠问题会报错） :param local_path: 本地路径 :return: \u0026#34;\u0026#34;\u0026#34; # 判断远程路径是否存在 if not check_remote_path(remote_path): return False local_path, remote_path = Path(local_path), Path(remote_path) def find_files(remote_path, local_path): for sftp_attr in sftp.listdir_attr(normpath(remote_path)): filename = sftp_attr.filename if filename.startswith(\u0026#39;.\u0026#39;): # 过滤隐藏文件 continue local_dir, remote_dir = local_path.joinpath(filename), remote_path.joinpath(filename) # 若为目录，则递归调用 if stat.S_ISDIR(sftp_attr.st_mode): # st_mode判断文件类型（目录还是文件） local_dir.mkdir(parents=True, exist_ok=True) # parents为True支持多级创建，exist_ok 存在就不创建 find_files(remote_dir, local_dir) else: local_dir.parent.mkdir(parents=True, exist_ok=True) sftp.get(normpath(remote_dir), normpath(local_dir)) # 下载目录 if remote_path_isdir(remote_path): find_files(remote_path, local_path) # 下载单个文件，如果没有设置本地文件名，默认为远程路径中的名字 else: if local_path.is_dir(): local_path.mkdir(parents=True, exist_ok=True) local_path = local_path.joinpath(remote_path.name) else: local_path.parent.mkdir(parents=True, exist_ok=True) sftp.get(normpath(remote_path), normpath(local_path)) return True 下载\n注意：本地目录必须存在，否则会报错\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def put(local_path, remote_path): local_path, remote_path = Path(local_path), Path(remote_path) if local_path.is_dir(): for path in local_path.rglob(\u0026#39;[!.]*\u0026#39;): # 拼接远程路径，relative_to获取相对路径 remote = remote_path.joinpath(path.relative_to(local_path)) if path.is_file(): check_remote_path(remote.parent, is_mkdir=True) # 目标机器上不存在此路径需要创建 sftp.put(normpath(path), normpath(remote)) # 上传单个文件 else: check_remote_path(remote_path.parent, is_mkdir=True) if remote_path_isdir(remote_path): # 若远程路径是一个目录，就将本地文件名作为默认名字 remote_path = remote_path.joinpath(local_path.name) sftp.put(normpath(local_path), normpath(remote_path)) return True 关闭 sftp.close()\n封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 class SSHConnection: def __init__(self, host, user, password, port=22, mylogger=None): self._host = host self._user = user self._password = password self._port = port self._transport = None self._sftp = None self._client = None self.mylogger = mylogger self.connect() def connect(self): # 密钥方式 # private = paramiko.RSAKey.from_private_key_file(\u0026#39;/xxx/xxx\u0026#39;) # transport = paramiko.Transport((self._host, self._port)) # transport.connect(username=self._user, pkey=private) # 密码连接方式(通过Transport连接，或者通过SSHClient连接) # transport 一种加密的会话，会创建一个加密通道 transport = paramiko.Transport((self._host, self._port)) transport.connect(username=self._user, password=self._password) self._transport = transport def exec_cmd(self, commands: list) -\u0026gt; List: \u0026#34;\u0026#34;\u0026#34; :param commands: 待执行命令，支持list :return: 执行状态 \u0026#34;\u0026#34;\u0026#34; # 实例化SSHClient if not self._client: self._client = paramiko.SSHClient() self._client._transport = self._transport status = [] for cmd in commands: stdin, stdout, stderr = self._client.exec_command(cmd) self.writer(f\u0026#34;start run command: {cmd}\u0026#34;) while True: err, out = stderr.readline().strip(), stdout.readline().strip() if not (out or err): break if out: self.writer(out) if err: self.writer(err, level=\u0026#39;error\u0026#39;) status.append(stdout.channel.recv_exit_status()) return status def remote_path_isdir(self, remote_path): \u0026#34;\u0026#34;\u0026#34; 检查一个远程路径是否为目录 :param remote_path: :return: \u0026#34;\u0026#34;\u0026#34; attr = self._sftp.lstat(self.normpath(remote_path)) return stat.S_ISDIR(attr.st_mode) def check_remote_path(self, remote_path, is_mkdir=False): \u0026#34;\u0026#34;\u0026#34; 判断目标机器路径是否存在 :param remote_path: :param is_mkdir: 若为True，则会创建此路径 :return: \u0026#34;\u0026#34;\u0026#34; remote_path = self.normpath(remote_path) try: self._sftp.lstat(remote_path) return True except FileNotFoundError: if is_mkdir: self._sftp.mkdir(remote_path) else: return False @staticmethod def check_local_path(local_path, is_mkdir=True): \u0026#34;\u0026#34;\u0026#34; 判断本地路径是否存在 :param local_path: :param is_mkdir: :return: \u0026#34;\u0026#34;\u0026#34; if isinstance(local_path, Path): local_path = str(local_path) if not os.path.exists(local_path) and is_mkdir: os.makedirs(local_path) def writer(self, message, level=None): \u0026#34;\u0026#34;\u0026#34; 自定义写入文件方法，同时支持logger或文件对象 :param message: :param level: :return: \u0026#34;\u0026#34;\u0026#34; if self.mylogger: if isinstance(self.mylogger, logging.Logger): if not level: self.mylogger.info(message) elif level == \u0026#39;error\u0026#39;: self.mylogger.error(message) else: self.mylogger.warning(message) else: self.mylogger.write(message+\u0026#34;\\n\u0026#34;) else: print(message+\u0026#34;\\n\u0026#34;) def get(self, remote_path, local_path): \u0026#34;\u0026#34;\u0026#34; 下载文件或目录 :param remote_path: 目标机器路径（注意路径反斜杠问题会报错） :param local_path: 本地路径 :return: \u0026#34;\u0026#34;\u0026#34; if not self._sftp: # 创建一个已连通的sftp client # self._sftp = paramiko.SFTPClient.from_transport(self._transport) self._sftp = self._transport.open_sftp_client() if not self.check_remote_path(remote_path): self.writer(f\u0026#34;路径不存在：{remote_path}\u0026#34;, level=\u0026#39;error\u0026#39;) return False local_path, remote_path = Path(local_path), Path(remote_path) def find_files(remote_path, local_path): for sftp_attr in self._sftp.listdir_attr(self.normpath(remote_path)): filename = sftp_attr.filename if filename.startswith(\u0026#39;.\u0026#39;): # 过滤隐藏文件 continue local_dir, remote_dir = local_path.joinpath(filename), remote_path.joinpath(filename) # 若为目录，则递归调用 if stat.S_ISDIR(sftp_attr.st_mode): # st_mode判断文件类型（目录还是文件） local_dir.mkdir(parents=True, exist_ok=True) # parents为True支持多级创建，exist_ok 存在就不创建 find_files(remote_dir, local_dir) else: local_dir.parent.mkdir(parents=True, exist_ok=True) self._sftp.get(self.normpath(remote_dir), self.normpath(local_dir)) self.writer(f\u0026#34;download file {remote_dir} -\u0026gt; {local_dir} successful!\u0026#34;) # 下载目录 if self.remote_path_isdir(remote_path): find_files(remote_path, local_path) # 下载单个文件，如果没有设置本地文件名，默认为远程路径中的名字 else: if local_path.is_dir(): local_path.mkdir(parents=True, exist_ok=True) local_path = local_path.joinpath(remote_path.name) else: local_path.parent.mkdir(parents=True, exist_ok=True) self._sftp.get(self.normpath(remote_path), self.normpath(local_path)) self.writer(f\u0026#34;download file {remote_path} -\u0026gt; {local_path} successful!\u0026#34;) return True def put(self, local_path, remote_path): if not self._sftp: self._sftp = paramiko.SFTPClient.from_transport(self._transport) local_path, remote_path = Path(local_path), Path(remote_path) if local_path.is_dir(): for path in local_path.rglob(\u0026#39;[!.]*\u0026#39;): # 拼接远程路径，relative_to获取相对路径 remote = remote_path.joinpath(path.relative_to(local_path)) if path.is_file(): self.check_remote_path(remote.parent, is_mkdir=True) # 目标机器上不存在此路径需要创建 self._sftp.put(self.normpath(path), self.normpath(remote)) self.writer(f\u0026#34;upload the file {path} successful!\u0026#34;) # 上传单个文件 else: self.check_remote_path(remote_path.parent, is_mkdir=True) if self.remote_path_isdir(remote_path): # 若远程路径是一个目录，就将本地文件名作为默认名字 remote_path = remote_path.joinpath(local_path.name) self._sftp.put(self.normpath(local_path), self.normpath(remote_path)) self.writer(f\u0026#34;upload the file {local_path} successful!\u0026#34;) return True def close(self): if self._client: self._client.close() if self._transport: self._transport.close() if self._sftp: self._sftp.close() self._client, self._transport, self._sftp = None, None, None @staticmethod def normpath(path): \u0026#34;\u0026#34;\u0026#34; 由于windows和linux操作系统不同，路径格式会出现不统一的情况，反斜杠不处理的话会出现很多问题 替换windows路径中的\\ :param path: :return: \u0026#34;\u0026#34;\u0026#34; if isinstance(path, Path): path = str(path) return path.replace(\u0026#39;\\\\\u0026#39;, \u0026#39;/\u0026#39;) fabric fabric是基于paramiko的进一步封装，使用起来更加方便。fabric共有三个版本：fabric1、fabric2、fabric3，其中fabric3是非官方版本，所以不推荐使用，建议使用fabric2版本。\n官方文档：https://www.fabfile.org/\n安装 pip install fabric或者pip install fabric2，都是安装最新的官方版本。\n使用 连接 1 2 3 4 5 6 7 8 9 from fabric import Connection conn = Connection(f\u0026#34;{user}@{host}:{port}\u0026#34;, connect_kwargs={\u0026#34;password\u0026#34;: password}, connect_timeout=5, # 5s超时 ) # 多个命令之间用\u0026amp;\u0026amp;或;连接 conn.run(\u0026#34;ls\u0026#34;) 关于run()方法的参数说明：\n参数 解释 hide=True 服务器的输出信息不会在控制台打印 warn=True 忽略异常信息（即exit code不等于0的命令），会将异常信息输出到stderr，若设置False则会报SystemExit异常，之前在测试环境就是因为没有处理这个SystemExit异常（SystemExit和Exception是同级关系），导致整个进程崩溃 pty=True 交互式伪终端时使用，其他不建议使用 watchers 监听Responder列表，自动响应 out_stream 文件对象，可以记录命令输出信息（fabric1版本的参数好像是stdout） err_stream 文件对象，可以记录命令错误输出信息 ret = run()方法返回值：\n1 2 3 ret.stdout.strip() # 正常输出 ret.stderr.strip() # 异常输出 ret.failed # 执行状态True or False 执行交互式命令 如果需要用户主动输入命令，可以使用Responder，第一个参数为需要匹配的字符串，第二个参数为响应的内容。由于交互式终端原因，此处需要设置pty=True，否则运行会提示：no tty present and no askpass program specified，若提示xx user is not in the sudoers file. This incident will be reported，需要通过登录root用户，修改sudoers文件。\n特别注意：模式匹配字符串要转义，否则会匹配不到，进而一直阻塞无响应（凡是需要用户介入的操作，都会一直卡在那里直到给出响应）；response内容必须以\\n紧接结尾，相当于输入完成后执行回车操作。\n1 2 3 4 5 6 7 8 from invoke import Responder from fabric import Connection sudopass = Responder(pattern=fr\u0026#39;\\[sudo\\] password for {user}:\u0026#39;, response=f\u0026#39;{password}\\n\u0026#39;,) conn = Connection(f\u0026#34;{user}@{host}:{port}\u0026#34;, connect_kwargs={\u0026#34;password\u0026#34;: password}) # 如果要监听多个，则放入列表中传递给watchers conn.run(\u0026#34;sudo whoami\u0026#34;, pty=True, watchers=[sudopass]) with用法：\n1 2 3 4 5 6 7 8 9 10 11 # 执行完自动关闭 with Connection(f\u0026#34;{user}@{host}:{port}\u0026#34;, connect_kwargs={\u0026#34;password\u0026#34;: password}) as c: c.run(\u0026#34;cd /home\u0026#34;) # with cd: 退出with上下文之后会回到当前登录用户的家目录 conn = Connection(f\u0026#34;{user}@{host}:{port}\u0026#34;, connect_kwargs={\u0026#34;password\u0026#34;: password}) with conn.cd(\u0026#34;/home/yuqiuwen/PythonProjects/zhongxin/\u0026#34;): # 以下操作均在当前目录下执行 ... # with settings在fabric2中遗弃了 执行本地命令 1 2 3 4 5 # Connection.local方法(实际上是invoke.run的装饰器) conn.local(\u0026#34;dir\u0026#34;) # 或者invoke.run from invoke import run 操作多台机器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from fabric import SerialGroup as Group results = Group(\u0026#39;web1\u0026#39;, \u0026#39;web2\u0026#39;, \u0026#39;mac1\u0026#39;).run(\u0026#39;uname -s\u0026#39;) print(results) \u0026lt;GroupResult: { \u0026lt;Connection \u0026#39;web1\u0026#39;\u0026gt;: \u0026lt;CommandResult \u0026#39;uname -s\u0026#39;\u0026gt;, \u0026lt;Connection \u0026#39;web2\u0026#39;\u0026gt;: \u0026lt;CommandResult \u0026#39;uname -s\u0026#39;\u0026gt;, \u0026lt;Connection \u0026#39;mac1\u0026#39;\u0026gt;: \u0026lt;CommandResult \u0026#39;uname -s\u0026#39;\u0026gt;, }\u0026gt; for connection, result in results.items(): print(\u0026#34;{0.host}: {1.stdout}\u0026#34;.format(connection, result)) web1: Linux web2: Linux mac1: Darwin 1 2 3 4 5 6 7 8 9 10 11 12 from fabric import SerialGroup as Group # 当目标机器文件不存在时执行上传和解压缩操作 def upload_and_unpack(c): if c.run(\u0026#39;test -f /opt/mydata/myfile\u0026#39;, warn=True).failed: c.put(\u0026#39;myfiles.tgz\u0026#39;, \u0026#39;/opt/mydata\u0026#39;) c.run(\u0026#39;tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz\u0026#39;) # 如果用户名不同的话可以用这种方式： # Group(\u0026#39;user@host1\u0026#39;, \u0026#39;user2@host2\u0026#39;, \u0026#39;user3@host3\u0026#39;, connect_kwargs={\u0026#39;password\u0026#39;: \u0026#39;123456\u0026#39;}) for connection in Group(\u0026#39;host1\u0026#39;, \u0026#39;host2\u0026#39;, \u0026#39;host3\u0026#39;, user=\u0026#39;root\u0026#39;, connect_kwargs={\u0026#39;password\u0026#39;: \u0026#39;123456\u0026#39;}): upload_and_unpack(connection) 上传文件 fabric只能上传单个文件，如果想上传多个文件，有两种方案：本地打包之后上传，然后在远程服务器执行解压操作，或者通过遍历文件夹循环调用get上传，本地目录遍历用到了pathlib2.Path中的rglob方法。注意：远程路径如果不存在会报错。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 conn = Connection(f\u0026#34;{user}@{host}:{port}\u0026#34;, connect_kwargs={\u0026#34;password\u0026#34;: password}) sftp = conn.sftp() def put(local_path, remote_path): local_path, remote_path = Path(local_path), Path(remote_path) if local_path.is_dir(): for path in local_path.rglob(\u0026#39;[!.]*\u0026#39;): # 过滤掉以点开头的文件 # 拼接远程路径，relative_to获取相对路径 remote = remote_path.joinpath(path.relative_to(local_path)) if path.is_file(): check_remote_path(remote.parent, is_mkdir=True) # 目标机器上不存在此路径需要创建 conn.put(normpath(path), normpath(remote)) # 上传单个文件 else: check_remote_path(remote_path.parent, is_mkdir=True) if remote_isdir(remote_path): # 若远程路径是一个目录，就将本地文件名作为默认名字 remote_path = remote_path.joinpath(local_path.name) conn.put(normpath(local_path), normpath(remote_path)) def check_remote_path(remote_path, is_mkdir=False): \u0026#34;\u0026#34;\u0026#34; 判断目标机器路径是否存在 :param remote_path: :param is_mkdir: 若为True，则会创建此路径 :return: \u0026#34;\u0026#34;\u0026#34; remote_path = normpath(remote_path) try: sftp.lstat(remote_path) return True except FileNotFoundError: if is_mkdir: try: sftp.mkdir(remote_path) except PermissionError: ret = conn.run(f\u0026#34;sudo mkdir {remote_path}\u0026#34;, pty=True, watchers=[sudopass]) return not ret.failed else: return False 下载文件 fabric下载文件的方法和paramiko有略微细节差异，fabric下载文件时若本地路径不存在会自动创建，并且本地路径如果没提供文件名，会以远程文件名作为默认值，但前提是本地路径以/结尾，如果没有/，将以路径最后一部分作为文件名。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 def traverse_remote_files(remote_path: Path, local_path: Path): files_attr = sftp.listdir_attr(normpath(remote_path)) for file_attr in files_attr: filename = file_attr.filename if filename.startswith(\u0026#39;.\u0026#39;): # 过滤以点开头的目录或文件 continue # 此处的local和remote可能为目录，也可能为文件 local, remote = local_path.joinpath(filename), remote_path.joinpath(filename) # 若为目录，则递归调用 if stat.S_ISDIR(file_attr.st_mode): # st_mode判断文件类型（目录还是文件） yield from traverse_remote_files(remote, local) else: yield remote def remote_isdir(remote_path): \u0026#34;\u0026#34;\u0026#34; 检查一个远程路径是否为目录 :param remote_path: :return: \u0026#34;\u0026#34;\u0026#34; attr = sftp.lstat(normpath(remote_path)) return stat.S_ISDIR(attr.st_mode) def get(remote_path, local_path): local_path, remote_path = Path(local_path), Path(remote_path) if remote_isdir(remote_path): filepaths = traverse_remote_files(remote_path, local_path) for filepath in filepaths: relpath = filepath.relative_to(remote_path) local = local_path.joinpath(relpath) try: conn.get(normpath(filepath), normpath(local)) except FileNotFoundError: print(\u0026#34;文件不存在\u0026#34;) else: if not local_path.suffix: # 如果本地路径没有提供文件名，默认为远程文件名 local_path = local_path.joinpath(remote_path.name) try: conn.get(normpath(remote_path), normpath(local_path)) except FileNotFoundError: print(\u0026#34;文件不存在\u0026#34;) fab命令 fab \u0026ndash;help查看具体命令含义，这里列举的是fabric2的常用fab命令参数，和fabric1版本有很大不同。具体请参考Upgrading from 1.x — Fabric documentation (fabfile.org)\n参数 解释 -l 列举任务列表（被@task装饰的函数才能被识别） -c 从指定的任务模块名加载（默认为fabfile.py） -r 从指定的根目录路径查找任务模块 -f 指定配置文件路径 -H 指定目标主机，多个主机逗号隔开 -V 显示Fabric、Paramiko、Invoke版本 -w 和warn=True效果一样，出现异常不会结束 示例\n1 2 3 4 5 6 7 8 9 @task def task1(c, param): print(f\u0026#34;hello world, today is {param}\u0026#34;) @task def task2(c): ... # 如果入口文件为fabfile.py，可以直接执行 fab -H localhost task2 其中被@task装饰的函数必须有一个参数，否则会报：TypeError: Tasks must have an initial Context argument!。\n假如入口文件不是fabfile.py且不指定的话，会报Can't find any collection named 'fabfile'!，fabric2中通过-c来指定加载的任务列表，官方API文档也说明了：\u0026ndash;collection后面的参数是模块名而不是具体文件名。\n比如上述的task函数存在于服务器/home/yuqiuwen/PythonProjects/zhongxin/fab.py路径下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026#34;\u0026#34;\u0026#34;通过本地调用远程服务器的脚本\u0026#34;\u0026#34;\u0026#34; now = datetime.datetime.now().strftime(\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;) # fabric2中通过--参数名的形式来传递变量值，如果参数有空格，要用引号 conn.run(f\u0026#34;fab -c /home/yuqiuwen/PythonProjects/zhongxin/fab task1 --param \u0026#39;{now}\u0026#39;\u0026#34;) # 或者 -r指定查找目录 # conn.run(f\u0026#34;fab -r /home/yuqiuwen/PythonProjects/zhongxin/ -c fab task1 --param \u0026#39;{now}\u0026#39;\u0026#34;) \u0026gt;\u0026gt;\u0026gt; output: hello world, today is 2022-10-30 14:19:23 \u0026#34;\u0026#34;\u0026#34;通过本地调用本地的脚本\u0026#34;\u0026#34;\u0026#34; from invoke import run # windows环境下貌似给now参数加引号也会报错，不知道什么原因 run(f\u0026#34;fab -c 本地脚本路径 task1 --param \u0026#39;{now}\u0026#39;\u0026#34;, out_stream=f, err_stream=f) 除此之外，还可以通过fab -l -F指定显示格式：\n1 2 \u0026gt; fab -r ./ -c fab -l -F json output: {\u0026#34;name\u0026#34;: \u0026#34;fab\u0026#34;, \u0026#34;help\u0026#34;: null, \u0026#34;default\u0026#34;: null, \u0026#34;tasks\u0026#34;: [{\u0026#34;name\u0026#34;: \u0026#34;task1\u0026#34;, \u0026#34;help\u0026#34;: null, \u0026#34;aliases\u0026#34;: []}, {\u0026#34;name\u0026#34;: \u0026#34;task2\u0026#34;, \u0026#34;help\u0026#34;: null, \u0026#34;aliases\u0026#34;: []}], \u0026#34;collections\u0026#34;: []} 封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 class FabConnection: def __init__(self, host, user, password, port=22, mylogger=None): self._host = host self._user = user self._password = password self._port = port self._sftp = None self._conn = None self.mylogger = mylogger self.connect() def connect(self): # 密钥文件方式 # connect_kwargs={\u0026#34;key_filename\u0026#34;: \u0026#34;/home/myuser/.ssh/private.key\u0026#34;}, conn = Connection(f\u0026#34;{self._user}@{self._host}:{self._port}\u0026#34;, connect_kwargs={\u0026#34;password\u0026#34;: self._password}) self._conn = conn self._sftp = conn.sftp() def get(self, remote_path, local_path): local_path, remote_path = Path(local_path), Path(remote_path) if self.remote_isdir(remote_path): filepaths = self.traverse_remote_files(remote_path, local_path) for filepath in filepaths: relpath = filepath.relative_to(remote_path) local = local_path.joinpath(relpath) try: self._conn.get(self.normpath(filepath), self.normpath(local)) self.writer(f\u0026#34;download {relpath} successful!\u0026#34;) except FileNotFoundError: self.writer(f\u0026#34;FileNotFoundError: {filepath} -\u0026gt; {local}\u0026#34;, level=\u0026#39;error\u0026#39;) else: if not local_path.suffix: # 如果本地路径没有提供文件名，默认为远程文件名 local_path = local_path.joinpath(remote_path.name) try: self._conn.get(self.normpath(remote_path), self.normpath(local_path)) self.writer(f\u0026#34;download {remote_path} successful!\u0026#34;) except FileNotFoundError: self.writer(f\u0026#34;FileNotFoundError: {remote_path} -\u0026gt; {local_path}\u0026#34;, level=\u0026#39;error\u0026#39;) def put(self, local_path, remote_path): local_path, remote_path = Path(local_path), Path(remote_path) if local_path.is_dir(): for path in local_path.rglob(\u0026#39;[!.]*\u0026#39;): # 拼接远程路径，relative_to获取相对路径 remote = remote_path.joinpath(path.relative_to(local_path)) if path.is_file(): self.check_remote_path(remote.parent, is_mkdir=True) # 目标机器上不存在此路径需要创建 self._conn.put(self.normpath(path), self.normpath(remote)) self.writer(f\u0026#34;upload {path} successful!\u0026#34;) # 上传单个文件 else: self.check_remote_path(remote_path.parent, is_mkdir=True) if self.remote_isdir(remote_path): # 若远程路径是一个目录，就将本地文件名作为默认名字 remote_path = remote_path.joinpath(local_path.name) self._conn.put(self.normpath(local_path), self.normpath(remote_path)) self.writer(f\u0026#34;upload the file {local_path} successful!\u0026#34;) def traverse_remote_files(self, remote_path: Path, local_path: Path): files_attr = self._sftp.listdir_attr(self.normpath(remote_path)) for file_attr in files_attr: filename = file_attr.filename if filename.startswith(\u0026#39;.\u0026#39;): # 过滤以点开头的目录或文件 continue # 此处的local和remote可能为目录，也可能为文件 local, remote = local_path.joinpath(filename), remote_path.joinpath(filename) # 若为目录，则递归调用 if stat.S_ISDIR(file_attr.st_mode): # st_mode判断文件类型（目录还是文件） yield from self.traverse_remote_files(remote, local) else: yield remote def close(self): if self._conn: self._conn.close() if self._sftp: self._sftp.close() self._conn, self._sftp = None, None def writer(self, message, level=None): \u0026#34;\u0026#34;\u0026#34; 自定义写入文件方法，同时支持logger或文件对象 :param message: :param level: :return: \u0026#34;\u0026#34;\u0026#34; if self.mylogger: if isinstance(self.mylogger, logging.Logger): if not level: self.mylogger.info(message) elif level == \u0026#39;error\u0026#39;: self.mylogger.error(message) else: self.mylogger.warning(message) else: self.mylogger.write(message + \u0026#34;\\n\u0026#34;) else: print(message + \u0026#34;\\n\u0026#34;) @staticmethod def normpath(path): \u0026#34;\u0026#34;\u0026#34; 由于windows和linux操作系统不同，路径格式会出现不统一的情况，反斜杠不处理的话会出现很多问题 替换windows路径中的\\ :param path: :return: \u0026#34;\u0026#34;\u0026#34; if isinstance(path, Path): path = str(path) return path.replace(\u0026#39;\\\\\u0026#39;, \u0026#39;/\u0026#39;) def remote_isdir(self, remote_path): \u0026#34;\u0026#34;\u0026#34; 检查一个远程路径是否为目录 :param remote_path: :return: \u0026#34;\u0026#34;\u0026#34; attr = self._sftp.lstat(self.normpath(remote_path)) return stat.S_ISDIR(attr.st_mode) def check_remote_path(self, remote_path, is_mkdir=False): \u0026#34;\u0026#34;\u0026#34; 判断目标机器路径是否存在 :param remote_path: :param is_mkdir: 若为True，则会创建此路径 :return: \u0026#34;\u0026#34;\u0026#34; remote_path = self.normpath(remote_path) try: self._sftp.lstat(remote_path) return True except FileNotFoundError: if is_mkdir: try: self._sftp.mkdir(remote_path) except PermissionError: ret = self.run(f\u0026#34;sudo mkdir {remote_path}\u0026#34;, pty=True, watchers=[self.sudo_pass]) return not ret[2] else: return False @property def sudo_pass(self): sudopass = Responder(pattern=fr\u0026#39;[sudo] password for {self._user}:\u0026#39;, response=f\u0026#39;{self._password}\\n\u0026#39;, ) return sudopass def run(self, cmd, hide=False, warn=True, pty=False, watchers=None): # hide=True控制台不打印运行信息，当out_stream不为空时，hide参数无效 ret = self._conn.run(cmd, hide=hide, warn=warn, pty=pty, out_stream=self.mylogger, err_stream=self.mylogger, watchers=watchers, encoding=\u0026#34;utf8\u0026#34; ) stdout, stderr = ret.stdout.strip(), ret.stderr.strip() return stdout, stderr, ret.failed @property def conn(self): return self._conn ","date":"2022-10-30T00:00:00Z","permalink":"/p/paramiko-fabric/","title":"自动化运维工具Paramiko \u0026 Fabric"},{"content":" https://man.niaoge.com/\nLinux File System Commonds df 查看磁盘占用情况 1 df -h\t以易读格式显示 du 查看文件占用磁盘大小 1 2 du -sh 目录名 du -sh * | sort -nr| head #查看文件大小并排序 top 查看CPU占用率 1 top -u 用户名\t查看某用户的cpu使用情况 free 查看内存使用 1 free -m 以MB为单位显示内存使用情况 sort 排序 1 2 3 4 5 6 7 8 9 10 11 12 13 -f\t忽略大小写 -b\t忽略最前面的空格符 -M\t以月份名字排序 -n\t以纯数字排序 -r\t反向排序 -u\t重复数据只显示其中一行 -t\t分隔符，默认 tab -k\t以某个分隔区间排序 cat /etc/passwd | sort -t \u0026#39;:\u0026#39; -k 3 -n example: sort test.txt | uniq -d # 统计重复行 chmod 修改权限 1 chmod -R 777 /test\t更改test目录下所有文件权限为777 chown 更改所有者 1 chown yuqiuwen test.sh 修改test.sh文件所有者为yuqiuwen uname 显示系统信息 1 uname -a\t显示系统详细信息 tar 解压缩 1 2 tar -zxvf\t解压文件 tar -cvf\t打包文件 查看cpu配置 1 cat /proc/cpuinfo grep 1 2 3 4 5 6 -a\t将binary文件以text文件的方式搜寻数据 -c\t计算找到所搜寻字符串的次数 -i\t忽略大小写 -n\t输出行号 -v\t反向选择 --color=auto\t关键词部分加上颜色 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 正则匹配 ^ 行的开始 如：\u0026#39;^grep\u0026#39;匹配所有以grep开头的行。 $ 行的结束 如：\u0026#39;grep$\u0026#39;匹配所有以grep结尾的行。 . 匹配一个非换行符的字符 如：\u0026#39;gr.p\u0026#39;匹配gr后接一个任意字符，然后是p。 * 匹配零个或多个先前字符 如：\u0026#39;*grep\u0026#39;匹配所有一个或多个空格后紧跟grep的行。 .* 一起用代表任意字符。 \\?\t匹配其前面的字符0次或者1次； \\+\t匹配其前面的字符至少1次； [] 匹配一个指定范围内的字符，如\u0026#39;[Gg]rep\u0026#39;匹配Grep和grep。 [^] 匹配一个不在指定范围内的字符，如：\u0026#39;[^A-FH-Z]rep\u0026#39;匹配不包含A-R和T-Z的一个字母开头，紧跟rep的行。 \\(..\\) 标记匹配字符，如\u0026#39;\\(love\\)\u0026#39;，love被标记为1。 \\\u0026lt; 锚定单词的开始，如:\u0026#39;\\\u0026lt;grep\u0026#39;匹配包含以grep开头的单词的行。 \\\u0026gt; 锚定单词的结束，如\u0026#39;grep\\\u0026gt;\u0026#39;匹配包含以grep结尾的单词的行。 x\\{m\\} 重复字符x，m次，如：\u0026#39;0\\{5\\}\u0026#39;匹配包含5个o的行。 x\\{m,\\} 重复字符x,至少m次，如：\u0026#39;o\\{5,\\}\u0026#39;匹配至少有5个o的行。 x\\{m,n\\} 重复字符x，至少m次，不多于n次，如：\u0026#39;o\\{5,10\\}\u0026#39;匹配5--10个o的行。 \\w 匹配文字和数字字符，也就是[A-Za-z0-9]，如：\u0026#39;G\\w*p\u0026#39;匹配以G后跟零个或多个文字或数字字符，然后是p。 \\W \\w的反置形式，匹配一个或多个非单词字符，如点号句号等。 \\b 单词锁定符，如: \u0026#39;\\bgrep\\b\u0026#39;只匹配grep。 awk 必须外层用单引号，内层双引号\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 NF\t分割后当前行一共有多少字段 NR\t行数 FS\t字段分隔符，默认空格 FS 字段分隔符（默认是任何空格） OFS 输出字段分隔符（默认值是一个空格) RS 记录分隔符（默认是一个换行符） ORS 输出记录分隔符（默认值是一个换行符） ARGC 命令行参数的数目 ARGV 包含命令行参数的数组 -F\t指定分割符，默认空格 -f\t从脚本文件中读取 -v 赋值一个用户定义变量，将外部变量传递给awk 1 2 3 4 5 6 7 8 9 10 11 awk [option] pattern {action} file awk \u0026#39;END{print NR}\u0026#39; test.log\t# 统计文件行数 awk \u0026#39;NR==3,NR==6{print NR,$0}\u0026#39; test.log\t# 查看第3到第6行，$0代表整行,NR输出行号 awk \u0026#39;{print $1,$(NF-1)}\u0026#39; test.log\t# 查看第一列和倒数第二列 ifconfig eth0 | awk \u0026#39;NR==2{print $2}\u0026#39;\t# 取ifconfig eth0中第二行第二列的ip awk -F \u0026#34;:\u0026#34; -v OFS=\u0026#34;\\t\u0026#34; \u0026#39;NR==3,NR==6{print $1,$NF}\u0026#39; test.log\t# 指定输出分隔符 awk -v ORS=\u0026#34; \u0026#34; \u0026#39;NR==3,NR==6{print NR,$0}\u0026#39; test.log\t# 以空格作为换行标志 awk \u0026#39;BEGIN{print ARGV[0],ARGV[1]}\u0026#39; test.log\t# 访问数组中的第1、2个值 awk -v myname=\u0026#34;哈哈哈\u0026#34; \u0026#39;BEGIN{print myname}\u0026#39; sed health_center.log日志文件如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 -n\t仅显示处理后的结果 -e\t以选项中的指定的script来处理输入的文本文件 -i\t直接编辑源文件 p\t打印 d\t删除 a 在当前行下面插入文本。 i 在当前行上面插入文本。 s\t替换指定字符 g\t获得内存缓冲区的内容，并替代当前模板块中的文本。 //\t模式匹配 sed -n \u0026#39;1,6p\u0026#39; health_center.log\t# 统计1到6行的文本 sed -n \u0026#39;1,+6p\u0026#39; health_center.log\t# 输出第1行和之后的6行（共7行） sed -n \u0026#39;/ERROR/p\u0026#39; health_center.log\t# 输出包含ERROR的行 sed \u0026#34;/INFO/d\u0026#34; test.log\t# 删除含有INFO的行（不会操作源文件） sed -i \u0026#34;/INFO/d\u0026#34; test.log\t# 删除含有INFO的行（直接操作源文件） sed \u0026#39;10001,$d\u0026#39; test.log -i\t# 删除从10001行到末尾的行 sed \u0026#34;s/WARNING/ERROR/g\u0026#34; test.log -i\t# 将WARNING替换为ERROR sed -e \u0026#34;s/ERROR/WARNING/g\u0026#34; -e \u0026#34;s/2022-05-28/2023-05-28/g\u0026#34; test.log -i\t# 替换多个 sed \u0026#34;1a 123456\u0026#34; test.log -i\t# 在第1行的下面添加一行123456 sed \u0026#34;1i abcde\u0026#34; test.log -i\t# 在第1行的前面添加一行abcde sed \u0026#34;1a tomorrow will be fine.\\n okay okay\u0026#34; test.log -i\t# \\n表示在第1行的下面添加多行文本 sed \u0026#39;a ------------\u0026#39; test.log\t# 在每一行下面添加------------字符 ### 文本如下，获取第二行中的ip 172.26.25.254 eth0: flags=4163\u0026lt;UP,BROADCAST,RUNNING,MULTICAST\u0026gt; mtu 1500 inet 172.26.25.254 netmask 255.255.192.0 broadcast 172.26.63.255 ether 00:16:3e:15:c5:19 txqueuelen 1000 (Ethernet) RX packets 1338810002 bytes 88450660493 (82.3 GiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 21251292571 bytes 1463867614512 (1.3 TiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 # 结合正则表达式，2s中的2代表第二行 ifconfig eth0 | sed -e \u0026#34;2s/^.*inet //\u0026#34; -e \u0026#34;2s/net.*$//p\u0026#34; -n wc 统计字符 1 2 3 -l\t行 -w\t英文字 -m\t字符 ps 查看进程 1 ps -ef | grep python\t显示系统中的python进程 kill 杀死进程 1 2 3 4 # grep -v grep 过滤掉grep本身所占进程 # awk \u0026#39;{print $2}\u0026#39;\t提取找到的进程行记录中第二列的参数，也就是python的进程号 # xargs kill -9\t把前面的参数都传递给后面的命令 kill -9 ps -ef | grep python | grep -v grep | awk \u0026#39;{print $2}\u0026#39; | xargs kill -9 netstat 查看网络端口信息 1 2 netstat -ntlp ss -nltp curl 获取网址信息 1 2 3 4 5 6 7 curl www.baidu.com\t获取该网址的文本信息 curl -i www.baidu.com\t获取该网址的文本信息以及协议头部信息 curl -x www.baidu.com\t使用代理获取网页文本信息 curl -X POST --header \u0026#34;Content-Type:application/json\u0026#34; --data \u0026#39;{}\u0026#39; www.baidu.com/getAllUserInfo\t使用post模拟json格式请求接口 tail 根据位置输出文件内容 1 2 3 4 5 6 7 8 9 tail -n 10 error.log\t查看最新10行 tail -n +20 error.log\t从第20行开始输出 tail -c 10 error.log\t输出最后10个字符 tail -c +10 error.log\t从第10个字符开始输出 tail -f error.log\t持续输出指定文件最新10行 sz rz 上传下载 1 2 3 sz filename\t下载文件到本地 rz filename\t上传文件到服务器 sftp 传输文件 1 2 3 4 5 6 sftp root@10.2.21.21 sftp\u0026gt; get /aa/bb/cc.txt /home/\t# 将21机器上的cc.txt文件下载到home目录 sftp\u0026gt; put /aa/bb/cc.txt /home/\t# 将cc.txt文件上传到21机器上的home目录 sftp\u0026gt; ls sftp\u0026gt; pwd //查询当前工作目录 rsync传输文件 1 rsync -av -e ssh --exclude \u0026#39;exclude_path绝对路径\u0026#39; 本地路径 远程路径 test 判断文件是否存在 1 test -e file.txt \u0026amp;\u0026amp; echo 11 || echo 10\t#存在则输出11 环境变量 1 2 3 4 5 6 ~/.bashrc 生效时间：使用相同的用户打开新的终端时生效，或者手动source ~/.bashrc生效 生效期限：永久有效 生效范围：仅对当前用户有效 如果有后续的环境变量加载文件覆盖了PATH定义，则可能不生效 会在每次运行Shell脚本的时候读取一次 1 2 3 4 5 6 ~/.bash_profile 生效时间：使用相同的用户打开新的终端时生效，或者手动source ~/.bash_profile生效 生效期限：永久有效 生效范围：仅对当前用户有效 如果没有~/.bash_profile文件，则可以编辑~/.profile文件或者新建一个 只在用户登录的时候读取一次 1 2 3 4 /etc/bashrc\t/etc/profile(bash_profile) 生效时间：新开终端生效，或者手动source /etc/bashrc生效 生效期限：永久有效 生效范围：对所有用户有效 1 2 3 4 /etc/environment 生效时间：新开终端生效，或者手动source /etc/environment生效 生效期限：永久有效 生效范围：对所有用户有效 环境变量加载顺序(系统级-\u0026gt;用户级)：\n/etc/environment /etc/profile ~/.bash_profile Linux环境变量配置全攻略 - 悠悠i - 博客园 (cnblogs.com)\nshell 批量替换文件名 1 2 # 替换aa_bb_finished.jpg为aa_bb.jpg for file in ls *fin*jpg;do mv $file `echo ${file//_finished/}`;done ","date":"2022-10-27T00:00:00Z","permalink":"/p/linux-commands/","title":"Linux Commands"},{"content":"Markdown For Typora Overview Markdown is created by Daring Fireball; the original guideline is here. Its syntax, however, varies between different parsers or editors. Typora is using GitHub Flavored Markdown.\nBlock Elements Paragraph and line breaks A paragraph is simply one or more consecutive lines of text. In markdown source code, paragraphs are separated by two or more blank lines. In Typora, you only need one blank line (press Return once) to create a new paragraph.\nPress Shift + Return to create a single line break. Most other markdown parsers will ignore single line breaks, so in order to make other markdown parsers recognize your line break, you can leave two spaces at the end of the line, or insert \u0026lt;br/\u0026gt;.\nHeaders Headers use 1-6 hash (#) characters at the start of the line, corresponding to header levels 1-6. For example:\n1 2 3 4 5 # This is an H1 ## This is an H2 ###### This is an H6 In Typora, input ‘#’s followed by title content, and press Return key will create a header.\nBlockquotes Markdown uses email-style \u0026gt; characters for block quoting. They are presented as:\n1 2 3 4 5 6 7 \u0026gt; This is a blockquote with two paragraphs. This is first paragraph. \u0026gt; \u0026gt; This is second pragraph. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. \u0026gt; This is another blockquote with one paragraph. There is three empty line to seperate two blockquote. In Typora, inputting ‘\u0026gt;’ followed by your quote contents will generate a quote block. Typora will insert a proper ‘\u0026gt;’ or line break for you. Nested block quotes (a block quote inside another block quote) by adding additional levels of ‘\u0026gt;’.\nLists Input * list item 1 will create an unordered list - the * symbol can be replace with + or -.\nInput 1. list item 1 will create an ordered list - their markdown source code is as follows:\n1 2 3 4 5 6 7 8 9 ## un-ordered list * Red * Green * Blue ## ordered list 1. Red 2. Green 3.\tBlue Task List Task lists are lists with items marked as either [ ] or [x] (incomplete or complete). For example:\n1 2 3 4 5 - [ ] a task list item - [ ] list syntax required - [ ] normal **formatting**, @mentions, #1234 refs - [ ] incomplete - [x] completed You can change the complete/incomplete state by clicking on the checkbox before the item.\n(Fenced) Code Blocks Typora only supports fences in GitHub Flavored Markdown. Original code blocks in markdown are not supported.\nUsing fences is easy: Input ``` and press return. Add an optional language identifier after ``` and we\u0026rsquo;ll run it through syntax highlighting:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Here\u0026#39;s an example: ​``` function test() { console.log(\u0026#34;notice the blank line before this function?\u0026#34;); } ​``` syntax highlighting: ​```ruby require \u0026#39;redcarpet\u0026#39; markdown = Redcarpet.new(\u0026#34;Hello World!\u0026#34;) puts markdown.to_html ​``` Math Blocks You can render LaTeX mathematical expressions using MathJax.\nTo add a mathematical expression, input $$ and press the \u0026lsquo;Return\u0026rsquo; key. This will trigger an input field which accepts Tex/LaTex source. For example:\n$$ \\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix} \\mathbf{i} \u0026 \\mathbf{j} \u0026 \\mathbf{k} \\\\ \\frac{\\partial X}{\\partial u} \u0026 \\frac{\\partial Y}{\\partial u} \u0026 0 \\\\ \\frac{\\partial X}{\\partial v} \u0026 \\frac{\\partial Y}{\\partial v} \u0026 0 \\\\ \\end{vmatrix} $$In the markdown source file, the math block is a LaTeX expression wrapped by a pair of ‘$$’ marks:\n1 2 3 4 5 6 7 $$ \\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix} \\mathbf{i} \u0026amp; \\mathbf{j} \u0026amp; \\mathbf{k} \\\\ \\frac{\\partial X}{\\partial u} \u0026amp; \\frac{\\partial Y}{\\partial u} \u0026amp; 0 \\\\ \\frac{\\partial X}{\\partial v} \u0026amp; \\frac{\\partial Y}{\\partial v} \u0026amp; 0 \\\\ \\end{vmatrix} $$ You can find more details here.\nTables Input | First Header | Second Header | and press the return key. This will create a table with two columns.\nAfter a table is created, putting focus on that table will open up a toolbar for the table where you can resize, align, or delete the table. You can also use the context menu to copy and add/delete individual columns/rows.\nThe full syntax for tables is described below, but it is not necessary to know the full syntax in detail as the markdown source code for tables is generated automatically by Typora.\nIn markdown source code, they look like:\n1 2 3 4 | First Header | Second Header | | ------------- | ------------- | | Content Cell | Content Cell | | Content Cell | Content Cell | You can also include inline Markdown such as links, bold, italics, or strikethrough in the table.\nFinally, by including colons (:) within the header row, you can define text in that column to be left-aligned, right-aligned, or center-aligned:\n1 2 3 4 5 | Left-Aligned | Center Aligned | Right Aligned | | :------------ |:---------------:| -----:| | col 3 is | some wordy text | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | A colon on the left-most side indicates a left-aligned column; a colon on the right-most side indicates a right-aligned column; a colon on both sides indicates a center-aligned column.\nFootnotes 1 2 3 You can create footnotes like this[^footnote]. [^footnote]: Here is the *text* of the **footnote**. will produce:\nYou can create footnotes like this1.\nHover over the ‘footnote’ superscript to see content of the footnote.\nHorizontal Rules Inputting *** or --- on a blank line and pressing return will draw a horizontal line.\nYAML Front Matter Typora now supports YAML Front Matter. Input --- at the top of the article and then press Return to introduce a metadata block. Alternatively, you can insert a metadata block from the top menu of Typora.\nTable of Contents (TOC) Input [toc] and press the Return key. This will create a “Table of Contents” section. The TOC extracts all headers from the document, and its contents are updated automatically as you add to the document.\nSpan Elements Span elements will be parsed and rendered right after typing. Moving the cursor in middle of those span elements will expand those elements into markdown source. Below is an explanation of the syntax for each span element.\nLinks Markdown supports two styles of links: inline and reference.\nIn both styles, the link text is delimited by [square brackets].\nTo create an inline link, use a set of regular parentheses immediately after the link text’s closing square bracket. Inside the parentheses, put the URL where you want the link to point, along with an optional title for the link, surrounded in quotes. For example:\n1 2 3 This is [an example](http://example.com/ \u0026#34;Title\u0026#34;) inline link. [This link](http://example.net/) has no title attribute. will produce:\nThis is an example inline link. (\u0026lt;p\u0026gt;This is \u0026lt;a href=\u0026quot;http://example.com/\u0026quot; title=\u0026quot;Title\u0026quot;\u0026gt;)\nThis link has no title attribute. (\u0026lt;p\u0026gt;\u0026lt;a href=\u0026quot;http://example.net/\u0026quot;\u0026gt;This link\u0026lt;/a\u0026gt; has no)\nInternal Links You can set the href to headers, which will create a bookmark that allow you to jump to that section after clicking. For example:\nCommand(on Windows: Ctrl) + Click This link will jump to header Block Elements. To see how to write that, please move cursor or click that link with ⌘ key pressed to expand the element into markdown source.\nReference Links Reference-style links use a second set of square brackets, inside which you place a label of your choosing to identify the link:\n1 2 3 4 5 This is [an example][id] reference-style link. Then, anywhere in the document, you define your link label on a line by itself like this: [id]: http://example.com/ \u0026#34;Optional Title Here\u0026#34; In Typora, they will be rendered like so:\nThis is an example reference-style link.\nThe implicit link name shortcut allows you to omit the name of the link, in which case the link text itself is used as the name. Just use an empty set of square brackets — for example, to link the word “Google” to the google.com web site, you could simply write:\n1 2 3 4 [Google][] And then define the link: [Google]: http://google.com/ In Typora, clicking the link will expand it for editing, and command+click will open the hyperlink in your web browser.\nURLs Typora allows you to insert URLs as links, wrapped by \u0026lt;brackets\u0026gt;.\n\u0026lt;i@typora.io\u0026gt; becomes i@typora.io.\nTypora will also automatically link standard URLs. e.g: www.google.com.\nImages Images have similar syntax as links, but they require an additional ! char before the start of the link. The syntax for inserting an image looks like this:\n1 2 3 ![Alt text](/path/to/img.jpg) ![Alt text](/path/to/img.jpg \u0026#34;Optional title\u0026#34;) You are able to use drag \u0026amp; drop to insert an image from an image file or your web browser. You can modify the markdown source code by clicking on the image. A relative path will be used if the image that is added using drag \u0026amp; drop is in same directory or sub-directory as the document you\u0026rsquo;re currently editing.\nIf you’re using markdown for building websites, you may specify a URL prefix for the image preview on your local computer with property typora-root-url in YAML Front Matters. For example, input typora-root-url:/User/Abner/Website/typora.io/ in YAML Front Matters, and then ![alt](/blog/img/test.png) will be treated as ![alt](file:///User/Abner/Website/typora.io/blog/img/test.png) in Typora.\nYou can find more details here.\nEmphasis Markdown treats asterisks (*) and underscores (_) as indicators of emphasis. Text wrapped with one * or _ will be wrapped with an HTML \u0026lt;em\u0026gt; tag. E.g:\n1 2 3 *single asterisks* _single underscores_ output:\nsingle asterisks\nsingle underscores\nGFM will ignore underscores in words, which is commonly used in code and names, like this:\nwow_great_stuff\ndo_this_and_do_that_and_another_thing.\nTo produce a literal asterisk or underscore at a position where it would otherwise be used as an emphasis delimiter, you can backslash escape it:\n1 \\*this text is surrounded by literal asterisks\\* Typora recommends using the * symbol.\nStrong A double * or _ will cause its enclosed contents to be wrapped with an HTML \u0026lt;strong\u0026gt; tag, e.g:\n1 2 3 **double asterisks** __double underscores__ output:\ndouble asterisks\ndouble underscores\nTypora recommends using the ** symbol.\nCode To indicate an inline span of code, wrap it with backtick quotes (`). Unlike a pre-formatted code block, a code span indicates code within a normal paragraph. For example:\n1 Use the `printf()` function. will produce:\nUse the printf() function.\nStrikethrough GFM adds syntax to create strikethrough text, which is missing from standard Markdown.\n~~Mistaken text.~~ becomes Mistaken text.\nUnderlines Underline is powered by raw HTML.\n\u0026lt;u\u0026gt;Underline\u0026lt;/u\u0026gt; becomes Underline.\nEmoji \u0026#x1f604; Input emoji with syntax :smile:.\nUser can trigger auto-complete suggestions for emoji by pressing ESC key, or trigger it automatically after enabling it on preference panel. Also, inputting UTF-8 emoji characters directly is also supported by going to Edit -\u0026gt; Emoji \u0026amp; Symbols in the menu bar (macOS).\nInline Math To use this feature, please enable it first in the Preference Panel -\u0026gt; Markdown Tab. Then, use $ to wrap a TeX command. For example: $\\lim_{x \\to \\infty} \\exp(-x) = 0$ will be rendered as LaTeX command.\nTo trigger inline preview for inline math: input “$”, then press the ESC key, then input a TeX command.\nYou can find more details here.\nSubscript To use this feature, please enable it first in the Preference Panel -\u0026gt; Markdown Tab. Then, use ~ to wrap subscript content. For example: H~2~O, X~long\\ text~/\nSuperscript To use this feature, please enable it first in the Preference Panel -\u0026gt; Markdown Tab. Then, use ^ to wrap superscript content. For example: X^2^.\nHighlight To use this feature, please enable it first in the Preference Panel -\u0026gt; Markdown Tab. Then, use == to wrap highlight content. For example: ==highlight==.\nHTML You can use HTML to style content what pure Markdown does not support. For example, use \u0026lt;span style=\u0026quot;color:red\u0026quot;\u0026gt;this text is red\u0026lt;/span\u0026gt; to add text with red color.\nEmbed Contents Some websites provide iframe-based embed code which you can also paste into Typora. For example:\n1 \u0026lt;iframe height=\u0026#39;265\u0026#39; scrolling=\u0026#39;no\u0026#39; title=\u0026#39;Fancy Animated SVG Menu\u0026#39; src=\u0026#39;http://codepen.io/jeangontijo/embed/OxVywj/?height=265\u0026amp;theme-id=0\u0026amp;default-tab=css,result\u0026amp;embed-version=2\u0026#39; frameborder=\u0026#39;no\u0026#39; allowtransparency=\u0026#39;true\u0026#39; allowfullscreen=\u0026#39;true\u0026#39; style=\u0026#39;width: 100%;\u0026#39;\u0026gt;\u0026lt;/iframe\u0026gt; Video You can use the \u0026lt;video\u0026gt; HTML tag to embed videos. For example:\n1 \u0026lt;video src=\u0026#34;xxx.mp4\u0026#34; /\u0026gt; Other HTML Support You can find more details here.\nLaTeX https://artofproblemsolving.com/wiki/index.php/LaTeX:Symbols\n思维导图 流程图 graph TD; A--\u003eB; A--\u003eC; B--\u003eD; graph LR; A--\u003eB; A--\u003eC; B--\u003eD;graph TD; /--\u003ebin; /--\u003eboot; /--\u003edev; /--\u003eetc; /--\u003ehome; /--\u003eroot; /--\u003erun; /--\u003esbin; /--\u003etmp; /--\u003eusr; /--\u003evar; home--\u003ealice; home--\u003ebob; home--\u003eeve;时序图 sequenceDiagram Alice-\u003e\u003eJohn: Hello John, how are you? Note left of Alice:提示 John--\u003e\u003eAlice: Great! Tom-\u003eJohn:Hey bro! Note right of Tom:Tom的描述（提示）甘特图 gantt dateFormat YYYY-MM-DD title 软件开发甘特图 section 设计 需求 :done, des1, 2022-01-06,2022-01-08 原型:active, des2, 2022-01-09, 3d UI设计: des3, after des2, 5d 未来任务: des4, after des3, 5d section 开发 学习准备理解需求:crit, done, 2022-01-06,24h 设计框架:crit, done, after des2, 2d 开发:crit, active, 3d 未来任务:crit, 5d section 测试 功能测试:active, a1, after des3, 3d 压力测试:after a1 , 20h 测试报告: 48h饼图 pie title Key elements in Product X \"Calcium\": 42.96 \"Potassium\": 50.05 \"Magnesium\": 10.01 \"Iron\": 5Docusaurus for MDX https://www.docusaurus.cn/docs/markdown-features\nHere is the text of the footnote.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2022-07-20T00:00:00Z","permalink":"/p/markdown-handbook/","title":"MarkDown Handbook"},{"content":" Django 文档 | Django 文档 | Django (djangoproject.com)\n查询 表结构\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Diabetes(BaseModel): user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True) pregnancies = models.IntegerField(blank=True, null=True, verbose_name=\u0026#39;怀孕次数\u0026#39;) glucose = models.IntegerField(blank=True, null=True, verbose_name=\u0026#39;葡萄糖\u0026#39;) blood_pressure = models.IntegerField(blank=True, null=True, verbose_name=\u0026#39;血压\u0026#39;) skin_thickness = models.IntegerField(blank=True, null=True, verbose_name=\u0026#39;皮肤厚度\u0026#39;) insulin = models.DecimalField(max_digits=4, decimal_places=1, blank=True, null=True, verbose_name=\u0026#39;胰岛素\u0026#39;) bmi = models.DecimalField(max_digits=4, decimal_places=2, blank=True, null=True, verbose_name=\u0026#39;身体质量指数\u0026#39;) pedigree_function = models.DecimalField(max_digits=4, decimal_places=3, blank=True, null=True, verbose_name=\u0026#39;遗传特性\u0026#39;) age = models.IntegerField(blank=True, null=True, verbose_name=\u0026#39;年龄\u0026#39;) outcome = models.IntegerField(blank=True, null=True, verbose_name=\u0026#39;预测结果,0正常/1发病\u0026#39;) result = models.DecimalField(max_digits=5, decimal_places=4, blank=True, null=True, verbose_name=\u0026#39;预测患病概率\u0026#39;) class Meta: db_table = \u0026#39;diabetes\u0026#39; verbose_name = \u0026#39;糖尿病数据\u0026#39; verbose_name_plural = verbose_name class User(AbstractUser): sexType = ((\u0026#39;M\u0026#39;, \u0026#39;male\u0026#39;), (\u0026#39;F\u0026#39;, \u0026#39;Female\u0026#39;)) roles = models.IntegerField(default=1, verbose_name=\u0026#39;角色\u0026#39;) code = models.CharField(max_length=256, null=True,blank=True, verbose_name=\u0026#39;邮箱验证码\u0026#39;) c_time = models.DateTimeField(auto_now_add=True, verbose_name=\u0026#39;注册提交时间\u0026#39;) is_remember = models.BooleanField(default=False, verbose_name=\u0026#39;是否记住登录状态\u0026#39;) sex = models.CharField(max_length=1, choices=sexType, null=True, blank=True) ORM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1.使用annotate和F重命名字段 user = User.objects.annotate(userName=F(\u0026#39;username\u0026#39;), pregnancies_count=Count(\u0026#39;diabetes\u0026#39;)) # 2.查询每个用户有多少条Diabetes记录 user = User.objects.annotate(pregnancies_count=Count(\u0026#39;diabetes\u0026#39;)) print(user.values(\u0026#39;pregnancies_count\u0026#39;)) # 3.外键反向查询：查询用户为song的所有diabetes数据 user = User.objects.filter(is_active=True, username=\u0026#39;song\u0026#39;).first() user.diabetes_set.all() # 或者通过related_name查询：user.related_name.all() # 4.查询Diabetes表中所有用户为song的记录 # 表名小写__字段名，或者related_name__字段名 diabetes = Diabetes.objects.filter(is_active=True, user__username=\u0026#39;song\u0026#39;).values() # 5.查询diabetes表中age=21的对应的所有user记录，queryset有重复可以使用distinct去重 user = User.objects.filter(is_active=True, diabetes__age=21).values().distinct() # 6.统计胰岛素含量的最高值和平均值之差 Diabetes.objects.filter(is_active=True).aggregate(insulin_diff=Max(\u0026#39;insulin\u0026#39;)-Avg(\u0026#39;insulin\u0026#39;)).values() 原生sql 复杂的sql语句可通过原生sql实现，绕过django模型层，直接执行sql语句\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from django.db import connection # 查询一条记录 def query_all(sql, params=None): with connection.cursor() as cursor: cursor.execute(sql, params) columns = [col[0] for col in cursor.description] return [dict(zip(columns, row))for row in cursor.fetchall()] # 查询多条记录 def query_one(sql, params=None): with connection.cursor() as cursor: print(sql) cursor.execute(sql, params) columns = [col[0] for col in cursor.description] return [dict(zip(columns, row))for row in cursor.fetchone()] # 执行查询 sql = f\u0026#34;\u0026#34;\u0026#34;select task_id, date_created, status, task_kwargs from django_celery_results_taskresult where task_id = %s\u0026#34;\u0026#34;\u0026#34; query = query_all(sql, [task_id, ]) Caution\n要避免 SQL 注入，你绝对不能在 SQL 字符串中用引号包裹 %s 占位符，使用 params 参数和不用引号包裹占位符使你免受 SQL 注入攻击\n序列化 反序列化：json数据转换为模型对象\n序列化：模型对象转换为json数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 post: s = DiabetesModelSerializer(data=data) s.save() get: diabetes = Diabetes.objects.filter(user=1, is_active=True) # 如果查询集有多条，设置many=True，否则查询单条记录 serializer = DiabetesModelSerializer(diabetes, many=True) return response_success(\u0026#34;查询成功\u0026#34;, serializer.data) put: diabetes = Diabetes.objects.get(user=1, is_active=True) #instance要更新的对象，partial默认false(需包含所有字段),设置为true表示局部修改 s = DiabetesModelSerializer(instance=diabetes, data=request.data, partial=False) s.save() delete: #采用逻辑删除，非物理删除，若queryset为单一，则删除一条，否则批量删除 Diabetes.objects.filter(user=1, is_active=True).update(is_active=False) 另一个案例 模型层 现有两张表：班级表和学生表（一对多关系）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Grade(BaseModel): name = models.CharField(max_length=256, null=True, blank=True, verbose_name=\u0026#39;班级名称\u0026#39;) class Meta: db_table = \u0026#39;grades\u0026#39; verbose_name = \u0026#39;班级表\u0026#39; verbose_name_plural = verbose_name class Students(BaseModel): sexTYPE = ((\u0026#39;M\u0026#39;, \u0026#39;male\u0026#39;), (\u0026#39;F\u0026#39;, \u0026#39;Female\u0026#39;)) name = models.CharField(max_length=256, null=True, blank=True, verbose_name=\u0026#39;姓名\u0026#39;) sex = models.CharField(max_length=1, choices=sexTYPE, null=True, blank=True, verbose_name=\u0026#34;性别\u0026#34;) stu_id = models.CharField(max_length=256, null=True, blank=True, verbose_name=\u0026#39;学号\u0026#39;) grade = models.ForeignKey(to=Grade, on_delete=models.SET_NULL, blank=True, null=True) class Meta: db_table = \u0026#39;students\u0026#39; verbose_name = \u0026#39;学生表\u0026#39; verbose_name_plural = verbose_name 路由 app路由文件\n1 2 3 4 5 6 7 8 9 10 11 12 from rest_framework import routers from django.urls import path, include from.views import * router = routers.SimpleRouter() router.register(r\u0026#34;stu\u0026#34;, DemoView) urlpatterns = [ path(r\u0026#39;\u0026#39;, include(router.urls)), path(r\u0026#39;grades/\u0026#39;, DemoView.as_view({\u0026#39;get\u0026#39;: \u0026#39;get_grades\u0026#39;})), ] 总路由文件\n1 2 3 4 urlpatterns = [ path(\u0026#39;admin/\u0026#39;, admin.site.urls), path(\u0026#39;api/\u0026#39;, include(\u0026#39;students.urls\u0026#39;)) ] 序列化器 DRF默认使用PrimaryKeyRelatedField序列化外键字段，为了前端展示，需要获取班级表的name和id字段\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from rest_framework import serializers from rest_framework.fields import DateTimeField from students.models import Students, Grade class GradeSerializer(serializers.ModelSerializer): class Meta: model = Grade fields = (\u0026#39;id\u0026#39;, \u0026#39;name\u0026#39;) class StudentSerializer(serializers.ModelSerializer): create_time = DateTimeField(format=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, required=False, read_only=True) update_time = DateTimeField(format=\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, required=False, read_only=True) # 序列化主表所有字段(需是fields指定的) grade = GradeSerializer() class Meta: model = Students fields = \u0026#34;__all__\u0026#34; 视图层 最基本的视图类如APIView，包含：get、post、delete、put，其他的还有GenericAPIView\nViewsets - Django REST framework (django-rest-framework.org)\n关于ViewSets我还没怎么研究，后面再看看吧。这里使用的是ModelViewSet视图集，ModelViewSet继承自GenericAPIView，包含：\nlist()、retrieve()、create()、update()、partial_update()。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import datetime import traceback from django.db.models import F, Q from rest_framework.decorators import action from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet # Create your views here. from .models import Students from.serializers import * from common.http_response import HttpResponse class DemoView(ModelViewSet): # permission_classes = [AllowAny] queryset = Students.objects.filter(is_active=True).order_by(\u0026#39;-update_time\u0026#39;) serializer_class = StudentSerializer def create(self, request, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34; 新增数据 :param request: :param args: :param kwargs: :return: \u0026#34;\u0026#34;\u0026#34; try: ser = StudentSerializer(data=request.data) print(ser) if ser.is_valid(raise_exception=True): ser.save() return HttpResponse.response_success(\u0026#34;创建成功\u0026#34;) except Exception as e: traceback.print_exc() return HttpResponse.response_failed(\u0026#34;创建失败\u0026#34;) # 自定义方法：url_path表示请求接口名，使用后可以不用在路由中单独注册, 访问格式：/stu/info @action(methods=[\u0026#39;get\u0026#39;], detail=False, url_path=\u0026#39;info\u0026#39;) def get_stu_info(self, request): \u0026#34;\u0026#34;\u0026#34; 获取学生信息，支持模糊查询 :param request: name :return: \u0026#34;\u0026#34;\u0026#34; try: name = request.GET.get(\u0026#39;name\u0026#39;) grade = request.GET.get(\u0026#39;grade\u0026#39;) stu_id = request.GET.get(\u0026#39;stu_id\u0026#39;) id = request.GET.get(\u0026#39;id\u0026#39;) if id and id != \u0026#39;\u0026#39;: stu = Students.objects.filter(id=id).order_by(\u0026#39;-update_time\u0026#39;) else: # grade__name__contains=grade 表示 在学生表中按班级名查询 stu = Students.objects.filter( Q(stu_id__contains=stu_id) \u0026amp; Q(name__contains=name) \u0026amp; Q(grade__name__contains=grade)).order_by(\u0026#39;-update_time\u0026#39;) # stu = stu.annotate(grade=F(\u0026#39;grade_id\u0026#39;)) ser = StudentSerializer(stu, many=True) return HttpResponse.response_success(\u0026#34;查询成功\u0026#34;, ser.data) except Exception as e: traceback.print_exc() return HttpResponse.response_failed(\u0026#34;查询失败\u0026#34;,) def put(self, request): \u0026#34;\u0026#34;\u0026#34; 修改方法 :param request: :return: \u0026#34;\u0026#34;\u0026#34; try: stu_id = request.data.get(\u0026#39;id\u0026#39;) stu = Students.objects.filter(is_active=True, id=stu_id).first() data = request.data cur_time = datetime.datetime.now() data[\u0026#39;create_time\u0026#39;], data[\u0026#39;update_time\u0026#39;] = cur_time, cur_time ser = StudentSerializer(instance=stu, data=data, partial=False) if ser.is_valid(): ser.save() return HttpResponse.response_success(\u0026#34;创建成功\u0026#34;,) except Exception as e: traceback.print_exc() return HttpResponse.response_failed(\u0026#34;创建失败\u0026#34;) @action(methods=[\u0026#39;GET\u0026#39;], detail=False) def get_grades(self, request): \u0026#34;\u0026#34;\u0026#34; 获取所有班级信息, 并对结果集字段重命名 :param request: :return: \u0026#34;\u0026#34;\u0026#34; grades = Grade.objects.annotate(label=F(\u0026#39;name\u0026#39;), value=F(\u0026#39;id\u0026#39;)).filter(is_active=True).values(\u0026#39;label\u0026#39;, \u0026#39;value\u0026#39;) return HttpResponse.response_success(\u0026#34;查询所有班级成功\u0026#34;, grades) 上面使用到了Q查询，配合\u0026amp;、|可以完成复杂的查询。使用~表示not反转： Q(question__startswith='Who') | ~Q(pub_date__year=2005)\nInfo\nQ对象必须位于关键字参数之前\n自定义类 响应封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from rest_framework.response import Response from rest_framework.status import * class HttpResponse(Response): def __init__(self, data_status=\u0026#39;\u0026#39;, message=\u0026#39;\u0026#39;, results=None, status=None, headers=None, exception=False, **kwargs): data = { \u0026#39;code\u0026#39;: data_status, \u0026#39;msg\u0026#39;: message } if results is not None: data[\u0026#39;results\u0026#39;] = results data.update(kwargs) super().__init__(data=data, status=status, headers=headers, exception=exception) @classmethod def response_success(cls, message=\u0026#39;\u0026#39;, data=None): return HttpResponse(HTTP_200_OK, message=message, results=data, status=HTTP_200_OK) @classmethod def response_failed(cls, message=\u0026#39;\u0026#39;, data=None): return HttpResponse(HTTP_500_INTERNAL_SERVER_ERROR, message=message, results=data, status=HTTP_500_INTERNAL_SERVER_ERROR) ","date":"2022-07-17T00:00:00Z","permalink":"/p/django-notes/","title":"Django笔记"}]