diff --git a/2017/05/19/2017-5-19-javacc-minilisp/index.html b/2017/05/19/2017-5-19-javacc-minilisp/index.html index 7a69ea13..1d5efc70 100644 --- a/2017/05/19/2017-5-19-javacc-minilisp/index.html +++ b/2017/05/19/2017-5-19-javacc-minilisp/index.html @@ -30,8 +30,8 @@ - + @@ -305,8 +305,8 @@

- +
diff --git a/2019/01/09/2019-1-9-bt-1/index.html b/2019/01/09/2019-1-9-bt-1/index.html index ad67e832..64f41d46 100644 --- a/2019/01/09/2019-1-9-bt-1/index.html +++ b/2019/01/09/2019-1-9-bt-1/index.html @@ -27,7 +27,7 @@ - + @@ -231,7 +231,7 @@

更新于 - + @@ -269,8 +269,8 @@

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string : num ':' {CHAR}*

num : 0
| [1-9][0-9]+
| '-' [1-9][0-9]+

integer : 'i' num 'e'

list : 'l' {element}* 'e'

dic : 'd' {pair}* 'e'

pair : string element

element : string
| integer
| list
| dic
+

翻译为eBNF语法呢,就是如下

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string : num ':' {CHAR}*

num : 0
| [1-9][0-9]+
| '-' [1-9][0-9]+

integer : 'i' num 'e'

list : 'l' {element}* 'e'

dic : 'd' {pair}* 'e'

pair : string element

element : string
| integer
| list
| dic

解码

根据BNF实现的解码代码如下, 把get_content()方法中path替换为种子文件的路径,运行就可以看到。 返回的解析结果中会有info_hash,该值是根据info的bencoding的二进制串计算的sha1值。这个值很重要 diff --git a/2023/01/12/2023-1-12-8-queens-chatgpt/index.html b/2023/01/12/2023-1-12-8-queens-chatgpt/index.html index 9f75e3da..437ceb22 100644 --- a/2023/01/12/2023-1-12-8-queens-chatgpt/index.html +++ b/2023/01/12/2023-1-12-8-queens-chatgpt/index.html @@ -32,8 +32,8 @@ - + @@ -271,8 +271,8 @@

最后
- +
diff --git a/about/index.html b/about/index.html index be186657..1a7237d1 100644 --- a/about/index.html +++ b/about/index.html @@ -19,15 +19,15 @@ - + - + - - + + @@ -200,10 +200,9 @@

关于作者
-

这是alenym@qq.com的技术博客,以下是作者一些个人项目。

-
diff --git a/page/3/index.html b/page/3/index.html index e53751bb..dd523382 100644 --- a/page/3/index.html +++ b/page/3/index.html @@ -306,7 +306,7 @@

更新于 - + diff --git a/search.xml b/search.xml index 8a275844..714d1b67 100644 --- a/search.xml +++ b/search.xml @@ -81,99 +81,6 @@ python默认的递归栈深度是10000。

python - - [leetcode 282]Expression Add Operators 原创解法 - /2016/12/19/leetcode-282/ - 题目概述

原题链接

-
Given a string that contains only digits 0-9 and a target value, return all possibilities to add binary operators (
-not unary) +, -, or * between the digits so they evaluate to the target value.
-
- "123", 6 -> ["1+2+3", "1*2*3"] 
- "232", 8 -> ["2*3+2", "2+3*2"]
- "105", 5 -> ["1*0+5","10-5"]
- "00", 0 -> ["0+0", "0-0", "0*0"]
- "3456237490", 9191 -> []
-
- - -

首先想到的解法就是遍历所有的组合,一一计算比较排除。

-
class Solution1(object):
def addOperators(self, num, target):
"""
:type num: str
:type target: int
:rtype: List[str]
"""
if num:
result = []
path = []
n = len(num)
self.find(num, 0, n, target, path, result)
return result

def find(self, num, begin, end, target, path, result):
if begin == end:
s = "".join(path)
if self.is_valid(path) and eval(s) == target:
result.append(s)
else:
path.append(num[begin])
self.find(num, begin + 1, end, target, path, result)
if begin < end - 1:
path.append("+")
self.find(num, begin + 1, end, target, path, result)
path.pop()
path.append("-")
self.find(num, begin + 1, end, target, path, result)
path.pop()
path.append("*")
self.find(num, begin + 1, end, target, path, result)
path.pop()
path.pop()

def is_valid(self, path):
flag = True
tmp = ""
for i in path:
if i in "0123456789":
tmp += i
elif i in "+-*":
if len(tmp) >= 2 and tmp[0] == '0':
flag = False
break
tmp = ""

if len(tmp) > 1 and tmp[0] == "0":
flag = False

return flag
- -

分析

没有意外,这种解法超时了。

-

先不考虑eval()函数是否应该使用。 -显然的情况是每一种字符组合至少遍历4遍。

- -

有办法使用动态规划么?把一个大问题替换成子问题进行求解。 -比如先计算一个字符的所有的解。然后考虑增加到两个字符。在第一个字符的解 -的基础上,考虑在两个字符中间插入加减乘。然后再在两个字符的解的基础上 -再加入第三个字符。以此类推。这种方法,并没有减少计算量,其实还是遍历 -所有的组合。然后得到最终的解。

-

那么最终我们可以选择的优化方向就是,使用深度优先遍历的时候,必须要在 -遍历的同时,进行计算。这样当到达结束判断的时候,能够直接判断。

-

简化问题

为了便于分析,首先简化问题。考虑如何计算字符串”123”,并且 -只允许插入加号或者不插入加号。

-

为了把问题组合描述为一个树的形式。我们把两个数字直接连接起来组成 -一个新的数字,看做一个操作,定义为.,那么a.b = ab,而数值计算 -的公式为a.b = a*10+b, -而且这个符号的优先级高于加号和乘号。 -这样可得如下树图。

-

tree pic

-

根据这种假定,对于任意数字字符串n1n2n3,可以得到 -的查找树如下图。

-

tree2 pic

-

通过的解法

这样我们有了解法2。

-
class Solution2(object):
def addOperators(self, num, target):
"""
这个居然通过了。呵呵。不过是勉强通过的。
:type num: str
:type target: int
:rtype: List[str]
"""
if num:
end = len(num)
result = []
path = [num[0]]
s1 = [int(num[0])]
s2 = []
begin = 1
self.find(begin, end, num, s1, s2, path, target, result)
return result
else:
return []

def find(self, begin, end, num, s1, s2, path, target, result):
if begin == end:
if s1[0] == target:
result.append("".join(path))
else:
ops = [("+", 10), ("-", 10), ("*", 20), (".", 30)]
ps = ["+", "-", "*", ""]
if begin + 1 < end:
for i in range(4):
s11 = s1[:]
s22 = s2[:]
path.append(ps[i] + num[begin])
self.helper(begin, end, num, ops[i], path, result, s11, s22, target)
path.pop()
elif begin + 1 == end:
for i in range(4):
s11 = s1[:]
s22 = s2[:]
path.append(ps[i] + num[begin])
self.helper2(begin, end, num, ops[i], path, result, s11, s22, target)
path.pop()

def helper(self, begin, end, num, op, path, result, s1, s2, target):
# 没有到达结尾
if s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
# 需要压栈延迟计算
s1.append(int(num[begin]))
s2.append(op)
self.find(begin + 1, end, num, s1, s2, path, target, result)

else:
# 需要先把s2出栈计算完成,直到栈中的符号级别小于当前。
is_valid = True
while s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
break
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break
if is_valid:
s1.append(int(num[begin]))
s2.append(op)
self.find(begin + 1, end, num, s1, s2, path, target, result)
else:
s1.append(int(num[begin]))
s2.append(op)
self.find(begin + 1, end, num, s1, s2, path, target, result)

def helper2(self, begin, end, num, op, path, result, s1, s2, target):
# 到达结尾
if s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
s1.append(int(num[begin]))
s2.append(op)

is_valid = True
while s2:
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break
if is_valid:
self.find(begin + 1, end, num, s1, s2, path, target, result)
else:
is_valid = True
while s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
break
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break

if is_valid:
s2.append(op)
s1.append(int(num[begin]))
while s2:
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break

if is_valid:
self.find(begin + 1, end, num, s1, s2, path, target, result)
else:
n2 = int(num[begin])
n1 = s1[-1]
is_valid = True
if op[0] == "+":
s1[-1] = n1 + n2
elif op[0] == "-":
s1[-1] = n1 - n2
elif op[0] == "*":
s1[-1] = n1 * n2
elif op[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False

if is_valid:
self.find(begin + 1, end, num, s1, s2, path, target, result)
-]]> - - leetcode - 算法 - python - - - - 使用pandas.read_csv()读取csv文件 - /2016/12/22/pandas-read-csv/ - 问题

以下Python代码实现对Excel转存的csv文件进行读取。

-
import pandas as pd
df = pd.read_csv(file_path + file_name + ".csv", encoding="gbk")
- - - -

csv文件入库是一件脏活。表面上看csv文件是一个非常简单的 -逗号分隔符文件。但是其实不然。Excel转存的csv文件并不是标准的以逗号作为分隔符, -并且对所有的项用双引号包裹。现在我就遇到了从Oracle导出的csv文件,以上的代码 -不起作用了。

-

解决问题

究竟怎么回事呢,找了一圈也没有发现使用pandas.read_csv()读取 -这种标准csv文件的方法。还是先把问题简化一下,看看Pythoncsv模块是如何读取的吧。 -简单的查找就可以找到答案。

-
import csv
csv.register_dialect(
'mydialect',
delimiter = ',',
quotechar = '"',
doublequote = True,
skipinitialspace = True,
lineterminator = '\r\n',
quoting = csv.QUOTE_MINIMAL)

print('\n Output from an iterable object created from the csv file')
with open('smallsample.csv', 'rb') as mycsvfile:
thedata = csv.reader(mycsvfile, dialect='mydialect')
for row in thedata:
print(row[0]+"\t \t"+row[1]+"\t \t"+row[4])
- -

好的,pandas.read_csv()肯定是要调用csv模块的,那么看看 -它的方法参数表吧。

-
-

pandas.read_csv(filepath_or_buffer, sep=’, ‘, delimiter=None, header=’infer’, names=None, index_col=None, usecols=None, squeeze=False, prefix=None, mangle_dupe_cols=True, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skipinitialspace=False, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, skip_blank_lines=True, parse_dates=False, infer_datetime_format=False, keep_date_col=False, date_parser=None, dayfirst=False, iterator=False, chunksize=None, compression=’infer’, thousands=None, decimal=’.’, lineterminator=None, quotechar=’”‘, quoting=0, escapechar=None, comment=None, encoding=None, dialect=None, tupleize_cols=False, error_bad_lines=True, warn_bad_lines=True, skipfooter=0, skip_footer=0, doublequote=True, delim_whitespace=False, as_recarray=False, compact_ints=False, use_unsigned=False, low_memory=True, buffer_lines=None, memory_map=False, float_precision=None)[source]¶

-
-

真是够长的,还是搜一下有没有dialect=,呵呵,果然有。

-
-

dialect : str or csv.Dialect instance, default None
- If None defaults to Excel dialect. Ignored if sep longer than 1 char See csv.Dialect documentation for more details

-
-

那么问题解决了。把第一段代码改改吧。

-
import pandas as pd
df = pd.read_csv(file_path + file_name + ".csv", encoding="gbk", dialect='mydialect')
- -

一运行还是报错,这是怎么回事呢,编码换成utf8,也不行。最后才发现 -需要使用gb18030才行。即使使用chardet的编码探测模块,也不一定能探测出来,因为整个文档 -只有少数字符是超出了gbk,所以不可能既高效又准确的解决这个问题。

-]]>
- - python - 技巧 - -
R和Python里得到传入参数的变量名 /2016/12/21/get-val-name-and-value/ @@ -325,6 +232,45 @@ Return the array [2, 1, 1, 0]. python + + 使用pandas.read_csv()读取csv文件 + /2016/12/22/pandas-read-csv/ + 问题

以下Python代码实现对Excel转存的csv文件进行读取。

+
import pandas as pd
df = pd.read_csv(file_path + file_name + ".csv", encoding="gbk")
+ + + +

csv文件入库是一件脏活。表面上看csv文件是一个非常简单的 +逗号分隔符文件。但是其实不然。Excel转存的csv文件并不是标准的以逗号作为分隔符, +并且对所有的项用双引号包裹。现在我就遇到了从Oracle导出的csv文件,以上的代码 +不起作用了。

+

解决问题

究竟怎么回事呢,找了一圈也没有发现使用pandas.read_csv()读取 +这种标准csv文件的方法。还是先把问题简化一下,看看Pythoncsv模块是如何读取的吧。 +简单的查找就可以找到答案。

+
import csv
csv.register_dialect(
'mydialect',
delimiter = ',',
quotechar = '"',
doublequote = True,
skipinitialspace = True,
lineterminator = '\r\n',
quoting = csv.QUOTE_MINIMAL)

print('\n Output from an iterable object created from the csv file')
with open('smallsample.csv', 'rb') as mycsvfile:
thedata = csv.reader(mycsvfile, dialect='mydialect')
for row in thedata:
print(row[0]+"\t \t"+row[1]+"\t \t"+row[4])
+ +

好的,pandas.read_csv()肯定是要调用csv模块的,那么看看 +它的方法参数表吧。

+
+

pandas.read_csv(filepath_or_buffer, sep=’, ‘, delimiter=None, header=’infer’, names=None, index_col=None, usecols=None, squeeze=False, prefix=None, mangle_dupe_cols=True, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skipinitialspace=False, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, skip_blank_lines=True, parse_dates=False, infer_datetime_format=False, keep_date_col=False, date_parser=None, dayfirst=False, iterator=False, chunksize=None, compression=’infer’, thousands=None, decimal=’.’, lineterminator=None, quotechar=’”‘, quoting=0, escapechar=None, comment=None, encoding=None, dialect=None, tupleize_cols=False, error_bad_lines=True, warn_bad_lines=True, skipfooter=0, skip_footer=0, doublequote=True, delim_whitespace=False, as_recarray=False, compact_ints=False, use_unsigned=False, low_memory=True, buffer_lines=None, memory_map=False, float_precision=None)[source]¶

+
+

真是够长的,还是搜一下有没有dialect=,呵呵,果然有。

+
+

dialect : str or csv.Dialect instance, default None
+ If None defaults to Excel dialect. Ignored if sep longer than 1 char See csv.Dialect documentation for more details

+
+

那么问题解决了。把第一段代码改改吧。

+
import pandas as pd
df = pd.read_csv(file_path + file_name + ".csv", encoding="gbk", dialect='mydialect')
+ +

一运行还是报错,这是怎么回事呢,编码换成utf8,也不行。最后才发现 +需要使用gb18030才行。即使使用chardet的编码探测模块,也不一定能探测出来,因为整个文档 +只有少数字符是超出了gbk,所以不可能既高效又准确的解决这个问题。

+]]>
+ + python + 技巧 + +
[leetcode 327]Count of Range Sum 原创解法 /2016/12/31/leetcode-327/ @@ -393,32 +339,52 @@ The three ranges are : [0, 0], [2, 2], [0, 2] and their respective sums are: -2, - [leetcode 200]Number of Islands 原创解法 - /2016/09/27/2016-9-27-leetcode-200/ - 题目概述

原题链接

-
Given a 2d grid map of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.
-
-Example 1:
-
-11110
-11010
-11000
-00000
-Answer: 1
-
-Example 2:
+    [leetcode 282]Expression Add Operators 原创解法
+    /2016/12/19/leetcode-282/
+     题目概述

原题链接

+
Given a string that contains only digits 0-9 and a target value, return all possibilities to add binary operators (
+not unary) +, -, or * between the digits so they evaluate to the target value.
 
-11000
-11000
-00100
-00011
-Answer: 3
+ "123", 6 -> ["1+2+3", "1*2*3"] 
+ "232", 8 -> ["2*3+2", "2+3*2"]
+ "105", 5 -> ["1*0+5","10-5"]
+ "00", 0 -> ["0+0", "0-0", "0*0"]
+ "3456237490", 9191 -> []
 
-

最终解法

解法的代码先贴出来了,解题思路后补啊。总体来说,就是一行一行的扫描。 -是个不错的在线算法,也就是说如果数据量非常大也没关系,呵呵。

-
class Solution(object):
def numIslands(self, grid):
"""
基本思路,一行一行的扫描。
:type grid: List[List[str]]
:rtype: int
"""
print("====")
n = len(grid)
if n == 0: return 0

length = len(grid[0])
h_pre = {} # key : 是 “i,j” 字符串。value =》 [accumulator] ,一个list包含了岛的索引。
h_curr = {} # 当前行
a = [] # 保存的是一个个的list,list的长度是1,值对应岛的索引。
debug = []
accumulator = 1 # 表示岛的自增索引,每当发现一个新的岛,自增。
for i in range(0, length):
if i == 0 and grid[0][i] == '1':
h_curr['0,' + str(i)] = [accumulator]
a.append(h_curr['0,' + str(i)])
debug.append('0,' + str(i))
elif i > 0 and grid[0][i] == '1':
if grid[0][i - 1] == '1':
h_curr[str(0) + ',' + str(i)] = h_curr[str(0) + ',' + str(i - 1)]
else:
accumulator += 1
h_curr['0,' + str(i)] = [accumulator]
a.append(h_curr['0,' + str(i)])
debug.append('0,' + str(i))

for i in range(1, n):
h_pre = h_curr
h_curr = {}
for j in range(0, length):
if j == 0 and grid[i][j] == '1':

if grid[i - 1][j] == '1':
h_curr[str(i) + ',' + str(j)] = h_pre[str(i - 1) + ',' + str(j)]
else:
accumulator += 1
h_curr[str(i) + ',' + str(j)] = [accumulator]
a.append(h_curr[str(i) + ',' + str(j)])
debug.append(str(i) + ',' + str(j))

elif j > 0 and grid[i][j] == '1':

if grid[i - 1][j] == '1' and grid[i][j - 1] == '1':
pre = h_curr[str(i) + ',' + str(j - 1)]
above = h_pre[str(i - 1) + ',' + str(j)]

if pre[0] == above[0]:
# 对pre的值进行更新
h_curr[str(i) + ',' + str(j)] = pre
else :
h_curr[str(i) + ',' + str(j)] = above
v1 = pre[0]
v2 = above[0] # z这里一定要换成静态的值,否则的话当a[k]的值进行更新时,会影响到pre的值。
for k in range(0, len(a)):
if a[k][0] == v1:
a[k][0] = v2


elif grid[i][j - 1] == '1' and grid[i - 1][j] != '1':
pre = h_curr[str(i) + ',' + str(j - 1)]
h_curr[str(i) + ',' + str(j)] = pre
elif grid[i][j - 1] != '1' and grid[i - 1][j] == '1':
above = h_pre[str(i - 1) + ',' + str(j)]
h_curr[str(i) + ',' + str(j)] = above
else:
accumulator += 1
h_curr[str(i) + ',' + str(j)] = [accumulator]
a.append(h_curr[str(i) + ',' + str(j)])
debug.append(str(i) + ',' + str(j))
s = set()
for item in a:
s.add(item[0])
return len(s)


if __name__ == "__main__":
grid = ["11000",
"11000",
"00100",
"00011"] # 3
print(Solution().numIslands(grid))
grid = ["10111",
"10101",
"11101"] # 1
print(Solution().numIslands(grid))
grid = ["1111111",
"0000001",
"1111101",
"1000101",
"1010101",
"1011101",
"1111111"] # 1
print(Solution().numIslands(grid))


grid = ["10011101100000000000",
"10011001000101010010",
"00011110101100001010",
"00011001000111001001",
"00000001110000000000",
"10000101011000000101",
"00010001010101010101",
"00010100110101101110",
"00001001100001000101",
"00100100000100100010",
"10010000000100101010",
"01000101011011101100",
"11010000100000010001",
"01001110001111101000",
"00111000110001010000",
"10010100001000101011",
"10100000010001010000",
"01100011101010111100",
"01000011001010010011",
"00000011110100011000"] # 58
print(Solution().numIslands(grid))
+

首先想到的解法就是遍历所有的组合,一一计算比较排除。

+
class Solution1(object):
def addOperators(self, num, target):
"""
:type num: str
:type target: int
:rtype: List[str]
"""
if num:
result = []
path = []
n = len(num)
self.find(num, 0, n, target, path, result)
return result

def find(self, num, begin, end, target, path, result):
if begin == end:
s = "".join(path)
if self.is_valid(path) and eval(s) == target:
result.append(s)
else:
path.append(num[begin])
self.find(num, begin + 1, end, target, path, result)
if begin < end - 1:
path.append("+")
self.find(num, begin + 1, end, target, path, result)
path.pop()
path.append("-")
self.find(num, begin + 1, end, target, path, result)
path.pop()
path.append("*")
self.find(num, begin + 1, end, target, path, result)
path.pop()
path.pop()

def is_valid(self, path):
flag = True
tmp = ""
for i in path:
if i in "0123456789":
tmp += i
elif i in "+-*":
if len(tmp) >= 2 and tmp[0] == '0':
flag = False
break
tmp = ""

if len(tmp) > 1 and tmp[0] == "0":
flag = False

return flag
+ +

分析

没有意外,这种解法超时了。

+

先不考虑eval()函数是否应该使用。 +显然的情况是每一种字符组合至少遍历4遍。

+
    +
  • 第一遍,得到组合
  • +
  • 第二遍,join
  • +
  • 第三遍,eval
  • +
  • 第四遍,is_valid排除
  • +
+

有办法使用动态规划么?把一个大问题替换成子问题进行求解。 +比如先计算一个字符的所有的解。然后考虑增加到两个字符。在第一个字符的解 +的基础上,考虑在两个字符中间插入加减乘。然后再在两个字符的解的基础上 +再加入第三个字符。以此类推。这种方法,并没有减少计算量,其实还是遍历 +所有的组合。然后得到最终的解。

+

那么最终我们可以选择的优化方向就是,使用深度优先遍历的时候,必须要在 +遍历的同时,进行计算。这样当到达结束判断的时候,能够直接判断。

+

简化问题

为了便于分析,首先简化问题。考虑如何计算字符串”123”,并且 +只允许插入加号或者不插入加号。

+

为了把问题组合描述为一个树的形式。我们把两个数字直接连接起来组成 +一个新的数字,看做一个操作,定义为.,那么a.b = ab,而数值计算 +的公式为a.b = a*10+b, +而且这个符号的优先级高于加号和乘号。 +这样可得如下树图。

