2023年3月

最近AI自动生成技术文章和答案在圈子里面引起了很大轰动,Stack Overflow暂时拒绝接收GPT生成的
结果
。我也经常性地浏览或者编写博客,但是最近我使用new bing或者ChatGPT的过程中,我不需要再从博客中去一篇篇搜索相关的信息,AI会直接将答案展现给我,虽然答案也不一定完全正确,但是稍作修改,基本上就是完全可用的状态。

回忆我写博客的初衷:

  • 将自己的工作积累下来,以后需要用的时候可以查阅。
  • 向他人分享自己的经验,并交流获得更多解决方案。
    这两点可以发现,核心目标都是为了以后能够更快速的获得可用的方案与代码段,现在AI已经基本上可以替代以上两个关键点了。

迷思

自从ChatGPT问世以来,很多人已经用它作为一项很重要的生产力工具了:我周边就流行一个说法“遇事不决GPT”,我也渐渐不再用搜索引擎,很多常规性的代码可以获取,通过调试prompt,就能得到我需要答案。

当我想开始写博客的时候,我陷入了一种焦虑:我写的内容是不是可以直接通过AI技术直接获取?既然别人不看,我自己也不看,那我还写这个东西干什么?于是我带着问题问ChatGPT:“现在GPT已经能够生成比较有质量的代码技术博客了,人类是否还有必要继续编写技术博客呢?”

它的回答:“虽然GPT已经能够生成一些有质量的技术博客,但是目前来说,它还无法完全取代人类编写技术博客的作用。这是因为在技术领域,不仅仅需要展示代码和解释技术原理,还需要结合实际项目中遇到的问题、解决方案以及经验教训等方面进行深度探讨。这种深度探讨往往需要对具体情况进行分析和总结,而这些都需要人类的思考和经验。此外,技术博客还需要与读者进行交流和互动,了解读者的需求和反馈,并进行进一步的探索和研究。

因此,尽管GPT已经在生成技术博客方面有了重大突破,但是人类编写技术博客的作用仍然非常重要,尤其是在深度探讨、经验分享和与读者交流等方面。”

发展方向

分析一下它的回复,大致意思是,如果博文不能在这几个方面提供价值,那么以后将没有什么意义了,我也再补充了一点:

经验与探讨

博文结合实际的案例进行分析,并提供FAQ等经验性内容。这样的“深度探讨”AI其实也可以生成一些内容,包括常见的问题、甚至对你描述的系统提供一个技术选型。随着以后AI的继续发展,AI提供的方案、分析已经FAQ会具备相当的可行性。

那么我们能做的就是提供一些应用于具体案例上的情况总结与分析,毕竟暂时就某一个具体项目而言,AI缺少了实地测试、试错的过程,不太能提供经验性的内容。

交流与互动

这个暂时AI替代不了,谁不喜欢点赞呢,哈哈哈。

多样的内容形式

现在AI输出的内容以文字和代码为主,输出的图片也多是拼凑计算出来。如果要有图文对应的内容,现在的AI基本无法正确输出。

未来

比起写一段段的文字,很多人更喜欢在既有文字上进行编排或修改(比如我),AI技术能够生成大量的可用的文字,这对他们非常有帮助。随着更多通识性的内容AI可以信手拈来,没有独特性的博客价值会越来越低,很多主题我写了草稿,但是现在看来似乎已经没有必要发出来了。

未来的博客目标应当是:用于备忘的博客内容使用AI辅助编写,增加效率;用心写的需要分享具体项目的解决方案或者相关思考。

P.S. 有人说程序员这个职业会被消灭,AI对于很多职业来说是降维打击什么的。从蒸汽机时代开始,纺织女工变成了机器操作工人,各种职业随着科技的发展不断被消灭,同时也会产生很多新的职业。AI的发展肯定会消灭一些职业,但是只要我们保持自我的判断力、保持不断学习,未来就能适应新的职业。

本文介绍基于
Python

ArcPy
模块,对
大量
栅格遥感影像文件进行
批量掩膜

批量重采样
的操作。

首先,我们来明确一下本文的具体需求。现有一个存储有大量
.tif
格式遥感影像的文件夹;且其中除了
.tif
格式的遥感影像文件外,还具有其它格式的文件。

我们希望,依据一个已知的面要素矢量图层文件,对上述文件夹中的全部
.tif
格式遥感影像进行掩膜,并对掩膜后的遥感影像文件再分别加以批量重采样,使得其空间分辨率为
1000
m。

明确了需求后,我们就可以开始具体的操作。首先,本文所需用到的代码如下。

