2GP Managed Package

Salesforce 第二代管控包(Second-Generation Managed Packages)是Salesforce最新推广的最佳实践。通过Salesforce CLI,我们可以很方便的制作和部署第二代管控包。本文只是简略的将核心步骤展示出来:

涉及的各种环境

整个第二代管控包需要用到4类org:

  1. Dev Hub Org:承载所有二代包。可以将各个命名空间的二代包链接到这个org。最好是PBO
  2. Namespace Org:一般是Developer org,在其中申请相应的命名空间,由于命名空间一旦申请便与该org永久绑定且不可修改,申请时请慎重。
  3. Scratch Orgs:在开发测试中使用。
  4. Production Orgs:按照和使用包的生产环境。

2GP Packages.drawio.svg
相关org关系示意图。

10步完成制作

  1. 创建一个 SFDX 项目

    sfdx force:project:create --outputdir expense-manager-workspace --projectname expenser-app
  2. 授权Dev Hub环境,该环境必须启用Dev Hub功能和未锁定包和第二代管控包功能

    sfdx auth:web:login -d -a devHub
  3. 创建一个草稿环境(scratch org)并在其中开发包

    sfdx force:org:create -f config/project-scratch-def.json -u scratchOrg1
  4. 保证所有要打包的组件都已经在当前的项目文件夹内
  5. sfdx-project.json文件中,使用命名空间属性指定命名空间。例如:“namespace”:“exp-mgr”
  6. 从 SFDX 项目文件夹,直接创建管控包:

    sfdx force:package:create --name "Expense Manager" --path force-app --packagetype Managed
  7. 检查项目文件夹中的sfdx-project.json文件,CLI 会自动更新项目文件,使其包含上一步创建的包的信息:

    {
    "packageDirectories": [
       {
          "path": "force-app",
          "default": true,
          "package": "Expense Manager",
          "versionName": "ver 0.1",
          "versionNumber": "0.1.0.NEXT"
       }
    ],
    "namespace": "exp-mgr",
    "sfdcLoginUrl": "https://login.salesforce.com",
    "sourceApiVersion": "51.0",
    "packageAliases": {
       "Expense Manager": "0Hoxxx"
    }
    }
  8. 创建一个包的版本,Salesforce CLI会自动处理包的版本号等

    sfdx force:package:version:create --package "Expense Manager" --installationkey test1234 --wait 10
  9. 在另一个创建好的草稿环境中,安装并测试这个版本的包:

    sfdx force:package:install --package "Expense [email protected]" --targetusername MyTestOrg1 
    --installationkey test1234 --wait 10 --publishwait 10
  10. 安装包后,打开scratch org查看包:

    sfdx force:org:open --targetusername MyTestOrg1
\frac{a}{b^2}

参考:

  1. Salesforce DX Developer Guide
  2. Link a Namespace to a Dev Hub Org
  3. Create and Register Your Namespace for Second-Generation Managed Packages
  4. Workflow for Second-Generation Managed Packages

云服务器中通过Docker镜像的方式使用Mysql数据库,数据安全考量定期备份数据库到阿里云OSS中

本文主要实现在阿里云ECS服务器中,通过脚本的方式实现将Docker中的Mysql数据库备份到阿里云OSS中。其他云服务商和其他环境的MySQL也可以使用类似的方式。具体方式不再赘述。

环境和工具

  1. 阿里云ECS服务器,安装Ubuntu或任何带有定时任务cron的Linux操作系统
  2. 阿里云OSS bucket和相应访问的账户
  3. 阿里云OSS命令行工具ossutil,可以到这里下载或查看详情
  4. MySQL镜像

具体步骤

开通OSS和相应子账号

  1. 进入阿里云OSS(对象存储)控制台,如果没有开通OSS服务可能会提示权限和开通确认。
  2. 创建bucket/存储桶,输入名字,比如xxxxbackup,记住这个名字以及界面中出现的Endpoint(地域节点),后续会用到。
    WX20220720-115411.png
  3. 创建一个有API的阿里云子账号,并赋予OSS读写权限。记住该账号的API和Secret,后续会用到。帮助文档
    WX20220720-115547.png