+

tree pic

+

根据这种假定,对于任意数字字符串n1n2n3,可以得到 +的查找树如下图。

+

tree2 pic

+

通过的解法

这样我们有了解法2。

+
class Solution2(object):
def addOperators(self, num, target):
"""
这个居然通过了。呵呵。不过是勉强通过的。
:type num: str
:type target: int
:rtype: List[str]
"""
if num:
end = len(num)
result = []
path = [num[0]]
s1 = [int(num[0])]
s2 = []
begin = 1
self.find(begin, end, num, s1, s2, path, target, result)
return result
else:
return []

def find(self, begin, end, num, s1, s2, path, target, result):
if begin == end:
if s1[0] == target:
result.append("".join(path))
else:
ops = [("+", 10), ("-", 10), ("*", 20), (".", 30)]
ps = ["+", "-", "*", ""]
if begin + 1 < end:
for i in range(4):
s11 = s1[:]
s22 = s2[:]
path.append(ps[i] + num[begin])
self.helper(begin, end, num, ops[i], path, result, s11, s22, target)
path.pop()
elif begin + 1 == end:
for i in range(4):
s11 = s1[:]
s22 = s2[:]
path.append(ps[i] + num[begin])
self.helper2(begin, end, num, ops[i], path, result, s11, s22, target)
path.pop()

def helper(self, begin, end, num, op, path, result, s1, s2, target):
# 没有到达结尾
if s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
# 需要压栈延迟计算
s1.append(int(num[begin]))
s2.append(op)
self.find(begin + 1, end, num, s1, s2, path, target, result)

else:
# 需要先把s2出栈计算完成,直到栈中的符号级别小于当前。
is_valid = True
while s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
break
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break
if is_valid:
s1.append(int(num[begin]))
s2.append(op)
self.find(begin + 1, end, num, s1, s2, path, target, result)
else:
s1.append(int(num[begin]))
s2.append(op)
self.find(begin + 1, end, num, s1, s2, path, target, result)

def helper2(self, begin, end, num, op, path, result, s1, s2, target):
# 到达结尾
if s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
s1.append(int(num[begin]))
s2.append(op)

is_valid = True
while s2:
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break
if is_valid:
self.find(begin + 1, end, num, s1, s2, path, target, result)
else:
is_valid = True
while s2:
op_pre = s2[-1]
if op_pre[1] < op[1]:
break
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break

if is_valid:
s2.append(op)
s1.append(int(num[begin]))
while s2:
op_pre = s2.pop()
n2 = s1.pop()
n1 = s1[-1]
if op_pre[0] == "+":
s1[-1] = n1 + n2
elif op_pre[0] == "-":
s1[-1] = n1 - n2
elif op_pre[0] == "*":
s1[-1] = n1 * n2
elif op_pre[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False
break

if is_valid:
self.find(begin + 1, end, num, s1, s2, path, target, result)
else:
n2 = int(num[begin])
n1 = s1[-1]
is_valid = True
if op[0] == "+":
s1[-1] = n1 + n2
elif op[0] == "-":
s1[-1] = n1 - n2
elif op[0] == "*":
s1[-1] = n1 * n2
elif op[0] == "." and n1 != 0:
s1[-1] = n1 * 10 + n2
else:
is_valid = False

if is_valid:
self.find(begin + 1, end, num, s1, s2, path, target, result)
]]>
leetcode @@ -427,96 +393,19 @@ Answer: 3 - 在WIN10下使用Cython - /2017/01/12/2017-1-12-cython-on-win10/ - Cython

Cython不是CPython的简称,而是一种提升Python代码执行效率的解决方案。 -据说一般达到30x。一会我们来看看是不是真的。这个技术对老鸟来说已经是好多 -年前的了。但是很多情况下python用户真的用不上,所以不知道也无妨。

+ 解决python3使用system-site-packages创建虚拟环境时没有pip的问题 + /2017/01/18/2017-1-18-venv-no-pip/ + python虚拟环境

python3.5开始,使用venv创建虚拟环境已经成为官方推荐的方案。 +查看官方文档,可以看到如果想要创建一个干净的虚拟环境。方式如下

+
python -m venv myenv
+
-

软件安装

我已经迁移到python3了,使用的是Anaconda。感谢Anaconda让我们的生活变 -的更加美好。以下软件安装的顺序不要错。

-
    -
  1. 安装Visual C++ Build Tools 2015
  2. -
  3. 安装Anaconda3
  4. -
-

跟着Cython的tutorial走

先创建一个helloworld.pyx文件。内容如下。

-
# helloworld.pyx
print("hello cython!")

def fib(n):
if n == 0 or n == 1:
return 1
else:
return fib(n - 2) + fib(n - 1)
- -

再创建一个setup.py文件。内容如下

-
#setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(
ext_modules = cythonize("helloworld.pyx")
)
- - - -

然后在命令行输入

-
D:\tmp\oo>python setup.py build_ext --inplace
-running build_ext
-building 'helloworld' extension
-C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -IC:\Anaconda3\include -IC:\Anaconda3\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\shared" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\um" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\winrt" /Tchelloworld.c /Fobuild\temp.win-amd64-3.5\Release\helloworld.obj
-helloworld.c
-C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\link.exe /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTUAC:NO /LIBPATH:C:\Anaconda3\libs /LIBPATH:C:\Anaconda3\PCbuild\amd64 "/LIBPATH:C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\LIB\amd64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.10240.0\ucrt\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.10240.0\um\x64" /EXPORT:PyInit_helloworld build\temp.win-amd64-3.5\Release\helloworld.obj /OUT:D:\tmp\oo\helloworld.cp35-win_amd64.pyd /IMPLIB:build\temp.win-amd64-3.5\Release\helloworld.cp35-win_amd64.lib
-helloworld.obj : warning LNK4197: export 'PyInit_helloworld' specified multiple times; using first specification
-   Creating library build\temp.win-amd64-3.5\Release\helloworld.cp35-win_amd64.lib and object build\temp.win-amd64-3.5\Release\helloworld.cp35-win_amd64.exp
-Generating code
-Finished generating code
-
-

(WIN10下的命令窗口终于可以使用ctrl-c和ctrl-v了。泪奔。) -可以发现多了一个.pyd文件。然后测试一下。

-
D:\tmp\oo>python
-Python 3.5.2 |Anaconda custom (64-bit)| (default, Jul  5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
-Type "help", "copyright", "credits" or "license" for more information.
->>> import helloworld as h
-hello cython!
->>>h.fib(3)
-3
-
-

真是不错啊。可是如果你换一个目录比如不在当前包含.pyd的目录下, -再想导入helloworld则是不行的。怎么回事,我们不是已经setup了么。 -原因在于--inplace这个参数,它表示只生成动态链接库。

-

与python代码进行测试比较

我们还在那个目录下创建一个test.py文件。

-
#test.py
def fib(n):
if n == 0 or n == 1:
return 1
else:
return fib(n - 2) + fib(n - 1)


def test_fib():
import timeit
n = 1000
t = timeit.timeit("fib(25)", number=n, setup="from __main__ import fib")
time = t / n
print("python fib : {:.8f}s".format(time))

def test_fib_cython():
import timeit
n = 1000
t = timeit.timeit("h.fib(25)", number=n, setup="import helloworld as h")
time = t / n
print("cython fib : {:.8f}s".format(time))

if __name__ == "__main__":
test_fib()
test_fib_cython()
- - -

然后运行

-
D:\tmp\oo>python test.py
-python fib : 0.05887972s
-hello cython!
-cython fib : 0.01739922s
-
-

怎么回事,说好的30x呢,还轻轻松松?别着急,我们把之前的helloworld.pyx -修改一下。

-
#helloworld.pyx
cpdef int fib(int n) :
if n == 0 or n == 1 :
return 1
else :
return fib(n - 2) + fib(n - 1)
- - -

再运行一下瞅瞅。

-
D:\tmp\oo>python test.py
-python fib : 0.05774631s
-hello cython!
-cython fib : 0.00059495s
-
-

真是不错哦,诚不我欺,100x。

-]]>
- - python - Cython - -
- - 解决python3使用system-site-packages创建虚拟环境时没有pip的问题 - /2017/01/18/2017-1-18-venv-no-pip/ - python虚拟环境

python3.5开始,使用venv创建虚拟环境已经成为官方推荐的方案。 -查看官方文档,可以看到如果想要创建一个干净的虚拟环境。方式如下

-
python -m venv myenv
-
- - - - -

如果要创建一个当前环境的副本,也就是说,可以引用当前环境的所有已经 -安装的包。那么需要增加--system-site-packages参数

-
python -m venv myenv2 --system-site-packages
+

如果要创建一个当前环境的副本,也就是说,可以引用当前环境的所有已经 +安装的包。那么需要增加--system-site-packages参数

+
python -m venv myenv2 --system-site-packages
 

两种方式,我们都希望能够独立使用pip来管理包。但是第二种方式发现并没有pip 文件。

@@ -685,6 +574,40 @@ coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167 的min改成max。如果这两个问题是等价的,那么我们的解法一定可以通过测试。并且似乎也不用去证明了。(其实这是最难的部分。)

以下是通过的memoization解法,算法的复杂度是O(n^2),与矩阵链问题的复杂度一样:

class Solution2(object):
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
list = self.get(nums) # 转换为n+1个矩阵的表示。每个pair tuple对应矩阵的长和宽。
print(list)
n += 1
m = [[None] * n for i in range(n)] # 声明一个子问题空间是n^2的记忆体
print(m)
for i in range(n):
m[i][i] = 0

return self.recursive(list, m, 0, n - 1)

def get(self, nums):
n = len(nums)
list = [(1, nums[0])]
for i in range(n):
if (i + 1 < n):
list.append((nums[i], nums[i + 1]))
else:
list.append((nums[i], 1))
return list

def recursive(self, list, m, i, j):
if m[i][j] is not None:
return m[i][j]
else:
max = -1
for k in range(i, j):
a = self.recursive(list, m, i, k)
b = self.recursive(list, m, k + 1, j)
v = a + b + list[i][0] * list[k][1] * list[j][1]
if v > max :
max = v
m[i][j] = max
return m[i][j]
+]]> + + leetcode + 算法 + python + + + + [leetcode 200]Number of Islands 原创解法 + /2016/09/27/2016-9-27-leetcode-200/ + 题目概述

原题链接

+
Given a 2d grid map of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.
+
+Example 1:
+
+11110
+11010
+11000
+00000
+Answer: 1
+
+Example 2:
+
+11000
+11000
+00100
+00011
+Answer: 3
+
+ + +

最终解法

解法的代码先贴出来了,解题思路后补啊。总体来说,就是一行一行的扫描。 +是个不错的在线算法,也就是说如果数据量非常大也没关系,呵呵。

+
class Solution(object):
def numIslands(self, grid):
"""
基本思路,一行一行的扫描。
:type grid: List[List[str]]
:rtype: int
"""
print("====")
n = len(grid)
if n == 0: return 0

length = len(grid[0])
h_pre = {} # key : 是 “i,j” 字符串。value =》 [accumulator] ,一个list包含了岛的索引。
h_curr = {} # 当前行
a = [] # 保存的是一个个的list,list的长度是1,值对应岛的索引。
debug = []
accumulator = 1 # 表示岛的自增索引,每当发现一个新的岛,自增。
for i in range(0, length):
if i == 0 and grid[0][i] == '1':
h_curr['0,' + str(i)] = [accumulator]
a.append(h_curr['0,' + str(i)])
debug.append('0,' + str(i))
elif i > 0 and grid[0][i] == '1':
if grid[0][i - 1] == '1':
h_curr[str(0) + ',' + str(i)] = h_curr[str(0) + ',' + str(i - 1)]
else:
accumulator += 1
h_curr['0,' + str(i)] = [accumulator]
a.append(h_curr['0,' + str(i)])
debug.append('0,' + str(i))

for i in range(1, n):
h_pre = h_curr
h_curr = {}
for j in range(0, length):
if j == 0 and grid[i][j] == '1':

if grid[i - 1][j] == '1':
h_curr[str(i) + ',' + str(j)] = h_pre[str(i - 1) + ',' + str(j)]
else:
accumulator += 1
h_curr[str(i) + ',' + str(j)] = [accumulator]
a.append(h_curr[str(i) + ',' + str(j)])
debug.append(str(i) + ',' + str(j))

elif j > 0 and grid[i][j] == '1':

if grid[i - 1][j] == '1' and grid[i][j - 1] == '1':
pre = h_curr[str(i) + ',' + str(j - 1)]
above = h_pre[str(i - 1) + ',' + str(j)]

if pre[0] == above[0]:
# 对pre的值进行更新
h_curr[str(i) + ',' + str(j)] = pre
else :
h_curr[str(i) + ',' + str(j)] = above
v1 = pre[0]
v2 = above[0] # z这里一定要换成静态的值,否则的话当a[k]的值进行更新时,会影响到pre的值。
for k in range(0, len(a)):
if a[k][0] == v1:
a[k][0] = v2


elif grid[i][j - 1] == '1' and grid[i - 1][j] != '1':
pre = h_curr[str(i) + ',' + str(j - 1)]
h_curr[str(i) + ',' + str(j)] = pre
elif grid[i][j - 1] != '1' and grid[i - 1][j] == '1':
above = h_pre[str(i - 1) + ',' + str(j)]
h_curr[str(i) + ',' + str(j)] = above
else:
accumulator += 1
h_curr[str(i) + ',' + str(j)] = [accumulator]
a.append(h_curr[str(i) + ',' + str(j)])
debug.append(str(i) + ',' + str(j))
s = set()
for item in a:
s.add(item[0])
return len(s)


if __name__ == "__main__":
grid = ["11000",
"11000",
"00100",
"00011"] # 3
print(Solution().numIslands(grid))
grid = ["10111",
"10101",
"11101"] # 1
print(Solution().numIslands(grid))
grid = ["1111111",
"0000001",
"1111101",
"1000101",
"1010101",
"1011101",
"1111111"] # 1
print(Solution().numIslands(grid))


grid = ["10011101100000000000",
"10011001000101010010",
"00011110101100001010",
"00011001000111001001",
"00000001110000000000",
"10000101011000000101",
"00010001010101010101",
"00010100110101101110",
"00001001100001000101",
"00100100000100100010",
"10010000000100101010",
"01000101011011101100",
"11010000100000010001",
"01001110001111101000",
"00111000110001010000",
"10010100001000101011",
"10100000010001010000",
"01100011101010111100",
"01000011001010010011",
"00000011110100011000"] # 58
print(Solution().numIslands(grid))
]]>
leetcode @@ -739,6 +662,83 @@ coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167 trie
+ + 在WIN10下使用Cython + /2017/01/12/2017-1-12-cython-on-win10/ + Cython

Cython不是CPython的简称,而是一种提升Python代码执行效率的解决方案。 +据说一般达到30x。一会我们来看看是不是真的。这个技术对老鸟来说已经是好多 +年前的了。但是很多情况下python用户真的用不上,所以不知道也无妨。

+ + +

软件安装

我已经迁移到python3了,使用的是Anaconda。感谢Anaconda让我们的生活变 +的更加美好。以下软件安装的顺序不要错。

+
    +
  1. 安装Visual C++ Build Tools 2015
  2. +
  3. 安装Anaconda3
  4. +
+

跟着Cython的tutorial走

先创建一个helloworld.pyx文件。内容如下。

+
# helloworld.pyx
print("hello cython!")

def fib(n):
if n == 0 or n == 1:
return 1
else:
return fib(n - 2) + fib(n - 1)
+ + + +

再创建一个setup.py文件。内容如下

+
#setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(
ext_modules = cythonize("helloworld.pyx")
)
+ + + +

然后在命令行输入

+
D:\tmp\oo>python setup.py build_ext --inplace
+running build_ext
+building 'helloworld' extension
+C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -IC:\Anaconda3\include -IC:\Anaconda3\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\shared" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\um" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\winrt" /Tchelloworld.c /Fobuild\temp.win-amd64-3.5\Release\helloworld.obj
+helloworld.c
+C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\link.exe /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTUAC:NO /LIBPATH:C:\Anaconda3\libs /LIBPATH:C:\Anaconda3\PCbuild\amd64 "/LIBPATH:C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\LIB\amd64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.10240.0\ucrt\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.10240.0\um\x64" /EXPORT:PyInit_helloworld build\temp.win-amd64-3.5\Release\helloworld.obj /OUT:D:\tmp\oo\helloworld.cp35-win_amd64.pyd /IMPLIB:build\temp.win-amd64-3.5\Release\helloworld.cp35-win_amd64.lib
+helloworld.obj : warning LNK4197: export 'PyInit_helloworld' specified multiple times; using first specification
+   Creating library build\temp.win-amd64-3.5\Release\helloworld.cp35-win_amd64.lib and object build\temp.win-amd64-3.5\Release\helloworld.cp35-win_amd64.exp
+Generating code
+Finished generating code
+
+

(WIN10下的命令窗口终于可以使用ctrl-c和ctrl-v了。泪奔。) +可以发现多了一个.pyd文件。然后测试一下。

+
D:\tmp\oo>python
+Python 3.5.2 |Anaconda custom (64-bit)| (default, Jul  5 2016, 11:41:13) [MSC v.1900 64 bit (AMD64)] on win32
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import helloworld as h
+hello cython!
+>>>h.fib(3)
+3
+
+

真是不错啊。可是如果你换一个目录比如不在当前包含.pyd的目录下, +再想导入helloworld则是不行的。怎么回事,我们不是已经setup了么。 +原因在于--inplace这个参数,它表示只生成动态链接库。

+

与python代码进行测试比较

我们还在那个目录下创建一个test.py文件。

+
#test.py
def fib(n):
if n == 0 or n == 1:
return 1
else:
return fib(n - 2) + fib(n - 1)


def test_fib():
import timeit
n = 1000
t = timeit.timeit("fib(25)", number=n, setup="from __main__ import fib")
time = t / n
print("python fib : {:.8f}s".format(time))

def test_fib_cython():
import timeit
n = 1000
t = timeit.timeit("h.fib(25)", number=n, setup="import helloworld as h")
time = t / n
print("cython fib : {:.8f}s".format(time))

if __name__ == "__main__":
test_fib()
test_fib_cython()
+ + +

然后运行

+
D:\tmp\oo>python test.py
+python fib : 0.05887972s
+hello cython!
+cython fib : 0.01739922s
+
+

怎么回事,说好的30x呢,还轻轻松松?别着急,我们把之前的helloworld.pyx +修改一下。

+
#helloworld.pyx
cpdef int fib(int n) :
if n == 0 or n == 1 :
return 1
else :
return fib(n - 2) + fib(n - 1)
+ + +

再运行一下瞅瞅。

+
D:\tmp\oo>python test.py
+python fib : 0.05774631s
+hello cython!
+cython fib : 0.00059495s
+
+

真是不错哦,诚不我欺,100x。

+]]>
+ + python + Cython + +
[leetcode 329]Longest Increasing Path in a Matrix 原创解法 /2017/03/12/2017-3-12-leetcode-329/ @@ -906,6 +906,41 @@ D:\work\grails> gradle + + Gradle脚本实现web开发框架的一键构建 + /2017/05/24/2017-5-24-gradle-build-project/ + 问题

Java世界里构建项目用什么工具呢,ant,maven和gradle。maven非常流行, +原因无非是maven仓库和项目架构的约定。但是ant构建过程更加容易理解, +因为ant展示了所有的操作。gradle拥有基于groovy语言的DSL语言且继承了 +maven仓库的思想所以笔者认为未来是属于gradle的。

+ + +

特别是最近在搭建springmvc-jpa-mysql开发框架的时候,发现怎么也找不到一个 +archetype可以做到一键构建demo project。

+

何谓一键构建

何谓一键构建呢,有人用maven也可以很快搭建一个,通常可以把之前的 +项目复制一份修改一下,似乎没有那么麻烦。但笔者还是觉得执行几个命令, +回车一下就构建完成,更加让人舒心。如图所示,创建一个空项目dir,然后把build.gradle +放入该路径下,执行

+
gradle init
+gradle build -x test
+gradle run
+
+

就可以在浏览器里访问http://localhost:8080/greeting?name=GoodJob链接了。 +以下是过程展示。当然了因为jar包都已经下载过了,所以似乎还比较快。 +否则的话一定会联网下载jar包的。 +gradle-build-process

+

以下导入IDEA中后所示的文件结构。 +gradle-project-structure

+

build.gradle文件内容

build.gradle构建基于spring-boot的springmvc-jpa-mysql框架demo项目的脚本如下。 +该脚本一目了然,无需赘述。

+
def createFile = { String path, String content ->
File f = file(path)
if (f.exists()) {
f.delete()
}
f.withWriter("UTF-8") { writer ->
writer.write(content)
}
}

task init(type: InitBuild, dependsOn : ["bak-build-file", "create-dirs", "create-files"]) {
doLast {
String buildGradleContent = '''
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
}
}


apply plugin: 'java\'
apply plugin: 'eclipse\'
apply plugin: 'idea\'
apply plugin: 'application\'
apply plugin: 'org.springframework.boot\'

jar {
baseName = 'spring-boot-demo\'
version = '0.1.0\'
}

repositories {
mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
compile("org.springframework.boot:spring-boot-starter-web") {
exclude module: "spring-boot-starter-tomcat"
}
compile("org.springframework.boot:spring-boot-starter-jetty")
compile("org.springframework.boot:spring-boot-starter-actuator")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-devtools")

compile("mysql:mysql-connector-java")
compile("org.apache.commons:commons-lang3:3.5")
testCompile("junit:junit")
testCompile("org.springframework.boot:spring-boot-starter-test")
}

mainClassName = "hello.Application"
'''
createFile("build.gradle", buildGradleContent)
}
}

task "bak-build-file"(type: Copy) {
from "build.gradle"
into "build.gradle.bak"
}

task "create-dirs" {
description "create spring-boot-jpa-test project dirs."
doLast {
file("src/main/java/hello").mkdirs()
file("src/main/resources/templates").mkdirs()
file("src/main/webapp/WEB-INF/classes").mkdirs()
file("src/test/java/hello").mkdirs()
file("src/test/resources").mkdirs()
}
}



