<script>、<img>、<link>
这些包含 src 属性的标签可以加载跨域资源。(只能GET)如何绕过同源策略?
<script>
允许跨域的特点,设置标签的src为目标域,动态生成需要的javascript内容1 | Access-Control-Allow-Origin: https://foo.example // 所允许的来源域 |
参考
如果都从源码编译安装软件,依赖的维护过于复杂,初始编译工具链的版本可能也不满足需求,如 gcc 版本过低。
如果申请 sudo 权限或者请求更新系统或安装 docker,后期责任难以界定,运维和管理员一般也不会同意。
所以,最优方案还是有需求的用户在个人目录维护自己的工具链和环境。下文方案为围绕 HomeBrew 构建。
如果你的系统比较新,可以直接尝试安装 HomeBrew
。
基于上面讨论的内容,公用服务器一般存在系统版本低的问题,是 centos7 或者 centos6 也毫不稀奇,而且如 glibc 等库的版本也非常低。
安装 HomeBrew 有两个强依赖,git 及 curl,而且依赖的版本都比较高,centos7 的版本也不能满足。
另外,由于 Brew 不少软件都需要从源码编译,gcc 和良好的网络环境也不可缺少。
幸好 miniconda 能够解决以上几点问题。miniconda 只是提供 HomeBrew 安装的依赖,后续可以删除。
配置 conda 源(可选): 新建 .condarc
,包含以下内容
1 | channels: |
下载及安装 miniconda
1 | 下载 |
这些环境变量也可以配置到 bashrc 等文件,使之永久生效
1 | 设置 curl 和 git,可选 |
由于没有 root 权限,HomeBrew 需要手动安装。
由于是手动安装,位置与默认安装位置不同,很多预编译的包就不能用了,都得从源码编译,所以网络和机器性能以及耐心很重要。
1 | 下载,也可使用清华源 https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git |
经过本强迫症的探索,终于找到基于 Karabiner-Elements + Automator + Logseq 的完美生词本方案。
最后的效果是,快捷键取词的同时记录单词卡片到Logseq对应的笔记。
参考知乎文章安装好《朗道英汉字典5.0》
这是为了有个释义简洁的词典,方便后续生成生词本词条
使用 macOS 自带应用 Automator(自动操作)编写workflow,将当前鼠标所在位置的文本提取并保存制卡。
首先打开 Automator.app 新建一个 Quick Aciont(快速操作)
然后依次拖入“获得词语定义”,“运行Shell脚本”等步骤,并调整如下几个位置的选项。
修改脚本里的代码为如下内容,生词本路径相应替换,并相应位置新建好生词本文件。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# -*- coding:utf-8 -*-
from __future__ import unicode_literals, print_function
import sys, os, io, subprocess
FILE=os.path.expanduser("~/weiyun_sync/!sync/logseq-note/pages/生词本.md")
output = []
text = sys.argv[1].decode('utf8') if sys.version_info.major == 2 else sys.argv[1]
lines = [i.strip() for i in text.splitlines() if i.strip()]
if len(lines) < 2:
exit(0)
word = lines[0]
if lines[1][0] == '*':
output.append('- {}\t{} [[card]]'.format(word, lines[1]))
lines = lines[2:]
else:
output.append('- {}\t [[card]]'.format(word))
lines = lines[1:]
output.append('\t- {}'.format(lines[0]))
for line in lines[1:]:
output.append('\t ' + line)
old_words = set()
with io.open(FILE, 'r', encoding='utf8') as fp:
for line in fp:
parts = line.split()
if line.startswith('-') and len(parts) > 1:
old_words.add(parts[1])
if word not in old_words:
with io.open(FILE, 'a', encoding='utf8') as fp:
fp.write('\n')
fp.write('\n'.join(output))
fp.write('\n')
subprocess.check_call(['osascript', '-e', u'display notification "添加 {}" with title "生词本"'.format(word)])
else:
subprocess.check_call(['osascript', '-e', u'display notification "跳过 {}" with title "生词本"'.format(word)])
选择路径保存好 workflow,然后在 键盘 - 快捷键 - 服务
中能看到新建的workflow。
为它设置快捷键 command + shift + alt + 1
Karabiner-Elements 是 macOS 平台的一个重新映射快捷键的软件。
这里我们使用它将“查询单词”和“触发workflow”整合在一起,当然它还支持很多用途,这里就不赘述了。
注意确保Karabiner相关权限,并且设置中下图相关设备是勾选状态
安装好Karabiner-Elements后,打开它的配置文件
路径在 /Users/<用户名>/.config/karabiner/karabiner.json
在 profiles -> complex_modifications -> rules
列表中增加一项配置,内容如下。
然后保存,Karabiner会自动加载新的配置。
这里是将鼠标的侧键(靠前的)映射为查单词的快捷键,实现一键查词。
也可以根据需要更改按键,通过EventViewer可以查看按键代码,配置文件格式可参考官方文档
1 | { |
可以看到 Logseq 中卡片生成的效果
在类UNIX系统中,一切被打开的文件、端口被抽象为文件描述符(file descriptor)
从python3.4开始,文件描述符默认是non-inheritable,也就是子进程不会共享文件描述符。
一般为了实现多进程、多线程的webserver,服务端口fd必须设置为继承(set_inheritable),这样才能多进程监听一个端口(配合SO_REUSEPORT)
典型的是使用flask的测试服务器的场景,这里我们写一段代码模拟。
1 | import socket, os |
我们通过lsof -p {pid}
可以看到这两个进程的所有文件描述符
server进程, 可以看到服务端口的fd是41
2
3
4
5
6
7
8
9
10
11COMMAND PID FD TYPE DEVICE SIZE/OFF NODE NAME
ptpython 6214 cwd DIR 253,0 4096 872946898 /
...
ptpython 6214 0u CHR 136,13 0t0 16 /dev/pts/13
ptpython 6214 1u CHR 136,13 0t0 16 /dev/pts/13
ptpython 6214 2u CHR 136,13 0t0 16 /dev/pts/13
ptpython 6214 3r CHR 1,9 0t0 2057 /dev/urandom
ptpython 6214 4u sock 0,7 0t0 58345077 protocol: TCP
ptpython 6214 5u a_inode 0,10 0 8627 [eventpoll]
ptpython 6214 6u unix 0x0000000000000000 0t0 58368029 socket
ptpython 6214 7u unix 0x0000000000000000 0t0 58368030 socket
sleep子进程,也拥有fd=4的文件描述符1
2
3
4
5
6
7COMMAND PID FD TYPE DEVICE SIZE/OFF NODE NAME
python 18022 cwd DIR 253,0 4096 872946898 /
...
python 18022 0u CHR 136,13 0t0 16 /dev/pts/13
python 18022 1u CHR 136,13 0t0 16 /dev/pts/13
python 18022 2u CHR 136,13 0t0 16 /dev/pts/13
python 18022 4u sock 0,7 0t0 58345077 protocol: TCP
如果server进程退出时,sleep进程没有退出,fd=4对应的端口就被占用了,服务也就不能正常启动了。
1 | import os |
使用subprocess库而不是os来启动子程序, 通过close_fds参数关闭多余的文件描述符1
2import subprocess
subprocess.call("python -c 'import time;time.sleep(1000)'", shell=True, close_fds=True)
当执行 docker stop xxx
时,docker会向主进程(pid=1)发送 SIGTERM
信号
如果在一定时间(默认为10s)内进程没有退出,会进一步发送 SIGKILL
直接杀死程序,该信号既不能被捕捉也不能被忽略。
一般的web框架或者rpc框架都集成了 SIGTERM
信号处理程序, 一般不用担心优雅退出的问题。
但是如果你的容器内有多个程序(称为胖容器,一般不推荐),那么就需要做一些操作保证所有程序优雅退出。
信号是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序活终端发送的命令(即信号)。
应用程序收到信号后,有三种处理方式:忽略,默认,或捕捉。
常见信号:
信号名称 | 信号数 | 描述 | 默认操作 |
---|---|---|---|
SIGHUP | 1 | 当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。 | 终止进程 |
SIGINT | 2 | 程序终止(interrupt)信号,在用户键入 Ctrl+C 时发出。 | 终止进程 |
SIGQUIT | 3 | 和SIGINT类似,但由QUIT字符(通常是Ctrl /)来控制。 | 终止进程并dump core |
SIGFPE | 8 | 在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术错误。 | 终止进程并dump core |
SIGKILL | 9 | 用来立即结束程序的运行。本信号不能被阻塞,处理和忽略。 | 终止进程 |
SIGALRM | 14 | 时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号。 | 终止进程 |
SIGTERM | 15 | 通常用来要求程序自己正常退出;kill 命令缺省产生这个信号。 | 终止进程 |
下面以 supervisor 为例,Dockerfile 如下
1 | FROM centos:centos7 |
正常情况,容器退出时supervisor启动的其他程序并不会收到 SIGTERM
信号,导致子程序直接退出了。
这里使用 trap
对程序的异常处理进行包装1
trap <siginal handler> <signal 1> <signal 2> ...
新建一个初始化脚本,init.sh
1
2
3
4
5
6
7
/usr/bin/supervisord -n -c /etc/supervisord.conf &
trap "supervisorctl stop all && sleep 3" TERM INT
wait
修改 ENTRYPOINT 为如下1
ENTRYPOINT ["sh", "/root/init.sh"]
先看看一开始的问题,可以看到这里lambda函数的返回值一直在变。
1 | xx = [] |
输出如下
1 | a: 3 |
由于Python没有块级作用域,所以循环会改变当前作用域变量的值,也就是循环变量泄露。
注意:Python3中列表推导式循环变量不会泄露,Python2中和常规循环一样泄露。
1 | x = -1 |
输出如下
1 | 6 : for x inside loop |
再讲一下闭包,在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包。
这里所谓的引用可以也就是内部函数记住了变量的名称(而不是值,这个从ast语法树可以看出),而变量对应的值是会变化的。
如果在循环中定义闭包,引用的变量的值在循环结束才统一确定为最后一次循环时的值,也就是延迟绑定(lazy binding)。
所以下面的例子,xx
的所有匿名函数的返回值均为3
1
2
3xx = []
for i in [1,2,3]:
xx.append(lambda: i)
再分析一开始的问题,这里的匿名函数引用了变量i
,而i
是全局变量,所以再次使用i
作为循环变量时,列表中的匿名函数引用的值就被覆盖了。
正确做法:
模式扩展(globbing),类似C语言中的宏展开,我们通常使用的通配符*
就是其中之一。
Bash 一共提供八种扩展,前4种为文件扩展,只有文件路径确实存在才会扩展。
~
波浪线扩展?
问号扩展*
星号扩展[]
方括号扩展{}
大括号扩展$var
变量扩展$(date)
命令扩展$((1 + 1))
算术扩展波浪线~
会自动扩展成当前用户的主目录。~user
表示扩展成用户user
的主目录。如果用户不存在,则波浪号扩展不起作用。
1 | bash-5.1$ echo ~/projects/ |
?
字符代表文件路径里面的任意单个字符,不包括空字符。
只有文件确实存在的前提下,才会发生扩展。
1 | bash-5.1$ touch {a,b}.txt ab.txt |
*
字符代表文件路径里面的任意数量的任意字符,包括零个字符。
1 | bash-5.1$ ls *.txt |
方括号扩展的形式是[...]
,只有文件确实存在的前提下才会扩展。
[^...]
和[!...]
。它们表示匹配不在方括号里面的字符
方括号扩展有一个简写形式[start-end]
,表示匹配一个连续的范围
1 | bash-5.1$ ls [ab].txt |
大括号扩展{...}
表示分别扩展成大括号里面的所有值
大括号也可以与其他模式联用,并且总是先于其他模式进行扩展。
1 | bash-5.1$ echo {1,2,3} |
Bash 将美元符号$
开头的词元视为变量,将其扩展成变量值
1 | bash-5.1$ echo $HOME |
$(...)
可以扩展成另一个命令的运行结果,该命令的所有输出都会作为返回值。
1 | bash-5.1$ echo $(date) |
$((...))
可以扩展成整数运算的结果
1 | bash-5.1$ echo $((1+1)) |
单引号用于保留字符的字面含义,在单引号里转义字符和模式扩展都会失效。
1 | bash-5.1$ ls '[ab].txt' |
双引号比单引号宽松,三个特殊字符除外:美元符号($
)、反引号(`
)和反斜杠(\
)。这三个字符,会被 Bash 自动扩展。
也就是说,相比单引号在双引号中变量扩展,命令扩展,算术扩展以及转义字符是有效的。
1 | bash-5.1$ echo "$((1+1))" |
1 | # 双引号中使用单引号 |
Here 文档(here document)是一种输入多行字符串的方法,格式如下。
它的格式分成开始标记(<< token
)和结束标记(token
), 一般用字符串EOF
作为token
1 | << token |
例如
1 | bash-5.1$ cat << EOF |
Here 文档还有一个变体,叫做 Here 字符串(Here string),使用三个小于号(<<<
)表示。
它的作用是将字符串通过标准输入,传递给命令。
1 | bash-5.1$ cat <<< foobar |
bash 是基于标准输入在不同进程间交互数据的,大部分功能都是在操作字符串,所以变量的默认类型也是字符串。
声明时等号两边不能有空格。
Bash 变量名区分大小写,HOME
和home
是两个不同的变量。
1 | bash-5.1$ foo=1 |
1 | # 查看所有变量, 其中包含父进程export的变量 |
用户创建的变量仅可用于当前 Shell,子 Shell 默认读取不到父 Shell 定义的变量。
如果希望子进程能够读到这个变量,需要使用export命令。
1 | bash-5.1$ bash -c set | grep foo |
平时所说的环境变量,就是init进程export输出的。子进程对变量的修改不会影响父进程,也就是说变量不是共享的。
1 | # 查看环境变量 |
下面是一些常见的环境变量。
BASHPID
:Bash 进程的进程 ID。BASHOPTS
:当前 Shell 的参数,可以用shopt
命令修改。DISPLAY
:图形环境的显示器名字,通常是:0
,表示 X Server 的第一个显示器。EDITOR
:默认的文本编辑器。HOME
:用户的主目录。HOST
:当前主机的名称。IFS
:词与词之间的分隔符,默认为空格。LANG
:字符集以及语言编码,比如zh_CN.UTF-8
。PATH
:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。PS1
:Shell 提示符。PS2
: 输入多行命令时,次要的 Shell 提示符。PWD
:当前工作目录。RANDOM
:返回一个0到32767之间的随机数。SHELL
:Shell 的名字。SHELLOPTS
:启动当前 Shell 的set
命令的参数TERM
:终端类型名,即终端仿真器所用的协议。UID
:当前用户的 ID 编号。USER
:当前用户的用户名。Bash 提供一些特殊变量。这些变量的值由 Shell 提供,用户不能进行赋值。
$?
: 上一个命令的退出码, 0为成功,其他为失败$$
: 当前进程的pid$_
: 为上一个命令的最后一个参数$!
: 为最近一个后台执行的异步命令的进程 ID。$0
: bash脚本的参数列表,0是脚本文件路径,1到n是第1到第n个参数${varname:-word}
: 如果变量varname存在且不为空,则返回它的值,否则返回word${varname:=word}
: 如果变量varname存在且不为空,则返回它的值,否则将它设为word,并且返回word。${varname:+word}
: 如果变量名存在且不为空,则返回word,否则返回空值。它的目的是测试变量是否存在。${varname:?message}
: 如果变量varname存在且不为空,则返回它的值,否则打印出varname: message,并中断脚本的执行。declare
命令的主要参数(OPTION)如下。
-a
:声明数组变量。-A
:声明关联数组变量。-f
:输出所有函数定义。-F
:输出所有函数名。-i
:声明整数变量。-p
:查看变量信息。-r
:声明只读变量。-x
:该变量输出为环境变量。bash 有字符串,数字,数字,关联数组四种数据类型,默认是字符串,其他类型需要手动声明。
语法 varname=value
1 | bash-5.1$ s1=abcdefg |
语法 ${#varname}
1 | bash-5.1$ echo ${#s1} |
语法 ${varname:offset:length}
, offset为负数的时候,前面要加空格,防止与默认值语法冲突。
1 | bash-5.1$ s1=abcdefg |
${variable#pattern}
: 删除最短匹配(非贪婪匹配)的部分,返回剩余部分${variable##pattern}
: 删除最长匹配(贪婪匹配)的部分,返回剩余部分匹配模式pattern可以使用*
、?
、[]
等通配符。
1 | $ myPath=/home/cam/book/long.file.name |
${variable%pattern}
: 删除最短匹配(非贪婪匹配)的部分,返回剩余部分${variable%%pattern}
: 删除最长匹配(贪婪匹配)的部分,返回剩余部分1 | $ path=/home/cam/book/long.file.name |
如果匹配pattern
则用replace
替换匹配的内容
${variable/pattern/replace}
: 替换第一个匹配${variable//pattern/replace}
: 替换所有匹配1 | $ path=/home/cam/foo/foo.name |
使用 declare -i
声明整数变量。
1 | # 声明为整数,可以直接计算,不需要使用$符号 |
Bash 的数值默认都是十进制,但是在算术表达式中,也可以使用其他进制。
number
:没有任何特殊表示法的数字是十进制数(以10为底)。0number
:八进制数。0xnumber
:十六进制数。base#number
:base
进制的数。1 | bash-5.1$ declare -i a=0x77 |
((...))
语法可以进行整数的算术运算。
支持的算术运算符如下。
+
:加法-
:减法*
:乘法/
:除法(整除)%
:余数**
:指数++
:自增运算(前缀或后缀)--
:自减运算(前缀或后缀)如果要读取算术运算的结果,需要在((...))
前面加上美元符号$((...))
,使其变成算术表达式,返回算术运算的值。
1 | bash-5.1$ echo $((1+1)) |
array=(item1 item2)
语法可初始化数组,括号内可以换行,多行初始化可以用#
注释。
1 | # 直接初始化数组 |
array[index]
语法可访问数组元素,不带index访问则是访问数组首个元素。
1 | bash-5.1$ a=(1 2 3) |
${#array[@]}
和 ${#array[*]}
可访问获得数组长度
1 | bash-5.1$ a=(1 2 3) |
${!array[@]}
或 ${!array[*]}
, 可以获得非空元素的下标
1 | bash-5.1$ a=(1 2 3) |
${array[@]:position:length}
的语法可以提取数组成员。
1 | bash-5.1$ a=({1..10}) |
数组末尾追加元素,可以使用+=
赋值运算符。
1 | bash-5.1$ a=({1..10}) |
删除一个数组成员,使用unset
命令。
1 | bash-5.1$ a=(1 2 3) |
declare -A
可以声明关联数组,关联数组使用字符串而不是整数作为数组索引。
除了初始化外,使用方法和数组基本相同
1 | bash-5.1$ declare -A a |
#
表示注释,每行从#
开始之后的内容代表注释,会被bash忽略.
1 | bash-5.1$ echo 1111 # 222 |
bash 和常规编程语言一样使用if
作为分支条件的关键字, fi
作为结束的关键字,else
和elif
子句是可选的
其中if
和elif
的condition
所判断的内容是命令的状态码是否为0,为0则执行关联的语句。
1 | 因为bash中分号(;)和换行是等价的,所以有下面两种风格,其他多行语句也是类似的 |
这里的condition
可以是多个命令,如command1 && command2
,或者command1 || command2
,则if判断的是这两个命令的状态码的逻辑计算结果。
condition
也是可以是command1; command2
, 则则if判断的是最后一个命令的状态码。
这里最常用的condition
是test
命令, 也就是[[]]
和[]
. test
是bash的内置命令,会执行给定的表达式,结果为真满足则返回状态码0, 否则返回状态码1.
下文循环语言的condition
也是相同的,就不赘述了
1 | bash-5.1$ test 1 -eq 1 |
[[]]
和[]
的区别是[[]]
内部支持&&
,||
逻辑判断,所以以下三种写法是等价的。
由于[
和]
是命令, 所以两侧一定要有空格,也是就是[ 1 -eq 1 ]
,否则bash会认为命令找不到。
1 | test |
判断条件支持且(&&)或(||)非(!)
1 | not |
使用[
和test
时,变量引用注意加双引号,否则得不到正确的结果,[[
则不需要。
1 | bash-3.2$ echo "$SSH_CLIENT" |
bash默认数据类型为字符串,所以常见的 >
, <
是用于字符串判断。
注意:字符串判断不支持>=
和<=
, 得使用逻辑组合来替代
-z string
:字符串串长度为0-n string
: 字符串长度大于0string1 == string2
: string1 等于 string2string1 = string2
: string1 等于 string2string1 > string2
: 如果按照字典顺序string1排列在string2之后string1 < string2
: 如果按照字典顺序string1排列在string2之前下面的表达式用于判断整数。
[ integer1 -eq integer2 ]
:如果integer1
等于integer2
,则为true
。[ integer1 -ne integer2 ]
:如果integer1
不等于integer2
,则为true
。[ integer1 -le integer2 ]
:如果integer1
小于或等于integer2
,则为true
。[ integer1 -lt integer2 ]
:如果integer1
小于integer2
,则为true
。[ integer1 -ge integer2 ]
:如果integer1
大于或等于integer2
,则为true
。[ integer1 -gt integer2 ]
:如果integer1
大于integer2
,则为true
。以下表达式用来判断文件状态。仅列举常用判断,详细支持列表参考 https://tldp.org/LDP/abs/html/fto.html
[ -a file ]
:如果 file 存在,则为true
。[ -d file ]
:如果 file 存在并且是一个目录,则为true
。[ -e file ]
:如果 file 存在,则为true
, 同-a
。[ -f file ]
:如果 file 存在并且是一个普通文件,则为true
。[ -h file ]
:如果 file 存在并且是符号链接,则为true
。[ -L file ]
:如果 file 存在并且是符号链接,则为true
, 同-h
。[ -p file ]
:如果 file 存在并且是一个命名管道,则为true
。[ -r file ]
:如果 file 存在并且可读(当前用户有可读权限),则为true
。[ -s file ]
:如果 file 存在且其长度大于零,则为true
。[ -w file ]
:如果 file 存在并且可写(当前用户拥有可写权限),则为true
。[ -x file ]
:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true
。bash也支持,switch case,语法如下。
1 | case EXPRESSION in |
例如1
2
3
4
5
6
7
8
9
10
11a=2
case $a in
1)
echo 11
;;
2)
echo 22
;;
*)
;;
esac
while
循环有一个判断条件,只要符合条件,就不断循环执行指定的语句。condition
与if语句的相同,就不赘述了。
1 | while condition; do |
until
循环与while
循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
1 | until condition; do |
for...in
循环用于遍历列表的每一项。
1 | for variable in list; do |
常见的几种用法1
2
3
4
5
6
7
8
9
10
11
12for i in 1 2 3; do
echo $i
done
for i in {1..3}; do
echo $i
done
list=(1 2 3)
for i in ${list[@]}; do
echo $i
done
for
循环还支持 C 语言的循环语法。
1 | for (( expression1; expression2; expression3 )); do |
上面代码中,expression1
用来初始化循环条件,expression2
用来决定循环结束的条件,expression3
在每次循环迭代的末尾执行,用于更新值。
注意,循环条件放在双重圆括号之中。另外,圆括号之中使用变量,不必加上美元符号$
。
例如1
2
3for ((i=1; i<=3; i++)); do
echo $i
done
Bash 提供了两个内部命令break
和continue
,用来在循环内部跳出循环。
break
命令立即终止循环,程序继续执行循环块之后的语句,即不再执行剩下的循环。continue
命令立即终止本轮循环,开始执行下一轮循环。
Bash 函数定义的语法有两种,其中fn
为定义的函数名称。
1 | 第一种 |
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
${N}
:函数的第一个到第N个的参数。$0
:函数所在的脚本名。$#
:函数的参数总数。$@
:函数的全部参数,参数之间使用空格分隔。$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。funcname arg1 arg ... argN
的语法进行函数调用。主要函数的返回值和输出值(标准输出)的区别,这和主流编程语言不同
1 | add() { |
return
命令用于从函数返回一个值。返回值和命令的状态码一样,可以用$?
拿到值。return
也可以不接具体的值,则返回值是return命令的上一条命令的状态码。
如果不加return
,则返回值是函数体最后一条命令的状态码。
1 | function func_return_value { |
Shebang(也称为Hashbang)是一个由井号和叹号构成的字符序列#!
, 其出现在可执行文本文件的第一行的前两个字符。
在文件中存在Shebang的情况下,类Unix操作系统的程序加载器会分析Shebang后的内容,将这些内容作为解释器指令,并调用该指令.
例如,shell脚本1
2
3
echo Hello, world!
python 脚本1
2
3#!/usr/bin/env python -u
print("Hello, world!")
每个命令都会返回一个退出状态码(有时候也被称为返回状态)。
成功的命令返回 0,不成功的命令返回非零值,非零值通常都被解释成一个错误码。行为良好的 UNIX 命令、程序和工具都会返回 0 作为退出码来表示成功,虽然偶尔也会有例外。
状态码一般是程序的main函数的返回码,如c,c++。
如果是bash脚本,状态码的值则是 exit
命令的参数值。
当脚本以不带参数的 exit
命令来结束时,脚本的退出状态码就由脚本中最后执行的命令来决定,这与函数的 return
行为是一致的。
特殊变量$?
可以查看上个命令的退出状态码
文件描述符在形式上是一个非负整数。指向内核为每一个进程所维护的该进程打开文件的记录表。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
每个Unix进程(除了可能的守护进程)应均有三个标准的POSIX文件描述符,对应于三个标准流:
0
:标准输入1
:标准输出2
:错误输出手动指定描述符1
2
3exec 3<> /tmp/foo #open fd 3.
echo "test" >&3
exec 3>&- #close fd 3.
系统自动分配描述符,bash4.1开始支持(在macos报错,原因不明)1
2
3
4
5
6
7
8!/bin/bash
FILENAME=abc.txt
exec {FD}<>"$FILENAME"
echo 11 >&FD
echo 22 >&FD
FD>&-
command > file
: 将输出重定向到 file。command < file
: 将输入重定向到 file。command >> file
: 将输出以追加的方式重定向到 file。n > file
: 将文件描述符为 n 的文件重定向到 file。n >> file
: 将文件描述符为 n 的文件以追加的方式重定向到 file。n >& m
: 将输出文件 m 和 n 合并。n <& m
: 将输入文件 m 和 n 合并。所以命令中常见的ls -al > output.txt 2>&1
, 就是将标准输出和错误输出都重定向到一个文件。
等价于ls -al &>output.txt
,本人偏好这种写法,比较简洁。
IFS决定了bash在处理字符串的时候是如何进行单词切分。
IFS的默认值是空格,TAB,换行符,即\t\n
1 | echo "$IFS" | cat -et |
例如,在for循环的时候,如何区分每个item1
2
3for i in `echo -e "foo bar\tfoobar\nfoofoo"`; do
echo "'$i' is the substring";
done
也可以自定义1
2
3
4
5
6
7OLD_IFS="$IFS"
IFS=":"
string="1:2:3"
for i in $string; do
echo "'$i' is the substring";
done
IFS=$OLD_IFS
linux进程分前台(fg)和后台(bg)。
在命令的末尾添加&
可以将命令后台执行,一般配合输出重定向使用。
jobs
可以查看当前bash进程的子进程,并通过fg
和bg
进行前台和后台切换。
%1
代表后台的第一个进程,以此类推%N
代表第n个.
control + Z
可以将当前前台程序暂停,配合bg
可以将其转后台。
wait [pid]
可以等待子进程结束,如果不带pid参数则等待所有子进程结束。
1 | bash-5.1$ sleep 100 |
可以利用jobs对后台进程并发数目进行控制1
2
3
4
5
6
7
8for i in {1..30}; do
sleep $((30+i)) &
if [[ $(jobs | wc -l ) -gt 10 ]]; then
jobs
wait
fi
done
wait
1 | [mysqld] |
1 | 启动MySQL |
1 | $ tree data |
1 | mysql> use mysql; update user set password=password('m654321') where user='root'; flush privileges; |
在浏览器中打开/search.xml发现以下错误。显然xml中有非法字符,xml解析产生了错误。
将search.xml文件保存,并用python打开,找到具体出错的位置。
utf8解码之后可以发现\x10
非法字符,将其删除,重新生成文章问题解决。
1 | './tmp.xml', 'rb').read() xxx = open( |
苹果在10.14的系统上增加了密码最小长度的限制
可以通过这个命令去除
1 | pwpolicy -clearaccountpolicies |
注意:去除限制之后,通过time machine备份的系统,还原到有限制的机器上,可能会导致一些bug。如卡死在登陆页面,部分应用的资源库损坏等等。
如果出现上面的问题,可以尝试重置下系统设置(选择任意一种,建议选1或2)
进入恢复模式(开机按command+r),删除对应用户账户配置文件(恢复模式下文件挂载路径可能不同), 重新开机
1 | mv ~/Library/Preferences ~/Library/Preferences.bak |
清除机器配置状态,重新设置一个新账户(开机按住command+s;)
1 | /sbin/mount -uaw |
1 | sudo spctl --master-disable |
macOS 10.14 以后系统动画越来越复杂,对显卡要求比较高,导致配置比较老的设备运行起来很卡顿。
特别是big sur系统,使用intel核显的机器都不建议安装。
通过关闭一些动画和特效可以让系统更流畅些
使用 karabiner 可以对系统全局快捷键和应用快捷键进行修改。
通过配置 karabiner 将 command,control,option 按键进行重映射,可以让 macOS 的键位 和 Windows/Linux 接近。
macOS 的命令应该是 Unix 风格的,和 Linux 有些不同,可选参数必须放在前面,会有些不方便。
比如1
2
3 mac
➜ ~ /bin/ls /tmp -al
ls: -al: No such file or directory
可以安装 gnu 工具,覆盖这些命令1
2
3brew install coreutils findutils gnu-tar htop
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH"
创建文件 ~/Library/KeyBindings/DefaultKeyBinding.dict
这里是清除了默认的 Control + Command + 方向键
的行为
修改完成后必须重新打开现有app才能生效
1 | { |
字符含义
Prefix | Meaning |
---|---|
~ | ⌥ Option key |
$ | ⇧ Shift key |
^ | ^ Control key |
@ | ⌘ Command key |
# | keys on number pad |
参考:
1 | # 关闭提示音 |
用户执行crontab -e
之后定时任务存储位置/private/var/at/tabs/<username>
macos 默认没有设置hostname,导致终端PS1
显示时会用网卡的mac地址作为主机名,看起来比较别扭。
可以通过sudo scutil --set HostName <name>
设置想要的主机名
1 | ➜ ~ hostname |
在终端中输入1
defaults write com.apple.systempreferences AttentionPrefBundleIDs 0 ; killall Dock
在 /etc/hosts 中加入1
2
3# disable mac update
0.0.0.0 swdist.apple.com.edgekey.net
0.0.0.0 swdist.apple.com.akadns.net
不是100%有效1
defaults write com.apple.Dock position-immutable -bool yes; killall Dock
._
开头的隐藏文件1 | defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true |
这时我们可以把表的数据分区存储,安装数据值的前缀或者时间字段来分区。
1 | CREATE TABLE test_part ( |
alter table test_part drop partition p1;
不可以删除hash或者key分区。
一次性删除多个分区,alter table test_part drop partition p1,p2;
ALTER TABLE test_part ADD partition (partition p20190306 VALUES LESS THAN (TO_DAYS(‘2019-03-07 00:00:00’)));
创建储存过程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23DROP PROCEDURE IF EXISTS proc1;
DELIMITER $$
SET AUTOCOMMIT = 0$$
CREATE PROCEDURE proc1()
BEGIN
DECLARE v_cnt DECIMAL (10) DEFAULT 0 ;
dd:LOOP
INSERT INTO test_part VALUES (
FLOOR(RAND()*100),
FLOOR(RAND()*1000),
UUID(),
DATE_ADD('2019-03-04 00:00:00', INTERVAL FLOOR(v_cnt / 5000) MINUTE)
);
COMMIT;
SET v_cnt = v_cnt+1 ;
IF v_cnt = 10000000 THEN LEAVE dd;
END IF;
END LOOP dd ;
END;$$
DELIMITER ;
```
调用储存过程
call proc1;`
windows下的网络共享只有SNAT那一部分,比如各自免费wifi软件。少了DNAT,外部网络就无法访问内部。
前置要求,需要两张网卡,无线或者有线均可。
打开:控制面板 》网络和Internet 》网络和共享中心 》更改适配器设置
右键A网卡 > 属性 > 共享 > 勾选允许 (win10可能有下拉选择,下拉选中B网卡)
netsh是Windows自带的端口转发/端口映射工具。
支持IPv4和IPv6,命令即时生效,重启系统后配置仍然存在。
1 | add v4tov4 [listenport=]integer>|servicename> \ |
将192.168.8.108的22端口映射到本地的2222端口
这样外部就可以通过本地的对外ip来ssh访问192.168.8.108了1
netsh interface portproxy add v4tov4 listenport=2222 connectaddress=192.168.8.108 connectport=22
一般情况下使用下列命令进行查看
1 | netsh interface portproxy show all |
为了简化部署,brpc 服务在 Docker 容器中运行。本地测试时功能一切正常,上到预发布环境时请求全部超时。
由于业务代码,brpc,docker环境,机房都是新的,在排查问题的过程中简直一头雾水。(当然根本原因还是水平不足)
发现请求超时后,开始用CURL测试接口,用真实数据验证发现请求都耗时1s,这和用c++的预期完全不符。
首先是怀疑业务代码有问题,逐行统计业务代码耗时,发现业务代码仅耗时10+ms。
用空数据访问接口,发现耗时也只有20+ms,这时开始怀疑brpc是不是编译得有问题,或者说和libtorch编译到一起不兼容。
这时我请教了一位同事,他对brpc比较熟悉,然后他说是curl实现的问题,和brpc没关系, 参考 brpc issue
1 | curl传输的时候,会设置 Expect: 100-continue, 这个协议brpc本身没有实现, 所以curl会等待一个超时。 |
所以这个1s超时是个烟雾弹,线上client是Python,不会有这个问题。
接着又是一通疯狂测试,各种角度体位测试。发现本机测试是ok的,透过lvs请求(跨机房)就会卡住直到超时,而且小body请求一切正常,大body请求卡住。
这时又开始怀疑brpc编译的不对,导致这个超时(brpc编译过程比较曲折,导致我不太有信心)。
于是我在这个服务器Docker中运行了另一个brpc服务,发现是一样的问题。
为了确认是否是brpc的问题,又写了个Python 的 echo server 进行测试,发现在docker中是一样的问题,但是不在docker中运行有一切正常。
这是时就可以确定,与代码无关,是docker或者lvs的问题,一度陷入僵局。
完全没思路,于是找来了运维的同学,这时运维提了一下,这个机房的mtu会小一点,于是一切串起来了。
马上测试,发现机房的mtu只有1450,而docker0网桥的默认mtu是1500,这就很能解释小body没问题,大body卡死。
1 | ~# netstat -i |
修改docker0的mtu,重启docker.service,一切问题都解决了。
1 | cat << EOF > docker/daemon.json |
这句话说得并不太懂,应该是“找到问题等于解决了90%”。
在互联网时代,找到了具体问题,在一通Google,基本等于解决了问题。你始终要确信,这个问题不应该只有你遇到。
在这个例子上,curl的误导大概花了一半的时间去定位。所以,定位问题首先得明确问题,如医生看病一样,确认问题发生的现象(卡住),位置(docker容器中)、程度(永久)、触发原因(大请求body)。
找专业人士寻求帮助是非常高效的,会大大缩短定位问题的时间,因为他们会运用经验和知识快速排除错误选项。
你所拥有的知识,并不是究竟的知识,也就是它所能应用的范围并不适应当前的场景,还有可能误导你。
你所缺失的知识,让你看不清前方正确的道路。
按我的知识,docker0是网桥,等价于交换机,除了性能问题,不应该导致丢包,就一直没往这个方向考虑(当然这块的知识也不扎实)。
MTU也不应该导致丢包,交换机应该会进行IP分片。
但忽略了一个点,虚拟的网桥并不是硬件网桥,可能并没有实现IP分片的逻辑(仅丢弃),又或者没有实现 PMTU(Path MTU Discovery)。
上面这点存疑,但更直接的原因是服务的IP包的 Don't fragment
flag 为1,也就是禁止分片(为什么设置还不清楚)。
最近终于找到一个接近完美的方案:Hyper-V + NFS + openmediavault
我对 NAS 几点硬性要求
所以之前一直用 windows 作为 NAS 系统,最大的问题是不支持共享文件夹的回收站。
现有 NAS 方案的对比
good | bad | |
---|---|---|
windows smb | 性能 | 无法扩展功能(如smb回收站,timemachine) |
wsl1 samba | 性能略低于smb,功能可扩展 | 部分共享文件无法打开, 配置略繁琐 |
wsl2 samba(omv) | 功能可扩展 | 无法解决桥接网络,需要更好的cpu |
nfs | 性能最强 | 无法扩展功能 |
群晖 / OMV / FreeNAS | 功能多,功能可扩展 | 无法管理底层文件 |
windows + nfs + vmware(群晖) | 可管理底层文件,功能可扩展 | 物理机休眠虚拟机无法唤醒 |
windows + nfs + hyper-v(omv) | 功能可扩展,性能接近smb,可休眠 | 需要更好的cpu,hyperv使用略繁琐 |
本方案的思路受之前文章的启发 在WSL2中安装openmediavault(OMV)
WSL2 的便利之处在于让 linux 系统能以一个足够快的速度访问 windows 文件,据了解是使用 9p(Plan 9 9p remote filesystem protocol) 协议进行文件共享。
那么我们只要在 Hyper-V 上运行虚拟机,并通过某种方式(这里选择NFS)共享文件到虚拟机里,就能绕开 WSL2 的缺点。
实现 NAS 的底层文件系统由 windows 管理, 上层文件共享等应用由虚拟机中的 NAS 系统处理,兼顾了稳定性和扩展性。
软件环境选择
DISM /Online /Enable-Feature /All /FeatureName:Microsoft-Hyper-V
安装 omv-extras
1 | wget -O - https://github.com/OpenMediaVault-Plugin-Developers/packages/raw/master/install | bash |
安装插件:openmediavault-sharerootfs(共享系统分区的文件夹) openmediavault-remotemount(远程挂载)
配置页面:服务 -> SMB/CIFS -> 设置 -> 高级设置 -> 扩展选项
1 | veto files = /.Trashes/$RECYCLE.BIN/System Volume Information/.recycle/ |
默认的回收站不能按天对文件进行分类,不方便进行清理
配置页面:服务 -> SMB/CIFS -> 共享 -> 共享文件夹
启用回收站选项,并在扩展选项中增加
1 | recycle:repository = .recycle/%U/today |
windows 系统中,增加定时任务,删除NFS共享目录中的回收站文件到系统回收站
Python 代码如下,超过一定时间的文件会被送到 windows 回收站
1 | import time |
也可考虑将上文 openmediavalut 替换成黑群晖系统,牺牲定制化能力的同时大大增加易用性。
当然,你也可以两者双修
一个典型的例子就是,SSH 登录远程计算机,打开一个远程窗口执行命令。这时,网络突然断线,再次登录的时候,是找不回上一次执行的命令的。因为上一次 SSH 会话已经终止了,里面的进程也随之消失了。
Tmux 可以维持和管理我们的远程终端会话,和服务断线重连后也不会丢失工作状态, 同时可以在一个终端连接中开启多个窗口(window)和窗格(pane)。
比如,下面就包含了2个窗口和3个窗格
具体 Tmux 的细节和使用可以参考 阮一峰的文章
但是 Tmux 也存在以下几个问题(个人观点)
而 iTerm2 内置了 Tmux 绑定功能,可以将 tmux 的窗口和窗格映射成原生的窗口和窗格,可以用 iTerm2 的菜单和快捷键来操作窗口,解决了前3点问题。
至于第4点,rzsz 由于 tmux 的实现机制决定了是无解的。
然而 lonnywong 实现了替代方案 trzsz ,完美解决了文件上传下载的问题,亲测非常好用。
可以对 tmux 窗口的映射进行一些定制
iTerm2 对于 tmux 会话有一个profile,建议对终端颜色和外观进行一些定制化,将原生窗口和 tmux 窗口区分开来。
对于终端机器的 Tmux 版本有要求,需要支持-CC
命令
具体方法
tmux -CC
tmux -CC attach
tmux -CC new -A -s main
ssh -t user@host 'tmux -CC new -A -s main'
还可以使用 iTerm 的 tmux dashboard 来管理多个会话。
echo 'set -g default-terminal "screen-256color"' >> ~/.tmux.conf
127.0.0.1:12345
向 127.0.0.1:6379
发送 SLAVEOF 127.0.0.1 6379
, 则服务器(12345)将成为服务器(6379)的从服务器。Redis的复制功能分为同步(RDB文件)和命令传播(同步写命令)两个阶段,具体步骤如下。
SYNC
SYNC
后执行BGSAVE
生成RDB文件,同时用缓冲区记录之后所有写命令。由于RDB的生成发送非常耗时,主从短暂断线的情况下,也需要重复生成,主从同步的效率就非常低了。
Redis从2.8版本开始, 使用PSYNC
命令替换SYNC
,增加了部分同步功能,对断线重连的情况进行了优化。
主从服务器都记录了复制偏移量,记录了主服务器发出的字节数和从服务器收到的字节数,并且主服务器使用一个复制积压缓冲区记录最近发出的数据(FIFO)。
同步时,从服务器会发送复制偏移量。
同时,主服务器每次启动时会生成运行id,防止主服务器重启后复制混乱。
所谓持久化,就是将Redis在内存中的数据库状态以某次格式保存到磁盘里面,避免数据意外丢失。
Redis有两种持久化方式:RDB (Redis Database)、AOF (Append Only File)
RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,包含了Redis数据库的所有数据。
RDB是将redis中所有db中的所有键值对以如下格式进行储存
有两个命令可以生成RDB文件,SAVE
和 BGSAVE
。生成RDB文件时,redis会遍历所有非空db的所有键值对按一定格式存储到RDB文件中。
SAVE
命令会在当前进程进行,期间服务器会阻塞,不能处理任何请求。BGSAVE
命令会fork一个子进程来创建RDB(利用Copy-on-write),服务继续处理命令请求。
为了避免竞争条件和性能问题,SAVE
和 BGSAVE
任意时刻只能有一个在执行。
用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,就会触发RDB保存.
save选项的格式是 save seconds option_times
。例如save 900 1
,若服务器在900秒之内, 对数据库进行了至少1次修改,则执行BGSAVE。
BGSAVE也已可能会阻塞请求,因为磁盘io满了,这时如果有fsync操作,服务也会阻塞。
可以设置 no-appendfsync-on-rewrite yes
, 在子进程处理和写硬盘时, 主进程不调用 fsync() 操作。
服务器启动时会自动载入RDB文件,Redis并没有专门用于载人RDB文件的命令。
如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
由于RDB生成的机制决定了,RDB文件总是会和redis内存有部分不一致,RDB文件会缺少从上次BGSAVE开始到当前时刻的所有改动。AOF持久化的存在就是为了解决该问题。
AOF持久化是通过保存执行的写命令来记录数据库状态。
因为Redis的命令请求协议是纯文本格式,所以AOF文件类似如下。1
2*2\r\n$6\r\nSELECT\r\n$l\r\nO\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
appendfsync决定如何刷新文件缓存到硬盘,该选项的值直接影响的效率和安全性。
当故障停机时,文件缓冲区内的数据会丢失。
appendfsync有以下选项
AOF的还原就是模仿客户端逐条执行文件里的命令。
AOF的还原时机也是服务启动时,并且在还原过程中能正常执行的只有 PUBSUB 等模块。
步骤如下:
由于AOF是直接记录的写命令而不是数据库状态,所以文件中包含很多冗余语句,导致文件膨胀。
比如下面的这些命令,其实最终数据库状态等价于lpush numbers 333
, 前4条语句都是冗余的。1
2
3
4
5
6
7
8
9
10
11
12127.0.0.1:6379> lpush numbers 111
(integer) 1
127.0.0.1:6379> lpush numbers 222
(integer) 2
127.0.0.1:6379> lpop numbers
"222"
127.0.0.1:6379> lpop numbers
"111"
127.0.0.1:6379> lpush numbers 333
(integer) 1
127.0.0.1:6379> lrange numbers 0 -1
1) "333"
为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite) 功能。
AOF文件重写是遍历redis的所有键值对,生成对应的redis命令,写入到一个新的文件中,并替换旧AOF文件。
所以AOF文件重写和旧AOF文件并没有关系,更应该称之为AOF重生成。
AOF重写程序在子进程里执行, 这样做可以同时达到两个目的:
在AOF重新过程中,所有命令会额外会写一份到AOF重写缓冲区中,当新AOF文件生成时,父进程会将AOF重写缓冲区的内容追加到新AOF文件中,并替换旧AOF文件。
为防止AOF重写失败,AOF缓冲区在重写过程中依然正常工作。
注意:redis主从是基于RDB + 命令传播
,并没有利用AOF文件,与MySQL的binlog不同。
数独的解法需 遵循如下规则:
虽然玩法简单,但提供的数字却千变万化,所以很适合用程序来求解。
类似这种需要穷举的问题一般采用回溯法,也就是暴力求解,在最坏的情况下时间复杂度为指数。
回溯法采用试错的思想,它尝试分步的去解决一个问题。在解决问题的过程中,如果现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
回溯法通常用最简单的递归方法来实现,下面是回溯法的一般结构。
理解回溯法首先要理解递归,理解递归的过程,以及递归的返回值。可以通过查看斐波那契数列的递归实现的调用栈来理解递归的过程。
1 | def backtracking(args): |
回到数独解法上来
首先看最简单的暴力解法
这里有几个点和模板不一样
can_stop
判断get_all_choices
最多有 行 x 列 x 数字取值 = 9 x 9 x 9
种情况1 | # 0 代表空位 |
所谓的剪枝,就是递归每一层的时候,并不是所有情况都是有效的,可以跳过这些分支。
以该题为例,第一行第三列可选取值并不是1到9,而是1,2,4
,而且随着我们不断把空位填满,越后面的点选择越少。
可以用集合存储每个空位的行、列、九宫方向的可选值,三者交集就是该点的可选值。(用位替换集合还可以进一步优化算法)
因为填充一个空位,会影响该位置行、列、九宫上的所有空位的取值,所有不直接存储该点的可选值。
1 | EMPTY = 0 |
https://zh.wikipedia.org/wiki/%E6%95%B8%E7%8D%A8
https://zh.wikipedia.org/wiki/%E5%9B%9E%E6%BA%AF%E6%B3%95
https://www.jianshu.com/p/8e694d079a76
有两种常见的实现方法
特点
这里的Python实现是方案1
具体步骤
1 | from math import log, ceil |
换个角度思考,这句话用在学习上,是再贴切不过了。当我们接触一个新领域时,其中的每个知识细节,都是魔鬼,稍有不慎就会被魔鬼击败,产生厌学情绪,再起不能。只有除尽这些魔鬼,才能攀上知识的高峰。
但光学会这些细节的点还远远不够,你的知识都是僵化的,是零散不成体系的的,难以真正应用。
就如将英语单词表背得滚瓜烂熟,倘若不识得语法与文法,断然是写不出文章的。
总地来说,学习分为两个阶段,“为学日益”和“为道日损”,也就是积累具体细节知识点,然后再将这些知识点进行归纳总结整理,将知识抽象并构建体系。
注:此处的“为学日益”和“为道日损”与道德经中原义不同
华罗庚教授曾把读书的过程归纳为“由薄到厚”与“由厚到薄”两个阶段,也是异曲同工;硅谷钢铁侠马斯克常说的“第一性原理”,就是第二步“为道日损”所得到的结果;再进一步,禅宗所说的“渐修”和“顿悟”也是一样的。
这两个阶段总体上是同等重要的,但在学习的不同阶段又该有所侧重。
下面将理论展开论述,我们又能引申出一些结论,得到一些新的看法。
先讲第一阶段,这一阶段积累具体细节知识点,那么多大规模的知识点可以成为一个细节呢?
这点因人而异,因当前知识储备而异,评判标准就是能否说出该知识点的定义和用途,如若不能就应该继续往下看构成该知识点的子知识点,直至满足标准为止。
由此可知,学习是有极限的,当最小的知识点都不能领会时,继续学习就无从谈起了。
就如数学,当不能理解1 + 1 = 2
时,在这之上构建的一切都与你无缘了。
所以学习还是要靠智商的,只不过要求极低。
在这个阶段比较有效的方法有
当学习到达一定的阶段,就该对知识进行归纳整理,以便进入下一阶段的学习。
因为人脑所能同时关注的知识点是有上限的,如果不加以归纳整理进行抽象,体现出来就是学了新的忘了旧的。
日常生活的很多场景都与这个有关,比如人们在争辩的时候经常会扣帽子,这就是一种抽象,降低了受众的认知难度,在传播中是有利的。“xx人偷井盖”,这就是典型的扣帽子,一个地方人何其多,每个人又不一样,贴切地描述出这个整体是非常难的,扣帽子就是舍弃了其他特征,用某几个特征来概括整体,这样带了的坏处就是不准确,失之偏颇。
这一阶段影响的是对知识的应用,也就是将一系列知识点抽象成一个整体,用于构造其他知识。
抽象有好的有坏的,知识点的名称就是对其内涵的一种抽象,好的抽象应该是可以望文生义的。就像写代码,好的函数命名应该是表达了函数的用途,也就是对具体实现该函数语句的抽象。
类比也是一种抽象,而且是一种极其高效的抽象,将具体的细节和已有的知识点关联起来。但类比也是危险的,很容易做出不合适的类比,也就是引喻失义。所以类比更像是方便法门,一条通向“第一性原理”的捷径,有其适用场景,不应该作为“第一性原理”本身牢记。
由此可以看出很多学习的方法其实都走在歧路上。
如学英语,一味在单词和例句中深挖,最终还是不能流畅的写作交流,这是第二阶段缺失造成的。
又如罗辑思维,听他灌输各种大而无当的大道理,只会显得很轻浮,除了增加谈资之外无有用处,这是第一阶段缺失造成的。
最后,希望我这胡言乱语对读者有些微帮助。
]]>