# -*- coding: utf-8 -*-
"""
Created on Fri Apr 15 16:44:26 2022

@author: fkxxgis
"""

import arcpy
from arcpy.sa import *

tif_file_path="E:/LST/Data/NDVI/03_Mosaic/"
shp_file="E:/LST/Data/Region/YellowRiver_nineprovince.shp"
out_file_path="E:/LST/Data/NDVI/04_Mask/"
resample_file_path="E:/LST/Data/NDVI/05_Resample/"
arcpy.env.workspace=tif_file_path
arcpy.env.extent=shp_file

tif_file_name=arcpy.ListRasters("*","tif")
for tif_file in tif_file_name:
    mask_result=ExtractByMask(tif_file,shp_file)
    mask_result_path=out_file_path+"/"+tif_file.strip(".tif")+"_Mask.tif"
    mask_result.save(mask_result_path)
    
arcpy.env.workspace=out_file_path
tif_file_name=arcpy.ListRasters("*","tif")
for tif_file in tif_file_name:
    resample_file_name=tif_file.strip(".tif")+"_Re.tif"
    arcpy.Resample_management(tif_file,resample_file_path+resample_file_name,
                              1000,"BILINEAR")

其中,
tif_file_path
是原有掩膜前遥感图像的保存路径,
shp_file
是已知面要素矢量图层文件的保存路径,
out_file_path
是我们新生成的掩膜后遥感影像的保存路径,
resample_file_path
则是最终重采样后遥感影像的保存路径。

在这里,我们首先利用
arcpy.ListRasters()
函数,获取路径下原有的全部
.tif
格式的图像文件,并存放于
tif_file_name
中;随后,遍历
tif_file_path
路径下全部
.tif
格式图像文件(即遍历
tif_file_name
),并利用
ExtractByMask()
函数进行掩膜操作;其次,对于掩膜好的图层,在其原有文件名后添加
"_Mask.tif"
后缀,作为新文件的文件名。

对全部图像文件完成掩膜操作后,我们继续进行重采样操作。和前述代码思路类似,我们依然还是先遍历文件,并在其原有文件名后添加
"_Re.tif"
后缀,作为新文件的文件名;随后,利用
Resample_management()
函数进行重采样。其中,
1000
表示重采样的空间分辨率,在这里单位为米;
"BILINEAR"
表示用双线性插值的方法完成重采样。

以上便是本次操作的全部代码;我们这里选择在
IDLE (Python GUI)
中运行代码。运行完毕,得到的一个结果文件如下图;可以看到,遥感影像已经完成了掩膜,且空间分辨率已经为
1000
m。

至此,大功告成。

事件总线Mitt使用非常简单,本篇随笔介绍在Vue3+TypeScript 前端项目中使用的一些场景和思路。我们在Vue 的项目中,经常会通过emits 触发事件来通知组件或者页面进行相应的处理,不过我们使用事件总线Mitt来操作一些事件的处理,也是非常方便的。

Mitt 的GitHub官网地址如下所示:
https://github.com/developit/mitt
, 它的安装和其他插件一样,我们不再赘述,只讲述它的如何使用。

Mitt
具有以下优点:

  • 零依赖、体积超小,压缩后只有
    200b
  • 提供了完整的
    typescript
    支持,能自动推导出参数类型。
  • 基于闭包实现,没有烦人的
    this
    困扰。
  • 为浏览器编写但也支持其它
    javascript
    运行时,浏览器支持
    ie9+
    (需要引入
    Map

    polyfill
    )。
  • 与框架无关,可以与任何框架搭配使用。
Mitt 只是提供了几个简单的方法,如on,off, emit 等基础的几个函数。
在JS中我们使用的话,不需要类型化事件的类型,如下代码所示。
import mitt from 'mitt'const emitter=mitt()//订阅一个具体的事件
emitter.on('foo', e => console.log('foo', e) )//订阅所有事件
emitter.on('*', (type, e) =>console.log(type, e) )//发布一个事件
emitter.emit('foo', { a: 'b'})//根据订阅的函数来取消订阅
functiononFoo() {}
emitter.on(
'foo', onFoo) //listen emitter.off('foo', onFoo) //unlisten //只传一个参数,取消订阅同名事件 emitter.off('foo') //unlisten //取消所有事件 emitter.all.clear()

而我们如果在Vue3 + TypeScript 环境中使用的话,就需要类型化事件的类型,已达到强类型的处理目的。

import mitt from "mitt";