task "create-files" {
description "create spring-boot-jpa-test related files."
doLast {
String ApplicationContent, HelloControllerContent, UserRepositoryContent, UserContent, GreetingControllerContent
String ApplicationTestsContent, ServletInitializerContent, HelloControllerTestContent
String applicationPropertiesContent
String webXmlContent, indexJspContent, greetingHtmlContent

ApplicationContent = '''
package hello;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {

System.out.println("Let's inspect the beans provided by Spring Boot:");

String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
//System.out.println(beanName);
}

};
}

}

'''
HelloControllerContent = '''
package hello;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}

}
'''
UserRepositoryContent = '''
package hello;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

/**
* Created by YM on 5/16/2017.
*/
public interface UserRepository extends JpaRepository<User, Long> {

User findByName(String name);
User findByNameAndAge(String name, Integer age);

@Query("from User u where u.name=:name")
User findUser(@Param("name") String name);
}
'''
UserContent = '''
package hello;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

/**
* Created by YM on 5/16/2017.
*/
@Entity
public class User {

@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

public Integer getAge() {
return age;
}

public String getName() {
return name;
}

public Long getId() {
return id;
}

public void setAge(Integer age) {
this.age = age;
}

public void setId(Long id) {
this.id = id;
}

public void setName(String name) {
this.name = name;
}
}
'''
ApplicationTestsContent = '''
import hello.Application;
import hello.User;
import hello.UserRepository;
import java.util.Date;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;

/**
* Created by YM on 5/16/2017.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;

@Test
public void test() throws Exception {
// 创建10条记录
userRepository.save(new User("AAA", 10));
userRepository.save(new User("BBB", 20));
userRepository.save(new User("CCC", 30));
userRepository.save(new User("DDD", 40));
userRepository.save(new User("EEE", 50));
userRepository.save(new User("FFF", 60));
userRepository.save(new User("GGG", 70));
userRepository.save(new User("HHH", 80));
userRepository.save(new User("III", 90));
userRepository.save(new User("JJJ", 100));
// 测试findAll, 查询所有记录
Assert.assertEquals(10, userRepository.findAll().size());
// 测试findByName, 查询姓名为FFF的User
Assert.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());
// 测试findUser, 查询姓名为FFF的User
Assert.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());
// 测试findByNameAndAge, 查询姓名为FFF并且年龄为60的User
Assert.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());
// 测试删除姓名为AAA的User
userRepository.delete(userRepository.findByName("AAA"));
// 测试findAll, 查询所有记录, 验证上面的删除是否成功
Assert.assertEquals(9, userRepository.findAll().size());
}
}
'''
GreetingControllerContent = '''
package hello;


import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class GreetingController {

@RequestMapping("/greeting")
public String greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name,
Model model) {
model.addAttribute("name", name);
return "greeting";
}

}
'''
ServletInitializerContent = '''
package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

/**
* Created by YM on 5/16/2017.
*/
public class ServletInitializer extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(hello.Application.class);
}
}
'''
HelloControllerTestContent = '''
package hello;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import java.net.URL;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloControllerTest {

@LocalServerPort
private int port;

private URL base;

@Autowired
private TestRestTemplate template;

@Before
public void setUp() throws Exception {
this.base = new URL("http://localhost:" + port + "/");
}

@Test
public void getHello() throws Exception {
ResponseEntity<String> response = template.getForEntity(base.toString(),
String.class);
assertThat(response.getBody(), equalTo("Greetings from Spring Boot!"));
}
}
'''
createFile("src/main/java/hello/Application.java", ApplicationContent)
createFile("src/main/java/hello/HelloController.java", HelloControllerContent)
createFile("src/main/java/hello/UserRepository.java", UserRepositoryContent)
createFile("src/main/java/hello/User.java", UserContent)
createFile("src/main/java/hello/GreetingController.java", GreetingControllerContent)
createFile("src/main/java/hello/ServletInitializer.java", ServletInitializerContent)
createFile("src/test/java/ApplicationTests.java", ApplicationTestsContent)
createFile("src/test/java/hello/HelloControllerTest.java", HelloControllerTestContent)

applicationPropertiesContent = '''
spring.datasource.driverClassName=
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.properties.hibernate.hbm2ddl.auto=create
'''
createFile("src/main/resources/application.properties", applicationPropertiesContent)
println "Please modify src/main/resources/application.properties"

webXmlContent = '''
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
'''
indexJspContent = '''
<html>
<body>
<h2>Hello World!</h2>
</body>
</html>
'''
greetingHtmlContent = '''
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>
'''
createFile("src/main/webapp/WEB-INF/web.xml", webXmlContent)
createFile("src/main/webapp/index.jsp", indexJspContent)
createFile("src/main/resources/templates/greeting.html", greetingHtmlContent)

}
}

+]]>
+ + java + gradle + build + +
How to resize VirtualBox fixed image size and keep contents unchanged ? /2017/07/09/2017-7-9-resize-fixed-image/ @@ -961,6 +996,74 @@ Replace the swap UUID with the new one (run sudo blkid to find it) after the pri virutalbox + + 宏扩展的执行逻辑初探(一) + /2018/11/09/minilisp/ + 宏是怎么扩展的呢?

比较流行的编程语言中,Java,Python都没有定义宏的功能。c语言中的宏限制比较大, +没有Lisp中的宏强大。以下提到的宏,指的是Lisp方言中的宏。

+

宏的求值可以分为两个步骤

+
    +
  1. 扩展(expand)–编译期
  2. +
  3. 求值(eval) –运行期
  4. +
+

这看起来很简单,但是实际写macro的时候,就会有很多的困惑。这里的主要矛盾就是理解 +以下两个过程,

+
    +
  1. 在一个宏的内部调用另一个函数
  2. +
  3. 在一个宏的内部调用另一个宏
  4. +
+

不同的lisp方言有不同的实现。以下用Clojure来研究宏的用法。

+ + +

宏内部调用函数(call function inside macro)

宏扩展阶段,就会调用函数进行求值。

+
(use 'clojure.walk)
(defmacro fool [x]
(+ x 1))

(macroexpand-all '(fool 2))
+

运行结果如下,可以看到宏扩展阶段已经调用+函数进行了求值。

+
#'user/fool
3
+ +

这里的+是一个函数。

+

宏内部调用宏(call macro inside macro)

继续上面的例子,我们定义一个加法宏,然后在fool的内部调用该宏。

+
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
(plus x 1))

(macroexpand-all '(fool 2))
+ +

运行这段代码会得到以下的错误信息。重点就是理解这里为什么会报错。

+
CompilerException java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.Number, compiling:(/tmp/form-init4487875798119154898.clj:8:3) 
+ +

这里报错说是Symbol无法转换为数字。可是我们明明输入的是2。

+

事实的过程如下,

+
    +
  1. 扩展 (fool 2)
  2. +
  3. 进入fool的body
  4. +
  5. 扩展 (plus x 1) — 这里x是一个Symbol,而不是x-value
  6. +
  7. 进入宏plus的body
  8. +
  9. 对(+ x 1)求值 —到这里报错了,因为x是一个Symbol。
  10. +
+

所以,如果直接在一个宏的内部调用另一个宏,很可能编译都无法通过。因为编译期会对宏进行扩展, +但不会绑定实参。

+

那么怎么正确的使用呢,这里引入了一个概念叫emit a macro call inside a macro

+
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
(list 'plus x 1))

(macroexpand-1 '(fool 2)) ;; (plus 2 1)
(macroexpand-all '(fool 2)) ;; 3
+ +

结果如下,只扩展一次,会得到(plus 2 1), 而完全扩展会得到3

+
=> nil
#'user/plus
#'user/fool
(plus 2 1)
3
+ +

具体过程如下,

+
    +
  1. 扩展(fool 2)
  2. +
  3. 进入fool的body
  4. +
  5. 求值(list ‘plus x-value 1) – 这里x-value是2
  6. +
  7. 得到(plus x-value 1) – 这里x-value是2
  8. +
  9. 发现仍然是一个宏,继续扩展
  10. +
  11. 进入plus的body
  12. +
  13. 求值(+ x-value 1)为3 – 这里x-value是2
  14. +
+

语法引用(Syntax Quote)

正是因为需要频繁的使用list来进行宏调用,因此发明了这样的语法糖。

+
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
`(plus ~x 1)) ;; <- 这里等价于 (list 'plus x 1))

(macroexpand-1 '(fool 2))
(macroexpand-all '(fool 2))
+ +]]>
+ + clojure + lisp + macro + +
Write a simple parser for MiniLisp by using JavaCC /2017/05/19/2017-5-19-javacc-minilisp/ @@ -1019,159 +1122,8 @@ MiniLisp项目下的life.lisp和nqueens.lisp文件内容完整解析测试。 ]]> java - javacc lisp - - - - Gradle脚本实现web开发框架的一键构建 - /2017/05/24/2017-5-24-gradle-build-project/ - 问题

Java世界里构建项目用什么工具呢,ant,maven和gradle。maven非常流行, -原因无非是maven仓库和项目架构的约定。但是ant构建过程更加容易理解, -因为ant展示了所有的操作。gradle拥有基于groovy语言的DSL语言且继承了 -maven仓库的思想所以笔者认为未来是属于gradle的。

- - -

特别是最近在搭建springmvc-jpa-mysql开发框架的时候,发现怎么也找不到一个 -archetype可以做到一键构建demo project。

-

何谓一键构建

何谓一键构建呢,有人用maven也可以很快搭建一个,通常可以把之前的 -项目复制一份修改一下,似乎没有那么麻烦。但笔者还是觉得执行几个命令, -回车一下就构建完成,更加让人舒心。如图所示,创建一个空项目dir,然后把build.gradle -放入该路径下,执行

-
gradle init
-gradle build -x test
-gradle run
-
-

就可以在浏览器里访问http://localhost:8080/greeting?name=GoodJob链接了。 -以下是过程展示。当然了因为jar包都已经下载过了,所以似乎还比较快。 -否则的话一定会联网下载jar包的。 -gradle-build-process

-

以下导入IDEA中后所示的文件结构。 -gradle-project-structure

-

build.gradle文件内容

build.gradle构建基于spring-boot的springmvc-jpa-mysql框架demo项目的脚本如下。 -该脚本一目了然,无需赘述。

-
def createFile = { String path, String content ->
File f = file(path)
if (f.exists()) {
f.delete()
}
f.withWriter("UTF-8") { writer ->
writer.write(content)
}
}

task init(type: InitBuild, dependsOn : ["bak-build-file", "create-dirs", "create-files"]) {
doLast {
String buildGradleContent = '''
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
}
}


apply plugin: 'java\'
apply plugin: 'eclipse\'
apply plugin: 'idea\'
apply plugin: 'application\'
apply plugin: 'org.springframework.boot\'

jar {
baseName = 'spring-boot-demo\'
version = '0.1.0\'
}

repositories {
mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
compile("org.springframework.boot:spring-boot-starter-web") {
exclude module: "spring-boot-starter-tomcat"
}
compile("org.springframework.boot:spring-boot-starter-jetty")
compile("org.springframework.boot:spring-boot-starter-actuator")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-devtools")

compile("mysql:mysql-connector-java")
compile("org.apache.commons:commons-lang3:3.5")
testCompile("junit:junit")
testCompile("org.springframework.boot:spring-boot-starter-test")
}

mainClassName = "hello.Application"
'''
createFile("build.gradle", buildGradleContent)
}
}

task "bak-build-file"(type: Copy) {
from "build.gradle"
into "build.gradle.bak"
}

task "create-dirs" {
description "create spring-boot-jpa-test project dirs."
doLast {
file("src/main/java/hello").mkdirs()
file("src/main/resources/templates").mkdirs()
file("src/main/webapp/WEB-INF/classes").mkdirs()
file("src/test/java/hello").mkdirs()
file("src/test/resources").mkdirs()
}
}



task "create-files" {
description "create spring-boot-jpa-test related files."
doLast {
String ApplicationContent, HelloControllerContent, UserRepositoryContent, UserContent, GreetingControllerContent
String ApplicationTestsContent, ServletInitializerContent, HelloControllerTestContent
String applicationPropertiesContent
String webXmlContent, indexJspContent, greetingHtmlContent

ApplicationContent = '''
package hello;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {

System.out.println("Let's inspect the beans provided by Spring Boot:");

String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
//System.out.println(beanName);
}

};
}

}

'''
HelloControllerContent = '''
package hello;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}

}
'''
UserRepositoryContent = '''
package hello;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

/**
* Created by YM on 5/16/2017.
*/
public interface UserRepository extends JpaRepository<User, Long> {

User findByName(String name);
User findByNameAndAge(String name, Integer age);

@Query("from User u where u.name=:name")
User findUser(@Param("name") String name);
}
'''
UserContent = '''
package hello;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

/**
* Created by YM on 5/16/2017.
*/
@Entity
public class User {

@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

public Integer getAge() {
return age;
}

public String getName() {
return name;
}

public Long getId() {
return id;
}

public void setAge(Integer age) {
this.age = age;
}

public void setId(Long id) {
this.id = id;
}

public void setName(String name) {
this.name = name;
}
}
'''
ApplicationTestsContent = '''
import hello.Application;
import hello.User;
import hello.UserRepository;
import java.util.Date;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;

/**
* Created by YM on 5/16/2017.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTests {
@Autowired
private UserRepository userRepository;

@Test
public void test() throws Exception {
// 创建10条记录
userRepository.save(new User("AAA", 10));
userRepository.save(new User("BBB", 20));
userRepository.save(new User("CCC", 30));
userRepository.save(new User("DDD", 40));
userRepository.save(new User("EEE", 50));
userRepository.save(new User("FFF", 60));
userRepository.save(new User("GGG", 70));
userRepository.save(new User("HHH", 80));
userRepository.save(new User("III", 90));
userRepository.save(new User("JJJ", 100));
// 测试findAll, 查询所有记录
Assert.assertEquals(10, userRepository.findAll().size());
// 测试findByName, 查询姓名为FFF的User
Assert.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());
// 测试findUser, 查询姓名为FFF的User
Assert.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());
// 测试findByNameAndAge, 查询姓名为FFF并且年龄为60的User
Assert.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());
// 测试删除姓名为AAA的User
userRepository.delete(userRepository.findByName("AAA"));
// 测试findAll, 查询所有记录, 验证上面的删除是否成功
Assert.assertEquals(9, userRepository.findAll().size());
}
}
'''
GreetingControllerContent = '''
package hello;


import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class GreetingController {

@RequestMapping("/greeting")
public String greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name,
Model model) {
model.addAttribute("name", name);
return "greeting";
}

}
'''
ServletInitializerContent = '''
package hello;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

/**
* Created by YM on 5/16/2017.
*/
public class ServletInitializer extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(hello.Application.class);
}
}
'''
HelloControllerTestContent = '''
package hello;

import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;

import java.net.URL;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloControllerTest {

@LocalServerPort
private int port;

private URL base;

@Autowired
private TestRestTemplate template;

@Before
public void setUp() throws Exception {
this.base = new URL("http://localhost:" + port + "/");
}

@Test
public void getHello() throws Exception {
ResponseEntity<String> response = template.getForEntity(base.toString(),
String.class);
assertThat(response.getBody(), equalTo("Greetings from Spring Boot!"));
}
}
'''
createFile("src/main/java/hello/Application.java", ApplicationContent)
createFile("src/main/java/hello/HelloController.java", HelloControllerContent)
createFile("src/main/java/hello/UserRepository.java", UserRepositoryContent)
createFile("src/main/java/hello/User.java", UserContent)
createFile("src/main/java/hello/GreetingController.java", GreetingControllerContent)
createFile("src/main/java/hello/ServletInitializer.java", ServletInitializerContent)
createFile("src/test/java/ApplicationTests.java", ApplicationTestsContent)
createFile("src/test/java/hello/HelloControllerTest.java", HelloControllerTestContent)

applicationPropertiesContent = '''
spring.datasource.driverClassName=
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.properties.hibernate.hbm2ddl.auto=create
'''
createFile("src/main/resources/application.properties", applicationPropertiesContent)
println "Please modify src/main/resources/application.properties"

webXmlContent = '''
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
'''
indexJspContent = '''
<html>
<body>
<h2>Hello World!</h2>
</body>
</html>
'''
greetingHtmlContent = '''
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>
'''
createFile("src/main/webapp/WEB-INF/web.xml", webXmlContent)
createFile("src/main/webapp/index.jsp", indexJspContent)
createFile("src/main/resources/templates/greeting.html", greetingHtmlContent)

}
}

-]]>
- - java - gradle - build - -
- - 宏扩展的执行逻辑初探(一) - /2018/11/09/minilisp/ - 宏是怎么扩展的呢?

比较流行的编程语言中,Java,Python都没有定义宏的功能。c语言中的宏限制比较大, -没有Lisp中的宏强大。以下提到的宏,指的是Lisp方言中的宏。

-

宏的求值可以分为两个步骤

-
    -
  1. 扩展(expand)–编译期
  2. -
  3. 求值(eval) –运行期
  4. -
-

这看起来很简单,但是实际写macro的时候,就会有很多的困惑。这里的主要矛盾就是理解 -以下两个过程,

-
    -
  1. 在一个宏的内部调用另一个函数
  2. -
  3. 在一个宏的内部调用另一个宏
  4. -
-

不同的lisp方言有不同的实现。以下用Clojure来研究宏的用法。

- - -

宏内部调用函数(call function inside macro)

宏扩展阶段,就会调用函数进行求值。

-
(use 'clojure.walk)
(defmacro fool [x]
(+ x 1))

(macroexpand-all '(fool 2))
-

运行结果如下,可以看到宏扩展阶段已经调用+函数进行了求值。

-
#'user/fool
3
- -

这里的+是一个函数。

-

宏内部调用宏(call macro inside macro)

继续上面的例子,我们定义一个加法宏,然后在fool的内部调用该宏。

-
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
(plus x 1))

(macroexpand-all '(fool 2))
- -

运行这段代码会得到以下的错误信息。重点就是理解这里为什么会报错。

-
CompilerException java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.Number, compiling:(/tmp/form-init4487875798119154898.clj:8:3) 
- -

这里报错说是Symbol无法转换为数字。可是我们明明输入的是2。

-

事实的过程如下,

-
    -
  1. 扩展 (fool 2)
  2. -
  3. 进入fool的body
  4. -
  5. 扩展 (plus x 1) — 这里x是一个Symbol,而不是x-value
  6. -
  7. 进入宏plus的body
  8. -
  9. 对(+ x 1)求值 —到这里报错了,因为x是一个Symbol。
  10. -
-

所以,如果直接在一个宏的内部调用另一个宏,很可能编译都无法通过。因为编译期会对宏进行扩展, -但不会绑定实参。

-

那么怎么正确的使用呢,这里引入了一个概念叫emit a macro call inside a macro

-
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
(list 'plus x 1))

(macroexpand-1 '(fool 2)) ;; (plus 2 1)
(macroexpand-all '(fool 2)) ;; 3
- -

结果如下,只扩展一次,会得到(plus 2 1), 而完全扩展会得到3

-
=> nil
#'user/plus
#'user/fool
(plus 2 1)
3
- -

具体过程如下,

-
    -
  1. 扩展(fool 2)
  2. -
  3. 进入fool的body
  4. -
  5. 求值(list ‘plus x-value 1) – 这里x-value是2
  6. -
  7. 得到(plus x-value 1) – 这里x-value是2
  8. -
  9. 发现仍然是一个宏,继续扩展
  10. -
  11. 进入plus的body
  12. -
  13. 求值(+ x-value 1)为3 – 这里x-value是2
  14. -
-

语法引用(Syntax Quote)

正是因为需要频繁的使用list来进行宏调用,因此发明了这样的语法糖。

-
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
`(plus ~x 1)) ;; <- 这里等价于 (list 'plus x 1))

(macroexpand-1 '(fool 2))
(macroexpand-all '(fool 2))
- -]]>
- - clojure - lisp - macro - -
- - [leetcode 352]原创解法 - /2018/11/08/2018-11-8-leetcode-352/ - 题目概述

原题链接

-
-

Given a data stream input of non-negative integers a1, a2, …, an, …, summarize the numbers seen so far as a list of disjoint intervals.

-

For example, suppose the integers from the data stream are 1, 3, 7, 2, 6, …, then the summary will be:

-

[1, 1] -[1, 1], [3, 3] -[1, 1], [3, 3], [7, 7] -[1, 3], [7, 7] -[1, 3], [6, 7]

-
- - -

解题思路

断断续续思考了一段时间。naive的想法考虑了以下几点,

-
    -
  1. 一个list存放interval,然后遍历。
  2. -
  3. 需要查比邻的interval的值,因为需要合并。
  4. -
  5. 需要插入新的interval
  6. -
-

从这几点首先想到用二叉搜索树来存放interval,这样既能快速查到,又能保证有序, -但是实现起来的时候,发现有两个问题,

-
    -
  1. 合并节点非常复杂(其实可以做到)
  2. -
  3. 简单的二叉搜索树无法保证是平衡树(这个非常影响性能)
  4. -
-

最后考虑的结果是这样的,

-
    -
  1. 用list存放有序的interval
  2. -
  3. 使用二分查找保证快速找到最近的interval
  4. -
  5. 合并interval的时候比较简单快速。
  6. -
-

一遍通过的解法

编码的过程,比较慢,写了快一个小时,因为要仔细考虑边界值的问题, -但是一遍就通过了测试,比较开心,毕竟该题号称hard

-
# Definition for an interval.
class Interval(object):
def __init__(self, s=0, e=0):
self.start = s
self.end = e

def __repr__(self):
return "[{},{}]".format(self.start, self.end)


def nearest(arr, val):
"""
返回最接近的interval的索引
:param arr:
:param val:
:return:
"""
start = 0
end = len(arr) - 1
mid = int((start + end) / 2)
while start < end:
item = arr[mid]
if item.start - 1 <= val <= item.end + 1:
return mid
elif val < item.start - 1:
end = mid
elif val > item.end + 1:
if start == mid:
start = mid + 1
else:
start = mid

mid = int((start + end) / 2)
return mid


class SummaryRanges(object):

def __init__(self):
"""
Initialize your data structure here.
"""
self.arr = []

def addNum(self, val):
"""
:type val: int
:rtype: void
"""
if len(self.arr) == 0:
interval = Interval(val, val)
self.arr.append(interval)
else:
i = nearest(self.arr, val)
item = self.arr[i]
if item.start <= val <= item.end:
return
elif val == item.start - 1:
item.start = val
while i > 0 and self.arr[i - 1].end == item.start - 1:
self.arr[i - 1].end = item.end
self.arr.pop(i)
item = self.arr[i - 1]
i = i - 1
elif val == item.end + 1:
item.end = val
while i < len(self.arr) - 1 and self.arr[i + 1].start == item.end + 1:
item.end = self.arr[i + 1].end
self.arr.pop(i + 1)
elif val < item.start - 1:
interval = Interval(val, val)
self.arr.insert(i, interval)
elif val > item.end + 1:
interval = Interval(val, val)
self.arr.insert(i + 1, interval)

def getIntervals(self):
"""
:rtype: List[Interval]
"""
return self.arr


# Your SummaryRanges object will be instantiated and called as such:
# obj = SummaryRanges()
# obj.addNum(val)
# param_2 = obj.getIntervals()

if __name__ == '__main__':
obj = SummaryRanges()
obj.addNum(1)
print(obj.getIntervals()) # [1,1]
obj.addNum(3)
print(obj.getIntervals()) # [1,1],[3,3]
obj.addNum(7)
print(obj.getIntervals()) # [1,1],[3,3],[7,7]
obj.addNum(2)
print(obj.getIntervals()) # [1,3],[7,7]
obj.addNum(6)
print(obj.getIntervals()) # [1,3],[6,7]
- -

高效的Python List

我一开始不愿意使用list的本质原因是需要在List的中间删除和插入数据,因为 -对于一个size为n的list,list.pop(0)的时间复杂度是O(n)。 -但是Python的List似乎仍然非常高效,即使在list中间删除和插入数。我本来以为 -我的这个解法会超时呢,结果居然击败的80%的提交。

-]]>
- - leetcode - 算法 - python + javacc
@@ -1202,6 +1154,32 @@ gradle run macro + + [leetcode 337]打败了100&#37;的解法 + /2018/02/24/2018-2-24-robber-3/ + 题目概述

原题链接

+
The thief has found himself a new place for his thievery again. There is only one entrance to this area, called the "root." Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that "all houses in this place forms a binary tree". It will automatically contact the police if two directly-linked houses were broken into on the same night.
+Determine the maximum amount of money the thief can rob tonight without alerting the police.
+
+
Example 1:
3
/ \
2 2
\ \
4 1
Maximum amount of money the thief can rob = 3 + 4 + 1 = 8.
+ + + +

最快的解法

以下代码打败了100%的其它python3解法。 +带记忆体的递归,也就是动态规划。

+
class Solution:

def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
self.memo = {}
return self.dp(root)

def dp(self, root):
if root is None:
return 0
else:
if root in self.memo :
return self.memo[root]

v1 = self.dp(root.left) + self.dp(root.right)

v2 = root.val
if root.left is not None:
v2 += self.dp(root.left.left) + self.dp(root.left.right)
if root.right is not None:
v2 += self.dp(root.right.left) + self.dp(root.right.right)
self.memo[root] = max(v1, v2)
return self.memo[root]
+ + + +

截图留念

              You are Here!
Your runtime beats 100.00% of python3 submissions.
+

robber3_submit_result

+]]>
+ + leetcode + 算法 + python + +
[leetcode 341]练练手 /2018/08/18/2018-8-18-leetcode-341/ @@ -1302,24 +1280,46 @@ port = 6969 # get from tracker list - [leetcode 337]打败了100&#37;的解法 - /2018/02/24/2018-2-24-robber-3/ - 题目概述

原题链接

-
The thief has found himself a new place for his thievery again. There is only one entrance to this area, called the "root." Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that "all houses in this place forms a binary tree". It will automatically contact the police if two directly-linked houses were broken into on the same night.
-Determine the maximum amount of money the thief can rob tonight without alerting the police.
-
-
Example 1:
3
/ \
2 2
\ \
4 1
Maximum amount of money the thief can rob = 3 + 4 + 1 = 8.
- + [leetcode 352]原创解法 + /2018/11/08/2018-11-8-leetcode-352/ + 题目概述

原题链接

+
+

Given a data stream input of non-negative integers a1, a2, …, an, …, summarize the numbers seen so far as a list of disjoint intervals.

+

For example, suppose the integers from the data stream are 1, 3, 7, 2, 6, …, then the summary will be:

+

[1, 1] +[1, 1], [3, 3] +[1, 1], [3, 3], [7, 7] +[1, 3], [7, 7] +[1, 3], [6, 7]

+
-

最快的解法

以下代码打败了100%的其它python3解法。 -带记忆体的递归,也就是动态规划。

-
class Solution:

def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
self.memo = {}
return self.dp(root)

def dp(self, root):
if root is None:
return 0
else:
if root in self.memo :
return self.memo[root]

v1 = self.dp(root.left) + self.dp(root.right)

v2 = root.val
if root.left is not None:
v2 += self.dp(root.left.left) + self.dp(root.left.right)
if root.right is not None:
v2 += self.dp(root.right.left) + self.dp(root.right.right)
self.memo[root] = max(v1, v2)
return self.memo[root]
- - +

解题思路

断断续续思考了一段时间。naive的想法考虑了以下几点,

+
    +
  1. 一个list存放interval,然后遍历。
  2. +
  3. 需要查比邻的interval的值,因为需要合并。
  4. +
  5. 需要插入新的interval
  6. +
+

从这几点首先想到用二叉搜索树来存放interval,这样既能快速查到,又能保证有序, +但是实现起来的时候,发现有两个问题,

+
    +
  1. 合并节点非常复杂(其实可以做到)
  2. +
  3. 简单的二叉搜索树无法保证是平衡树(这个非常影响性能)
  4. +
+

最后考虑的结果是这样的,

+
    +
  1. 用list存放有序的interval
  2. +
  3. 使用二分查找保证快速找到最近的interval
  4. +
  5. 合并interval的时候比较简单快速。
  6. +
+

一遍通过的解法

编码的过程,比较慢,写了快一个小时,因为要仔细考虑边界值的问题, +但是一遍就通过了测试,比较开心,毕竟该题号称hard

+
# Definition for an interval.
class Interval(object):
def __init__(self, s=0, e=0):
self.start = s
self.end = e

def __repr__(self):
return "[{},{}]".format(self.start, self.end)


def nearest(arr, val):
"""
返回最接近的interval的索引
:param arr:
:param val:
:return:
"""
start = 0
end = len(arr) - 1
mid = int((start + end) / 2)
while start < end:
item = arr[mid]
if item.start - 1 <= val <= item.end + 1:
return mid
elif val < item.start - 1:
end = mid
elif val > item.end + 1:
if start == mid:
start = mid + 1
else:
start = mid

mid = int((start + end) / 2)
return mid


class SummaryRanges(object):

def __init__(self):
"""
Initialize your data structure here.
"""
self.arr = []

def addNum(self, val):
"""
:type val: int
:rtype: void
"""
if len(self.arr) == 0:
interval = Interval(val, val)
self.arr.append(interval)
else:
i = nearest(self.arr, val)
item = self.arr[i]
if item.start <= val <= item.end:
return
elif val == item.start - 1:
item.start = val
while i > 0 and self.arr[i - 1].end == item.start - 1:
self.arr[i - 1].end = item.end
self.arr.pop(i)
item = self.arr[i - 1]
i = i - 1
elif val == item.end + 1:
item.end = val
while i < len(self.arr) - 1 and self.arr[i + 1].start == item.end + 1:
item.end = self.arr[i + 1].end
self.arr.pop(i + 1)
elif val < item.start - 1:
interval = Interval(val, val)
self.arr.insert(i, interval)
elif val > item.end + 1:
interval = Interval(val, val)
self.arr.insert(i + 1, interval)

def getIntervals(self):
"""
:rtype: List[Interval]
"""
return self.arr


# Your SummaryRanges object will be instantiated and called as such:
# obj = SummaryRanges()
# obj.addNum(val)
# param_2 = obj.getIntervals()

if __name__ == '__main__':
obj = SummaryRanges()
obj.addNum(1)
print(obj.getIntervals()) # [1,1]
obj.addNum(3)
print(obj.getIntervals()) # [1,1],[3,3]
obj.addNum(7)
print(obj.getIntervals()) # [1,1],[3,3],[7,7]
obj.addNum(2)
print(obj.getIntervals()) # [1,3],[7,7]
obj.addNum(6)
print(obj.getIntervals()) # [1,3],[6,7]
-

截图留念

              You are Here!
Your runtime beats 100.00% of python3 submissions.
-

robber3_submit_result

+

高效的Python List

我一开始不愿意使用list的本质原因是需要在List的中间删除和插入数据,因为 +对于一个size为n的list,list.pop(0)的时间复杂度是O(n)。 +但是Python的List似乎仍然非常高效,即使在list中间删除和插入数。我本来以为 +我的这个解法会超时呢,结果居然击败的80%的提交。

]]>
leetcode @@ -1327,6 +1327,101 @@ Determine the maximum amount of money the thief can rob tonight without alerting python
+ + BitTorrent协议(四)之bitfield消息 + /2019/01/21/2019-1-23-bt-4/ + bitfield消息