下载OSS命令行工具ossutil到本地系统

  1. 通过wget https://gosspublic.alicdn.com/ossutil/1.7.13/ossutil64下载到本地。这里是64位操作系统,32位可以去下载界面查看对应的下载地址。
  2. 增加运行权限:chmod +x ossutil64
  3. 移动到Path中:sudo mv ossutil64 /bin/

编写脚本

这里需要注意docker容器的名称,容器中应该有MYSQL用户名和密码的环境变量;示例中使用的是root作为用户名;另外备份临时文件夹也需要提前使用mkdir建好

脚本内容示例:

#!/bin/sh 

mysql_dump_path=备份文件临时目录,如/var/dbbackup/ 

#oss-config 
oss_endpoint="阿里云OSS Endpoint" 
oss_bucket="阿里云OSS bucket名称" 
oss_accesskeyid="子账号AccessKeyId" 
oss_accesskeysecret="子账号 AccessKey Secret" 
backup_name=`date +%Y%m%d%H%M%S`
cd ${mysql_dump_path} 

# 使用 mysqldump 备份整个数据库到临时文件夹中
# 注意docker修改docker镜像名称,这里是some-mysql
docker exec some-mysql sh -c 'mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > ${backup_name}.sql

# 打包/pack 
tar zcf ${backup_name}.tar.gz ${backup_name}.sql 
rm -f ${backup_name}.sql 

# 备份到OSS
ossutil64 cp ${backup_name}.tar.gz oss://${oss_bucket}/mysql/backup/${backup_name}.tar.gz -f -e ${oss_endpoint} -i ${oss_accesskeyid} -k ${oss_accesskeysecret} 
rm -f ${backup_name}.tar.gz 
echo "备份完成"

将脚本保存至任意一个不错的位置,比如/var/backup/dbbackup.sh,赋予可执行权限:sudo chmod +x /var/backup/dbbackup.sh

试着运行一下脚本文件sudo /var/backup/dbbackup.sh,再去oss控制台该bucket下-文件管理处查看是否有备份文件存入。如果一切没有问题,则就可以加入cron了。

加入cron计划任务

注:如果脚本位置和临时备份文件夹位置是当前用户无法直接编辑的,则应使用一下方法,在系统级别运行计划任务;用户级别会因为权限问题出错

使用你喜欢的编辑器打开/etc/crontab文件,并在文件末尾加上这行:

23 2 * * * root /var/backup/dbbackup.sh

这表示在每天的02:23:00运行我们的备份脚本。

重新启动cron计划任务:sudo service cron reload

在次日2:23分之后检查OSS控制台后台,即可发现整个数据库已经备份到阿里云OSS。

参考

  1. 使用ossutil定时备份自建mysql数据库到阿里云OSS
  2. Cron介绍
  3. docker hub: mysql

问题和原因

jupyter notebook通过nbconvert调用Latex导出PDF,由于nvconvert中转换模版中并不包含中文支持的package申明,导致Latex渲染时无法识别中文字符和对应的字体。出现类似:

LaTeX Error: Unicode character 正 (U+6B63) not set up for use with LaTeX.

的报错。导出的PDF文件中,所有中文字符都显示为空白。

解决方案

有两种方法可以解决:

  1. 先从Notebook导出为Latex,再使用如TexShop等工具打开Latex文件并添加中文支持,之后导出
  2. 修改nbconvert的模版,直接添加中文支持,这样即可直接导出PDF

这里说第二种办法:

首先通过jupyter --paths获得Jupyter相关路径,

% jupyter --paths

输出类似:

config:
    /Users/xxx/.jupyter
    /Users/xxx/custom_path/venv/etc/jupyter
    /usr/local/etc/jupyter
    /etc/jupyter
data:
    /Users/xxx/Library/Jupyter
    /Users/xxx/custom_path/venv/share/jupyter
    /usr/local/share/jupyter
    /usr/share/jupyter
runtime:
    /Users/xxx/Library/Jupyter/runtime