type Events
={
foo: string;
bar: number;
};
//提供泛型参数让 emitter 能自动推断参数类型 const emitter = mitt<Events>();//'e' 被推断为string类型 emitter.on("foo", (e) =>{
console.log(e);
});
//ts error: 类型 string 的参数不能赋值给类型 'number' 的参数 emitter.emit("bar", "xx");//ts error: otherEvent 不存在与 Events 的key中 emitter.on("otherEvent", () =>{//});

在前端项目使用的时候,我们在utils/mitt.ts中定义默认导出的mitt对象,如下代码所示。

//utils/mitt.ts
import mitt, { Emitter } from'mitt';//类型
const emitter: Emitter<MittType> = mitt<MittType>();//导出
export default emitter;

在其中的MittType类型,可以单独文件放置TypeScript的预定义文件目录中,如types/mitt.d.ts

而我们在使用的时候,直接导入该对象就可以了,如下代码所示。

declare type MittType<T = any> ={
openSetingsDrawer
?: string;
restoreDefault
?: string;
setSendColumnsChildren: T;

..................
//省略其他事件类型 noticeRead: number;//消息已读事件 lastAddParentId?: string | number;//新增记住最后的父信息 };

例如我们定义一个更新和记住父菜单的Mitt 事件,在页面加载完毕的时候监听事件,在页面退出的时候关闭事件即可,如下代码所示是在菜单列表页面中处理的。

<script lang="ts" setup name="sysMenu">import { onMounted, onUnmounted, reactive, ref } from'vue';
import mittBus from
'/@/utils/mitt';
......
onMounted(async ()
=>{
handleQuery();

mittBus.on(
'submitRefresh', () => {
handleQuery();
});
mittBus.on('lastAddParentId', (pid) => {
state.lastAddParentId = pid as string;//记住最后的父菜单ID
});
});

onUnmounted(()
=>{
mittBus.off(
'submitRefresh');
mittBus.off('lastAddParentId'
);
});
</script>

在新增菜单的时候我们触发对应刷新事件
submitRefresh
,以及触发选择的父记录ID的事件
lastAddParentId
,这样就可以做相应的处理了。

例如在菜单的编辑子控件页面中,我们触发对应的事件逻辑代码如下所示。

//关闭弹窗
const closeDialog = () =>{
mittBus.emit(
'submitRefresh');
state.isShowDialog
= false;
};
//提交 const submit = () =>{
ruleFormRef.value.validate(async (valid:
boolean) =>{if (!valid) return;if (state.ruleForm.id != undefined && state.ruleForm.id > 0) {
await menuApi.update(state.ruleForm);
}
else{
await menuApi.add(state.ruleForm);
//记住最后的菜单 mittBus.emit('lastAddParentId', state.ruleForm.pid);
}
closeDialog();
});
};

如果为了减少每次重复的导入mitt,也可以把它全局挂载到变量中,统一入口进行访问,详细可以参考随笔《
在基于vue-next-admin的Vue3+TypeScript前端项目中,为了使用方便全局挂载的对象接口
》处理即可。

const $u: $u_interface ={
message,
test,
util,
date,
crypto,
base64,
$t: i18n.global.t,
fun: commonFunction(),

cloneDeep,
debounce,
throttle,
mitt
};
//安装$u组件到app上 import type { App } from 'vue';
export
default{
install(app: App
<Element>) {//挂载全局 app.config.globalProperties.$u =$u;
}
};

1.手动实现中缀转后缀

中缀表达式转后缀表达式

2.代码实现中缀转后缀并计算表达式结果

为了简化问题,假设算术运算符仅由加、减、乘、除4种运算符和左、右括号组成。

step1: 声明栈结构

#include <iostream>
#include <string>
using namespace std;

#define MaxSize 100
template <class DataType>
struct SeqStack
{
    DataType data[MaxSize];
    DataType *top;
};

step2: 定义函数
TranslateInfixExp
实现中缀表达式转后缀表达式