BEP3的peer message章节说明了几种消息类型。 +其中bitfield消息理解起来是容易的,但是实际过程中却略有不同。

+ + +
+

‘bitfield’ is only ever sent as the first message. Its payload is a bitfield with each index that downloader has sent set to one and the rest set to zero. Downloaders which don’t have anything yet may skip the ‘bitfield’ message. The first byte of the bitfield corresponds to indices 0 - 7 from high bit to low bit, respectively. The next one 8-15, etc. Spare bits at the end are set to zero.

+
+

这段话的意思很简单,举例说明一下,如果一个文件有10个分片,那么如果没有下载任何数据,那么bitfield的值应该是(为了便于阅读每个字节的二进制码 以一个空格键隔开)

+
0b0000 0000 0000
+
+

如果下载了piece 0, 则值应该是

+
0b1000 0000 0000 
+
+

如果全部下载完成,则应该是

+
0b1111 1111 1100
+
+

一共是10个1,表示10个piece。

+

但是实际上呢,对于一个下载完成的文件,获取到的bitfield值是0b111111111100么?

+

我们可以自己制作一个种子来验证一下。

+

本地验证

用uTorrent软件选择本地的一个文件制作一个种子文件。如下图所示

+

file-bt

+

该文件有110个片。

+

我们读取代码如下

+
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__author__ = 'ym'
"""
Date : '2019/1/22'
Description :

"""
from ym.bt.peer_protocol import pack_handshake, unpack_handshake, sendall, recv, pack_extend
from ym.bt.parse_torrent import BDecode
from ym.bt.udp_announce import ip_me
import socket
import struct
import logging
from bitstring import BitArray, Bits

log = logging.getLogger()


def download():
try:
info_hash = 'c99c3b7a5ba31a8966e6c9a40bc4f83887a107e5'
ip, port = ip_me(), 40959
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
sock.connect((ip, port))

handshake = pack_handshake(info_hash)
rt = sendall(sock, handshake)
if rt is None:
return
print("sent={}, data={}".format(rt, handshake))
data = recv(sock, 68)

tup = unpack_handshake(data)
if tup is None:
return
print(tup)

# interested
data = struct.pack(">i B", 1, 2)
rt = sendall(sock, data)
if rt is None:
return
bitfield = None
while True:
data = recv(sock, 4)
if data is None:
return
num, = struct.unpack(">i", data)
tmp = recv(sock, 1)

if tmp is None:
return
id, = struct.unpack(">B", tmp)
print("id={}".format(id))
tmp = recv(sock, int(num) - 1)
print("recv={}".format(tmp))
if id == 5:
# bitfield
bitfield = BitArray(tmp)
print(bitfield.bin)
break
else:
pass



except Exception as e:
log.exception(e)


if __name__ == '__main__':
download()


+ +

我们看到日志如下

+
/usr/local/bin/python3.7 /Users/ym/charm/pytest/ym/bt/bt_download.py
+sent=ok, data=b'\x13BitTorrent protocol\x00\x00\x00\x00\x00\x10\x00\x01\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5ym111111111111111111'
+Handshake(fixed_num=19, bt_head=b'BitTorrent protocol', reserved=b'\x00\x00\x00\x00\x00\x10\x00\x05', info_hash=b'\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5', peer_id=b'-UM1870-\x14\xab\x9c\xa8$G\x8euX\x91\xad\x9e')
+id=20
+recv=b'\x00d1:ei0e4:ipv616:\xfe\x80\x00\x00\x00\x00\x00\x00\x04:^:D[\x0c\x9212:complete_agoi1e1:md11:upload_onlyi3e12:ut_holepunchi4e11:ut_metadatai2e6:ut_pexi1e12:ut_recommendi5e10:ut_commenti6ee13:metadata_sizei2270e1:pi40959e4:reqqi255e1:v19:\xc2\xb5Torrent Mac 1.8.76:yourip4:\xc0\xa8+Re'
+id=5
+recv=b'\xff\xff\xffg~\xaf\xbf=\x7f\xff\xeb&\xdd\x98'
+1111111111111111111111110110011101111110101011111011111100111101011111111111111111101011001001101101110110011000
+
+

这是怎么回事,为什么中间会有这么多0呢,不是已经下载完成了么?

+
111111111111111111111111011001110111111010101111101111110011110...
+
+

应该是110个1才对。而且如果再运行几遍会发现每次有不同的index的片是0。

+

have消息

原来这是uTorrent软件实现的一个策略,即使已经下载完成,也不会返回110个1的bitfield。而是用have消息来 +补全,也就是id为4的peer消息。have消息的值是一个piece的index值,表示该piece下载完成。

+

因此,我们需要根据bitfield消息和have消息来看是否下载完成。代码修改一下,每接收到一个have消息就把相应的index +置为1。

+
from ym.bt.peer_protocol import pack_handshake, unpack_handshake, sendall, recv, pack_extend
from ym.bt.parse_torrent import BDecode
from ym.bt.udp_announce import ip_me
import socket
import struct
import logging
from bitstring import BitArray, Bits

log = logging.getLogger()


def download():
try:
info_hash = 'c99c3b7a5ba31a8966e6c9a40bc4f83887a107e5'
ip, port = ip_me(), 40959
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
sock.connect((ip, port))

handshake = pack_handshake(info_hash)
rt = sendall(sock, handshake)
if rt is None:
return
print("sent={}, data={}".format(rt, handshake))
data = recv(sock, 68)

tup = unpack_handshake(data)
if tup is None:
return
print(tup)

# interested
data = struct.pack(">i B", 1, 2)
rt = sendall(sock, data)
if rt is None:
return
bitfield = None
while True:
data = recv(sock, 4)
if data is None:
return
num, = struct.unpack(">i", data)
tmp = recv(sock, 1)

if tmp is None:
return
id, = struct.unpack(">B", tmp)
print("id={}".format(id))
tmp = recv(sock, int(num) - 1)
print("recv={}".format(tmp))
if id == 5:
# bitfield
bitfield = BitArray(tmp)
print(bitfield.bin)
elif id == 4:
# have
index, = struct.unpack(">i", tmp)
print("index={}".format(index))
if bitfield.bin[index] == '0':
bitfield.set('1', index)
print(bitfield.bin)
else:
pass



except Exception as e:
log.exception(e)


if __name__ == '__main__':
download()
+ +

我们再看一下日志

+
/usr/local/bin/python3.7 /Users/ym/charm/pytest/ym/bt/bt_download.py
+sent=ok, data=b'\x13BitTorrent protocol\x00\x00\x00\x00\x00\x10\x00\x01\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5ym111111111111111111'
+Handshake(fixed_num=19, bt_head=b'BitTorrent protocol', reserved=b'\x00\x00\x00\x00\x00\x10\x00\x05', info_hash=b'\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5', peer_id=b'-UM1870-\x14\xabY\x08\r\x82=\xd1J^\x930')
+id=20
+recv=b'\x00d1:ei0e4:ipv616:\xfe\x80\x00\x00\x00\x00\x00\x00\x04:^:D[\x0c\x9212:complete_agoi1e1:md11:upload_onlyi3e12:ut_holepunchi4e11:ut_metadatai2e6:ut_pexi1e12:ut_recommendi5e10:ut_commenti6ee13:metadata_sizei2270e1:pi40959e4:reqqi255e1:v19:\xc2\xb5Torrent Mac 1.8.76:yourip4:\xc0\xa8+Re'
+id=5
+recv=b'\xfb\xdf\xbf\xee\xdf\xdf\x9d\xef\xb9\xfa\xfbv^\xf8'
+1111101111011111101111111110111011011111110111111001110111101111101110011111101011111011011101100101111011111000
+id=4
+recv=b'\x00\x00\x002'
+index=50
+1111101111011111101111111110111011011111110111111011110111101111101110011111101011111011011101100101111011111000
+
+...
+...
+...
+
+id=4
+recv=b'\x00\x00\x00b'
+index=98
+1111111111011111111111111111111111111111111111111111111111111111111111111111111111111111011111111111111111111100
+id=4
+recv=b'\x00\x00\x00X'
+index=88
+1111111111011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100
+id=4
+recv=b'\x00\x00\x00\n'
+index=10
+1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100
+already recv:b''
+timed out
+Traceback (most recent call last):
+  File "/Users/ym/charm/pytest/ym/bt/peer_protocol.py", line 40, in recv
+    tmp = sock.recv(n - len(data))
+socket.timeout: timed out
+
+Process finished with exit code 0
+
+

可以看到,确实是110个1。

+]]>
+ + bitTorrent + protocol + p2p + +
BitTorrent协议(五)之下载文件 /2019/01/25/2019-1-25-bt-5/ @@ -1870,187 +1965,45 @@ index=102, begin=0 16384 request piece=103 sent=b'\x00\x00\x00\r\x06\x00\x00\x00g\x00\x00\x00\x00\x00\x00@\x00' -piece -index=103, begin=0 -16384 -request piece=104 -sent=b'\x00\x00\x00\r\x06\x00\x00\x00h\x00\x00\x00\x00\x00\x00@\x00' -piece -index=104, begin=0 -16384 -request piece=105 -sent=b'\x00\x00\x00\r\x06\x00\x00\x00i\x00\x00\x00\x00\x00\x00@\x00' -piece -index=105, begin=0 -16384 -request piece=106 -sent=b'\x00\x00\x00\r\x06\x00\x00\x00j\x00\x00\x00\x00\x00\x00@\x00' -piece -index=106, begin=0 -16384 -request piece=107 -sent=b'\x00\x00\x00\r\x06\x00\x00\x00k\x00\x00\x00\x00\x00\x00@\x00' -piece -index=107, begin=0 -16384 -request piece=108 -sent=b'\x00\x00\x00\r\x06\x00\x00\x00l\x00\x00\x00\x00\x00\x00@\x00' -piece -index=108, begin=0 -16384 -request piece=109 -sent=b'\x00\x00\x00\r\x06\x00\x00\x00m\x00\x00\x00\x00\x00\x005v' -piece -index=109, begin=0 -13686 -completed. -0xfffffffffffffffffffffffffffc -download ok. - -Process finished with exit code 0 -
-]]>
- - bitTorrent - protocol - p2p - -
- - BitTorrent协议(四)之bitfield消息 - /2019/01/21/2019-1-23-bt-4/ - bitfield消息

BEP3的peer message章节说明了几种消息类型。 -其中bitfield消息理解起来是容易的,但是实际过程中却略有不同。

- - -
-

‘bitfield’ is only ever sent as the first message. Its payload is a bitfield with each index that downloader has sent set to one and the rest set to zero. Downloaders which don’t have anything yet may skip the ‘bitfield’ message. The first byte of the bitfield corresponds to indices 0 - 7 from high bit to low bit, respectively. The next one 8-15, etc. Spare bits at the end are set to zero.

-
-

这段话的意思很简单,举例说明一下,如果一个文件有10个分片,那么如果没有下载任何数据,那么bitfield的值应该是(为了便于阅读每个字节的二进制码 以一个空格键隔开)

-
0b0000 0000 0000
-
-

如果下载了piece 0, 则值应该是

-
0b1000 0000 0000 
-
-

如果全部下载完成,则应该是

-
0b1111 1111 1100
-
-

一共是10个1,表示10个piece。

-

但是实际上呢,对于一个下载完成的文件,获取到的bitfield值是0b111111111100么?

-

我们可以自己制作一个种子来验证一下。

-

本地验证

用uTorrent软件选择本地的一个文件制作一个种子文件。如下图所示

-

file-bt

-

该文件有110个片。

-

我们读取代码如下

-
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__author__ = 'ym'
"""
Date : '2019/1/22'
Description :

"""
from ym.bt.peer_protocol import pack_handshake, unpack_handshake, sendall, recv, pack_extend
from ym.bt.parse_torrent import BDecode
from ym.bt.udp_announce import ip_me
import socket
import struct
import logging
from bitstring import BitArray, Bits

log = logging.getLogger()


def download():
try:
info_hash = 'c99c3b7a5ba31a8966e6c9a40bc4f83887a107e5'
ip, port = ip_me(), 40959
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
sock.connect((ip, port))

handshake = pack_handshake(info_hash)
rt = sendall(sock, handshake)
if rt is None:
return
print("sent={}, data={}".format(rt, handshake))
data = recv(sock, 68)

tup = unpack_handshake(data)
if tup is None:
return
print(tup)

# interested
data = struct.pack(">i B", 1, 2)
rt = sendall(sock, data)
if rt is None:
return
bitfield = None
while True:
data = recv(sock, 4)
if data is None:
return
num, = struct.unpack(">i", data)
tmp = recv(sock, 1)

if tmp is None:
return
id, = struct.unpack(">B", tmp)
print("id={}".format(id))
tmp = recv(sock, int(num) - 1)
print("recv={}".format(tmp))
if id == 5:
# bitfield
bitfield = BitArray(tmp)
print(bitfield.bin)
break
else:
pass



except Exception as e:
log.exception(e)


if __name__ == '__main__':
download()


- -

我们看到日志如下

