开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Mac下SourceTree导出更新文件

发表于 2024-04-26

SourceTree可视化git管理工具功能很强大,支持自定义操作可以写脚本,现在教大家做一个导出更新文件的脚本吧!

添加自定义操作

截屏2024-04-26下午3.40.32.png

选择自定义文件导出

截屏2024-04-26下午3.42.33.png

sourcetree_exports_file.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sh复制代码#!/bin/bash
#仓库路径
REPO_PATH=$1
#导出文件的目录以及新建文件夹路径
FOLDER="/Users/nightkid/Downloads"
FOLDER_DATE=`date +%Y%m%d_%H_%M_%S`
PROJECT_NAME=${REPO_PATH##*/}
for arg in "$@"
do
if [ $1 != $arg ]
then
srcfile="$REPO_PATH/$arg"
echo $arg
cpfile="$FOLDER/${PROJECT_NAME}_$FOLDER_DATE/$arg"
folder=${cpfile%/*}
`mkdir -p $folder`
result=`cp -f $srcfile $cpfile`
echo "$arg"
fi
#打开导出文件夹
open $FOLDER/${PROJECT_NAME}_$FOLDER_DATE
done

选择版本号内容导出文件

截屏2024-04-26下午3.42.14.png

sourcetree_commit_exports.sh

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
sh复制代码#!/bin/bash
REPO_PATH=$1
GIT_SHA=$2
#导出文件保存路径
FOLDER="/Users/nightkid/Downloads"
FOLDER_DATE=`date +%Y%m%d_%H_%M_%S`
PROJECT_NAME=${REPO_PATH##*/}
`mkdir -p $FOLDER`
filepath=$(cd $REPO_PATH; pwd)
split_str=`git show --format= --name-status $GIT_SHA`
echo "$split_str" > "$FOLDER/tmp.txt"

split_array=()
split_len=0
while read line
do
split_array[$split_len]=$line
split_len=$split_len+1

done < "$FOLDER/tmp.txt"

for ((i=0;i<${split_array[@]};i++));do
{
string=${split_array[$i]}

array=(${string//,/ })
if [ "$array[0]" != "D" ]
then
echo ${array[1]}

srcfile="$REPO_PATH/${array[1]}"
cpfile="$FOLDER/${PROJECT_NAME}_$FOLDER_DATE/${array[1]}"
folder=${cpfile%/*}
`mkdir -p $folder`
`cp -f $srcfile $cpfile`
fi
# for var in ${array[@]}
# do
# echo $var
# done
}
open $FOLDER/${PROJECT_NAME}_$FOLDER_DATE
done

最后选择对应的版本、文件右键

截屏2024-04-26下午3.47.58.png

可能的错误

1
2
3
4
5
6
bash复制代码当提示一下错误: 
launch path not accessible Completed with errors, see above

您可以通过授予您的用户执行该文件的权限来解决此问题:

chmod +x your-script.sh

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在 GPU 云上跑 LLama3

发表于 2024-04-26

LLama 3 模型已经开源了,感觉有一大波 Chinese -LLama 3 正在赶来的路上。如果你也想基于 LLama 3 训练一个自己的模型,那这篇教程就教你怎么来做。

在本文中,我们将介绍LLama 3,这是下一代最先进的开源大型语言模型。我们将了解LLama 3相对于LLama 2的进步。然后,我们将利用 Paperspace GPU 云的能力来深入探索,并尝试上手这个模型。因为 Paperspace 上有包括 H100、A100 等一系列 GPU 资源可以使用。

简单聊聊 LLama3

这里给还未了解 LLama 3 的开发者们,简要地介绍一下它。如果你已经是 AI 圈的老手,那么可以跳过这个章节。

Meta 最近宣布了LLama 3,这是下一代最先进的开源大型语言模型。

LLama 3 现在拥有 8B(80亿)和 70B(700亿)参数的语言模型。该模型已在各种任务中都有不俗的表现,并提供更好的推理能力。该模型已经开源,可供商业使用,并且开发者们可以在 AI 应用、开发者工具等方面进行创新。

LLama 3 有四个版本的大型语言模型(LLM)。这些模型有两种参数规模:8B 和 70B 参数,每种都有基础(预训练)和 instruct-tuned 版本。它们可以在不同类型的消费级硬件上平稳运行,并支持 8K(8000)token 的上下文长度。

  • Meta-Llama-3-8b:基础 8B 模型
  • Meta-Llama-3-8b-instruct:基于 8B 模型的 instruct-tuned 版本
  • Meta-Llama-3-70b:基础 70B 模型
  • Meta-Llama-3-70b-instruct:基于 70B 模型的 instruct-tuned 版本

LLama 3 的增强

最新的 8B 和 70B 参数的 LLama 3 模型,相比 LLama 2 有显著进步。有一些人表示,这是为大型语言模型设定了新的标准。由于更好的预训练和微调方法,它们已成为同类模型中的顶级存在。后训练增强中错误明显减少了,并提高了模型在推理、生成代码和遵循指令方面的性能。简而言之,LLama 3 比之前的很多模型都更先进、更灵活。下图是源自 Meta 官方的数据。

图:LLama 3 性能基准

在开发 LLama 3时,主要关注点是模型在现实生活情境中的优化。为此,他们创建了一个评估集,包含 1800 个 prompt,涵盖 12 个关键任务:寻求建议、编码和总结。此外,验证数据集也被禁止研发团队访问,以防止模型过拟合。将 LLama 3 与 Claude Sonnet、Mistral Medium 和 GPT-3.5 进行人工评估后,发现它在各种任务和场景中的结果都有不错的表现。

图:显示了人工评估在几个类别和提示下的结果

为了增强 LLama 3 的能力,Meta 专注于扩大预训练规模和完善后训练技术。

他们扩大了预训练规模,并制定了一系列详细的 scaling laws,以优化计算性能。令人惊讶的是,即使在训练了大量数据——高达 15T(万亿) 个 token 之后——其性能仍呈对数线性增长。结合使用各种并行化方法和定制的 GPU 集群,与LLama 2 相比,训练效率有效提高了三倍。

对于指令微调,Meta 考虑了不同的技术,如监督微调和偏好优化。此外,详细制定了训练数据和从偏好排名中学习,这有效提高了模型的性能,特别是在推理和编码任务中。这些改进使模型能够更好地理解和响应复杂任务。

模型架构

在设计 LLama 3 时,采用标准的解码器只有变换器架构,优化了编码效率和推理效率。与LLama 2相比,采用了有 128K 个标记词汇表的分词器,能更有效地对语言进行编码。此外,为了在推理期间让 LLama 3 模型更快,还引入了不同大小的分组查询注意力(GQA)。在训练期间,使用了 8192 个 token 的序列和一种掩码技术,以保持文档边界内的注意力。

LLama 3 在超过 15T token 的公开数据集上进行了预训练——比 LLama 2 使用的数据大 7 倍,而且代码量也大了 4 倍。该模型包含超过 5% 的非英语数据,涵盖 30 种语言,以便实现支持多语言。

为了保持在高质量的数据上进行训练,Meta 还构建了一系列数据过滤管道,还使用了启发式过滤器和文本分类器等,目的就是为了提高了模型性能。

运行 LLama 3 Demo

在我们开始之前,请确保在 huggingface.co 上获得对“meta-llama/Meta-Llama-3-70B”模型的访问权限。另外,我们在这里使用的是 Paperspace 平台上的 GPU,如果你手上还没有合适的 GPU 和机器,可以考虑这个平台。

要使用 Llama 3,我们首先要升级 transformers 包。

1
2
lua复制代码#upgrade the transformer package
pip install -U "transformers==4.40.0" --upgrade

接下来,运行以下代码段。根据 Hugging Face 博客的提示,该模型通常需要大约 16GB 的 RAM,包括像 3090 或 4090 这样的 GPU。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码import transformers
import torch

model_id = "meta-llama/Meta-Llama-3-8B-Instruct"

pipeline = transformers.pipeline(
"text-generation",
model="meta-llama/Meta-Llama-3-8B-Instruct",
model_kwargs={"torch_dtype": torch.bfloat16},
device="cuda",
)

pipeline("Hey how are you doing today?")

如果你收到错误信息 “RuntimeError: cutlassF: no kernel found to launch!”,请尝试下面的代码,并再次运行该cell。

1
2
python复制代码torch.backends.cuda.enable_mem_efficient_sdp(False)
torch.backends.cuda.enable_flash_sdp(False)

生成的文本:“Hey how are you doing today? I hope you’re having a great day so far! I just”

在这里需要注意几件事情:

  • 在我们的示例案例中,我们使用了’bfloat16’来加载模型。最初,Meta 使用的是’bfloat16’。因此,这是一种官方推荐的运行方式,可以确保最佳精度或进行评估。当然,你也可以尝试使用 float16,根据你硬件配置的情况,这可能会更快。
  • 你还可以自动压缩模型,将其加载为 8 位或 4 位模式。在 4 位模式下运行需要的内存更少,使其能兼容许多消费级GPU 和性能较弱的 GPU。以下是如何以4位模式加载示例代码段。
1
2
3
4
5
6
7
8
9
python复制代码pipeline = transformers.pipeline(
"text-generation",
model="meta-llama/Meta-Llama-3-8B-Instruct",
model_kwargs={
"torch_dtype": torch.float16,
"quantization_config": {"load_in_4bit": True},
"low_cpu_mem_usage": True,
},
)

LLama 3的未来

尽管当前的 8B(80亿)和 70B(700亿)参数模型给人留下了深刻印象,但 Meta 的工程师正在研究支持超过 400B(4000亿)参数的更大模型。这些模型仍在训练中。在未来几个月里,它们将更强的新功能,如多模态性、多语言对话能力、更长的上下文理解能力以及整体更强的能力。

Meta的LLama 3 最引人注目的一点就是开源。发布的基于文本的模型是LLama 3系列模型中的第一批。正如 Meta 所说,他们的主要目标是使 LLama 3 多语言和多模态,拥有更长的上下文支持,并持续改进核心大型语言模型(LLM)能力(如推理和编码)的整体性能。

我们迫不及待地想看看 GenAI 领域的下一个热点会是什么了。

最后,如果你正在计划训练自己的大语言模型,欢迎注册体验 DigitalOcean 旗下的 GPU 云服务 “Paperspace”,支持包括 H100、A100、4090 等多种 GPU,并预装 ML 框架。随时扩展,按需停止,只需按使用量付费。

如果需要预约更多 GPU 资源或希望了解方案详情,可以与 DigitalOcean 中国区独家战略合作伙伴卓普云咨询。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【高级RAG技巧】使用二阶段检索器平衡检索的效率和精度 一

发表于 2024-04-26

一 传统方法

之前的文章已经介绍过向量数据库在RAG(Retrieval Augmented Generative)中的应用,本文将会讨论另一个重要的工具-Embedding模型。
在这里插入图片描述一般来说,构建生产环境下的RAG系统是直接使用Embedding模型对用户输入的Query进行向量化表示,并且从已经构建好的向量数据库中检索出相关的段落用户大模型生成。但是这种方法很明显会受到Embedding模型性能的影响,比如是否支持多语言、跨语言检索、训练数据的质量等。因此,以改进Embedding模型为目标提升RAG系统性能一般会有两种做法:

方法1,在使用向量模型获取密集向量后,再使用Cross-encoder作为精排模型,对第一次召回的结果进行重排,以提高最终结果的质量。
方法2,使用稀疏向量搭配密集向量进行召回。密集向量即对整个文本进行向量化表示产生的向量,稀疏向量则是对原始文本的编码表示,如TF-IDF,BM25等。其中,稀疏向量可以帮助识别和捕捉特定的语义信息,比如文本中的关键词,而密集向量则可以提供更丰富的语义表达,是通过大量长文本学习而来的。通过同时将这两种向量进行召回,可以获得更丰富、更全面的信息,从而提升 RAG 的效果。

方法1和方法2既可以独立使用,也可以搭配使用,这就大大增加了算法工程师的武器库,通过搭积木的方式来提高RAG系统的效果。

二 Reranker模型剖析

本文主要讨论二阶段检索的方法,对于稀疏向量的讨论不在本文范围之内。

使用精排模型的原因

原因之一,向量数据库中存储大量的向量数据,而想要从大量数据中精确地检索出最相似的向量势必会带来极大的延迟,这在任何应用中都是无法接受的,因此向量数据库一般都会通过建立索引来优化查找过程,这实际上是一种近似检索而非精确检索,是准确与效率的折衷方案。如果数据库中没有那么多的向量,或者全表检索的时间损耗能够接受,则完全没必要用近似检索,但这在实际生产系统中是不可能的。

其二,LLM受限于上下文长度的限制,所以能利用到的向量片段数量是有限的,尽管现在越来越多的研究使得LLM的上下文窗口越来越长,如改进位置编码或者注意力机制等,但也有实验表明再长的上下文输入给大模型总会由遗失或者处理不好的情况,称为“Lost in middle”现象。因此,更好的办法是把更有用的上下文筛选出来,重点在于提升质而不是量。

精排模型结构

下面有两个模型,分别是Bi-Encoder和Cross-Encoder。左边的Bi-Encoder实际上就是我们常见的向量化模型,需要注意的是模型的结构并不是有2个Bert,而是只有1个Bert模型,但是对于两个句子分别做了向量化表示,最终比较这两个向量的相似度。这里‘Bi’表示的是处理的过程,而不是实际的结构。

在这里插入图片描述

而精排模型用到的Cross-Encoder结构则是把两个句子的输入concat到一起,最终预测这两个句子是否相关,有点类似于Bert训练时的next sentence prediction(NSP)。不难发现,这实际上就是一个分类模型,而且并不会产生具体的向量表示,只会产生一个介于0和1之间的值,用于表示句子对的相似性。而且,在使用时,需要将Query与待查询的句子拼接到一起再输入给模型,所以这就决定了输入的句子对数量不能太多,否则带来的计算耗时将会是无法估量的。

精排模型的本质

直观上来讲,用Bi-Encoder“目的更加通用”,是为了得到输入的向量化表示,这些向量不仅有大小,还有方向,因此就有这么一个学习向量化时遇到的经典例子:国王−男人=女王−女人国王-男人=女王-女人国王−男人=女王−女人。

而使用了Cross-Encoder的精排模型,因为输入部分包含了两个原始的句子,因此是从文本空间直接表示句子的相似度,而不是到了向量空间再表示。具体而言就是:传统的向量是经过了模型输出之后再根据不同的算子计算向量之间的相似度,而这个模型是句子对由模型得到端到端的结果。

能否再使用一次向量比较?或者说一阶段后再对检索出的向量计算精确的相似度有意义吗?
意义不大。因为这时候就算再计算精确的向量相似度,也只是在经过转换后的向量空间进行比较,因为这个空间比较“通用”,必定有所损失,无法在某种程度上准确表示所有的向量。

精排模型为什么准呢?
如前面说的,Bi-Encoder必须将所有的文档都映射到一个向量中,这个过程会丢失信息。而且,检索时query和已有的向量是没有交互的,是离线的状态,只是通过额外的相似度计算方法得到比较相似的向量,对query来说,缺乏必要的上下文信息。

而刚好精排模型是同时对两个句子进行编码的,这时候对query来说会含有上下文信息,而且从原始空间映射到隐空间是一个句子对而不是单独的句子,也更保证最终得到的相似度包含用户查询的文档信息。但是相对的,精排模型的结构决定了它的计算是实时的,而且计算量会比单纯的相似度计算大不少。

现有的精排模型

现有开源的Reranker模型已经非常多了,国内比较出门的有Jina-8k、bge-reranker以及bce-reranker等,且更新得非常快,目前看起来就是哪个最新用哪个,基本差异不大。值得一提的是Jina的模型是基于Alibi改进的注意力机制,所以能支持8K的上下文窗口,而bce本身只支持512k的上下文窗口,但是对较长的上下文进行切分,并且以其中一段最好的得分作为最终的相似度。bge模型没看过源码,等以后有时间针对这几个模型再研究一番。

总结

虽然目前二阶段方法用来提升RAG的性能表现越来越受到关注,但是具体来看,其中所含的技术都是早就有的内容。Cross-Encoder这种架构在当时显得比较鸡肋,只能用来比较句子的相似度,甚至无法输出向量,在大部分自然语言处理场景中都不受待见,谁能想到在如今又焕发生机了呢?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微软一面:订单超时未支付,如何自动关闭? 方案1 定时器

发表于 2024-04-26

hi,你好,我是猿java

最近,有小伙伴私信反馈微软一面的系统设计题:订单超时未支付,如何自动关闭?

说实话,微软能问出这种面试题确实很诧异,难道是互联网人已经大量涌进了微软这个养老基地,开始卷它?言归正传,有网购经验的小伙伴应该知道,如果订单在规定的时间内没有支付,订单就会被系统自动关闭,作为技术人员,该如何设计这个功能,今天我们来分析 4种有概括性的方案。

说明:

  • 规定的时间:即超时时间,这个因平台而异,有的场景是 10分钟超时,比如 12306 购票,有的平台是 15分钟或者 30分钟超时,有些秒杀场景是 1分钟超时,具体超时值需要根据具体的业务来定。
  • 自动关闭的原因:对于有库存的业务,需要释放库存,否则容易导致少卖。

方案1: 定时器

定时器应该是我们最可能想到的方案,通过定时器去数据库轮询待支付的订单信息,然后在业务代码中判断订单是否超时,更新订单状态。

实现思路

下单的时候把超时需要关闭订单的具体时间值(pay_expire_time)保存到表里,给pay_expire_time字段添加一个索引,然后设置一个定时器,每 3s/5s 去数据库查询一次数据,查询 SQL 如下:

1
sql复制代码select id from order where order_status = '待支付' && pay_expire_time > now()

优点

定时器方案最大的优点就是实现简单

缺点

  1. 需要定时从数据库拉取数据,对数据库会产生一定的压力
  2. 需要将数据加载到内存中进行业务逻辑处理,如果拉取的数据量比较大,可能会消耗过多的内存导致OOM
  3. 定时器是周期性的,对于超时订单的关闭可能存在延时

适用场景

适用于数据量比较小的业务场景,比如,很多小公司在业务处于 0-1 的过程中,该方案比较常见。

方案2: 被动关闭

服务器不做订单的主动关闭,而是在客户端拉取订单时按需被动关闭订单,比如,客户端通过接口获取服务器的时间以及订单超时时间,然后计算并判断订单是否超时,如果超时,将订单显示成超时的样式,
并隐式向服务器发送一个订单超时关闭的请求,服务器再做相应的逻辑处理。

优点

  • 实现简单
  • 按需处理订单超时,客户端需要用到数据再做判断

缺点

  • 订单长时间未关闭,在业务意义上可能是脏数据
  • 客户端需要做超时的逻辑判断并且发送超时关闭请求

适用场景

适用于数据量比较小的业务场景,被动关闭通常会结合定时器方案使用。

方案3: MQ延时消息

有些 MQ有延时消息的特性,比如阿里开源的 RocketMQ,用户创建订单时,同时往 MQ中发送一条延时消息,用户支付成功后,删除对应的延时消息(开源版本的没有此功能)
,当消费者消费到消息时再做超时的相关处理

优点

  • 利用中间件 MQ的特性,避免业务端自己实现

缺点

  • 开源的 RocketMQ 消息延时只有特定的几个级别,所以业务的超时时间要和 RocketMQ的延时级别相匹配才能使用
  • 强依赖中间件 MQ,所以 MQ的稳定性以及是否丢消息直接影响该功能
  • 使用开源版的 RocketMQ,没有删除延时消息的功能,因此需要对每条消息都做超时判断,增加了很多无效的数据处理
  • 如果数据量比较大,MQ可能会堆积产生延时

适用场景

适用于搭建了 RocketMQ的公司,想利用 RocketMQ的延时消息来实现该功能

方案4: 超时中心

很多大厂都有自己的分布式超时系统 TOC(Timeout Coordinator System),不同的公司可能叫法不一样,这里给出一种实现思路:

实现思路

1. 订单创建

  • 用户通过前端下单,请求发送到OMS(Order Management System)订单管理系统
  • OMS创建订单,并将订单状态设置为“待支付”或类似的初始状态

2. 注册超时事件

  • 在订单创建的同时,OMS向 TOC注册一个超时事件
  • 超时事件包含了订单ID、超时时间(例如30分钟)和超时后需要执行的动作(例如关闭订单)

3. 超时监控

  • TOC系统开始监控这个订单的超时事件
  • 如果在设定的超时时间内,用户完成了支付,OMS会更新订单状态并通知 TOC取消超时事件

4. 超时处理

  • 如果超时时间到达,而订单状态仍然是“待支付”,TOC将触发超时动作
  • TOC通知 OMS订单已超时

5. 订单关闭

  • OMS收到 OTC超时通知后,执行订单关闭操作
  • OMS将订单状态更新为“已关闭”或“超时关闭”

6. 超时后处理

  • OMS可能需要处理一些后续任务,如释放库存、退还优惠券等
  • 完成这些操作后,该订单的超时处理流程结束

优点

  • 专门的人做专门的事,性能,稳定性,扩展性有保障
  • 可以处理海量数据

缺点

  • 实现和维护成本比较高
  • 实现复杂度比较高,方案中可能会涉及分布式调度,MQ等技术栈

适用场景

适合数据量比较大的中大型公司,需要专门的团队负责

如何选择?

架构领域有句经典名言:没有最好的架构,只有够用(合适)的架构。

这个理论用于订单超时关闭场景同样适用,分布式超时中心方案看起来高大上,而且能处理海量数据,但是,如果把这套方案用于几个人的团队,有一种杀鸡用牛刀的架势,可能不但解决不了问题,还会给团队带来技术难题和成本负担。

因此,架构一定要基于业务,方案也一定要基于业务。

对于定时器方法,看起来简单粗暴,但是很多公司的业务在 0-1的过程中,它就是首选,因为实现的成本很低,没有什么技术难点,
假设每条订单的数据大小是 1K,5万条订单的数据大概在 50M左右,用定时器把数据加载到内存中处理也是ok的,所以该方案适合每天的订单量在万级别,或者再保守一些,每日订单总量在 1万以下。

对于 MQ延时消息方案,首先需要考虑自建 MQ 还是购买 MQ云产品,以及两种方案带来的技术难点或者购买成本,基于先有 MQ这个条件再看它适合的业务场景,在根据每日的订单量是百万级别,十万级别还是万级别来选择 MQ的配置。

分布式超时中心方案技术难度和维护成本都比较高,一般适合一些独角兽企业,大厂或者财力和技术能力都不错的公司。

总结

本文分析了具有概括性的 4种订单超时关闭方案,也是业内使用比较多的方案,可能每个公司实现的细节或者叫法略有差异,但是思想是相通的。

当然还有其他方案实现,比如 Redis,但是不推荐

通过本文,我们可以根据不同的业务体量给出不同的方案或者设计思路,以及各个方案的优缺点,具体细节可以结合具体业务细节。

没有最好的架构,只有够用(适合)的架构,很多时候在选择方案的时候一定要结合实际业务,切勿过度设计,但是也不能缺乏设计的能力

原创好文

  • 字节 2面:MySQL枚举类型该如何定义? enum?tinyint?char?varchar?
  • 腾讯女后端设计了一套短链系统,当场就想给她offer!
  • 肝了一周,彻底弄懂了 CMS收集器原理,这个轮子造的真值!
  • 9款常见的 JVM垃圾回收器
  • 美团一面:Git 是如何工作的?(推荐阅读)
  • 阿里 P7二面:Redis 执行 Lua,能保证原子性吗?
  • 当下环境,程序员需要修炼的 3项技能
  • AI是打工人的下一个就业风口吗?
  • 和斯坦福博士写代码的一个月
  • 肝了一周,这下彻底把 MySQL的锁搞懂了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《斋藤康毅-深度学习入门》读书笔记04-神经网络的学习 深度

发表于 2024-04-26

I’m a very very quick learner. – Lou Bloom, Nightcrawler

本章主题是神经网络如何自主地从数据中学习,从而得到最优权重和偏置参数值。由此引入了损失函数的概念,用来量化当前的权重参数的不合适度,学习的目标就是找到让损失函数最小的那一组参数值,使用的方法是梯度法。

深度学习的意义

由于实际的神经网络具有海量的层数和参数,参数可达成千上万乃至上亿个,凭人力根本无法进行计算,因此需要深度学习来确定参数。

对于机器学习来说,在识别手写数字的场景里,通常需要先从图像中提取特征量(表现为向量的形式),然后通过机器学习技术来学习这些特征量的模式。特征量用来把图片转换为向量。

而对于深度学习而言,提取特征量这一操作也是由机器学习完成的。

image.png
图:人工学习->机器学习->深度学习

过拟合 over fitting

指参数对某一个数据集过度拟合的情况,即对该数据集精确度很高,但对其它数据集则不然。

过拟合是深度学习中经常要面对的一个问题。

损失函数 loss function

神经网络以损失函数作为量化指标,来寻找最优权重参数,一般采用均方误差和交叉熵误差。

均方误差 mean squared error

image.png
图:均方误差算式

各参数说明如下:

  • yk:神经网络输出
  • tk:监督数据
  • k:数据维度
1
2
3
py复制代码# 均方误差
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)

one-hot表示

将正确解标签表示为1,其他标签表示为0。如在数字识别的例子里,一条监督数据是[0,0,1,0,0,0,0,0,0,0],表示当前数字为2。如果使用非one-hot表示,则t直接就是2。

交叉熵误差 cross entropy error

image.png
图:交叉熵误差的算式

解标签tk采用one-hot表示,只有正确解为1,其它值为0。因此上式可以简化为E=-logyk。自然对数log是在(0,1]区间单调递增的,递增范围是负无穷大~0,因此yk越接近1,E的值越小,也就表明越接近正确(最佳)结果。

1
2
3
4
py复制代码# 交叉熵误差
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))

作为保护性对策,增加一个极小值delta防止出现log(0)的边缘场景。

mini-batch 学习

为了在一次运算中使用更多数据验证参数的损失值,可以通过mini-batch的思路,即每次对批量数据计算损失函数,先求和再平均,已进行正规化。

image.png
图:批量计算交叉熵误差

叫mini-batch是因为相对于完整的数据集,只选取其中的一小部分(mini)进行计算。防止计算量过大。可以理解为抽样调查

1
2
3
4
5
6
py复制代码# 通过numpy进行抽样
train_size = x_train_.shape[0] # 60000条
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size) # 随机抽取10个下标
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

这段逻辑用如果用Java也能实现,但肯定比不上python简洁

numpy中关于array的维度操作

1
2
3
4
5
6
7
8
9
10
11
py复制代码# nparray_test.py 测试
import numpy as np

a = np.array([[11,22,33], [44,55,66]])
print(a.ndim) # 2
print(a.size) # 6,即数组内元素个数
b = a.reshape(1, a.size)
print(b) # [[11 22 33 44 55 66]]
print(b.ndim) # 2,仍然是二维
print(b.size) # 6,元素数不变
print(b.shape) # (1,6)

数值微分

导数用来表示某个瞬间的变化量,即瞬时速度。

image.png
图:导数

  • 前向差分:f(x+h)与f(x)之间的差分,因为偏离所以有误差
  • 中心差分:f(x+h)与f(x-h)之间的差分,更接近真实值
1
2
3
4
py复制代码# 中心差分
def numerical_diff(f, x)
h = 1e-4 # 0.0001
return (f(x+h)-f(x-h))/(2*h)

数值微分例子

对于下述二次函数,通过python代码计算其微分,以及在函数上x=5处的斜率,并绘图。

image.png

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
py复制代码import numpy as np
import matplotlib.pylab as plt

def numerical_diff(f, x):
h = 1e-4 # 0.0001
return (f(x+h) - f(x-h)) / (2*h)

def function_1(x):
return 0.01*x**2 + 0.1*x

def tangent_line(f, x):
d = numerical_diff(f, x)
print(d)
y = f(x) - d*x
return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)

image.png

偏导数

含有多个变量的函数的导数称为偏导数,在使用时应当声明是对其中哪一个变量求导。

image.png

上式的python实现为:

1
2
3
py复制代码def function_2(x):
return x[0]**2 + x[1]**2
# 或 return np.sum(x**2)

对应图像是:

image.png

其中对x0、x1求导分别写作:

image.png

在求导时,将已知参数值带入,对未知参数进行中心差分求导,例如求x0=3、x1=4时关于x0的偏导数:

1
2
3
4
py复制代码def function_tmp1(x0):
return x0**2 + 4.0**2.0

numerical_diff(function_tmp1, 3.0) # 6.00000

梯度

导数代表某个时间的瞬时速度,对于多维向量每一维求导,则得到了它的梯度(gradient)。当我们声明梯度时,需要说明是在x0=?、x1=?、...xn=?所有变量处的梯度。

image.png

对于函数f和多维参数x计算梯度的方法如下(含批处理):

谨记:x是一个多维向量(即张量)

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
py复制代码import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D

def _numerical_gradient_no_batch(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)

for idx in range(x.size):
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)

x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)

x[idx] = tmp_val # 还原值

return grad


def numerical_gradient(f, X):
if X.ndim == 1:
return _numerical_gradient_no_batch(f, X)
else:
grad = np.zeros_like(X)

for idx, x in enumerate(X):
grad[idx] = _numerical_gradient_no_batch(f, x)

return grad

梯度指向各点处函数值减小最快的方向。

image.png

梯度法

梯度法就是利用梯度的概念来寻找函数最小值的方法,通过不断沿着梯度方向前进,进而不断缩小损失函数值。

  • 极小值:局部最小值
  • 最小值:全局最小值
  • 鞍点:从某个方向看是极小值,从另一方向看是极大值

寻找最小值的梯度法称为梯度下降法(gradient descent method),寻找最大值的梯度法称为梯度上升法(gradient ascent method)。一般来说神经网络中梯度法是指梯度下降法。

在某一点上根据梯度方向前进,这就是梯度法的大白话表述。前进的步长用η表示,称为学习率(learning rate)。学习率决定在一次学习(前进)中,应当学习多少,以及在多大程度上更新参数。像学习率这样影响深度学习的值,称为超参数,以与权重等普通参数区分。

image.png
图:更新(学习)一次

学习率过大或者过小,都无法抵达一个“好的位置”,在神经网络中一般会一边改变学习率的值一半尝试学习,以便确认学习是否正常进行。

  • 学习率过大:得到一个很大的发散的值
  • 学习率过小:没学完就结束了

梯度下降法的python实现:

1
2
3
4
5
6
7
8
py复制代码def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x

for i in range(step_num): # 前进步数
grad = numerical_gradient(f, x) # 该点梯度
x -= lr * grad # 向梯度方向前进

return x

神经网络的梯度

在使用神经网络时,需要得出最优的权重矩阵,利用梯度法可以对这个矩阵进行计算,权重W神经网络和梯度表示如下:

image.png

由上例,定义一个simpleNet的简单神经网络:

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
py复制代码import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3) # 预定一个随机参数神经网络

def predict(self, x): # 计算网络输出
return np.dot(x, self.W)

def loss(self, x, t): # 计算交叉熵损失值
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)

return loss

x = np.array([0.6, 0.9]) // 输入
t = np.array([0, 0, 1]) // 标签集

net = simpleNet()

f = lambda w: net.loss(x, t) # 使用lambda简化函数写法
dW = numerical_gradient(f, net.W)

print(dW) # 打印神经网络的梯度

有了上面的梯度算法,就可以设置步长和步数,经过迭代得到最小损失函数。

学习算法实现

调整权重和偏置以便拟合训练数据的过程称为学习,分为以下步骤:(由于第一步是随机抽样,因此该方法也称为随机梯度下降法 stochastic gradient descent)

  1. 抽样 mini-batch:从训练数据中随机选出一部分数据,用这部分数据进行学习
  2. 计算梯度:随机设定权重,计算各个权重参数梯度,梯度表示损失函数的值减小最多的方向
  3. 更新参数:由上一步得出的梯度计算新的权重参数
  4. 重复1~3步

类定义:双层神经网络 TwoLayerNet

首先定义双层网络的结构,它包含每一层的权重和偏置,并且提供计算输出的predict函数,计算交叉熵损失的loss函数,计算批量精确度的accuracy函数,以及生成梯度矩阵的numerical_gradient函数。

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
py复制代码# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
from common.functions import *
from common.gradient import numerical_gradient


class TwoLayerNet:

# hidden_size 隐藏层(第1层)的神经元数
# weight_init_std 初始权重
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) # 维度为input_size * hidden_size
self.params['b1'] = np.zeros(hidden_size) # 偏置初始化为0
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) # hidden_size * output_size
self.params['b2'] = np.zeros(output_size)

# 根据定义计算输出
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']

a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1) # 隐藏层激活用sigmoid
a2 = np.dot(z1, W2) + b2
y = softmax(a2) # 输出层激活用softmax

return y

# x:输入数据, t:监督数据
def loss(self, x, t):
y = self.predict(x)

return cross_entropy_error(y, t)

# 这里x可以是batch输入
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)

accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy

# x:输入数据, t:监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t) # 损失

grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

return grads # 损失的梯度

# 计算梯度
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {} # 初始化为Map

batch_num = x.shape[0]

# forward 前向输出
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)

# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)

da1 = np.dot(dy, W2.T)
dz1 = sigmoid_grad(a1) * da1
grads['W1'] = np.dot(x.T, dz1)
grads['b1'] = np.sum(dz1, axis=0)

return grads

实现深度学习

引入epoch的概念,它是一个单位,表示训练集中全部数据均被使用过一次时的更新次数。对于10000条数据的训练集来说,如果每个mini-batch学习100条,则epoch=100。

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
py复制代码# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) #

iters_num = 10000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1 # 步长

train_loss_list = [] # 列表记录损失下降
train_acc_list = [] # 列表记录精度上升
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
# 取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

# 计算梯度
#grad = network.numerical_gradient(x_batch, t_batch)
grad = network.gradient(x_batch, t_batch) # 每次学习都更新一遍梯度

# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]

loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)

if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc : {:.4f}, test acc : {:.4f}".format(train_acc, test_acc))

# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

在每个epoch里,计算一次训练集和测试集的精度accuracy,并且显示在图像上,两条曲线吻合,说明没有发生过拟合。

image.png
图:未发生过拟合

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

鸿蒙Next开发之-解决鸿蒙next开发如何给测试同学测试包

发表于 2024-04-26

前言

目前公司和华为达成了合作,计划在鸿蒙正式发布next版本之前,上架公司APP的鸿蒙版本。方便用户升级手机到鸿蒙next之后,能正常使用公司APP,保证用户的既得利益。

最近我们计划在4月底上线公司的鸿蒙第一版APP,在开发和测试中遇到一个问题。那就是如何把开发同学的APP包直接丢给测试同学进行测试。由于这一块鸿蒙可能借鉴了iOS的经验,导致我们目前没办法直接给到测试同学一个APP,然后进行安装。因此目前我们有如下做法:

  • 1、每次改动都让测试拿手机来开发同学电脑上进行打包安装测试。
  • 2、测试直接安装鸿蒙开发环境,将代码拉到本地,运行指定分支的代码进行安装测试。

以上方法可想而知,对开发和测试都是极其不友好的。

因此考虑是否能进行优化这其中的流程。

开发环境

  • Mac
  • DevEco Studio NEXT Developer Preview2
  • HarmonyOS next Developer Preview2
  • java version “11.0.18” 2023-01-17 LTS
  • hdc 1.2.0a
  • 手机:Mate 60 (HarmonyOS NEXT Developer Preview2)

项目代码架构

  • 方式1:单HAP+可选(多HSP)+可选(多HAR)
  • 方式2:多HAP+可选(多HSP)+可选(多HAR)

目前我们项目采用的是方式1

思考

目前鸿蒙next版本的APP想要安装到鸿蒙测试手机上,(都必须得对APP进行签名)。

HarmonyOS通过数字证书(.cer文件)和Profile文件(.p7b文件)来对应用/元服务进行管控。

因此只有签名过的HAP才能安装到设备上运行。
有以下两种方式:

  • 1、将包上传到华为的AppGallery Connect,AppGallery Connect会重新解压APP,然后签名,之后你就可以在华为的应用商店进行下载安装。
  • 开发者直接使用手机连接idea,使用自动签名or手动签名进行安装

就目前来看,好像没有办法像Android一样丢一个APK给测试,或者给他一个网站,让测试下载下来直接安装。
真的就不行吗?确实不行,但可以优化。

DevEco Studio如何安装APP

然后我观察到idea是如何将HAP安装到测试手机上。在控制台我看到如下输出:

1
2
3
4
5
6
7
8
shell复制代码$ hdc shell aa force-stop com.byhk.app
$ hdc uninstall com.byhk.app in 669 ms
$ hdc shell mkdir data/local/tmp/e865b2e1cb8147408400c284260da55c
$ hdc file send /XXX/xxx/build/default/outputs/default/xxx-signed.hsp "data/local/tmp/e865b2e1cb8147408400c284260da55c" in 22 s 117 m
$ hdc file send /XXX/app/build/default/outputs/default/app-default-signed.hap "data/local/tmp/e865b2e1cb8147408400c284260da55c" in 1 min 50 s 580 ms
$ hdc shell bm install -p data/local/tmp/e865b2e1cb8147408400c284260da55c in 11 s 907 ms
$ hdc shell rm -rf data/local/tmp/e865b2e1cb8147408400c284260da55c
$ hdc shell aa start -a AppAbility -b com.byhk.app -D in 89 ms

简单介绍一下上面的命令作用:

  • hdc shell aa force-stop:强制停止APP,后面跟包名
  • hdc uninstall:卸载APP,后面跟包名
  • hdc shell mkdir:使用shell命令创建一个目录。
  • hdc file send:将hap和hsp发送到这个目录中,可能有人会问,为什么没有HAR。因为HAR的代码复制到了其依赖的module里面了。
  • hdc shell bm install -p:安装这个目录里面所有的东西
  • hdc shell rm -rf:移除刚刚的目录
  • hdc shell aa start -a AppAbility -b 包名 -D:启动APP

需要注意的是,send到这个目录的HAP,和HSP,都是进行了签名的。

重点:这个时候我就在想,如果我有一个已经签名的.app文件,我直接给到测试或者丢到一个网站上。并且给他写一份脚本,这个脚本将APP进行解压,然后把解压后的hap、hsp文件send到这个目录进行安装,是不是就行了。有道理。说干就干。

生成.app

这个很简单直接在DevEco Studio上进行操作。操作如下:

image.png

最后会在build目录下生成两个.app文件,如下:

image.png

其中一个签名了,一个未签名。

这儿有个坑,app签名了,但你解压之后的hap、hsp是没有进行签名的。

因此我需要对HAP和HSP分别进行签名。

对HAP、HSP进行签名

对HAP和HSP进行签名,需要java环境和鸿蒙的签名工具:hap_sign_tool.jar

hap_sign_tool.jar目录: 在${HOS_SDK_HOME}\HarmonyOS-NEXT-DP2\base\toolchains\lib,
如图:

image.png

使用如下命令对HAP进行签名。

1
matlab复制代码java -jar hap-sign-tool.jar sign-app -keyAlias "key0" -signAlg "SHA256withECDSA" -mode "localSign" -appCertFile "test.cer" -profileFile "test.p7b" -inFile "hap-unsigned.hap" -keystoreFile "test.p12" -outFile "result\hap-signed.hap" -keyPwd "123456" -keystorePwd "123456" -signCode "1"

关于该命令中需要修改的参数说明如下,其余参数不需要修改:

  • keyAlias:密钥别名。
  • appCertFile:申请的调试证书文件,格式为.cer。
  • profileFile:申请的调试Profile文件,格式为.p7b。
  • inFile:通过hvigor打包生成的未携带签名信息的HAP。
  • keystoreFile:密钥库文件,格式为.p12。
  • outFile:经过签名后生成的携带签名信息的HAP。
  • keyPwd:密钥口令。
  • keystorePwd:密钥库口令。
  • signCode:是否开启代码签名,缺省为开启。1表示开启,0表示不开启。从NEXT Developer Preview2版本开始,需开启代码签名,否则将导致构建出的包无法安装到设备上。

脚本编写

这儿我使用的是Python语言,脚本分为:

  • 签名脚本
  • 安装脚本

签名脚本

1、解压.app文件

1
2
3
4
5
6
7
python复制代码def unzip_app(app_file, signed_dir, output_dir):
print("开始解压APP")
os.makedirs(output_dir, exist_ok=True)
os.makedirs(signed_dir, exist_ok=True)
with zipfile.ZipFile(app_file, 'r') as zip_ref:
zip_ref.extractall(output_dir)
print("解压成功")

2、签名Hap、Hsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码def signed_app(extracted_dir, signed_dir):
print("开始签名APP")
sign_files(extracted_dir, signed_dir, '.hap')
sign_files(extracted_dir, signed_dir, '.hsp')
print("签名完成")

def sign_files(source_dir, target_dir, extension):
files = [f for f in os.listdir(source_dir) if f.endswith(extension)]
for file in files:
sign_file(source_dir, target_dir, file)

def sign_file(source_dir, target_dir, file):
sign_command = f"java -jar {hap_sign_tool} sign-app -keyAlias {alias} -signAlg \"SHA256withECDSA\" -mode \"localSign\" -appCertFile \"{cer_path}\" -profileFile \"{profile_file_path}\" -inFile \"{os.path.join(source_dir, file)}\" -keystoreFile \"{k12_path}\" -outFile \"{target_dir}/{file.split('.')[0]}-signed.{file.split('.')[1]}\" -keyPwd \"{pwd}\" -keystorePwd \"{pwd}\""
subprocess.run(sign_command, shell=True)

3、重新压缩生成zip包

1
2
3
4
5
6
7
8
lua复制代码def zip_signed_files(signed_dir):
os.makedirs(os.path.dirname(out_put_file_path), exist_ok=True)
print("开始压缩已签名文件")
with zipfile.ZipFile(out_put_file_path, 'w') as zip_ref:
for root, _, files in os.walk(signed_dir):
for file in files:
zip_ref.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), signed_dir))
print("压缩完成")

4、其他常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码app_file = "xxx/jojo-read-default-signed.app" //app地址
hap_sign_tool = "xxx/Huawei/Sdk/HarmonyOS-NEXT-DP2/base/toolchains/lib/hap-sign-tool.jar" //签名工具地址
cer_path = "xxx/app.cer" //cer地址
profile_file_path="xxx/app.p7b" //prpfile地址
k12_path = "xxx/app.p12" //.p12地址
alias = "ohos"
pwd = "123456" //密码

app_file_path = os.path.dirname(app_file)
file_name = os.path.basename(app_file)
package_name = "com.byhk.app" //包名
extracted_dir = app_file_path + "/zip"
signed_dir = app_file_path + "/signed"
out_put_file_dir = app_file_path + "/out/"
out_put_file_path = out_put_file_dir + file_name

完成以上,就会在out目录下生成一个.zip文件,这个文件就可以丢给测试了。但测试还需要将其解压send到手机上,因此还需要一个安装脚本。

安装脚本

1、解压文件

1
2
3
4
5
6
python复制代码def unzip_app(app_file, output_dir):
print("开始解压APP")
os.makedirs(output_dir, exist_ok=True)
with zipfile.ZipFile(app_file, 'r') as zip_ref:
zip_ref.extractall(output_dir)
print("解压成功")

2、停止并卸载应用

1
2
3
4
5
6
python复制代码def stop_and_uninstall_app(package_name):
print("停止并卸载应用")
subprocess.run(f"hdc shell aa force-stop {package_name}", shell=True)
subprocess.run(f"hdc uninstall {package_name}", shell=True)
subprocess.run("hdc shell rm -rf data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)
subprocess.run("hdc shell mkdir data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)

3、发送解压文件到手机

1
2
3
4
5
scss复制代码def send_files_to_device(extracted_dir, package_name):
print("发送文件到设备")
files = [f for f in os.listdir(extracted_dir) if f.endswith('.hap') or f.endswith('.hsp')]
for file in files:
subprocess.run(f"hdc file send {os.path.join(extracted_dir, file)} data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)

4、安装app并启动应用

1
2
3
4
5
6
python复制代码def start_app(package_name):
print("启动应用")
subprocess.run(f"hdc shell bm install -p data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)
subprocess.run(f"hdc shell rm -rf data/local/tmp/6927085eaa4645068468e40d6c797292", shell=True)
subprocess.run(f"hdc shell aa start -a AppAbility -b {package_name}", shell=True)
print("启动成功")

5、其他常量

1
2
3
4
5
6
ini复制代码app_file = "xxx/app-signed.zip"
package_name = "com.byhk.app"

app_file_path = os.path.dirname(app_file)
extracted_dir = app_file_path + "/zip"
unzip_app_and_send_files(app_file, package_name, extracted_dir)

只需要测试在本地安装hdc命令环境和python环境,配置好zip包路径,即可运行脚本安装app。

后续计划使用flutter写一个桌面工具,包含app下载和安装。

完整脚本,放在文章末尾。有需要的自提。

以下是关于自动签名和手动签名的方法。

自动签名

这种方式,仅限于开发者自己调试使用。方便快捷。操作如下:

进入File > Project Structure… > Project > Signing Configs界面,勾选“Automatically generate signature”(如果是HarmonyOS工程,需同时勾选“Support HarmonyOS”),即可完成签名。如果未登录,请先单击Sign In进行登录,然后自动完成签名。如下:

image.png

手动签名

相比自动签名,手动签名就相对来说比较麻烦一点。官方文档:developer.huawei.com/consumer/cn…

操作流程图

image.png

准备证书请求文件

需要通过DevEco Studio来生成密钥(.p12)和证书请求文件(.csr)。

生成密钥(.p12)

  1. 在顶部菜单栏选择“Build > Generate Key and CSR”。

image.png

  1. 点击“New”

image.png

  • Key store file:设置密钥库文件存储路径,并填写p12文件名。
  • Password:设置密钥库密码,必须由大写字母、小写字母、数字和特殊符号中的两种以上字符的组合,长度至少为8位。请记住该密码,后续签名配置需要使用。
  • Confirm password:再次输入密钥库密码。

生成证书请求文件(.csr)

image.png

  • Alias:密钥的别名信息,用于标识密钥名称。请记住该别名,后续签名配置需要使用。
  • Password:密钥对应的密码,与密钥库密码保持一致,无需手动输入。
  • Validity:证书有效期,建议设置为25年及以上,覆盖应用/元服务的完整生命周期。
  • Certificate:输入证书基本信息,如组织、城市或地区、国家码等。

点击“Next”

20240426145120.jpg

选择路径和输入名称及其后缀。

至此完成了。密钥(.p12)和证书文件(.csr),Android同学很熟悉。

申请调试证书

登录AppGallery Connect,选择“用户与访问”。

image.png
在左侧导航栏选择“证书管理”,进入证书管理页面,。

image.png

点击右上角“新增证书”

image.png

选择刚刚生成的csr文件。

image.png

会生成一个cer格式的证书文件,将其报错下来。

注册调试设备

在刚刚的证书管理下面,点击设备管理。

image.png
如果有多个设备,可以批量添加

申请调试的profile

1、登录AppGallery Connect,选择“我的项目”。
2. 找到您的项目,点击您创建的HarmonyOS应用/元服务。

image.png

  1. 选择“HarmonyOS应用 > HAP Provision Profile管理”,进入“管理HAP Provision Profile”页面,点击右上角“添加”。

image.png

至此,我们已经完成了profile的文件生成。(后缀名为.p7b),将其下载下来保存到本地。

(备注:每次新增设备,都需要重新生成调试profile文件)

目前我们电脑上有以下文件了:

image.png

脚本

  • 签名脚本
  • 安装脚本

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Volo 0100 正式发布!

发表于 2024-04-26

图片

Volo 是字节跳动服务框架团队研发的 高性能、可扩展性强 的 Rust RPC 框架,使用了 Rust 最新的 AFIT 和 RPITIT 特性。

文章来源|CloudWeGo 开源社区

Volo 0.10.0 版本中,我们更多地关注可扩展性和易用性。

01 Break Change

错误处理重构

原先的错误类型存在诸如语义不清晰、可维护性不强、可扩展性不强、容易误用等问题,因此在新版中,我们重构了整个错误处理部分,极大加强了错误处理部分的语义清晰度和可维护性,并通过类型系统降低误用概率。

迁移指南

Server Handler 迁移
  1. 如果原先使用 anyhow::Result,把anyhow:Result改为 volo_thrift::ServerResult 即可:

图片

  1. 如果原先使用 Result<XResp, anyhow::Error> ,将 anyhow::Error 改为 volo_thrift::ServerError 即可:

图片

  1. 如果原先使用了 Exception 的用户,需要将返回类型 Result<XResp, volo_thrift::UserException<XException>>改为Result<volo_thrift::MaybeException<XResp, XException>,volo_thrift::ServerError>,同时将原先返回 Err(UserError::UserException(exception)) 的地方改为使用 Ok(MaybeException::Exception(exception)) 即可:

图片

  1. 如果改完之后,在返回 anyhow::Error 时出现报错,可以手动加一个 .into()。

图片

  1. 如果改完之后,在 ? 返回错误处出现报错,可以尝试先转换成 anyhow::Error 再返回。

图片

Service 中间件迁移

对于不感知用户错误的中间件来说,本次修改应该不带来 break change;如果有需要感知用户错误,那么只需要把原来的 volo_thrift::Error 改为 volo_thrift::ServerError/ClientError 即可。

Client 迁移

client 部分的错误从原来的 ResponseError 改为了 ClientError,按编译器报错提示匹配新的错误 variant 即可。

IDL 管理文件 volo.yml 格式重构

新版 yml 配置的结构更加清晰,且更易于维护,并主要解决了旧版中无法支持 git 跨仓库引用的问题,具体的功能和配置参数见 config。另外,对于 volo-cli 命令行工具,我们将之前的 idl 命令名字修改为了 repo。

config: www.cloudwego.io/zh/docs/vol…

迁移指南

安装 volo-cli 0.10.0 版本,并在 volo.yml 目录下执行 volo migrate 命令即可自动迁移。

默认生成的 Enum 类型修改

在新版生成代码中,默认生成的 Enum 类型修改为了 i32 wrapper 的 newtype 类型,以便于更好的向前兼容 IDL enum 字段中枚举值的修改。

迁移指南

将 enum 字段中枚举值名字修改为对应生成的名字即可,如 Foo::Bar -> Foo::BAR。

02 完整 Release Note

完整的 Release Note 可以参考 Volo Changelog。

项目地址

GitHub:github.com/cloudwego

官网:<www.cloudwego.io>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Sublime Text 3配置C/C++开发环境 一、引言

发表于 2024-04-26

《开发工具系列》

image.png

一、引言

C/C++ 的开发工具有很多,相信大家多多少少都有所了解,比如 Visual C++、Codeblocks、VSCode、Dev C++ 等等;本篇 Huazie 介绍一个比较轻量级的开发环境 Sublime Text 3,并用它来配置 C/C++ 开发环境。

二、主要内容

2.1 初识 Sublime Text 3

Sublime Text 3 是一款流行的文本编辑器,它的特点是体积小巧、启动速度快、界面简洁美观。它具有强大的代码编辑功能,支持多种编程语言。此外,Sublime Text 3 还具有丰富的插件生态系统,用户可以根据自己的需求安装各种插件来扩展其功能。

Sublime Text 3 的一些主要特点,如下所示:

  • 强大的代码编辑功能:Sublime Text 3 提供了许多实用的代码编辑功能,如自动完成、代码高亮、代码片段等,大大提高了编程效率。
  • 支持多种编程语言:Sublime Text 3 支持多种编程语言,包括 HTML、CSS、JavaScript、Python、Ruby、PHP 等,用户可以根据需要选择不同的语言模式。
  • 插件生态系统:Sublime Text 3 拥有丰富的插件生态系统,用户可以通过安装插件来扩展其功能,如 Emmet(用于编写 HTML 和 CSS)、Package Control(用于安装和管理插件)等。
  • 自定义快捷键:Sublime Text 3 允许用户自定义快捷键,以便更快速地执行常用操作。
  • 多窗口编辑:Sublime Text 3 支持多窗口编辑,用户可以同时打开多个文件进行编辑,方便进行代码对比和复制粘贴操作。
  • 跨平台支持:Sublime Text 3 支持 Windows、Mac 和 Linux 操作系统,用户可以在不同的平台上使用相同的设置和插件。
  • 版本控制集成:Sublime Text 3 可以与版本控制系统(如 Git)集成,方便用户进行代码版本管理。

2.2 接入 mingw-w64

Mingw-w64 是一个用于在 Windows 系统上支持 GCC 编译器的完整运行时环境。这个项目是原始 Mingw.org 项目的进一步发展,旨在支持 64位 和 32位 Windows 操作系统原生的二进制文件。在 2007 年,Mingw-w64 从原始 Mingw.org项目分叉出来,提供对 64位和 新API 的支持。从那时起,它逐渐得到了广泛的应用和传播。

Mingw-w64 提供了一百万行以上的头文件、库和运行时,用于在 Windows 上链接和运行代码。它包括 Winpthreads 库(用于 C++11 线程支持)和 Winstorecompat库(一个便利库,可简化与 Windows 应用商店的一致性)。与VisualStudio 相比,Mingw-w64 在数学支持方面更为完善,并且执行速度更快。

2.2.1 下载 mingw-w64

参考这个帖子内容,下载一下 mingw-w64:

tieba.baidu.com/p/548754485…

我目前用的 MinGW-w64 9.0.0,有需要的可以在评论回复 或者 私信我。

2.2.2 环境变量配置

不管是安装版的,还是免安装版的,都会有类似如下的目录:

image.png

上图中的 bin 目录,我们点进去看下:

image.png

实际上这里的 gcc.exe 和 g++.exe 就是 C/C++ 程序编译和运行的关键所在,当然这里还有其他的可执行程序,感兴趣的朋友可以自行搜索了解,这里不展开了。

现在,Huazie 以 window 11 系统为例,介绍下配置环境变量,如下:

右击 Window 图标,打开下图并选择 系统:

image.png

点击 高级系统设置,打开系统属性页面,点击 环境变量 :

image.png

找到 Path 环境变量,配置上面你的 mingw64 的 bin 目录进去:

image.png

2.3 配置 C/C++ 开发环境

打开 Sublime Text 3,我们可以看到如下的界面:

image.png

菜单栏选择 Tools => Build System => New Build System

image.png

2.3.1 C Build System 配置

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
c复制代码{
"windows": {
"cmd": ["gcc", "-std=c11", "${file}", "-o", "${file_base_name}.exe"],
},
"cmd": ["gcc", "-std=c11", "${file}", "-o", "${file_base_name}"],
"file_regex": "^(.*)\\(([0-9]+),([0-9]+)\\) (Error|Fatal): (.*)$",
"working_dir": "${file_path}",
"selector": "source.c",
"encoding": "cp936",
"variants": [
{
"name": "Run",
"shell": true,
"windows": {
"shell_cmd" : "start cmd /c \"\"${file_base_name}.exe\" & echo. & pause\""
}
},
{
"name": "Build and Run",
"shell": true,
"windows": {
"shell_cmd": "gcc -std=c11 \"${file}\" -o \"${file_base_name}.exe\" && start cmd /c \"\"${file_base_name}.exe\" & echo. & pause\""
},
}
]
}

上述内容保存在,前面打开的 New Build System 中,并命名为 C.sublime-build:

image.png

2.3.2 C++ Build System 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码{  
"cmd": ["g++", "${file}", "-o", "${file_path}/${file_base_name}", "-Wall" ,"&&","start","cb_console_runner.exe","${file_path}/${file_base_name}"],
"file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
"working_dir": "${file_path}",
"selector": "source.c, source.c++",
"shell": true,
"encoding": "cp936",
"variants":
[
{
"name": "Run",
"cmd": ["start","cb_console_runner.exe","${file_path}/${file_base_name}"]
}
]
}

重新 New Build System,将上述内容保存其中,并命名为 CPP.sublime-build

image.png

2.4 编写 C/C++ 代码

现在让我们开始编写吧!!!

2.4.1 第一个 C 代码【helloworld.c】

1
2
3
4
5
6
7
8
c复制代码#include<stdio.h>

int main()
{
printf("hello world!\n");
printf("[C]Author: Huazie");
return 0;
}

2.4.2 第一个 C++ 代码【helloworld.cpp】

1
2
3
4
5
6
7
8
9
cpp复制代码#include <iostream>
using namespace std;

int main()
{
cout << "Hello World" << endl;
cout << "[C++]Author: Huazie";
return 0;
}

2.5 运行 C/C++ 代码

2.5.1 运行 C 代码

菜单栏 Tools => Build System ,然后 选择 C,就是前面的 C.sublime-build。

然后直接 Ctrl + B,编译运行当前的程序,运行截图如下所示:

image.png

2.5.2 运行 C++ 代码

菜单栏 Tools => Build System ,然后 选择 CPP,就是前面的 CPP.sublime-build。

然后直接 Ctrl + B,编译运行当前的程序,运行截图如下所示:

image.png

三、总结

本篇 Huazie 介绍了 Sublime Text 3 配置 C/C++ 的相关内容,感兴趣的朋友赶紧配置起来,有任何问题可以随时评论区沟通。

四、更新

经过上面的配置,大家都能写自己的第一个 C/C++ 代码了,但是有些小伙伴慢慢使用发现,如果输出的内容包含中文,打印出来的信息是乱码的。有关这个问题,请查看笔者的另一篇博文《Sublime Text 3 解决中文乱码问题》。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

未命名

发表于 2024-04-26

这里每天分享一个 iOS 的新知识,快来关注我吧

苹果官宣 5 月 7 日举办“Let Loose”活动

Apple 宣布将于北京时间 5 月 7 日晚上 10 点举行一场特别活动,预计将发布新的 iPad Pro 和 iPad Air 型号,以及更新的 Apple Pencil 和妙控键盘等配件。

以下是一些关于新 iPad 的一些预测:

  1. 配备 M3 芯片
  2. OLED 显示屏
  3. 更薄的外壳、更薄的边框
  4. 支持横向前置摄像头
  5. 支持 iPad Pro 的全新妙控键盘
  6. 新的 Apple Pencil,并可能支持 Vision Pro

在苹果的历史上,5 月只举办过两场苹果活动:1999 年的 WWDC,和 1998 年 5 月的第一代 iMac 发布!这次是第三场。

iOS 17.5 Beta 3 发布

Apple 发布了即将推出的 iOS 17.5 和 iPadOS 17.5 更新的第三个测试版。

本次更新更改包括 Apple News+、 Game Center 排行榜集成、播客小组件的颜色匹配等功能。

苹果削减 Vision Pro 出货量

根据郭明錤的一份分析报告,苹果已经下调了 Vision Pro 的出货量预期。苹果现在的目标是今年 Vision Pro 的出货量在 40 万至 45 万台之间。这低于最初市场共识的 70 万至 80万台。

Vision Pro 在美国的需求“大幅下降,超出了预期”,这直接导致了其出货量削减。

另外该分析透露,苹果将在 6 月的 WWDC 之前在其他国家/地区发布 Vision Pro。

iPhone 16 将配备电容触摸按钮

据经济日报报道,iPhone 16 可能会配备新的触摸电容式按钮。

这些按钮会提供触觉反馈,而不是传统的物理按钮,有点类似于 iPhone 7 上的 Home 按钮。

苹果收购法国人工智能公司

苹果收购了总部位于巴黎的人工智能初创公司 Datakalab,该公司专注于算法压缩和嵌入式 AI 系统。

这项收购于去年 12 月 17 日完成,是悄悄进行的。虽然该交易的财务细节尚未披露,但此举可以肯定是苹果 AI 战略的一部分,预计将在 iOS 18 中引入的 iPhone 16 等设备。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用python 下载&替换markdown文章中的图床url

发表于 2024-04-26

1、问题&困扰

下边这个问题困扰我有段时间了:

因为文章是在掘金,知乎这类技术网站发表的,在发表时文章中的图片都是上传到了掘金 or 知乎的图床上了,不管是用的哪家哪个大厂的图床,我总是担心哪一天网站崩掉或者其他什么不可控的事情发生,那我博客中的所有的图床文件(大部分都是图片)那岂不是都访问不到了? 像这样:
image.png
这就尴尬了,我一般写文章很重视图解,没有图片的文章那没法串联起来,所以我总是想的把文章复制到本机上,但是复制到本地上的文章中的图片不用说也仍然是访问的三方的图床呀,如下:
image.png

2、我的目标

  1. 将这类三方图床文件下载到本地
  2. 将markdown中的图传连接替换为指向本机的图片路径,如下:image.png

我不可能一个个手动去下载替换,那样士可忍孰不可忍。所以我决定使用python批量替换,基本逻辑是:

  1. 找到替换前的markdown文章集合
  2. 依次读取文章内容,并使用正则找到图床文件,并下载到本地(会在当前目录创建个专门放图片的文件夹)
  3. 替换markdown中的图床链接为本地的图片路径。
  4. 另外我还生成了word文件,以作备份。

3、使用python实现

读取指定文件夹中的文章集合,并解析,正则匹配与下载替换:

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
python复制代码
from functools import partial

from util.download_util import download_pic
from util.file_util import is_dir_existed, search_all_file, read_file_text_content, write_text_to_file
from util.logger_util import default_logger
import os
import re
import time

logger = default_logger()
origin_md_dir = os.path.join("/Users/hzz/Documents/黄壮壮_写作/已发表的博客_掘金存储图片/") # 原始md文件目录
local_md_dir = os.path.join("/Users/hzz/Documents/黄壮壮_写作/已发表博客_本地存储图片/") # 本地md文件目录
local_doc_dir = os.path.join("/Users/hzz/Documents/黄壮壮_写作/已发表博客_doc/") # 本地doc文件目录
server_md_dir = os.path.join(os.getcwd(), "server_md") # 转换成自己的图片服务器md文件目录
pic_match_pattern = re.compile(r'(]: |()+(http.*?.(png|PNG|jpg|JPG|gif|GIF|svg|SVG|webp|awebp|image))??()?)',
re.M) # 匹配图片的正则
order_set = {i for i in range(1, 500000)} # 避免图片名重复后缀
generate_server_md = False # 是否在本地化后生成为自己图床的md文件
generate_doc = True # 是否在本地化后生成doc文件


# 检索md文件
def retrieve_md(file_dir):
logger.info("检索路径 → %s" % file_dir)
md_file_list = search_all_file(file_dir, target_suffix_tuple=('md', "MD"))
file_name = input('请输入指定的文件: ')

if len(file_name)!=0:
filtered_files = []
for filename in md_file_list:
if file_name in filename:
# 这里可以添加更多逻辑处理
filtered_files.append(filename)
md_file_list=filtered_files

if len(md_file_list) == 0:
logger.info("未检测到Markdown文件,请检查后重试!")
exit(-1)
else:
logger.info("检测到Markdown文件 → %d个" % len(md_file_list))
logger.info("=" * 64)
for pos, md_file in enumerate(md_file_list):
logger.info("%d、%s" % (pos + 1, md_file))
logger.info("=" * 64)
logger.info("执行批处理操作")
process_md(md_file_list)


# 处理文件列表
def process_md(file_list):
for file in file_list:
to_local_md(file)


# 转换成本地MD
def to_local_md(md_file):
logger.info("处理文件:【%s】" % md_file)
# 获取md文件名
md_file_name = os.path.basename(md_file)
# 生成md文件的目录、图片目录,doc目录
new_md_dir = os.path.join(local_md_dir, md_file_name[:-3])
new_picture_dir = os.path.join(new_md_dir, "images")
is_dir_existed(new_md_dir)
is_dir_existed(new_picture_dir)
# 生成md文件路径
new_md_file_path = os.path.join(new_md_dir, md_file_name)
new_doc_file_path = os.path.join(local_doc_dir, md_file_name[:-3].replace("知乎盐选 ", "") + ".docx")
# 读取md文件内容
old_content = read_file_text_content(md_file)
# 替换原内容
new_content = pic_match_pattern.sub(partial(pic_to_local, pic_save_dir=new_picture_dir), old_content)
# 生成新的md文件
write_text_to_file(new_content, new_md_file_path)
logger.info("新md文件已生成 → {}".format(new_md_file_path))
# 生成新的doc文件
if generate_doc:
os.chdir(new_md_dir)
logger.info("新doc文件已生成 → {}".format(new_doc_file_path))
logger.info("=" * 64)
if generate_server_md:
to_server_md(new_md_file_path)


# 远程图片转换为本地图片
def pic_to_local(match_result, pic_save_dir):
logger.info("替换前的图片路径:{}".format(match_result[2]))
# 生成新的图片名
img_file_name = "{}_{}.{}".format(int(round(time.time())), order_set.pop(), "png")# match_result[3]
# 拼接图片相对路径(Markdown用到的)
relative_path = 'images/{}'.format(img_file_name)
# 拼接图片绝对路径,下载到本地
absolute_path = os.path.join(pic_save_dir, img_file_name)
logger.info("替换后的图片路径:{}".format(relative_path))
# 下载图片
download_pic(absolute_path, match_result[2])
# 还需要拼接前后括号()
return "{}{}{}".format(match_result[1], relative_path, match_result[4])

# 转换成自己的图片服务器md文件
def to_server_md(md_file):
pass


if __name__ == '__main__':
is_dir_existed(origin_md_dir)
is_dir_existed(local_md_dir, is_recreate=True)
is_dir_existed(local_doc_dir, is_recreate=True)
is_dir_existed(server_md_dir, is_recreate=True)
# loop = asyncio.get_event_loop()
retrieve_md(origin_md_dir)

下载图片 download_util 工具类:

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
python复制代码

import os
import time

import requests


from util.logger_util import default_logger

logger = default_logger()

default_headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Encoding': 'gzip, deflate, br',
'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?0',
'Upgrade-Insecure-Requests': '1'
}


def download_pic(pic_path, url, headers=None):
try:
if headers is None:
headers = default_headers
if url.startswith("http") | url.startswith("https"):
if os.path.exists(pic_path):
logger.info("图片已存在,跳过下载:%s" % pic_path)
else:
res1 = requests.get(url, headers=headers,verify=False)

# res1.encoding = "utf8"
with open(pic_path, "wb+") as f:
f.write(res1.content)
else:
logger.info("图片链接格式不正确:%s - %s" % (pic_path, url))
time.sleep(1)
except Exception as e:
logger.info("下载异常:{}\n{}".format(url, e))

logger 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码
import logging


# 默认日志工具
def default_logger():
custom_logger = logging.getLogger("CpPythonBox")
if not custom_logger.hasHandlers():
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
fmt='%(asctime)s %(process)d:%(processName)s- %(levelname)s === %(message)s',
datefmt="%Y-%m-%d %H:%M:%S %p"))
custom_logger.addHandler(handler)
custom_logger.setLevel(logging.INFO)
return custom_logger

file_util 工具类:

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
python复制代码
def search_all_file(file_dir=os.getcwd(), target_suffix_tuple=()):
""" 递归遍历文夹与子文件夹中的特定后缀文件

Args:
file_dir (str): 文件目录
target_suffix_tuple (Tuple(Str)): 文件目录

Returns:
list : 文件路径列表
"""
file_list = []
# 切换到目录下
os.chdir(file_dir)
file_name_list = os.listdir(os.curdir)
for file_name in file_name_list:
# 获取文件绝对路径
file_path = "{}{}{}".format(os.getcwd(), os.path.sep, file_name)
# 判断是否为目录,是往下递归
if os.path.isdir(file_path):
# print("[-]", file_path)
file_list.extend(search_all_file(file_path, target_suffix_tuple))
os.chdir(os.pardir)
elif target_suffix_tuple is not None and file_name.endswith(target_suffix_tuple):
# print("[!]", file_path)
file_list.append(file_path)
else:
pass
# print("[+]", file_path)
return file_list



def write_text_to_file(content, file_path, mode="w+"):
""" 将文字写入到文件中

Args:
content (str): 文字内容
file_path (str): 写入文件路径
mode (str): 文件写入模式,w写入、a追加、+可读写

Returns:
None
"""
with lock:
try:
with open(file_path, mode, encoding='utf-8') as f:
f.write(content + "\n", )
except OSError as reason:
print(str(reason))



def read_file_text_content(file_path):
""" 以文本形式读取文件内容

Args:
file_path (str): 文件路径

Returns:
str: 文件内容
"""
if not os.path.exists(file_path):
return None
else:
with open(file_path, 'r+', encoding='utf-8') as f:
return f.read()


def is_dir_existed(file_path, mkdir=True, is_recreate=False):
""" 判断目录是否存在,不存在则创建

Args:
file_path (str): 文件路径
mkdir (bool): 不存在是否新建
is_recreate (bool): 存在是否删掉重建

Returns:
默认返回None,如果mkdir为False返回文件是否存在
"""
if mkdir:
if not os.path.exists(file_path):
os.makedirs(file_path)
else:
if is_recreate:
delete_file(file_path)
if not os.path.exists(file_path):
os.makedirs(file_path)
else:
return os.path.exists(file_path)

4、效果

执行过程:
image.png
image.png
本机图片:
image.png
image.png
markdown中展示:
image.png

所有文章都替换完成:
image.png

同样的,知乎等其他网站上的文章只要是markdown格式的 图床文件能请求成功的 都可以使用此工具实现下载到本地并替换到文章中去实现本机存储图片永久保留。如果帮助到了你,麻烦点赞收藏评论三连!😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋😋

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…111213…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%