/* 中缀表达式转后缀表达式 */
void TranslateInfixExp(string exp, string &result)
{
    if (exp.empty())
        return;
    // step1: 定义操作符栈并初始化栈
    struct SeqStack<char> opStack;
    opStack.top = opStack.data;

    // step2: 遍历中缀表达式
    char cur;
    for (int i = 0; i < exp.size(); i++)
    {
        cur = exp[i];
        switch (cur)
        {
        // 遇到 '(' ,入栈
        case '(':
            *(opStack.top++) = cur;
            break;
        // 遇到 ')' ,依次将栈中的运算符出栈并加入后缀表达式中,直至栈顶元素为 '(' ,'(' 出栈
        case ')':
            while (*(opStack.top - 1) != '(')
            {
                result.push_back(*(--opStack.top));
                result.push_back(' ');
            }
            opStack.top--;
            break;
        // 遇到 '+' 或 '-',依次将优先级不低于所读运算符的栈顶元素出栈并加入后缀表达式,然后将所读运算符入栈
        case '+':
        case '-':
            while ((opStack.top != opStack.data) && *(opStack.top - 1) != '(')
            {
                result.push_back(*(--opStack.top));
                result.push_back(' ');
            }
            *(opStack.top++) = cur;
            break;
        // 遇到 '*' 或 '/' ,依次将优先级不低于所读运算符的栈顶元素出栈并加入后缀表达式,然后将所读运算符入栈
        case '*':
        case '/':
            while ((opStack.top != opStack.data) && (*(opStack.top - 1) == '*') || (*(opStack.top - 1) == '/'))
            {
                result.push_back(*(--opStack.top));
                result.push_back(' ');
            }
            *(opStack.top++) = cur;
            break;
        // 遇到数字字符,直接入栈
        default:
            while (cur >= '0' && cur <= '9')
            {
                result.push_back(cur);
                cur = exp[++i];
            }
            result.push_back(' ');
            i--; // 回退至后续首个尚未进行优先级判断的操作字符
            break;
        }
    }
    // step3: 将栈内剩余元素依次出栈
    while (opStack.top != opStack.data)
    {
        result.push_back(*(--opStack.top));
        result.push_back(' ');
    }
    return;
}

注意:

  1. 在将中缀表达式转后缀表达式过程中,每输出一个数字字符,需要在其后补一个空格(与其他相邻数字字符隔开),否则一连串数字字符放在一起无法区分是一个数字还是两个数字。
  2. 遇到数字字符入栈时,若当前运算项位数>1时,需要在当前数字字符入栈后后移一位并重复入栈(代码中switch的default段代码),并在运算项入栈完毕之后需要将索引i回退至后续首个尚未进行优先级判断的运算符上(即非数字字符)。

step3: 定义函数
CaculatePostFixExp
实现后缀表达式结果计算

/* 计算后缀表达式结果 */
float CaculatePostFixExp(string exp)
{
    float result = 0;
    if (exp.empty())
    {
        cout << "The expression is wrong!\n";
        exit(-1);
    }

    // step1 : 定义一个数据字符栈,并初始化
    struct SeqStack<float> numStack;
    numStack.top = numStack.data;

    // step2 : 遍历后缀表达式
    char cur;
    for(int i=0; i<exp.size(); i++)
    {
        cur = exp[i];
        if (cur >= '0' && cur <= '9') // 若当前字符为数字字符
        {
            float value = 0;
            while (cur != ' ')
            {
                value = value * 10 + cur - '0';
                cur = exp[++i];
            }
            *(numStack.top++) = value;
        }
        else if(cur != ' ') // 若当前字符是运算符(空格直接忽略)
        {
            float num1 = *(--numStack.top);
            float num2 = *(--numStack.top);
            switch (cur)
            {
            case '+':
                *(numStack.top++) = num2 + num1;
                break;
            case '-':
                *(numStack.top++) = num2 - num1;
                break;
            case '*':
                *(numStack.top++) = num2 * num1;
                break;
            case '/':
                *(numStack.top++) = num2 / num1;
                break;
            default:
                break;
            }
        }
    }
    // step3 : 栈中最终元素出栈,即为所求表达式的值 
    if (numStack.top != numStack.data)
    {
        result = *(--numStack.top);
        return result;
    }
    else
    {
        cout << "The expression is wrong!\n";
        exit(-1);
    }
}

注意:

若当前字符为运算符且为减号'-'时,先出栈的为减数,后出栈的为被减数;对于除法'/'也一样。

step4: main函数调用

int main()
{
    string infixExp;   // 存储用户输入的中缀表达式
    string postfixExp; // 存储转换后的后缀表达式
    double result;     // 存储后缀表达式计算机结果
    cout << "Please enter an infix expression:\n";
    cin >> infixExp; // 6+(7-1)*3+10/2
    cout << "The infix expression is:  " << infixExp << endl;
    TranslateInfixExp(infixExp, postfixExp);
    cout << "The postfix expression is:  " << postfixExp << endl;
    result = CaculatePostFixExp(postfixExp);
    cout << "The postfix expression calculation result is:  " << result << endl;
    return 0;
}

输出:

Please enter an infix expression:
6+(7-1)*3+10/2
The infix expression is:  6+(7-1)*3+10/2
The postfix expression is:  6 7 1 - 3 * + 10 2 / +
The postfix expression calculation result is:  29