-
/usr/local/bin/python3.7 /Users/ym/charm/pytest/ym/bt/bt_download.py
-sent=ok, data=b'\x13BitTorrent protocol\x00\x00\x00\x00\x00\x10\x00\x01\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5ym111111111111111111'
-Handshake(fixed_num=19, bt_head=b'BitTorrent protocol', reserved=b'\x00\x00\x00\x00\x00\x10\x00\x05', info_hash=b'\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5', peer_id=b'-UM1870-\x14\xab\x9c\xa8$G\x8euX\x91\xad\x9e')
-id=20
-recv=b'\x00d1:ei0e4:ipv616:\xfe\x80\x00\x00\x00\x00\x00\x00\x04:^:D[\x0c\x9212:complete_agoi1e1:md11:upload_onlyi3e12:ut_holepunchi4e11:ut_metadatai2e6:ut_pexi1e12:ut_recommendi5e10:ut_commenti6ee13:metadata_sizei2270e1:pi40959e4:reqqi255e1:v19:\xc2\xb5Torrent Mac 1.8.76:yourip4:\xc0\xa8+Re'
-id=5
-recv=b'\xff\xff\xffg~\xaf\xbf=\x7f\xff\xeb&\xdd\x98'
-1111111111111111111111110110011101111110101011111011111100111101011111111111111111101011001001101101110110011000
-
-

这是怎么回事,为什么中间会有这么多0呢,不是已经下载完成了么?

-
111111111111111111111111011001110111111010101111101111110011110...
-
-

应该是110个1才对。而且如果再运行几遍会发现每次有不同的index的片是0。

-

have消息

原来这是uTorrent软件实现的一个策略,即使已经下载完成,也不会返回110个1的bitfield。而是用have消息来 -补全,也就是id为4的peer消息。have消息的值是一个piece的index值,表示该piece下载完成。

-

因此,我们需要根据bitfield消息和have消息来看是否下载完成。代码修改一下,每接收到一个have消息就把相应的index -置为1。

-
from ym.bt.peer_protocol import pack_handshake, unpack_handshake, sendall, recv, pack_extend
from ym.bt.parse_torrent import BDecode
from ym.bt.udp_announce import ip_me
import socket
import struct
import logging
from bitstring import BitArray, Bits

log = logging.getLogger()


def download():
try:
info_hash = 'c99c3b7a5ba31a8966e6c9a40bc4f83887a107e5'
ip, port = ip_me(), 40959
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
sock.connect((ip, port))

handshake = pack_handshake(info_hash)
rt = sendall(sock, handshake)
if rt is None:
return
print("sent={}, data={}".format(rt, handshake))
data = recv(sock, 68)

tup = unpack_handshake(data)
if tup is None:
return
print(tup)

# interested
data = struct.pack(">i B", 1, 2)
rt = sendall(sock, data)
if rt is None:
return
bitfield = None
while True:
data = recv(sock, 4)
if data is None:
return
num, = struct.unpack(">i", data)
tmp = recv(sock, 1)

if tmp is None:
return
id, = struct.unpack(">B", tmp)
print("id={}".format(id))
tmp = recv(sock, int(num) - 1)
print("recv={}".format(tmp))
if id == 5:
# bitfield
bitfield = BitArray(tmp)
print(bitfield.bin)
elif id == 4:
# have
index, = struct.unpack(">i", tmp)
print("index={}".format(index))
if bitfield.bin[index] == '0':
bitfield.set('1', index)
print(bitfield.bin)
else:
pass



except Exception as e:
log.exception(e)


if __name__ == '__main__':
download()
- -

我们再看一下日志

-
/usr/local/bin/python3.7 /Users/ym/charm/pytest/ym/bt/bt_download.py
-sent=ok, data=b'\x13BitTorrent protocol\x00\x00\x00\x00\x00\x10\x00\x01\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5ym111111111111111111'
-Handshake(fixed_num=19, bt_head=b'BitTorrent protocol', reserved=b'\x00\x00\x00\x00\x00\x10\x00\x05', info_hash=b'\xc9\x9c;z[\xa3\x1a\x89f\xe6\xc9\xa4\x0b\xc4\xf88\x87\xa1\x07\xe5', peer_id=b'-UM1870-\x14\xabY\x08\r\x82=\xd1J^\x930')
-id=20
-recv=b'\x00d1:ei0e4:ipv616:\xfe\x80\x00\x00\x00\x00\x00\x00\x04:^:D[\x0c\x9212:complete_agoi1e1:md11:upload_onlyi3e12:ut_holepunchi4e11:ut_metadatai2e6:ut_pexi1e12:ut_recommendi5e10:ut_commenti6ee13:metadata_sizei2270e1:pi40959e4:reqqi255e1:v19:\xc2\xb5Torrent Mac 1.8.76:yourip4:\xc0\xa8+Re'
-id=5
-recv=b'\xfb\xdf\xbf\xee\xdf\xdf\x9d\xef\xb9\xfa\xfbv^\xf8'
-1111101111011111101111111110111011011111110111111001110111101111101110011111101011111011011101100101111011111000
-id=4
-recv=b'\x00\x00\x002'
-index=50
-1111101111011111101111111110111011011111110111111011110111101111101110011111101011111011011101100101111011111000
-
-...
-...
-...
-
-id=4
-recv=b'\x00\x00\x00b'
-index=98
-1111111111011111111111111111111111111111111111111111111111111111111111111111111111111111011111111111111111111100
-id=4
-recv=b'\x00\x00\x00X'
-index=88
-1111111111011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100
-id=4
-recv=b'\x00\x00\x00\n'
-index=10
-1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100
-already recv:b''
-timed out
-Traceback (most recent call last):
-  File "/Users/ym/charm/pytest/ym/bt/peer_protocol.py", line 40, in recv
-    tmp = sock.recv(n - len(data))
-socket.timeout: timed out
-
-Process finished with exit code 0
-
-

可以看到,确实是110个1。

-]]>
- - bitTorrent - protocol - p2p - -
- - BitTorrent协议(一)之解析种子文件 - /2019/01/09/2019-1-9-bt-1/ - bt种子文件

bt通过种子文件分享已经是一个过去时了,2009年btChina就已经关闭了。现在一般都是 -使用磁力链接来分享文件。那么为什么种子文件分享不再流行了呢?为什么要用磁力链接呢? -磁力链接怎么实现的呢?

-

嗯这是这个系列要研究的问题。但是要研究磁力链接的实现原理,最好先从种子文件开始。

- - -

种子文件(metainfo files)的定义

官网文档 BEP3 中的metainfo files章节 -讲的很清楚。

-

简单的说就是把tracker列表和分享的文件信息编码为一个二进制的文件。

-
    -
  • tracker列表是类似如下的列表
  • -
-
http://tracker.trackerfix.com:80/announce

udp://9.rarbg.me:2710/announce

udp://9.rarbg.to:2710/announce
- -

如果想要找到下载源,就要通过tracker找到peer节点。

-
    -
  • 分享的文件信息(info)
  • -
-

包含了文件的大小,分块个数,分块的sha1散列值。

-

编码方式(bencoding)

bt文件的编码逻辑取名为bencoding。

-
-

Strings are length-prefixed base ten followed by a colon and the string. For example 4:spam corresponds to ‘spam’.

-

Integers are represented by an ‘i’ followed by the number in base 10 followed by an ‘e’. For example i3e corresponds to 3 and i-3e corresponds to -3. Integers have no size limitation. i-0e is invalid. All encodings with a leading zero, such as i03e, are invalid, other than i0e, which of course corresponds to 0.

-

Lists are encoded as an ‘l’ followed by their elements (also bencoded) followed by an ‘e’. For example l4:spam4:eggse corresponds to [‘spam’, ‘eggs’].

-

Dictionaries are encoded as a ‘d’ followed by a list of alternating keys and their corresponding values followed by an ‘e’. For example, d3:cow3:moo4:spam4:eggse corresponds to {‘cow’: ‘moo’, ‘spam’: ‘eggs’} and d4:spaml1:a1:bee corresponds to {‘spam’: [‘a’, ‘b’]}. Keys must be strings and appear in sorted order (sorted as raw strings, not alphanumerics).

-
-

翻译为BNF语法呢,就是如下

-
string : num ':' {CHAR}*

num : 0
| [1-9][0-9]+
| '-' [1-9][0-9]+

integer : 'i' num 'e'

list : 'l' {element}* 'e'

dic : 'd' {pair}* 'e'

pair : string element

element : string
| integer
| list
| dic
- -

解码

根据BNF实现的解码代码如下, 把get_content()方法中path替换为种子文件的路径,运行就可以看到。 -返回的解析结果中会有info_hash,该值是根据info的bencoding的二进制串计算的sha1值。这个值很重要 -因为之后很多协议都会用到。

-
# -*- coding: utf-8 -*-

__author__ = 'ym'
"""
Date : '2019/1/7'
Description : 解析torrent文件

"""

from datetime import datetime


class BDecode(object):
def __init__(self, arr):
self.arr = arr
self.n = len(arr)
self.i = 0

def parse(self):
return self.dic()

def peek(self):
next = None
if self.i < self.n:
next = self.arr[self.i]
return chr(next)

def next(self):
next = None
if self.i < self.n:
next = self.arr[self.i]
self.i += 1
return chr(next)

def num(self):
num_str = ""
peek = self.peek()

if peek is None:
raise Exception("malformed num.")

if peek == '-' and self.peek(1) in '123456789':
self.next() # ignore '-'
while self.peek() in "0123456789":
num_str += self.next()
if num_str[0] in '0' and len(num_str) > 1:
raise Exception("error : 0 starts with num")
return -int(num_str)
elif peek in '0123456789':
while self.peek() in "0123456789":
num_str += self.next()
if num_str[0] in '0' and len(num_str) > 1:
raise Exception("error : 0 starts with num")
return int(num_str)
else:
raise Exception("malformed num.")

def string(self, pieces=False):
length = self.num()
if self.next() != ':':
raise Exception("String must contain colon")
s = self.arr[self.i:(self.i + length)]
self.i += length

if not pieces:
return s.decode("utf8")
else:
# pieces maps to a string whose length is a multiple of 20.
# It is to be subdivided into strings of length 20,
# each of which is the SHA1 hash of the piece at the corresponding index.
result = []
for j in range(0, length, 20):
hash = s[j:j + 20]
result.append(hash.hex().lower())
return result

def integer(self, timestamp=False):
if self.next() != "i":
raise Exception("Integer must begin with i")
val = self.num()

if timestamp:
val = datetime.fromtimestamp(val).__str__()

if self.next() != "e":
raise Exception("Integer must end with e")
return val

def element(self, pieces=False, timestamp=False):
peek = self.peek()
if peek == 'i':
return self.integer(timestamp)
elif peek == "l":
return self.list()
elif peek == 'd':
return self.dic()
elif peek in "0123456789":
return self.string(pieces)
else:
raise Exception("not recognize.")

def list(self):
if self.next() != "l":
raise Exception("list must begin with l")
result = []
while self.peek() != 'e':
result.append(self.element())
self.next()
return result

def dic(self):
if self.next() != 'd':
raise Exception("dic must begin with d")
result = dict()

while self.peek() != "e":
key = self.string()
val = None
if key == "pieces":
val = self.element(pieces=True)
elif key == 'creation date':
val = self.element(timestamp=True)
else:
info_start = None
info_end = None
if key == 'info':
info_start = self.i
val = self.element()
if key == 'info':
info_end = self.i
result['info_hash'] = self.sha1(self.arr[info_start:info_end])
result[key] = val

self.next()
return result

def sha1(self, info):
import hashlib
p = hashlib.sha1()
p.update(info)
return p.hexdigest()


def get_content():
path = "/Users/ym/tmp/venom.torrent"
with open(path, "rb") as f:
return f.read()


def main():
content = get_content()
result = BDecode(content).parse()
import pprint
pprint.pprint(result)


def test_string():
s = "4:abcd".encode("utf8")
result = BDecode(s).string()
print(result)


def test_integer():
s = "i123e".encode("utf8")
print(BDecode(s).integer())


if __name__ == '__main__':
main()
+piece +index=103, begin=0 +16384 +request piece=104 +sent=b'\x00\x00\x00\r\x06\x00\x00\x00h\x00\x00\x00\x00\x00\x00@\x00' +piece +index=104, begin=0 +16384 +request piece=105 +sent=b'\x00\x00\x00\r\x06\x00\x00\x00i\x00\x00\x00\x00\x00\x00@\x00' +piece +index=105, begin=0 +16384 +request piece=106 +sent=b'\x00\x00\x00\r\x06\x00\x00\x00j\x00\x00\x00\x00\x00\x00@\x00' +piece +index=106, begin=0 +16384 +request piece=107 +sent=b'\x00\x00\x00\r\x06\x00\x00\x00k\x00\x00\x00\x00\x00\x00@\x00' +piece +index=107, begin=0 +16384 +request piece=108 +sent=b'\x00\x00\x00\r\x06\x00\x00\x00l\x00\x00\x00\x00\x00\x00@\x00' +piece +index=108, begin=0 +16384 +request piece=109 +sent=b'\x00\x00\x00\r\x06\x00\x00\x00m\x00\x00\x00\x00\x00\x005v' +piece +index=109, begin=0 +13686 +completed. +0xfffffffffffffffffffffffffffc +download ok. -

运行结果

用最近的毒液电影的种子进行解析,打印如下, -其中announce-list就是tracker列表。

-
/usr/local/bin/python3.7 /Users/ym/charm/pytest/ym/bt/parse_torrent.py
{'announce': 'http://tracker.trackerfix.com:80/announce',
'announce-list': [['http://tracker.trackerfix.com:80/announce'],
['udp://9.rarbg.me:2710/announce'],
['udp://9.rarbg.to:2710/announce']],
'comment': 'Torrent downloaded from https://rarbg.to',
'created by': 'mktorrent 1.0',
'creation date': '2018-11-28 16:04:22',
'info': {'files': [{'length': 100074, 'path': ['English.srt']},
{'length': 31, 'path': ['RARBG.txt']},
{'length': 4431023676,
'path': ['Venom.2018.720p.WEBRip.x264.AAC2.0-SHITBOX.mp4']}],
'name': 'Venom.2018.720p.WEBRip.x264.AAC2.0-SHITBOX',
'piece length': 1048576,
'pieces': ['a958677e48a77aff63574c885d7fd70915159034',
'0c713356b63454a914452cdcc76a8470fb4bc419',
...
...
...
'b926b048253bc506cb3f4e52acab9df6b93cf614',
'610f8485ab8c56f53f594e09730a34e8529e13b4']},
'info_hash': '33297ac9c46f071506711f12814a3dd8ed8b73ed'}

Process finished with exit code 0

+Process finished with exit code 0 +
]]>
bitTorrent @@ -2058,35 +2011,6 @@ Process finished with exit code 0 p2p
- - 使用delve调试K3s - /2019/11/19/debug-k3s/ - k3s是什么

K3s是什么?k8s的精简版。编译之后执行程序大小不到50M。 -可以用在物联网的边缘计算侧。如果想深入了解k8s,那么k3s是个很好的起点。 -那么如果能够断点调试k3s,就更好了。下面我们来看看怎么做。

- - -

步骤

-

运行远程调试之后,成功。 -remote-debug

-]]>
- - k3s - delve - golang - -
BitTorrent协议(六)之种子嗅探器 /2019/02/18/2019-2-18-bt-6/ @@ -2155,6 +2079,82 @@ Process finished with exit code 0

用info_hash过滤日志,可以看到metadata包含了一个files项,该项包含了多个文件。