找到...share/jupyter文件夹所在位置,并定位到...share/jupyter/nbconvert/templates/latex文件夹,里面有nbconvert转换LaTex模版,
nvconvert-latex.png
通过修改base.tex.j2模版文件,添加xeCJK包申明和相关字体即可影响所有相关模版:

    \usepackage{xeCJK}
    \setCJKmainfont{STSong}

添加完成后,模版文件应类似:

((*- block header -*))
    ((* block docclass *))\documentclass[11pt]{article}((* endblock docclass *))

    ((* block packages *))
    \usepackage{xeCJK}
    \setCJKmainfont{STSong}
    \usepackage{iftex}
    ...

注意,字体部分可以通过Mac的字体书(Font Book)找到合适的中文字体名称,不一定非得使用宋体(STSong)。另外除了主要字体,也可以定义其他情况的字体,若加粗和倾斜时应用的字体。

相关文章

github:jupyter/nbconvert
Mac环境下jupyter notebook导出PDF显示中文的解决方案 (nbconvert 6, python 3.9)
convert notebook (contains Chinese words) to PDF successfully but those Chinese words are missing

通过在.Net Core应用中运行Python脚本,可以非常方便的进行某些功能的处理。特别是考虑到Python是解释型语言,不需要像.Net 那样编译再部署。

通过使用NuGet中的IronPythonIronPython.StdLib:

public class PythonScript
{
    private ScriptEngine _engine;

    public PythonScript()
    {
        _engine = Python.CreateEngine();
    }

    public TResult RunFromString<TResult>(string code, string variableName)
    {
        // for easier debugging write it out to a file and call: _engine.CreateScriptSourceFromFile(filePath);
        ScriptSource source = _engine.CreateScriptSourceFromString(code, SourceCodeKind.Statements);
        CompiledCode cc = source.Compile();

        ScriptScope scope = _engine.CreateScope();
        cc.Execute(scope);

        return scope.GetVariable<TResult>(variableName);
    }
}

然后使用运行脚本:

var py = new PythonScript();
var result = py.RunFromString<int>("d = 8", "d");
Console.WriteLine(result);

原生的ProcessProcessStartInfo (个人实践出来的)

注意,需要通过执行含有Python可执行文件和参数的shell脚本来实现执行Python脚本, 有点绕,具体就是:

1, 创建一个run.batWindows系统:

C:\YOUR_PYTHON_PATH\python.exe %*

2, 创建一个run.sh文件给LInux系统:

#!/bin/bash
/usr/bin/python3 "[email protected]"

3, 使用Process and ProcessStartInfo 来运行脚本:

string fileName = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
    fileName = "path_to_bat/run.bat"
}
else
{
    fileName = "path_to_bat/run.sh"
}

ProcessStartInfo start = new ProcessStartInfo
{
    FileName = fileName,
    Arguments = string.Format("\"{0}\" \"{1}\"", script, args),
    UseShellExecute = false,
    CreateNoWindow = true,
    RedirectStandardOutput = true,
    RedirectStandardError = true
};
using Process process = Process.Start(start);

变量script指代需要执行的Python脚本所在位置,变量args指代脚本需要的参数,若有多个参数,酌情修改程序片段。

参考

  1. How to use Python in .NET-Core application?
  2. How to run python script from C# code by using command

Regex 正则表达式

A regular expression (shortened as regex or regexp; also referred to as rational expression) is a sequence of characters that specifies a search pattern in text. Usually such patterns are used by string-searching algorithms for "find" or "find and replace" operations on strings, or for input validation. It is a technique developed in theoretical computer science and formal language theory.

正则表达式(英语:Regular Expression,常简写为regex、regexp或RE),又称正则表示式、正则表示法、规则表达式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。

Regular Expression的Regular一般被译为正则正规常规。此处的Regular即是规则规律的意思,Regular Expression即“描述某种规则的表达式”之意。