本篇参考:

salesforce 零基础开发入门学习(十一)sObject及Schema深入

https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_entitydefinition.htm

https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_fielddefinition.htm

想获取metadata相关的信息,我们第一件事想起来的可能就是 Schema Namespace,通过Schema命名空间的 DescribeSObjectResult 以及 DescribeFieldResult可以搞定很多事情。然而不是所有的metadata信息或者表字段信息都可以在 Schema命名空间下获取,今天我们讲一下表字段的 Durable Id的概念以及 EntityDefinition 以及 FieldDefinition的简单实用。

我们先来看下方的两个图的区别。

下图为Account表的自定义字段,我们看到 FieldAndRelationships后面是一个15位的ID

下图为Account的标准字段,我们可以看到URL直接展示的是Field API 名称。

其实不只是标准和自定义字段的区别,表同样适用于这个情况。所以问题来了, 15位ID是什么? 如何获取到?

15位ID是 Durable Id,用于作为表或者字段的唯一标识符。在使用之前一定要检索这个值,因为这个值不能保证从一个版本到另一个版本都是一样的。为了简化查询,可以使用这个字段。

接下来的问题就是如何获取,可能大部分人第一想法就是查看 Schema命名空间,然后查看 DescribeObjectResult以及DescribeFieldResult这两个类的方法,很可惜,这两个类里面都没有相关的方法,那如何进行获取呢? 揭示今天的主角, Tooling API中的 EntityDefinition 以及 FieldDefinition这两个表。

一. FieldDefinition以及 EntityDefinition简单介绍

EntityDefinition:此表用于提供对标准和自定义表的基于行级别的针对metadata的访问。基于此表的查询,也可以查询相关的子表的查询,官方文档中也同样做了一些描述。我们看一下下面的简单的例子:下图的搜索用于搜索Account表的 DeveloperName, NewUrl,同时搜索 Account的两个子信息。

1. Account表所有的字段信息,字段信息搜索了 DeveloperName以及 DurableId,

2. Account表所有的Record Type信息。

SELECTDeveloperName,QualifiedApiName,NewUrl,
(
SELECT Id, DeveloperName, DurableId FROMFields),
(
SELECT Name FROMRecordTypes)FROMEntityDefinitionWHERE QualifiedApiName = 'Account'

除了上述的子查询以外,此表还有很多的允许查询的子表信息,更多可以查看官方文档。我们看一下输出的大致信息以及结构

通过这里我们可以看出来结构,以及针对标准和自定义字段的 DurableId的区别了。

FieldDefinition:此表用于提供对标准和自定义字段的基于行级别的针对metadata的访问。上面的demo中返回的结构还是太过庞大,如果我们只想返回某个字段的信息,我们便可以通过 fieldDefinition的查询进行更好的结果返回,下方demo返回 Account Industry的信息

SELECTId, DeveloperName, DurableIdFROMFieldDefinitionWHERE DeveloperName = 'Industry' 
AND EntityDefinition.QualifiedApiName = 'Account'

我们可以看一下结果展示

二. demo

这两个表有很多字段,感兴趣的小伙伴可以自行查看每个字段的含义。我们再结合着上一篇的demo进行优化,封装一个方法,通过object api name以及field api name进行获取custom metadata type中维护的default value.

public withsharing class CommonUtils {publicstatic String getDefaultValueFromMetadataType(String objectApiName, String fieldApiName) {
List
<Default_Value__mdt> defaultValueList = Default_Value__mdt.getAll().values();
String result
= '';
String durableId;
List
<FieldDefinition> fieldDefinitionList = [SELECT Id, DurableId
FROM FieldDefinition
WHERE EntityDefinition.QualifiedApiName = :objectApiName
AND QualifiedApiName = :fieldApiName
];if(fieldDefinitionList != null &&!fieldDefinitionList.isEmpty()) {
durableId
= fieldDefinitionList.get(0).DurableId;
}
if(String.isNotBlank(durableId)) {for(Default_Value__mdt valueItem : defaultValueList) {if(durableId.equalsIgnoreCase(valueItem.Field_Name__c)) {
result
=valueItem.Default_Value__c;
}
}
}
returnresult;
}
}

简单调用的结果显示:

总结:
此篇仅是对于上一篇的补充,简单介绍了 DurableId以及 EntityDefinition和 FieldDefinition的概念和使用。篇中没有介绍特别详细字段以及limitation,感兴趣的可以自行查看。篇中有问题欢迎指出,有不懂欢迎留言。