root@ubuntu:~# cat log.txt | grep "54730eeeb5d74a58f49c6da72eb90be922556f0a"| grep "metadata_dic" 
request_metadata|info_hash=54730eeeb5d74a58f49c6da72eb90be922556f0a|metadata_dic={'files': [{'ed2k': b'\xc4"%lT\xa14s\xd86\xd7a\xb1:\x8bC', 'filehash': '788c531731917e92334c03423df5433b4d3d941d', 'length': 228076, 'path': ['C9~Fang Ping Bi Cheng Xu防屏蔽程序.rar'], 'path.utf-8': ['C9~Fang Ping Bi Cheng Xu防屏蔽程序.rar']}, {'ed2k': b'\xff,5\xb4W\x1b\xf1\xb0\x16\x13=\x0c\xc1\xc5\xb56', 'filehash': 'b760813c2b6690123eb174b5561ce6b4a0b6e000', 'length': 1966849, 'path': ['HOTAVXXX,Free Adult Movie, Fastest & Newest Porn Movie Site.jpg'], 'path.utf-8': ['HOTAVXXX,Free Adult Movie, Fastest & Newest Porn Movie Site.jpg']}, {'ed2k': b'\xfaggnH\xf8e}O\x97\x04\xbb\x86\x9e\x92\xf5', 'filehash': '9a8b43c663ecc46d3a29196cf527d8431fef842b', 'length': 180, 'path': ['HOTAVXXX~最新最快的AV影片每日更新.url'], 'path.utf-8': ['HOTAVXXX~最新最快的AV影片每日更新.url']}, {'ed2k': b'\xff,5\xb4W\x1b\xf1\xb0\x16\x13=\x0c\xc1\xc5\xb56', 'filehash': 'b760813c2b6690123eb174b5561ce6b4a0b6e000', 'length': 1966849, 'path': ['HOTAVXXX、自由な成人映画、最も速く&最新ポルノ映画サイト.jpg'], 'path.utf-8': ['HOTAVXXX、自由な成人映画、最も速く&最新ポルノ映画サイト.jpg']}, {'ed2k': b'$\x13\xfd\\\x8b\xa5\xee=\xc0\xfb\x88\xff~\x03/\x18', 'filehash': 'dddbfe3e917739e1da7afcdd8a78e1a6677b6648', 'length': 235, 'path': ['QQ愛真人視頻交友聊天室.url'], 'path.utf-8': ['QQ愛真人視頻交友聊天室.url']}, {'ed2k': b'/\xe2\xcf\x1b\xc06\xea[\xda\x93\xb9\xd0\\\xc1\x14\x98', 'filehash': '715c4e653db4bb74bb7e40073d55113f512299e9', 'length': 628211, 'path': ['SIS001全面封殺.jpg'], 'path.utf-8': ['SIS001全面封殺.jpg']}, {'ed2k': b'\xf3\x19\x893\x8b\r\xc4\r\x99|W)\xf4\xce\x00J', 'filehash': 'c4eadc74cdbf36df8a66710f62932a60b1a0949a', 'length': 267, 'path': ['[城風 - C9]~成人精品長篇區 最新http一手資源.url'], 'path.utf-8': ['[城風 - C9]~成人精品長篇區 最新http一手資源.url']}, {'ed2k': b'r\xbab\x10i\xc4\x10<f$\x10+p\x84\xd2Z', 'filehash': 'c6a1cb245511e4013c7510220ce3a6d912f8290f', 'length': 217, 'path': ['dioguitar23(第六天魔王)@草榴社區.url'], 'path.utf-8': ['dioguitar23(第六天魔王)@草榴社區.url']}, {'ed2k': b'\xe4z.\xa4\xfa\x88h\xfe\x07\xedy\xc3\x13D\xe8\x7f', 'filehash': 'dddd22e1c4027a8e5c6cec98a4ef3dd29b951682', 'length': 229, 'path': ['dioguitar23@ HD1080.org.url'], 'path.utf-8': ['dioguitar23@ HD1080.org.url']}, {'ed2k': b'\xf9\xee!X\xb3\x9d\xd9\x104\xc52\xc4\x872&a', 'filehash': '7191792635319be376c5ff3edd251e7bda81d069', 'length': 267, 'path': ['dioguitar23@AV 天空.url'], 'path.utf-8': ['dioguitar23@AV 天空.url']}, {'ed2k': b'\xba\x18f&*y&\xf5H\xe8\x1f;8x\x1eA', 'filehash': 'd774c64c18ec44e607ba6825675e841332a0d4f4', 'length': 188, 'path': ['dioguitar23@D.C.資訊交流網.url'], 'path.utf-8': ['dioguitar23@D.C.資訊交流網.url']}, {'ed2k': b'\x91\x97\x15O\rxh\x7fa\xbf^\xbf \x187K', 'filehash': 'f06835576489512e2c4a3ff58a5c4a2fed3131a3', 'length': 174, 'path': ['dioguitar23@KTzone.url'], 'path.utf-8': ['dioguitar23@KTzone.url']}, {'ed2k': b"\xf7\xf1\x9c\xa0+\x15\xc5_\xdb2u'\x81\xcb\xefU", 'filehash': '25a54a9fdab494a60fdbd206089ee75c08913f24', 'length': 235, 'path': ['dioguitar23@SexInSex! Board.url'], 'path.utf-8': ['dioguitar23@SexInSex! Board.url']}, {'ed2k': b'\x10k\x08\xafU\xcb\x05\x8b\x0f\xf9\xaf\xcb\r\xdd\xf3\xc4', 'filehash': 'a9bb602e06bb33483a1960db95bc65b535831c5d', 'length': 1086, 'path': ['dioguitar23@公仔箱論壇.url'], 'path.utf-8': ['dioguitar23@公仔箱論壇.url']}, {'ed2k': b'6-*X\x12\xf3f\xbe\xd8q\x15\xbe\x91[\xee4', 'filehash': 'd19dd08e932c7db940491e5ac259c6ad0ff225c3', 'length': 188, 'path': ['dioguitar23@痴漢俱樂部.url'], 'path.utf-8': ['dioguitar23@痴漢俱樂部.url']}, {'ed2k': b'\x89\xe1CN\xef\xd1\x83\xc2\x8c?,|;\xdb\t\x10', 'filehash': '47255a6ce6a43dc9af1de780fcffd210de793bfd', 'length': 190, 'path': ['dioguitar23@香港廣場.url'], 'path.utf-8': ['dioguitar23@香港廣場.url']}, {'ed2k': b'\xaaJh&\x1f\x88\x93\xda\x9bL\xca\xd9\x9c+V\xa9', 'filehash': '168c3faa1f01a31b31c8d5a1bb7e699333e0bfe4', 'length': 226, 'path': ['dioguitar23_Plus28 討論區.url'], 'path.utf-8': ['dioguitar23_Plus28 討論區.url']}, {'ed2k': b"\x04k`{\x0b'\xee\x80\xb4\x8e\x91=\xa92\xaf\x95", 'filehash': 'c7181e934379905222bf363317d8ae8ddfb12eb4', 'length': 220, 'path': ['dioguitar23_Touch99.url'], 'path.utf-8': ['dioguitar23_Touch99.url']}, {'ed2k': b'\xaf\xbe+\xd5\xde\x96\xdc\xe0\xa8a\x18\x87b\x8d\xf6\x0c', 'filehash': 'a92caa360c647d14ae05655c6e43e9f28d7edcf5', 'length': 208, 'path': ['dioguitar23_WK綜合論壇.url'], 'path.utf-8': ['dioguitar23_WK綜合論壇.url']}, {'ed2k': b'\x05>\x9b.\nS\xb2\x9c&L\xbc5H\x82\xcc\xcb', 'filehash': '6fa079db3cafb196163e9a6b126f60a5d4aa85b1', 'length': 156, 'path': ['dioguitar23_mimip2p.url'], 'path.utf-8': ['dioguitar23_mimip2p.url']}, {'ed2k': b'\xc3|\x1fKA\xb7\xdd\xb9\x87\xd3}\x04\x8f}\xb3m', 'filehash': '1a709fb06ff75ced80995ab8fcdefa69744d7aa0', 'length': 229, 'path': ['dioguitar23_※=色界论坛=※ (开放注册) 色界论坛.url'], 'path.utf-8': ['dioguitar23_※=色界论坛=※ (开放注册) 色界论坛.url']}, {'ed2k': b'>\x13\x98U3q\xf6u\xe6\x06\xc4\xa4\x91\xb24\xc5', 'filehash': '350cdfa69ce763f8c2fad85e1f0f4323c3fb5dfa', 'length': 208, 'path': ['dioguitar23_九九情色帝国.url'], 'path.utf-8': ['dioguitar23_九九情色帝国.url']}, {'ed2k': b'E\x0c\x8a\xed5{\x00\xe6\xdb\xfc\x9b\xb1|U%\xf5', 'filehash': '384d925cb1da6605d49014622ed9f0b455efe343', 'length': 261, 'path': ['dioguitar23_性吧春暖花开,春暖花开性吧有你.url'], 'path.utf-8': ['dioguitar23_性吧春暖花开,春暖花开性吧有你.url']}, {'ed2k': b'\xefE\xbe\xc2\xcf(\xf5\xe3\x18\x04\xdan\x8f\x08j\xcf', 'filehash': '2509953b0831793667a904ee08229da3114835cf', 'length': 214, 'path': ['dioguitar23_找樂子論壇.url'], 'path.utf-8': ['dioguitar23_找樂子論壇.url']}, {'ed2k': b"QuDc'zc]\xcb\xeb\xc0\x86-\xfe\xa5\xf4", 'filehash': '2e92574d6c46add45048b08aa15b4eae132c2cc1', 'length': 235, 'path': ['dioguitar23_無限討論區.url'], 'path.utf-8': ['dioguitar23_無限討論區.url']}, {'ed2k': b'tf\xe0\xda\x8c&\x96A\x9d\x9e~P\xe9\xd6\x83\xdb', 'filehash': '6b2a83a72df99bd7f2a197cb6fd423a41e061b70', 'length': 138, 'path': ['hav.tv-新幹線ONLINE~慶祝開站包月對折優惠~免費影片天看到爽!!.url'], 'path.utf-8': ['hav.tv-新幹線ONLINE~慶祝開站包月對折優惠~免費影片天看到爽!!.url']}, {'ed2k': b'\'\xf0\x08\xf1 {\xa9"\x13//(\xc7o\x9d`', 'filehash': '226f0d140af4689833613f0f21a95608fff324ef', 'length': 4324384768, 'path': ['hotavxxx.com_DSAM-29.ISO'], 'path.utf-8': ['hotavxxx.com_DSAM-29.ISO']}, {'ed2k': b'\x92]\xef\x0b\xa5\xee\xff\xd9\xb9\xd2\xd2\xc2J\x9e\x1a\xdc', 'filehash': 'eb8316fc354049e3bc1fb251bf6928dc55ed157a', 'length': 131523, 'path': ['hotavxxx.com_DSAM-29.jpg'], 'path.utf-8': ['hotavxxx.com_DSAM-29.jpg']}, {'ed2k': b'0\xaf\xc8Q\xc9\xa3\xfb\xacn\xf4\xde\xc1\r\x9c\xda\xbd', 'filehash': 'aa6975d6341d2a079661c066eb5244ee0fd1aaff', 'length': 2254263, 'path': ['hotavxxx.com_DSAM-29A.jpg'], 'path.utf-8': ['hotavxxx.com_DSAM-29A.jpg']}, {'ed2k': b'\xa1\xaf\x1b\xd7\x84\xd2g\x87\x99>\x82\xec\x94j\xb1U', 'filehash': '7254c5e02a0119f38e9c0e7576a1d8ac04b59f67', 'length': 214, 'path': ['❤dioguitar23❤18P2P-2013年04月2日 18p2p開放註冊3天.url'], 'path.utf-8': ['❤dioguitar23❤18P2P-2013年04月2日 18p2p開放註冊3天.url']}, {'ed2k': b'\xe0\x82]\xe7\xe2\xc3\xe0\x9bn\x81\x1d\xa4\x0fr\x9f+', 'filehash': 'cad3021392437e48d35faa57861e8ce0a89a6a0d', 'length': 233, 'path': ['アジア表動画公開板 [城風 - C9].url'], 'path.utf-8': ['アジア表動画公開板 [城風 - C9].url']}, {'ed2k': b'\x9a\xdeh5D\x1e}5`FeM\xf3\xcf\x1dt', 'filehash': '37402d4e9e9fedb26ff5c833fbc399fb5462a4af', 'length': 168, 'path': ['京色屋~即日起大降價.一片只要30元起.藍光片只要150元.url'], 'path.utf-8': ['京色屋~即日起大降價.一片只要30元起.藍光片只要150元.url']}, {'ed2k': b'W\x9a\xf4E\x1d\xb0b(\x05_v\x00\xef\xe5\xe4\xe6', 'filehash': '6d6df82810c4aa00152c3912d28bc13b6589a780', 'length': 223, 'path': ['堂本屋 TW DMM~即日起大降價.一片只要30元起.藍光片只要150元.url'], 'path.utf-8': ['堂本屋 TW DMM~即日起大降價.一片只要30元起.藍光片只要150元.url']}, {'ed2k': b'\x83\x05t=\xda\x85\x84\xbc\x1c\xb3J\xac\x04\x94\xfea', 'filehash': '52dcc77f3f2aad3ffd1cf4af73157deb1aeb8f4c', 'length': 2994, 'path': ['更多精彩.rar'], 'path.utf-8': ['更多精彩.rar']}, {'ed2k': b'<\xf8Y\xfd\x08\x99.\xc5\xec\x0e\x03V\r\xb4%\x06', 'filehash': 'ab91ce91df44509574450c875435f3c2a7bf18cf', 'length': 226, 'path': ['金花娱乐城-澳门网上真人赌博,90后性感荷官24小时在线存提款(5分钟到账).url'], 'path.utf-8': ['金花娱乐城-澳门网上真人赌博,90后性感荷官24小时在线存提款(5分钟到账).url']}, {'ed2k': b'\xfc*"\xad\xdbva\xdf\xbf\x92\x0e\x83.\x08/\x84', 'filehash': 'b60f8d54f1756a04753905be9a9dc7982f33ad14', 'length': 235, 'path': ['魔王の家-情慾視界~最新最快的成人資訊平台 魔王の家,http--bbs.hotavxxx.com.url'], 'path.utf-8': ['魔王の家-情慾視界~最新最快的成人資訊平台 魔王の家,http--bbs.hotavxxx.com.url']}], 'name': 'DSAM-29-DVD', 'name.utf-8': 'DSAM-29-DVD', 'piece length': 1048576, 'pieces': ['557c428f62f38fe8ac8d11411bc9796abb28254d', '53effb4976edd7d32c9be7a0315bfd40ff93875e', 'c38ea041059367193bc3e9cc6fac40f67cfb377f', '63d640ba1942f834e4ec0340f84d8c0703a1f992', '8b783f6a2dc8ec649ad78acc8c49f892a8bf7663', '961a8f0b0cdfe010c979061082982c3548173948', '57dd2532983cf4d30bd442281d007ce0189b6eac', 'd858de50e2a5d5dd60945ac687baa159697d88cd', 'f7a262965c87d1c4d3cc10660176c4d063d16807', '43f7e4f982c4a47d94757071e8b280798d5fbfa9', ...
+]]> + + bitTorrent + protocol + p2p + +
+ + 使用delve调试K3s + /2019/11/19/debug-k3s/ + k3s是什么

K3s是什么?k8s的精简版。编译之后执行程序大小不到50M。 +可以用在物联网的边缘计算侧。如果想深入了解k8s,那么k3s是个很好的起点。 +那么如果能够断点调试k3s,就更好了。下面我们来看看怎么做。

+ + +

步骤

+

运行远程调试之后,成功。 +remote-debug

+]]>
+ + k3s + delve + golang + +
+ + BitTorrent协议(一)之解析种子文件 + /2019/01/09/2019-1-9-bt-1/ + bt种子文件

bt通过种子文件分享已经是一个过去时了,2009年btChina就已经关闭了。现在一般都是 +使用磁力链接来分享文件。那么为什么种子文件分享不再流行了呢?为什么要用磁力链接呢? +磁力链接怎么实现的呢?

+

嗯这是这个系列要研究的问题。但是要研究磁力链接的实现原理,最好先从种子文件开始。

+ + +

种子文件(metainfo files)的定义

官网文档 BEP3 中的metainfo files章节 +讲的很清楚。

+

简单的说就是把tracker列表和分享的文件信息编码为一个二进制的文件。

+ +
http://tracker.trackerfix.com:80/announce

udp://9.rarbg.me:2710/announce

udp://9.rarbg.to:2710/announce
+ +

如果想要找到下载源,就要通过tracker找到peer节点。

+ +

包含了文件的大小,分块个数,分块的sha1散列值。

+

编码方式(bencoding)

bt文件的编码逻辑取名为bencoding。

+
+

Strings are length-prefixed base ten followed by a colon and the string. For example 4:spam corresponds to ‘spam’.

+

Integers are represented by an ‘i’ followed by the number in base 10 followed by an ‘e’. For example i3e corresponds to 3 and i-3e corresponds to -3. Integers have no size limitation. i-0e is invalid. All encodings with a leading zero, such as i03e, are invalid, other than i0e, which of course corresponds to 0.

+

Lists are encoded as an ‘l’ followed by their elements (also bencoded) followed by an ‘e’. For example l4:spam4:eggse corresponds to [‘spam’, ‘eggs’].

+

Dictionaries are encoded as a ‘d’ followed by a list of alternating keys and their corresponding values followed by an ‘e’. For example, d3:cow3:moo4:spam4:eggse corresponds to {‘cow’: ‘moo’, ‘spam’: ‘eggs’} and d4:spaml1:a1:bee corresponds to {‘spam’: [‘a’, ‘b’]}. Keys must be strings and appear in sorted order (sorted as raw strings, not alphanumerics).

+
+

翻译为eBNF语法呢,就是如下

+
string : num ':' {CHAR}*

num : 0
| [1-9][0-9]+
| '-' [1-9][0-9]+

integer : 'i' num 'e'

list : 'l' {element}* 'e'

dic : 'd' {pair}* 'e'

pair : string element

element : string
| integer
| list
| dic
+ +

解码

根据BNF实现的解码代码如下, 把get_content()方法中path替换为种子文件的路径,运行就可以看到。 +返回的解析结果中会有info_hash,该值是根据info的bencoding的二进制串计算的sha1值。这个值很重要 +因为之后很多协议都会用到。

+
# -*- coding: utf-8 -*-

__author__ = 'ym'
"""
Date : '2019/1/7'
Description : 解析torrent文件

"""

from datetime import datetime


class BDecode(object):
def __init__(self, arr):
self.arr = arr
self.n = len(arr)
self.i = 0

def parse(self):
return self.dic()

def peek(self):
next = None
if self.i < self.n:
next = self.arr[self.i]
return chr(next)

def next(self):
next = None
if self.i < self.n:
next = self.arr[self.i]
self.i += 1
return chr(next)

def num(self):
num_str = ""
peek = self.peek()

if peek is None:
raise Exception("malformed num.")

if peek == '-' and self.peek(1) in '123456789':
self.next() # ignore '-'
while self.peek() in "0123456789":
num_str += self.next()
if num_str[0] in '0' and len(num_str) > 1:
raise Exception("error : 0 starts with num")
return -int(num_str)
elif peek in '0123456789':
while self.peek() in "0123456789":
num_str += self.next()
if num_str[0] in '0' and len(num_str) > 1:
raise Exception("error : 0 starts with num")
return int(num_str)
else:
raise Exception("malformed num.")

def string(self, pieces=False):
length = self.num()
if self.next() != ':':
raise Exception("String must contain colon")
s = self.arr[self.i:(self.i + length)]
self.i += length

if not pieces:
return s.decode("utf8")
else:
# pieces maps to a string whose length is a multiple of 20.
# It is to be subdivided into strings of length 20,
# each of which is the SHA1 hash of the piece at the corresponding index.
result = []
for j in range(0, length, 20):
hash = s[j:j + 20]
result.append(hash.hex().lower())
return result

def integer(self, timestamp=False):
if self.next() != "i":
raise Exception("Integer must begin with i")
val = self.num()

if timestamp:
val = datetime.fromtimestamp(val).__str__()

if self.next() != "e":
raise Exception("Integer must end with e")
return val

def element(self, pieces=False, timestamp=False):
peek = self.peek()
if peek == 'i':
return self.integer(timestamp)
elif peek == "l":
return self.list()
elif peek == 'd':
return self.dic()
elif peek in "0123456789":
return self.string(pieces)
else:
raise Exception("not recognize.")

def list(self):
if self.next() != "l":
raise Exception("list must begin with l")
result = []
while self.peek() != 'e':
result.append(self.element())
self.next()
return result

def dic(self):
if self.next() != 'd':
raise Exception("dic must begin with d")
result = dict()

while self.peek() != "e":
key = self.string()
val = None
if key == "pieces":
val = self.element(pieces=True)
elif key == 'creation date':
val = self.element(timestamp=True)
else:
info_start = None
info_end = None
if key == 'info':
info_start = self.i
val = self.element()
if key == 'info':
info_end = self.i
result['info_hash'] = self.sha1(self.arr[info_start:info_end])
result[key] = val

self.next()
return result

def sha1(self, info):
import hashlib
p = hashlib.sha1()
p.update(info)
return p.hexdigest()


def get_content():
path = "/Users/ym/tmp/venom.torrent"
with open(path, "rb") as f:
return f.read()


def main():
content = get_content()
result = BDecode(content).parse()
import pprint
pprint.pprint(result)


def test_string():
s = "4:abcd".encode("utf8")
result = BDecode(s).string()
print(result)


def test_integer():
s = "i123e".encode("utf8")
print(BDecode(s).integer())


if __name__ == '__main__':
main()
+ +

运行结果

用最近的毒液电影的种子进行解析,打印如下, +其中announce-list就是tracker列表。

+
/usr/local/bin/python3.7 /Users/ym/charm/pytest/ym/bt/parse_torrent.py
{'announce': 'http://tracker.trackerfix.com:80/announce',
'announce-list': [['http://tracker.trackerfix.com:80/announce'],
['udp://9.rarbg.me:2710/announce'],
['udp://9.rarbg.to:2710/announce']],
'comment': 'Torrent downloaded from https://rarbg.to',
'created by': 'mktorrent 1.0',
'creation date': '2018-11-28 16:04:22',
'info': {'files': [{'length': 100074, 'path': ['English.srt']},
{'length': 31, 'path': ['RARBG.txt']},
{'length': 4431023676,
'path': ['Venom.2018.720p.WEBRip.x264.AAC2.0-SHITBOX.mp4']}],
'name': 'Venom.2018.720p.WEBRip.x264.AAC2.0-SHITBOX',
'piece length': 1048576,
'pieces': ['a958677e48a77aff63574c885d7fd70915159034',
'0c713356b63454a914452cdcc76a8470fb4bc419',
...
...
...
'b926b048253bc506cb3f4e52acab9df6b93cf614',
'610f8485ab8c56f53f594e09730a34e8529e13b4']},
'info_hash': '33297ac9c46f071506711f12814a3dd8ed8b73ed'}

Process finished with exit code 0

]]>
bitTorrent @@ -2269,58 +2269,6 @@ csn为UTF-8

docker
- - 在mac下跑一个Ingress的例子 - /2020/07/06/run-ingress-example-on-mac/ - Ingress是什么

在Kubernetes中,Ingress是一个对象,该对象允许从Kubernetes集群外部访问Kubernetes服务。 您可以 -通过创建一组规则来配置访问权限,这些规则定义了哪些入站连接可以访问哪些服务。

- - -

这里有篇文章很好的说明了Ingress,并给出了例子 —— Kubernetes Ingress with Nginx Example

-

但是要想跑一下,首先要有一个k8s集群。下面首先在mac上安装一个k8s集群。

-

在mac上安装k8s集群

我的mbp的配置是8G内存。

-
    -
  1. 下载Docker.dmg
  2. -
  3. 从这里下载搭建k8s所需要的镜像——https://github.com/gotok8s/k8s-docker-desktop-for-mac -使用的方法就是如下,我用的docker desktop的kubernates的版本是1.19.3所以,相应的要把文件images中的版本号进行修改,以匹配。
    $ git clone https://github.com/gotok8s/k8s-docker-desktop-for-mac.git
    $ cd k8s-docker-desktop-for-mac
    $ cat images
    k8s.gcr.io/kube-proxy:v1.19.3=gotok8s/kube-proxy:v1.19.3
    k8s.gcr.io/kube-controller-manager:v1.19.3=gotok8s/kube-controller-manager:v1.19.3
    k8s.gcr.io/kube-scheduler:v1.19.3=gotok8s/kube-scheduler:v1.19.3
    k8s.gcr.io/kube-apiserver:v1.19.3=gotok8s/kube-apiserver:v1.19.3
    k8s.gcr.io/coredns:1.7.0=gotok8s/coredns:1.7.0
    k8s.gcr.io/pause:3.2=gotok8s/pause:3.2
    k8s.gcr.io/etcd:3.4.13-0=gotok8s/etcd:3.4.13-0
    $ ./load_images.sh
  4. -
  5. 启动docker desktop,并在dashboard中修改docker使用的资源大小。我分配了5G, -交换内存设置为4G,cpu数量为3
  6. -
  7. 勾选Enable KubernetesShow system containers (advanced),启动,然后耐心等待。
  8. -
-

需要等待的时间比较长,因为还会下载多个镜像,如docker/desktop-kubernetes等等。

-

安装ingress-nginx-controller

我们从ingress-nginx的官网可以看到不同的安装方式,对于 Docker for Mac,一行命令搞定

-
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud/deploy.yaml
- -

这个需要下载镜像quay.io/kubernetes-ingress-controller/nginx-ingress-controller,所以也需要时间。直到看到如下pods

-
ingress-nginx   ingress-nginx-admission-create-frllm       0/1     Completed   0          56m
ingress-nginx ingress-nginx-admission-patch-gbwpd 0/1 Completed 0 56m
ingress-nginx ingress-nginx-controller-8f7b9d799-c67xw 1/1 Running 2 56m
- -

跑一个Ingress的例子

Kubernetes Ingress with Nginx Example中的yaml文件需要外网才能看到。

-

我在此列出三个yaml文件

-

apple.yaml:

-
kind: Pod
apiVersion: v1
metadata:
name: apple-app
labels:
app: apple
spec:
containers:
- name: apple-app
image: hashicorp/http-echo
args:
- "-text=apple"

---

kind: Service
apiVersion: v1
metadata:
name: apple-service
spec:
selector:
app: apple
ports:
- port: 5678 # Default port for image
- -

banana.yaml :

-
kind: Pod
apiVersion: v1
metadata:
name: banana-app
labels:
app: banana
spec:
containers:
- name: banana-app
image: hashicorp/http-echo
args:
- "-text=banana"

---

kind: Service
apiVersion: v1
metadata:
name: banana-service
spec:
selector:
app: banana
ports:
- port: 5678 # Default port for image
- -

ingress.yaml:

-
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: example-ingress
annotations:
ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /apple
backend:
serviceName: apple-service
servicePort: 5678
- path: /banana
backend:
serviceName: banana-service
servicePort: 5678
- -

执行命令,等待一下。因为要下载一个镜像“jettech/kube-webhook-certgen”

-
$ kubectl apply -f apple.yaml
$ kubectl apply -f banana.yaml
$ kubectl create -f ingress.yaml
- -

查看ingress

-
$ kubectl describe ingress -n default example-ingress
Name: example-ingress
Namespace: default
Address: localhost
Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
Host Path Backends
---- ---- --------
*
/apple apple-service:5678 (10.1.0.30:5678)
/banana banana-service:5678 (10.1.0.28:5678)
Annotations: ingress.kubernetes.io/rewrite-target: /
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal CREATE 45m nginx-ingress-controller Ingress default/example-ingress
Normal UPDATE 44m nginx-ingress-controller Ingress default/example-ingress
Normal CREATE 14m nginx-ingress-controller Ingress default/example-ingress
- -

最后测试

-

$ curl -kL http://localhost/apple
apple

$ curl -kL http://localhost/banana
banana

$ curl -kL http://localhost/notfound
default backend - 404

-]]>
- - docker - k8s - ingress - mac - -
基于Spring Integration实现文件写入 /2020/08/14/write-file-with-spring-integration/ @@ -2397,6 +2345,58 @@ remote path为/home/keyvalue/ym/operation,local path为/Use remote + + 在mac下跑一个Ingress的例子 + /2020/07/06/run-ingress-example-on-mac/ + Ingress是什么

在Kubernetes中,Ingress是一个对象,该对象允许从Kubernetes集群外部访问Kubernetes服务。 您可以 +通过创建一组规则来配置访问权限,这些规则定义了哪些入站连接可以访问哪些服务。

+ + +

这里有篇文章很好的说明了Ingress,并给出了例子 —— Kubernetes Ingress with Nginx Example

+

但是要想跑一下,首先要有一个k8s集群。下面首先在mac上安装一个k8s集群。

+

在mac上安装k8s集群

我的mbp的配置是8G内存。

+
    +
  1. 下载Docker.dmg
  2. +
  3. 从这里下载搭建k8s所需要的镜像——https://github.com/gotok8s/k8s-docker-desktop-for-mac +使用的方法就是如下,我用的docker desktop的kubernates的版本是1.19.3所以,相应的要把文件images中的版本号进行修改,以匹配。
    $ git clone https://github.com/gotok8s/k8s-docker-desktop-for-mac.git
    $ cd k8s-docker-desktop-for-mac
    $ cat images
    k8s.gcr.io/kube-proxy:v1.19.3=gotok8s/kube-proxy:v1.19.3
    k8s.gcr.io/kube-controller-manager:v1.19.3=gotok8s/kube-controller-manager:v1.19.3
    k8s.gcr.io/kube-scheduler:v1.19.3=gotok8s/kube-scheduler:v1.19.3
    k8s.gcr.io/kube-apiserver:v1.19.3=gotok8s/kube-apiserver:v1.19.3
    k8s.gcr.io/coredns:1.7.0=gotok8s/coredns:1.7.0
    k8s.gcr.io/pause:3.2=gotok8s/pause:3.2
    k8s.gcr.io/etcd:3.4.13-0=gotok8s/etcd:3.4.13-0
    $ ./load_images.sh
  4. +
  5. 启动docker desktop,并在dashboard中修改docker使用的资源大小。我分配了5G, +交换内存设置为4G,cpu数量为3
  6. +
  7. 勾选Enable KubernetesShow system containers (advanced),启动,然后耐心等待。
  8. +
+

需要等待的时间比较长,因为还会下载多个镜像,如docker/desktop-kubernetes等等。

+

安装ingress-nginx-controller

我们从ingress-nginx的官网可以看到不同的安装方式,对于 Docker for Mac,一行命令搞定

+
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud/deploy.yaml
+ +

这个需要下载镜像quay.io/kubernetes-ingress-controller/nginx-ingress-controller,所以也需要时间。直到看到如下pods

+
ingress-nginx   ingress-nginx-admission-create-frllm       0/1     Completed   0          56m
ingress-nginx ingress-nginx-admission-patch-gbwpd 0/1 Completed 0 56m
ingress-nginx ingress-nginx-controller-8f7b9d799-c67xw 1/1 Running 2 56m
+ +

跑一个Ingress的例子

Kubernetes Ingress with Nginx Example中的yaml文件需要外网才能看到。

+

我在此列出三个yaml文件

+

apple.yaml:

+
kind: Pod
apiVersion: v1
metadata:
name: apple-app
labels:
app: apple
spec:
containers:
- name: apple-app
image: hashicorp/http-echo
args:
- "-text=apple"

---

kind: Service
apiVersion: v1
metadata:
name: apple-service
spec:
selector:
app: apple
ports:
- port: 5678 # Default port for image
+ +

banana.yaml :

+
kind: Pod
apiVersion: v1
metadata:
name: banana-app
labels:
app: banana
spec:
containers:
- name: banana-app
image: hashicorp/http-echo
args:
- "-text=banana"

---

kind: Service
apiVersion: v1
metadata:
name: banana-service
spec:
selector:
app: banana
ports:
- port: 5678 # Default port for image
+ +

ingress.yaml:

+
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: example-ingress
annotations:
ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /apple
backend:
serviceName: apple-service
servicePort: 5678
- path: /banana
backend:
serviceName: banana-service
servicePort: 5678
+ +

执行命令,等待一下。因为要下载一个镜像“jettech/kube-webhook-certgen”

+
$ kubectl apply -f apple.yaml
$ kubectl apply -f banana.yaml
$ kubectl create -f ingress.yaml
+ +

查看ingress

+
$ kubectl describe ingress -n default example-ingress
Name: example-ingress
Namespace: default
Address: localhost
Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>)
Rules:
Host Path Backends
---- ---- --------
*
/apple apple-service:5678 (10.1.0.30:5678)
/banana banana-service:5678 (10.1.0.28:5678)
Annotations: ingress.kubernetes.io/rewrite-target: /
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal CREATE 45m nginx-ingress-controller Ingress default/example-ingress
Normal UPDATE 44m nginx-ingress-controller Ingress default/example-ingress
Normal CREATE 14m nginx-ingress-controller Ingress default/example-ingress
+ +

最后测试

+

$ curl -kL http://localhost/apple
apple

$ curl -kL http://localhost/banana
banana

$ curl -kL http://localhost/notfound
default backend - 404

+]]>
+ + docker + k8s + ingress + mac + +
自建根证书,中间证书和Server端X.509证书并搭建nginx验证Server端证书有效性 /2020/10/09/x509-ca/ @@ -2769,34 +2769,78 @@ cmd/dist - 通过ChatGPT实现Rust语言的八皇后算法 - /2023/01/12/2023-1-12-8-queens-chatgpt/ - ChatGPT是什么?

ChatGPT是基于OpenAI项目的聊天机器人。

-

OpenAI项目由特斯拉创建,而ChatGPT的母公司当前估值$29bln,据说微软准备购买$10bln。

+ 使用Iced的过程中理解Rust的关联类型 + /2023/02/28/2023-2-28-associated-type/ + 关联类型(Associated type)

关联类型是Rust为了解决类型参数(type parameter)之间的依赖关系而引入的。清楚的解释引入动机的文章就是 +RFC095 +。

-

目前ChatGPT不对中国开放,想要注册不光需要VPN,还需要手机短信验证。不过注册完成后登录不需要短信验证了。

-

具体怎么注册登录ChatGPT可以参考其它文章。

-

八皇后问题

八皇后问题(英文:Eight queens),是由国际象棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。 -问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击, -即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 -高斯认为有76种方案。

-

2023-1-12-8queens-problem

-

通过ChatGPT实现Rust语言的八皇后算法

直接问ChatGPT, “How to write eight queens algorithm in rust?” -如下图所示,直接就把rust代码实现显示出来,并进行了说明。

-

2023-1-12-8queens-chatgpt

-

把rust代码copy出来,增加测试代码test_eight_queen()

-
fn eight_queens() {
let mut board = vec![-1; 8];

solve(&mut board, 0);
}

fn solve(board: &mut Vec<i32>, target_row: i32) {
if target_row == 8 {
print_board(board);
return;
}

for column in 0..8 {
if is_safe(board, target_row, column) {
board[target_row as usize] = column as i32;
solve(board, target_row + 1);
}
}
}

fn is_safe(board: &Vec<i32>, row: i32, column: i32) -> bool {
for r in 0..row {
let c = board[r as usize];

if c == column ||
(row - r).abs() == (column - c).abs() {
return false;
}
}

return true;
}

fn print_board(board: &Vec<i32>) {
for r in 0..8 {
for c in 0..8 {
if board[r as usize] == c as i32 {
print!("Q ");
} else {
print!(". ");
}
}

println!("");
}

println!("");
}

#[cfg(test)]
mod tests {
#[test]
fn test_eight_queen() {
super::eight_queens();
}
}
+

同时,这篇文章详细说明了如何引用关联类型

+

一个实际的例子

我们在使用Iced(一个跨平台的GUI库)开发一个自定义样式的button的时候, +遇到了关联类型。

+

以下代码创建一个button对象,宽度80,高50,样式怎么输入呢?

+
button(
text(input).size(24)
.vertical_alignment(Vertical::Center)
.horizontal_alignment(Horizontal::Center)
)
.padding(10)
.width(80)
.height(50)
.style( ?? ) /// <-- 这里该写什么呢?
.on_press(msg)
+

看看这个style方法的签名。

+
iced_native::widget::button 
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
Renderer: crate::Renderer,
Renderer::Theme: StyleSheet,
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self
-------------------------------------------------------------
Sets the style variant of this Button.
+

style方法是Button结构体的一个关联方法。

+

Button结构体是一个组件,它有两个类型参数,Message和Renderer。 +其中Renderer类型参数的Bound在where中明确

+
    +
  • Renderer: crate::Renderer 需要实现crate::Renderer trait.
  • +
  • Renderer::Theme: StyleSheet 并且该crate::Renderer trait的Theme关联类型需要实现 StyleSheet trait.
  • +
+

crate::Renderer trait如下,有一个Theme关联类型

+
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized {
/// The supported theme of the [`Renderer`].
type Theme;
...
...
-

运行

直接运行,没有错误。

-
hello $ cargo test --bin hello eight_queens::tests::test_eight_queen -- --show-output
Finished test [unoptimized + debuginfo] target(s) in 0.11s
Running unittests src/main.rs (target/debug/deps/hello-257baa7e5ed28221)

running 1 test
test eight_queens::tests::test_eight_queen ... ok

successes:

---- eight_queens::tests::test_eight_queen stdout ----
Q . . . . . . .
. . . . Q . . .
. . . . . . . Q
. . . . . Q . .
. . Q . . . . .
. . . . . . Q .
. Q . . . . . .
. . . Q . . . .

Q . . . . . . .
. . . . . Q . .
. . . . . . . Q
. . Q . . . . .
. . . . . . Q .
. . . Q . . . .
. Q . . . . . .
. . . . Q . . .

Q . . . . . . .
. . . . . . Q .
. . . Q . . . .
. . . . . Q . .
. . . . . . . Q
. Q . . . . . .
. . . . Q . . .
. . Q . . . . .

Q . . . . . . .
. . . . . . Q .
. . . . Q . . .
. . . . . . . Q
. Q . . . . . .
. . . Q . . . .
. . . . . Q . .
. . Q . . . . .

. Q . . . . . .
. . . Q . . . .
. . . . . Q . .
. . . . . . . Q
. . Q . . . . .
Q . . . . . . .
. . . . . . Q .
. . . . Q . . .

. Q . . . . . .
. . . . Q . . .
. . . . . . Q .
Q . . . . . . .
. . Q . . . . .
. . . . . . . Q
. . . . . Q . .
. . . Q . . . .

. Q . . . . . .
. . . . Q . . .
. . . . . . Q .
. . . Q . . . .
Q . . . . . . .
. . . . . . . Q
. . . . . Q . .
. . Q . . . . .

... ignore
...
...

successes:
eight_queens::tests::test_eight_queen

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 22 filtered out; finished in 0.00s
+

还是回到这个style方法pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self

+

这个方法的入参style的类型是<Renderer::Theme as StyleSheet>::Style,这个PATH的前缀是 +<Renderer::Theme as StyleSheet> 表示实现了Renderer trait的类型的Theme关联类型 +需要实现StyleSheet trait。

+

因此<Renderer::Theme as StyleSheet>::Style完整的含义就是

+
    +
  1. 某个渲染类型 SomeRenderer 实现了crate::Renderer trait.
  2. +
  3. 某个样式板类型 SomeThemeType 实现了button::StyleSheet
  4. +
  5. 且 SomeRenderer::Theme = SomeThemeType
  6. +
  7. <Renderer::Theme as StyleSheet>::Style就是 SomeThemeType::Style = < ?? >.
  8. +
+

因此style方法的入参的实际类型就是SomeStyleSheet::Style的实际类型。所以我们要找到SomeRenderer和SomeThemeType。

+
    +
  1. 先看crate::Renderer trait的实现类型有那些,可以看到Renderer<B, T>
    impl<B, T> iced_native::Renderer for Renderer<B, T>
    where
    B: Backend,
    {
    type Theme = T;
    ...
    ...
  2. +
+

这个结构体如下,

+
/// A backend-agnostic renderer that supports all the built-in widgets.
#[derive(Debug)]
pub struct Renderer<B: Backend, Theme> {
backend: B,
primitives: Vec<Primitive>,
theme: PhantomData<Theme>,
}
+

这个似乎就是我们要找的 SomeRenderer。但是这个Renderer<B, T>有两个类型参数,第二个就是Theme类型参数。还是没有看到 +这个类型参数具体的类型是什么。继续看哪里使用了这个带有类型参数的结构体Renderer<B, T>

+
/// A [`wgpu`] graphics renderer for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
pub type Renderer<Theme = iced_native::Theme> =
iced_graphics::Renderer<Backend, Theme>;
+

这里看到声明了一个新的类型iced_wgpu::Renderer,并且指定了Theme = iced_style::theme::Theme。 +这个新的类型在哪里使用了呢?

+
impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
type Settings = Settings;
type Renderer = Renderer<Theme>;
type Surface = wgpu::Surface;

fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Self::Settings,
compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> {
let compositor = futures::executor::block_on(Self::request(
settings,
compatible_window,
))
.ok_or(Error::GraphicsAdapterNotFound)?;

let backend = compositor.create_backend();

Ok((compositor, Renderer::new(backend))) /// <--- 这里的Renderer就是指定了Theme的Renderer<B,T>
}
+

因此这个iced_wgpu::Renderer就是我们要找的SomeRenderer,而SomeRenderer::Theme就是 +iced_style::theme::Theme就是我们要找的SomeThemeType。

+
    +
  1. 接着看iced_style::theme::Theme类型如何实现button::StyleSheet trait +button_stylesheet_for_theme +从这里可以看到 SomeThemeType::Style = iced_style::theme::Button +因此style方法的入参是iced_style::theme::Button, 是一个枚举类型。
  2. +
+
/// The style of a button.
#[derive(Default)]
pub enum Button {
/// The primary style.
#[default]
Primary,
/// The secondary style.
Secondary,
/// The positive style.
Positive,
/// The destructive style.
Destructive,
/// The text style.
///
/// Useful for links!
Text,
/// A custom style.
Custom(Box<dyn button::StyleSheet<Style = Theme>>),
}
+

从定义可以看出,如果我们要实现自定义的样式就需要使用Button::Custom(Box::new(??)).

+

button::StyleSheet<Style = Theme>类型意思是某类型实现了button::StyleSheet trait, +并且关联类型Style = iced_style::theme::Theme.

+

所以我们可以写一个自定义的button样式了。

+
struct ButtonStyle;

impl StyleSheet for ButtonStyle {
type Style = iced_style::theme::Theme; ///<-- 这里可以简写为 type Style = Theme;

fn active(&self, style: &Self::Style) -> Appearance {
Appearance {
shadow_offset: Default::default(),
background: Some(Background::Color(Color::from_rgb8(204, 204, 204))),
border_radius: 5.0,
border_width: 0.0,
border_color: Default::default(),
text_color: Color::from_rgb8(0, 0, 0),
}
}
}
+

style方法的入参就是Button::Custom(Box::new(ButtonStyle{}))

+

最后style方法的使用如下

+
button(
text(input).size(24)
.vertical_alignment(Vertical::Center)
.horizontal_alignment(Horizontal::Center)
)
.padding(10)
.width(80)
.height(50)
.style( Button::Custom(Box::new(ButtonStyle{})) ) /// 这里该写什么呢?
.on_press(msg)
-

最后

ChatGPT给人非常惊艳的感觉。我考虑以后经常使用它,如果它一直免费的话。

+

关于Iced

Iced使用的Elm模型非常容易使用。我用它实现一个简单的计算器。 +calculator

+

总结

关联类型是Rust非常重要的特性。如果不深入理解的话,编译的错误信息都看不明白。

]]>
- 算法 - ChatGPT rust + iced
@@ -2905,124 +2949,34 @@ POST_BUILD 事件的触发是PicPicker链接完成,而不是复制文件到Pic - 使用Iced的过程中理解Rust的关联类型 - /2023/02/28/2023-2-28-associated-type/ - 关联类型(Associated type)

关联类型是Rust为了解决类型参数(type parameter)之间的依赖关系而引入的。清楚的解释引入动机的文章就是 -RFC095 -。

+ 通过ChatGPT实现Rust语言的八皇后算法 + /2023/01/12/2023-1-12-8-queens-chatgpt/ + ChatGPT是什么?

ChatGPT是基于OpenAI项目的聊天机器人。

+

OpenAI项目由特斯拉创建,而ChatGPT的母公司当前估值$29bln,据说微软准备购买$10bln。

-

同时,这篇文章详细说明了如何引用关联类型

-

一个实际的例子

我们在使用Iced(一个跨平台的GUI库)开发一个自定义样式的button的时候, -遇到了关联类型。

-

以下代码创建一个button对象,宽度80,高50,样式怎么输入呢?

-
button(
text(input).size(24)
.vertical_alignment(Vertical::Center)
.horizontal_alignment(Horizontal::Center)
)
.padding(10)
.width(80)
.height(50)
.style( ?? ) /// <-- 这里该写什么呢?
.on_press(msg)
-

看看这个style方法的签名。

-
iced_native::widget::button 
impl<'a, Message, Renderer> Button<'a, Message, Renderer>
where
Renderer: crate::Renderer,
Renderer::Theme: StyleSheet,
pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self
-------------------------------------------------------------
Sets the style variant of this Button.
-

style方法是Button结构体的一个关联方法。

-

Button结构体是一个组件,它有两个类型参数,Message和Renderer。 -其中Renderer类型参数的Bound在where中明确

-
    -
  • Renderer: crate::Renderer 需要实现crate::Renderer trait.
  • -
  • Renderer::Theme: StyleSheet 并且该crate::Renderer trait的Theme关联类型需要实现 StyleSheet trait.
  • -
-

crate::Renderer trait如下,有一个Theme关联类型

-
/// A component that can be used by widgets to draw themselves on a screen.
pub trait Renderer: Sized {
/// The supported theme of the [`Renderer`].
type Theme;
...
...
+

目前ChatGPT不对中国开放,想要注册不光需要VPN,还需要手机短信验证。不过注册完成后登录不需要短信验证了。

+

具体怎么注册登录ChatGPT可以参考其它文章。

+

八皇后问题

八皇后问题(英文:Eight queens),是由国际象棋棋手马克斯·贝瑟尔于1848年提出的问题,是回溯算法的典型案例。 +问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击, +即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 +高斯认为有76种方案。

+

2023-1-12-8queens-problem

+

通过ChatGPT实现Rust语言的八皇后算法

直接问ChatGPT, “How to write eight queens algorithm in rust?” +如下图所示,直接就把rust代码实现显示出来,并进行了说明。

+

2023-1-12-8queens-chatgpt

+

把rust代码copy出来,增加测试代码test_eight_queen()

+
fn eight_queens() {
let mut board = vec![-1; 8];

solve(&mut board, 0);
}

fn solve(board: &mut Vec<i32>, target_row: i32) {
if target_row == 8 {
print_board(board);
return;
}

for column in 0..8 {
if is_safe(board, target_row, column) {
board[target_row as usize] = column as i32;
solve(board, target_row + 1);
}
}
}

fn is_safe(board: &Vec<i32>, row: i32, column: i32) -> bool {
for r in 0..row {
let c = board[r as usize];

if c == column ||
(row - r).abs() == (column - c).abs() {
return false;
}
}

return true;
}

fn print_board(board: &Vec<i32>) {
for r in 0..8 {
for c in 0..8 {
if board[r as usize] == c as i32 {
print!("Q ");
} else {
print!(". ");
}
}

println!("");
}

println!("");
}

#[cfg(test)]
mod tests {
#[test]
fn test_eight_queen() {
super::eight_queens();
}
}
-

还是回到这个style方法pub fn style(mut self, style: <Renderer::Theme as StyleSheet>::Style) -> Self

-

这个方法的入参style的类型是<Renderer::Theme as StyleSheet>::Style,这个PATH的前缀是 -<Renderer::Theme as StyleSheet> 表示实现了Renderer trait的类型的Theme关联类型 -需要实现StyleSheet trait。

-

因此<Renderer::Theme as StyleSheet>::Style完整的含义就是

-
    -
  1. 某个渲染类型 SomeRenderer 实现了crate::Renderer trait.
  2. -
  3. 某个样式板类型 SomeThemeType 实现了button::StyleSheet
  4. -
  5. 且 SomeRenderer::Theme = SomeThemeType
  6. -
  7. <Renderer::Theme as StyleSheet>::Style就是 SomeThemeType::Style = < ?? >.
  8. -
-

因此style方法的入参的实际类型就是SomeStyleSheet::Style的实际类型。所以我们要找到SomeRenderer和SomeThemeType。

-
    -
  1. 先看crate::Renderer trait的实现类型有那些,可以看到Renderer<B, T>
    impl<B, T> iced_native::Renderer for Renderer<B, T>
    where
    B: Backend,
    {
    type Theme = T;
    ...
    ...
  2. -
-

这个结构体如下,

-
/// A backend-agnostic renderer that supports all the built-in widgets.
#[derive(Debug)]
pub struct Renderer<B: Backend, Theme> {
backend: B,
primitives: Vec<Primitive>,
theme: PhantomData<Theme>,
}
-

这个似乎就是我们要找的 SomeRenderer。但是这个Renderer<B, T>有两个类型参数,第二个就是Theme类型参数。还是没有看到 -这个类型参数具体的类型是什么。继续看哪里使用了这个带有类型参数的结构体Renderer<B, T>

-
/// A [`wgpu`] graphics renderer for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
pub type Renderer<Theme = iced_native::Theme> =
iced_graphics::Renderer<Backend, Theme>;
-

这里看到声明了一个新的类型iced_wgpu::Renderer,并且指定了Theme = iced_style::theme::Theme。 -这个新的类型在哪里使用了呢?

-
impl<Theme> iced_graphics::window::Compositor for Compositor<Theme> {
type Settings = Settings;
type Renderer = Renderer<Theme>;
type Surface = wgpu::Surface;

fn new<W: HasRawWindowHandle + HasRawDisplayHandle>(
settings: Self::Settings,
compatible_window: Option<&W>,
) -> Result<(Self, Self::Renderer), Error> {
let compositor = futures::executor::block_on(Self::request(
settings,
compatible_window,
))
.ok_or(Error::GraphicsAdapterNotFound)?;

let backend = compositor.create_backend();

Ok((compositor, Renderer::new(backend))) /// <--- 这里的Renderer就是指定了Theme的Renderer<B,T>
}
-

因此这个iced_wgpu::Renderer就是我们要找的SomeRenderer,而SomeRenderer::Theme就是 -iced_style::theme::Theme就是我们要找的SomeThemeType。

-
    -
  1. 接着看iced_style::theme::Theme类型如何实现button::StyleSheet trait -button_stylesheet_for_theme -从这里可以看到 SomeThemeType::Style = iced_style::theme::Button -因此style方法的入参是iced_style::theme::Button, 是一个枚举类型。
  2. -
-
/// The style of a button.
#[derive(Default)]
pub enum Button {
/// The primary style.
#[default]
Primary,
/// The secondary style.
Secondary,
/// The positive style.
Positive,
/// The destructive style.
Destructive,
/// The text style.
///
/// Useful for links!
Text,
/// A custom style.
Custom(Box<dyn button::StyleSheet<Style = Theme>>),
}
-

从定义可以看出,如果我们要实现自定义的样式就需要使用Button::Custom(Box::new(??)).

-

button::StyleSheet<Style = Theme>类型意思是某类型实现了button::StyleSheet trait, -并且关联类型Style = iced_style::theme::Theme.

-

所以我们可以写一个自定义的button样式了。

-
struct ButtonStyle;

impl StyleSheet for ButtonStyle {
type Style = iced_style::theme::Theme; ///<-- 这里可以简写为 type Style = Theme;

fn active(&self, style: &Self::Style) -> Appearance {
Appearance {
shadow_offset: Default::default(),
background: Some(Background::Color(Color::from_rgb8(204, 204, 204))),
border_radius: 5.0,
border_width: 0.0,
border_color: Default::default(),
text_color: Color::from_rgb8(0, 0, 0),
}
}
}
-

style方法的入参就是Button::Custom(Box::new(ButtonStyle{}))

-

最后style方法的使用如下

-
button(
text(input).size(24)
.vertical_alignment(Vertical::Center)
.horizontal_alignment(Horizontal::Center)
)
.padding(10)
.width(80)
.height(50)
.style( Button::Custom(Box::new(ButtonStyle{})) ) /// 这里该写什么呢?
.on_press(msg)
+

运行

直接运行,没有错误。

+
hello $ cargo test --bin hello eight_queens::tests::test_eight_queen -- --show-output
Finished test [unoptimized + debuginfo] target(s) in 0.11s
Running unittests src/main.rs (target/debug/deps/hello-257baa7e5ed28221)

running 1 test
test eight_queens::tests::test_eight_queen ... ok

successes:

---- eight_queens::tests::test_eight_queen stdout ----
Q . . . . . . .
. . . . Q . . .
. . . . . . . Q
. . . . . Q . .
. . Q . . . . .
. . . . . . Q .
. Q . . . . . .
. . . Q . . . .

Q . . . . . . .
. . . . . Q . .
. . . . . . . Q
. . Q . . . . .
. . . . . . Q .
. . . Q . . . .
. Q . . . . . .
. . . . Q . . .

Q . . . . . . .
. . . . . . Q .
. . . Q . . . .
. . . . . Q . .
. . . . . . . Q
. Q . . . . . .
. . . . Q . . .
. . Q . . . . .

Q . . . . . . .
. . . . . . Q .
. . . . Q . . .
. . . . . . . Q
. Q . . . . . .
. . . Q . . . .
. . . . . Q . .
. . Q . . . . .

. Q . . . . . .
. . . Q . . . .
. . . . . Q . .
. . . . . . . Q
. . Q . . . . .
Q . . . . . . .
. . . . . . Q .
. . . . Q . . .

. Q . . . . . .
. . . . Q . . .
. . . . . . Q .
Q . . . . . . .
. . Q . . . . .
. . . . . . . Q
. . . . . Q . .
. . . Q . . . .

. Q . . . . . .
. . . . Q . . .
. . . . . . Q .
. . . Q . . . .
Q . . . . . . .
. . . . . . . Q
. . . . . Q . .
. . Q . . . . .

... ignore
...
...

successes:
eight_queens::tests::test_eight_queen

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 22 filtered out; finished in 0.00s
-

关于Iced

Iced使用的Elm模型非常容易使用。我用它实现一个简单的计算器。 -calculator

-

总结

关联类型是Rust非常重要的特性。如果不深入理解的话,编译的错误信息都看不明白。

+

最后

ChatGPT给人非常惊艳的感觉。我考虑以后经常使用它,如果它一直免费的话。

]]>
+ 算法 rust - iced - -
- - 玩一下LangChain - /2023/06/08/2023-6-8-langchain-openai/ - 什么是LangChain

自从ChatGPT出现以来,就一直在使用,那么ChatGPT毕竟是有局限性的,因为ChatGPT训练的语料是有限的。很多问题回答不了, -也经常会胡言乱语闹笑话。

-

但是ChatGPT背后的大语言模型LLM是可以扩展的,也就是说,可以把特定的领域知识让LLM(大语言模型)学习。这样就在一定 -程度上解决了局限性。

-

LangChain项目就是这样的杀手锏,这里是官方文档

- - -

本文代码和例子参考了使用langchain打造自己的大型语言模型(LLMs),对中文资料进行处理。

-

OpenAI的key

LangChain是一个框架,如果要使用,则需要调用大语言模型的API。正好有一朋友申请了OPENAI_API_KEY,这样就可以开始 -跑跑代码了。

-

代码

以下是Python代码。

-
import os
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import TokenTextSplitter
from langchain.llms import OpenAI
from langchain.document_loaders import DirectoryLoader
from langchain.chains import RetrievalQAWithSourcesChain, ChatVectorDBChain
import jieba as jb

os.environ["OPENAI_API_KEY"] = "sk-xxxxxxx" # <-- 这里替换为申请的key


def preprocess_txt():
"""
由于中文的语法的特殊性,对于中文的文档必须要做一些预处理工作:词语的拆分,
也就是要把中文的语句拆分成一个个基本的词语单位,这里我们会用的一个分词工具:jieba,
它会帮助我们对资料库中的所有文本文件进行分词处理。不过我们首先将这3个时事新闻的文本文件放置到Data文件夹下面,
然后在data文件夹下面再建一个子文件夹:cut, 用来存放被分词过的文档:
"""
files = ['天龙八部.txt']

for file in files:
# 读取data文件夹中的中文文档
my_file = f"./data/{file}"
with open(my_file, "r", encoding='utf-8') as f:
data = f.read()

# 对中文文档进行分词处理
cut_data = " ".join([w for w in list(jb.cut(data))])
# 分词处理后的文档保存到data文件夹中的cut子文件夹中
cut_file = f"./data/cut/cut_{file}"
with open(cut_file, 'w') as f:
f.write(cut_data)
f.close()


def embeddings():
# 加载文档
loader = DirectoryLoader('./data/cut', glob='**/*.txt')
docs = loader.load()
# 文档切块
text_splitter = TokenTextSplitter(chunk_size=1000, chunk_overlap=0)
doc_texts = text_splitter.split_documents(docs)
# 调用openai Embeddings
embeddings = OpenAIEmbeddings()
# 向量化
vectordb = Chroma.from_documents(doc_texts, embeddings, persist_directory="./data/cut")
vectordb.persist()


def ask():
embeddings = OpenAIEmbeddings()

docsearch = Chroma(persist_directory="./data/cut", embedding_function=embeddings)

chain = ChatVectorDBChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), docsearch,
return_source_documents=True)