正则表达式用来做什么:

  1. 判断一个句子中是否有Lizzy, Hello everyone, my name is Li Zhiyan, you can call me Lizy.
  2. 判断一个字符串中是否有数字, Twenty two is 22
  3. 判断一个字符串中有英文字母, 23n20230n;23+&*^&*
  4. 判断一个字符串中是否有John或者Jone, Look! John's AirPods is becoming AirPod.
  5. 判断一个字符串是否是手机号码, 15195812506
  6. 判断一个字符串是否一Leon开头, Leon is from China.
  7. 判断一个字符串是否是邮件地址, [email protected]
  8. 使用非字符串分割字符串,A, B, c, D, E;F|g

正则表达式表达了什么

  1. 断言(Assertions)
    表示一个匹配在某些条件下发生。断言包含先行断言、后行断言和条件表达式。
  2. 字符类(Character Classes)
    区分不同类型的字符,例如区分字母和数字。
  3. 组和范围(Groups and Ranges)
    表示表达式字符的分组和范围。
  4. 量词(Quantifiers)
    表示匹配的字符或表达式的数量。
  5. Unicode 属性转义(Unicode Property Escapes)
    基于 unicode 字符属性区分字符。例如大写和小写字母、数学符号和标点。

在Python中,可以使用re模块和Pattern来做正则:

import re
p = re.compile(pattern, flags=0)
re.search(pattern, string, flags=re.IGNORECASE)
re.match(pattern, string, flags=0)
re.split(pattern, string, maxsplit=0, flags=0)
re.findall(pattern, string, flags=0)

Pattern.search(string[, pos[, endpos]])
Pattern.match(string[, pos[, endpos]])
Pattern.split(string, maxsplit=0)
Pattern.findall(string[, pos[, endpos]])

在JavaScript中,可以使用var re = /pattern/flags;

import re

p1 = re.compile('Lizzy')
s = 'Hello everyone, my name is Li Zhiyan, you can call me Lizzy.'
p1.search(s)

# [\d] = [^\D]
p2 = re.compile('[0-9]') # \d = [0123456789]n 
p2.search('Twenty two is 22')

p3 = re.compile('[a-zA-Z]') # \w (word) = [a-zA-Z0-9_]
p3.search('23n20230n;23+&*^&*')
<re.Match object; span=(2, 3), match='n'>


特殊符号

  1. \d: 匹配一个数字。等价于[0-9]。
  2. \D: 匹配一个非数字字符。等价于1
  3. \w: 匹配一个单字字符(字母、数字或者下划线)。等价于 [A-Za-z0-9_]。
  4. \W: 匹配一个非单字字符。等价于 2
  5. \t: 匹配一个水平制表符 (U+0009)。
  6. \v: 匹配一个垂直制表符 (U+000B)。
  7. \f: 匹配一个换页符 (U+000C)。
  8. \n: 匹配一个换行符 (U+000A)。
  9. \r: 匹配一个回车符 (U+000D)。
  10. \s: 匹配一个空白字符,包括空格、制表符、换页符和换行符。等价于[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]。
  11. \S: 匹配一个非空白字符。等价于 3
  12. .: 匹配除了换行符\n以外的任意一个字符
  13. \u{hhhh} or \u{hhhhh}: 仅当设置了u标志时)匹配一个十六进制数表示的 Unicode 字符。
# 使用|作为选择
p4 = re.compile('John|Jone')
p4.search('Look! John\'s AirPods is becoming AirPod.')

# 匹配次数 {m,n}
p5 = re.compile('^1[0-9]{10}') # '1\d{10}'
# p5.search('15195812506')
p5.match('15195812506')
<re.Match object; span=(0, 11), match='15195812506'>


匹配次数

  1. {m,n} m次到n次(包含)
  2. {m,} 至少m次
  3. {m} m次
  4. ? 0到1次
  5. + 至少1次
  6. * 任意次(0到∞)

指定匹配位置

  1. ^: 匹配字符串的开头
  2. $: 匹配字符串的末尾
  3. \b: 匹配单词(word)的边界
  4. \B: 匹配非单词边界
p6 = re.compile('^Leon')
s6 = 'Leon is from China.'
p6.search(s6)

# [email protected], \[email protected]  @b.com [\w-] [email protected]_
p7 = re.compile('[\w\.\-\+][email protected][\w-]+[\w\-\.]+([a-zA-Z0-9]+)$')
p7.match('[email protected]')