while True:
try:
# code to be executed repeatedly
user_input = input("What's your question: ")

chat_history = []
result = chain({"question": user_input, "chat_history": chat_history})

print("Answer: " + result["answer"].replace('\n', ' '))
except KeyboardInterrupt:
# code to be executed when Ctrl+C is pressed
break


if __name__ == '__main__':
preprocess_txt()
embeddings()
ask()
- -

代码依赖模块

Python3.11安装依赖

-
pip install -r requirements.txt
- -

requirements.txt内容如下:

-
aiohttp==3.8.4
aiosignal==1.3.1
anyio==3.7.0
argilla==1.8.0
async-timeout==4.0.2
attrs==23.1.0
backoff==2.2.1
certifi==2023.5.7
cffi==1.15.1
chardet==5.1.0
charset-normalizer==3.1.0
chromadb==0.3.26
click==8.1.3
clickhouse-connect==0.6.0
coloredlogs==15.0.1
commonmark==0.9.1
contourpy==1.0.7
cryptography==41.0.1
cycler==0.11.0
dataclasses-json==0.5.7
Deprecated==1.2.14
duckdb==0.8.0
et-xmlfile==1.1.0
fastapi==0.96.0
flatbuffers==23.5.26
fonttools==4.39.4
frozenlist==1.3.3
greenlet==2.0.2
h11==0.14.0
hnswlib==0.7.0
httpcore==0.16.3
httptools==0.5.0
httpx==0.23.3
humanfriendly==10.0
idna==3.4
jieba==0.42.1
joblib==1.2.0
kiwisolver==1.4.4
langchain==0.0.191
lxml==4.9.2
lz4==4.3.2
Markdown==3.4.3
marshmallow==3.19.0
marshmallow-enum==1.5.1
matplotlib==3.7.1
monotonic==1.6
mpmath==1.3.0
msg-parser==1.2.0
multidict==6.0.4
mypy-extensions==1.0.0
nltk==3.8.1
numexpr==2.8.4
numpy==1.23.5
olefile==0.46
onnxruntime==1.15.0
openai==0.27.7
openapi-schema-pydantic==1.2.4
openpyxl==3.1.2
overrides==7.3.1
packaging==23.1
pandas==1.5.3
pdf2image==1.16.3
pdfminer.six==20221105
Pillow==9.5.0
posthog==3.0.1
protobuf==4.23.2
pulsar-client==3.2.0
pycparser==2.21
pydantic==1.10.8
Pygments==2.15.1
pypandoc==1.11
pyparsing==3.0.9
python-dateutil==2.8.2
python-docx==0.8.11
python-dotenv==1.0.0
python-magic==0.4.27
python-pptx==0.6.21
pytz==2023.3
PyYAML==6.0
regex==2023.6.3
requests==2.31.0
rfc3986==1.5.0
rich==13.0.1
six==1.16.0
sniffio==1.3.0
SQLAlchemy==2.0.15
starlette==0.27.0
sympy==1.12
tabulate==0.9.0
tenacity==8.2.2
tiktoken==0.4.0
tokenizers==0.13.3
tqdm==4.65.0
typer==0.9.0
typing-inspect==0.9.0
typing_extensions==4.6.3
tzdata==2023.3
unstructured==0.7.1
urllib3==2.0.2
uvicorn==0.22.0
uvloop==0.17.0
watchfiles==0.19.0
websockets==11.0.3
wrapt==1.14.1
xlrd==2.0.1
XlsxWriter==3.1.2
yarl==1.9.2
zstandard==0.21.0
- -

文本

而文本则放在 “./data/“ 目录下,

-

天龙八部.txt 内容如下,就是一些明确的信息。

-
段誉和乔峰,虚竹是兄弟。

阿朱喜欢乔峰。

段誉喜欢王语嫣。
- -

如果直接问ChatGPT,”段誉的兄弟是谁”, “段誉有几个兄弟”则ChatGPT回答有些混乱。

-

chatgpt-duanyu-brothers

-

运行测试

直接运行后,测试提问,如下。

-

langchain1

-

langchain1

-

很有意思。

-

总结

    -
  1. 中文需要进行分词,与英文的处理不同。
  2. -
  3. 需要避免4096 TOKEN的限制,则只能使用文章中提到模型建立Chain。
    ChatVectorDBChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), docsearch,
    return_source_documents=True)
  4. -
  5. 还是需要VPN才能使用,另外OPEN_API_KEY是需要花钱的。
  6. -
-

参考

-]]>
- - langchain - openai + ChatGPT
@@ -3087,6 +3041,52 @@ command source ~/qtlldb/QtFormatters.lldb

lldb
+ + 玩一下LangChain + /2023/06/08/2023-6-8-langchain-openai/ + 什么是LangChain

自从ChatGPT出现以来,就一直在使用,那么ChatGPT毕竟是有局限性的,因为ChatGPT训练的语料是有限的。很多问题回答不了, +也经常会胡言乱语闹笑话。

+

但是ChatGPT背后的大语言模型LLM是可以扩展的,也就是说,可以把特定的领域知识让LLM(大语言模型)学习。这样就在一定 +程度上解决了局限性。

+

LangChain项目就是这样的杀手锏,这里是官方文档

+ + +

本文代码和例子参考了使用langchain打造自己的大型语言模型(LLMs),对中文资料进行处理。

+

OpenAI的key

LangChain是一个框架,如果要使用,则需要调用大语言模型的API。正好有一朋友申请了OPENAI_API_KEY,这样就可以开始 +跑跑代码了。

+

代码

以下是Python代码。

+
import os
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import TokenTextSplitter
from langchain.llms import OpenAI
from langchain.document_loaders import DirectoryLoader
from langchain.chains import RetrievalQAWithSourcesChain, ChatVectorDBChain
import jieba as jb

os.environ["OPENAI_API_KEY"] = "sk-xxxxxxx" # <-- 这里替换为申请的key


def preprocess_txt():
"""
由于中文的语法的特殊性,对于中文的文档必须要做一些预处理工作:词语的拆分,
也就是要把中文的语句拆分成一个个基本的词语单位,这里我们会用的一个分词工具:jieba,
它会帮助我们对资料库中的所有文本文件进行分词处理。不过我们首先将这3个时事新闻的文本文件放置到Data文件夹下面,
然后在data文件夹下面再建一个子文件夹:cut, 用来存放被分词过的文档:
"""
files = ['天龙八部.txt']

for file in files:
# 读取data文件夹中的中文文档
my_file = f"./data/{file}"
with open(my_file, "r", encoding='utf-8') as f:
data = f.read()

# 对中文文档进行分词处理
cut_data = " ".join([w for w in list(jb.cut(data))])
# 分词处理后的文档保存到data文件夹中的cut子文件夹中
cut_file = f"./data/cut/cut_{file}"
with open(cut_file, 'w') as f:
f.write(cut_data)
f.close()


def embeddings():
# 加载文档
loader = DirectoryLoader('./data/cut', glob='**/*.txt')
docs = loader.load()
# 文档切块
text_splitter = TokenTextSplitter(chunk_size=1000, chunk_overlap=0)
doc_texts = text_splitter.split_documents(docs)
# 调用openai Embeddings
embeddings = OpenAIEmbeddings()
# 向量化
vectordb = Chroma.from_documents(doc_texts, embeddings, persist_directory="./data/cut")
vectordb.persist()


def ask():
embeddings = OpenAIEmbeddings()

docsearch = Chroma(persist_directory="./data/cut", embedding_function=embeddings)

chain = ChatVectorDBChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), docsearch,
return_source_documents=True)

while True:
try:
# code to be executed repeatedly
user_input = input("What's your question: ")

chat_history = []
result = chain({"question": user_input, "chat_history": chat_history})

print("Answer: " + result["answer"].replace('\n', ' '))
except KeyboardInterrupt:
# code to be executed when Ctrl+C is pressed
break


if __name__ == '__main__':
preprocess_txt()
embeddings()
ask()
+ +

代码依赖模块

Python3.11安装依赖

+
pip install -r requirements.txt
+ +

requirements.txt内容如下:

+
aiohttp==3.8.4
aiosignal==1.3.1
anyio==3.7.0
argilla==1.8.0
async-timeout==4.0.2
attrs==23.1.0
backoff==2.2.1
certifi==2023.5.7
cffi==1.15.1
chardet==5.1.0
charset-normalizer==3.1.0
chromadb==0.3.26
click==8.1.3
clickhouse-connect==0.6.0
coloredlogs==15.0.1
commonmark==0.9.1
contourpy==1.0.7
cryptography==41.0.1
cycler==0.11.0
dataclasses-json==0.5.7
Deprecated==1.2.14
duckdb==0.8.0
et-xmlfile==1.1.0
fastapi==0.96.0
flatbuffers==23.5.26
fonttools==4.39.4
frozenlist==1.3.3
greenlet==2.0.2
h11==0.14.0
hnswlib==0.7.0
httpcore==0.16.3
httptools==0.5.0
httpx==0.23.3
humanfriendly==10.0
idna==3.4
jieba==0.42.1
joblib==1.2.0
kiwisolver==1.4.4
langchain==0.0.191
lxml==4.9.2
lz4==4.3.2
Markdown==3.4.3
marshmallow==3.19.0
marshmallow-enum==1.5.1
matplotlib==3.7.1
monotonic==1.6
mpmath==1.3.0
msg-parser==1.2.0
multidict==6.0.4
mypy-extensions==1.0.0
nltk==3.8.1
numexpr==2.8.4
numpy==1.23.5
olefile==0.46
onnxruntime==1.15.0
openai==0.27.7
openapi-schema-pydantic==1.2.4
openpyxl==3.1.2
overrides==7.3.1
packaging==23.1
pandas==1.5.3
pdf2image==1.16.3
pdfminer.six==20221105
Pillow==9.5.0
posthog==3.0.1
protobuf==4.23.2
pulsar-client==3.2.0
pycparser==2.21
pydantic==1.10.8
Pygments==2.15.1
pypandoc==1.11
pyparsing==3.0.9
python-dateutil==2.8.2
python-docx==0.8.11
python-dotenv==1.0.0
python-magic==0.4.27
python-pptx==0.6.21
pytz==2023.3
PyYAML==6.0
regex==2023.6.3
requests==2.31.0
rfc3986==1.5.0
rich==13.0.1
six==1.16.0
sniffio==1.3.0
SQLAlchemy==2.0.15
starlette==0.27.0
sympy==1.12
tabulate==0.9.0
tenacity==8.2.2
tiktoken==0.4.0
tokenizers==0.13.3
tqdm==4.65.0
typer==0.9.0
typing-inspect==0.9.0
typing_extensions==4.6.3
tzdata==2023.3
unstructured==0.7.1
urllib3==2.0.2
uvicorn==0.22.0
uvloop==0.17.0
watchfiles==0.19.0
websockets==11.0.3
wrapt==1.14.1
xlrd==2.0.1
XlsxWriter==3.1.2
yarl==1.9.2
zstandard==0.21.0
+ +

文本

而文本则放在 “./data/“ 目录下,

+

天龙八部.txt 内容如下,就是一些明确的信息。

+
段誉和乔峰,虚竹是兄弟。

阿朱喜欢乔峰。

段誉喜欢王语嫣。
+ +

如果直接问ChatGPT,”段誉的兄弟是谁”, “段誉有几个兄弟”则ChatGPT回答有些混乱。

+

chatgpt-duanyu-brothers

+

运行测试

直接运行后,测试提问,如下。

+

langchain1

+

langchain1

+

很有意思。

+

总结

    +
  1. 中文需要进行分词,与英文的处理不同。
  2. +
  3. 需要避免4096 TOKEN的限制,则只能使用文章中提到模型建立Chain。
    ChatVectorDBChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), docsearch,
    return_source_documents=True)
  4. +
  5. 还是需要VPN才能使用,另外OPEN_API_KEY是需要花钱的。
  6. +
+

参考

+]]>
+ + langchain + openai + +
BitTorrent协议(三)之磁力链接获取元数据 /2019/01/21/2019-1-21-bt-3/ diff --git a/sitemap.xml b/sitemap.xml index 5cb8e77a..d1eab269 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1,6 +1,15 @@ + + http://threelambda.com/2019/01/09/2019-1-9-bt-1/ + + 2023-07-15 + + monthly + 0.6 + + http://threelambda.com/about/index.html @@ -100,15 +109,6 @@ 0.6 - - http://threelambda.com/2019/01/09/2019-1-9-bt-1/ - - 2022-02-06 - - monthly - 0.6 - - http://threelambda.com/2020/03/09/connect-kafka/ @@ -128,7 +128,7 @@ - http://threelambda.com/2019/01/25/2019-1-25-bt-5/ + http://threelambda.com/2019/01/21/2019-1-23-bt-4/ 2022-02-06 @@ -137,7 +137,7 @@ - http://threelambda.com/2019/01/21/2019-1-23-bt-4/ + http://threelambda.com/2019/01/25/2019-1-25-bt-5/ 2022-02-06 @@ -146,7 +146,7 @@ - http://threelambda.com/2019/11/19/debug-k3s/ + http://threelambda.com/2019/02/18/2019-2-18-bt-6/ 2022-02-06 @@ -155,7 +155,7 @@ - http://threelambda.com/2019/02/18/2019-2-18-bt-6/ + http://threelambda.com/2019/11/19/debug-k3s/ 2022-02-06 @@ -173,7 +173,7 @@ - http://threelambda.com/2020/07/06/run-ingress-example-on-mac/ + http://threelambda.com/2020/09/08/remote-debug-with-clion/ 2022-02-06 @@ -182,7 +182,7 @@ - http://threelambda.com/2020/09/08/remote-debug-with-clion/ + http://threelambda.com/2020/07/06/run-ingress-example-on-mac/ 2022-02-06 @@ -254,7 +254,7 @@ - http://threelambda.com/2017/01/12/2017-1-12-cython-on-win10/ + http://threelambda.com/2017/10/07/leetcode-312/ 2022-02-06 @@ -263,7 +263,7 @@ - http://threelambda.com/2017/10/07/leetcode-312/ + http://threelambda.com/2017/01/12/2017-1-12-cython-on-win10/ 2022-02-06 @@ -464,49 +464,49 @@ - http://threelambda.com/tags/%E6%8A%80%E5%B7%A7/ + http://threelambda.com/tags/R/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/R/ + http://threelambda.com/tags/%E6%8A%80%E5%B7%A7/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/Cython/ + http://threelambda.com/tags/venv/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/venv/ + http://threelambda.com/tags/algorithm/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/algorithm/ + http://threelambda.com/tags/java/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/java/ + http://threelambda.com/tags/trie/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/trie/ + http://threelambda.com/tags/Cython/ 2023-07-15 weekly 0.2 @@ -541,28 +541,28 @@ - http://threelambda.com/tags/image/ + http://threelambda.com/tags/build/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/resize/ + http://threelambda.com/tags/image/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/virutalbox/ + http://threelambda.com/tags/resize/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/javacc/ + http://threelambda.com/tags/virutalbox/ 2023-07-15 weekly 0.2 @@ -576,14 +576,14 @@ - http://threelambda.com/tags/build/ + http://threelambda.com/tags/macro/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/macro/ + http://threelambda.com/tags/javacc/ 2023-07-15 weekly 0.2 @@ -653,77 +653,77 @@ - http://threelambda.com/tags/k8s/ + http://threelambda.com/tags/spring/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/ingress/ + http://threelambda.com/tags/integration/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/mac/ + http://threelambda.com/tags/write/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/spring/ + http://threelambda.com/tags/file/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/integration/ + http://threelambda.com/tags/debug/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/write/ + http://threelambda.com/tags/CLion/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/file/ + http://threelambda.com/tags/gdb/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/debug/ + http://threelambda.com/tags/remote/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/CLion/ + http://threelambda.com/tags/k8s/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/gdb/ + http://threelambda.com/tags/ingress/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/remote/ + http://threelambda.com/tags/mac/ 2023-07-15 weekly 0.2 @@ -800,14 +800,14 @@ - http://threelambda.com/tags/ChatGPT/ + http://threelambda.com/tags/rust/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/rust/ + http://threelambda.com/tags/iced/ 2023-07-15 weekly 0.2 @@ -849,28 +849,28 @@ - http://threelambda.com/tags/iced/ + http://threelambda.com/tags/ChatGPT/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/langchain/ + http://threelambda.com/tags/lldb/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/openai/ + http://threelambda.com/tags/langchain/ 2023-07-15 weekly 0.2 - http://threelambda.com/tags/lldb/ + http://threelambda.com/tags/openai/ 2023-07-15 weekly 0.2