p8 = re.compile('[,;\|]')
p8.split('A, B, c, D, E;F|g')
['A', ' B', ' c', ' D', ' E', 'F', 'g']


总结

转义

如果你需要使用任何特殊字符的字面值(例如,搜索字符''),你必须通过在它前面放一个反斜杠来转义它。 例如,要搜索'a'后跟''后跟'b',你应该使用/a*b/- 反斜杠“转义”字符'*',使其成为文字而非特殊符号。

高级话题

使用插入语

任何正则表达式的插入语都会使这部分匹配的副字符串被记忆。一旦被记忆,这个副字符串就可以被调用于其它用途,如同使用括号的子字符串匹配之中所述。

通过标志进行高级搜索

一般而言,各种语言中实现的正则表达式有可选参数 (flags) 允许全局和不分大小写搜索等。这些参数既可以单独使用也能以任意顺序一起使用, 并且被包含在正则表达式实例(模式)中。

Python中可以使用re.IGNORECASE不分大小写搜索:

import re
p = re.compile(pattern, flags=0)
re.search(pattern, string, flags=re.IGNORECASE)

JavaScript则是:

标志描述
g全局搜索。
i不区分大小写搜索。
m多行搜索。
s允许 . 匹配换行符。
u使用unicode码的模式进行匹配。
y执行“粘性(sticky)”搜索,匹配从目标字符串的当前位置开始。
let re = /^\w+\.\S+/gi;

用特殊字符检验输入

字符 含义
(x) 像下面的例子展示的那样,它会匹配 'x' 并且记住匹配项。其中括号被称为捕获括号。
模式`/(foo) (bar) \1 \2/`中的 '(foo)' 和 '(bar)' 匹配并记住字符串 "foo bar foo bar" 中前两个单词。模式中的`\1`和`\2`表示第一个和第二个被捕获括号匹配的子字符串,即 foo 和 bar,匹配了原字符串中的后两个单词。注意`\1`、`\2`、...、`\n` 是用在正则表达式的匹配环节,详情可以参阅后文的 `\n` 条目。而在正则表达式的替换环节,则要使用像 `$1`、`$2`、...、`$n` 这样的语法,例如,`'bar foo'.replace(/(...) (...)/, '$2 $1')`。$& 表示整个用于匹配的原字符串。
(?:x) 匹配 'x' 但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。
看看这个例子 `/(?:foo){1,2}/`。如果表达式是 `/foo{1,2}/,{1,2}` 将只应用于 'foo' 的最后一个字符 'o'。如果使用非捕获括号,则 `{1,2}` 会应用于整个 'foo' 单词。
x(?=y) 匹配'x'仅仅当'x'后面跟着'y'.这种叫做先行断言。
例如,`/Jack(?=Sprat)/`会匹配到'Jack'仅当它后面跟着'Sprat'。`/Jack(?=Sprat|Frost)/`匹配‘Jack’仅当它后面跟着'Sprat'或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。
(?<=y)x 匹配'x'仅当'x'前面是'y'.这种叫做后行断言。
例如,`/(?<=Jack)Sprat/`会匹配到' Sprat '仅仅当它前面是' Jack '。`/(?<=Jack|Tom)Sprat/`匹配‘ Sprat ’仅仅当它前面是'Jack'或者是‘Tom’。但是‘Jack’和‘Tom’都不是匹配结果的一部分。
x(?!y) 仅仅当'x'后面不跟着'y'时匹配'x',这被称为正向否定查找。
例如,仅仅当这个数字后面没有跟小数点的时候,`/\d+(?!\.)/` 匹配一个数字。正则表达式`/\d+(?!\.)/.exec("3.141")`匹配‘141’而不是‘3.141’

Reference / 参考

  1. Wiki:正则表达式
  2. JavaScript:正则表达式
  3. Python:Regular expression operations
  4. Java:Pattern
  5. .NET:Regular Expression Language - Quick Reference

  1. 0-9
  2. A-Za-z0-9_
  3. \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff