diff --git a/2016/09/27/2016-9-27-leetcode-200/index.html b/2016/09/27/2016-9-27-leetcode-200/index.html index 89268585..dadf4981 100644 --- a/2016/09/27/2016-9-27-leetcode-200/index.html +++ b/2016/09/27/2016-9-27-leetcode-200/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -346,12 +346,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2016/10/24/leetcode-224/index.html b/2016/10/24/leetcode-224/index.html index 8887ac8f..f30b0e86 100644 --- a/2016/10/24/leetcode-224/index.html +++ b/2016/10/24/leetcode-224/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -393,12 +393,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2016/12/19/leetcode-282/index.html b/2016/12/19/leetcode-282/index.html index 84418515..1c1e5dee 100644 --- a/2016/12/19/leetcode-282/index.html +++ b/2016/12/19/leetcode-282/index.html @@ -28,9 +28,9 @@ + - @@ -71,7 +71,7 @@ - + @@ -369,12 +369,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2016/12/21/get-val-name-and-value/index.html b/2016/12/21/get-val-name-and-value/index.html index 2b5325b7..376860cb 100644 --- a/2016/12/21/get-val-name-and-value/index.html +++ b/2016/12/21/get-val-name-and-value/index.html @@ -26,8 +26,8 @@ - + @@ -67,7 +67,7 @@ - + @@ -370,10 +370,10 @@

- #python - #R + #python + diff --git a/2016/12/22/pandas-read-csv/index.html b/2016/12/22/pandas-read-csv/index.html index 46bd0f32..2d2a4f0d 100644 --- a/2016/12/22/pandas-read-csv/index.html +++ b/2016/12/22/pandas-read-csv/index.html @@ -67,7 +67,7 @@ - + diff --git a/2016/12/26/leetcode-315/index.html b/2016/12/26/leetcode-315/index.html index 7adc6ac9..fcf3008a 100644 --- a/2016/12/26/leetcode-315/index.html +++ b/2016/12/26/leetcode-315/index.html @@ -28,9 +28,9 @@ + - @@ -71,7 +71,7 @@ - + @@ -408,12 +408,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2016/12/31/leetcode-327/index.html b/2016/12/31/leetcode-327/index.html index 487d1687..7890796a 100644 --- a/2016/12/31/leetcode-327/index.html +++ b/2016/12/31/leetcode-327/index.html @@ -30,9 +30,9 @@ + - @@ -73,7 +73,7 @@ - + @@ -384,12 +384,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2017/01/12/2017-1-12-cython-on-win10/index.html b/2017/01/12/2017-1-12-cython-on-win10/index.html index 4fd4958f..b90df903 100644 --- a/2017/01/12/2017-1-12-cython-on-win10/index.html +++ b/2017/01/12/2017-1-12-cython-on-win10/index.html @@ -67,7 +67,7 @@ - + diff --git a/2017/01/18/2017-1-18-venv-no-pip/index.html b/2017/01/18/2017-1-18-venv-no-pip/index.html index 43a0836f..1de4521e 100644 --- a/2017/01/18/2017-1-18-venv-no-pip/index.html +++ b/2017/01/18/2017-1-18-venv-no-pip/index.html @@ -70,7 +70,7 @@ - + diff --git a/2017/01/31/2017-1-31-leetcode-295/index.html b/2017/01/31/2017-1-31-leetcode-295/index.html index 166bea95..17ecf3bb 100644 --- a/2017/01/31/2017-1-31-leetcode-295/index.html +++ b/2017/01/31/2017-1-31-leetcode-295/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -385,12 +385,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2017/03/06/2017-3-6-grails-call-java/index.html b/2017/03/06/2017-3-6-grails-call-java/index.html index 07606d9e..3b362716 100644 --- a/2017/03/06/2017-3-6-grails-call-java/index.html +++ b/2017/03/06/2017-3-6-grails-call-java/index.html @@ -27,8 +27,8 @@ - + @@ -68,7 +68,7 @@ - + @@ -360,10 +360,10 @@

#java - #grails - #gradle + #grails + diff --git a/2017/03/12/2017-3-12-leetcode-329/index.html b/2017/03/12/2017-3-12-leetcode-329/index.html index cd3e8e0e..65ea20e5 100644 --- a/2017/03/12/2017-3-12-leetcode-329/index.html +++ b/2017/03/12/2017-3-12-leetcode-329/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -358,12 +358,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2017/03/23/2017-3-23-compile-and-run-clojure/index.html b/2017/03/23/2017-3-23-compile-and-run-clojure/index.html index 268dbebc..2b1b1406 100644 --- a/2017/03/23/2017-3-23-compile-and-run-clojure/index.html +++ b/2017/03/23/2017-3-23-compile-and-run-clojure/index.html @@ -68,7 +68,7 @@ - + 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 7f7f3f04..537cff53 100644 --- a/2017/05/19/2017-5-19-javacc-minilisp/index.html +++ b/2017/05/19/2017-5-19-javacc-minilisp/index.html @@ -68,7 +68,7 @@ - + diff --git a/2017/05/24/2017-5-24-gradle-build-project/index.html b/2017/05/24/2017-5-24-gradle-build-project/index.html index 02527cdf..8596d152 100644 --- a/2017/05/24/2017-5-24-gradle-build-project/index.html +++ b/2017/05/24/2017-5-24-gradle-build-project/index.html @@ -71,7 +71,7 @@ - + diff --git a/2017/07/09/2017-7-9-resize-fixed-image/index.html b/2017/07/09/2017-7-9-resize-fixed-image/index.html index a5956a23..a3dfcb0b 100644 --- a/2017/07/09/2017-7-9-resize-fixed-image/index.html +++ b/2017/07/09/2017-7-9-resize-fixed-image/index.html @@ -71,7 +71,7 @@ - + diff --git a/2017/10/07/leetcode-312/index.html b/2017/10/07/leetcode-312/index.html index a06a3419..c3d352ce 100644 --- a/2017/10/07/leetcode-312/index.html +++ b/2017/10/07/leetcode-312/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -354,12 +354,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2017/11/27/find-top-n/index.html b/2017/11/27/find-top-n/index.html index 9d19a390..e48ffb86 100644 --- a/2017/11/27/find-top-n/index.html +++ b/2017/11/27/find-top-n/index.html @@ -68,7 +68,7 @@ - + diff --git a/2018/02/24/2018-2-24-robber-3/index.html b/2018/02/24/2018-2-24-robber-3/index.html index 41a01ca6..56607066 100644 --- a/2018/02/24/2018-2-24-robber-3/index.html +++ b/2018/02/24/2018-2-24-robber-3/index.html @@ -27,9 +27,9 @@ + - @@ -70,7 +70,7 @@ - + @@ -340,12 +340,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2018/08/18/2018-8-18-leetcode-341/index.html b/2018/08/18/2018-8-18-leetcode-341/index.html index 9f69839f..8a3026d3 100644 --- a/2018/08/18/2018-8-18-leetcode-341/index.html +++ b/2018/08/18/2018-8-18-leetcode-341/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -345,12 +345,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2018/11/08/2018-11-8-leetcode-352/index.html b/2018/11/08/2018-11-8-leetcode-352/index.html index 6f1faa59..532a7168 100644 --- a/2018/11/08/2018-11-8-leetcode-352/index.html +++ b/2018/11/08/2018-11-8-leetcode-352/index.html @@ -26,9 +26,9 @@ + - @@ -68,7 +68,7 @@ - + @@ -360,12 +360,12 @@

+ #python + #leetcode #算法 - #python - diff --git a/2018/11/09/minilisp/index.html b/2018/11/09/minilisp/index.html index 688156a8..dd5aa8db 100644 --- a/2018/11/09/minilisp/index.html +++ b/2018/11/09/minilisp/index.html @@ -68,7 +68,7 @@ - + diff --git a/2018/12/09/macro/index.html b/2018/12/09/macro/index.html index 3a660f44..eb3938ba 100644 --- a/2018/12/09/macro/index.html +++ b/2018/12/09/macro/index.html @@ -68,7 +68,7 @@ - + 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 823f073a..30ed2815 100644 --- a/2019/01/09/2019-1-9-bt-1/index.html +++ b/2019/01/09/2019-1-9-bt-1/index.html @@ -68,7 +68,7 @@ - + diff --git a/2019/01/10/2019-1-10-bt-2/index.html b/2019/01/10/2019-1-10-bt-2/index.html index 3f5f3068..bf23f99a 100644 --- a/2019/01/10/2019-1-10-bt-2/index.html +++ b/2019/01/10/2019-1-10-bt-2/index.html @@ -68,7 +68,7 @@ - + diff --git a/2019/01/21/2019-1-21-bt-3/index.html b/2019/01/21/2019-1-21-bt-3/index.html index 86ef7a24..eb94d1d0 100644 --- a/2019/01/21/2019-1-21-bt-3/index.html +++ b/2019/01/21/2019-1-21-bt-3/index.html @@ -68,7 +68,7 @@ - + diff --git a/2019/01/21/2019-1-23-bt-4/index.html b/2019/01/21/2019-1-23-bt-4/index.html index 494e9c53..29477bf7 100644 --- a/2019/01/21/2019-1-23-bt-4/index.html +++ b/2019/01/21/2019-1-23-bt-4/index.html @@ -70,7 +70,7 @@ - + diff --git a/2019/01/25/2019-1-25-bt-5/index.html b/2019/01/25/2019-1-25-bt-5/index.html index f4d789de..43d9e2a1 100644 --- a/2019/01/25/2019-1-25-bt-5/index.html +++ b/2019/01/25/2019-1-25-bt-5/index.html @@ -68,7 +68,7 @@ - + diff --git a/2019/02/18/2019-2-18-bt-6/index.html b/2019/02/18/2019-2-18-bt-6/index.html index 60223afd..a2758fb5 100644 --- a/2019/02/18/2019-2-18-bt-6/index.html +++ b/2019/02/18/2019-2-18-bt-6/index.html @@ -68,7 +68,7 @@ - + diff --git a/2019/04/28/2019-4-28-byte-to-string-and-back/index.html b/2019/04/28/2019-4-28-byte-to-string-and-back/index.html index 93cc67bb..50fd2798 100644 --- a/2019/04/28/2019-4-28-byte-to-string-and-back/index.html +++ b/2019/04/28/2019-4-28-byte-to-string-and-back/index.html @@ -67,7 +67,7 @@ - + diff --git a/2019/11/19/debug-k3s/index.html b/2019/11/19/debug-k3s/index.html index 467101ec..c57a77be 100644 --- a/2019/11/19/debug-k3s/index.html +++ b/2019/11/19/debug-k3s/index.html @@ -70,7 +70,7 @@ - + diff --git a/2020/03/09/connect-kafka/index.html b/2020/03/09/connect-kafka/index.html index 1a51540f..e1d683c6 100644 --- a/2020/03/09/connect-kafka/index.html +++ b/2020/03/09/connect-kafka/index.html @@ -26,9 +26,9 @@ - + @@ -68,7 +68,7 @@ - + @@ -384,12 +384,12 @@

测试 - #docker - #kafka #container + #docker + diff --git a/2020/07/06/run-ingress-example-on-mac/index.html b/2020/07/06/run-ingress-example-on-mac/index.html index b6380015..432bbce5 100644 --- a/2020/07/06/run-ingress-example-on-mac/index.html +++ b/2020/07/06/run-ingress-example-on-mac/index.html @@ -26,9 +26,9 @@ + - @@ -69,7 +69,7 @@ - + @@ -364,12 +364,12 @@

+ #docker + #k8s #ingress - #docker - #mac diff --git a/2020/08/14/write-file-with-spring-integration/index.html b/2020/08/14/write-file-with-spring-integration/index.html index d499e0d5..858c4e64 100644 --- a/2020/08/14/write-file-with-spring-integration/index.html +++ b/2020/08/14/write-file-with-spring-integration/index.html @@ -69,7 +69,7 @@ - + diff --git a/2020/09/08/remote-debug-with-clion/index.html b/2020/09/08/remote-debug-with-clion/index.html index b1752c6d..ecd50dae 100644 --- a/2020/09/08/remote-debug-with-clion/index.html +++ b/2020/09/08/remote-debug-with-clion/index.html @@ -72,7 +72,7 @@ - + diff --git a/2020/10/09/x509-ca/index.html b/2020/10/09/x509-ca/index.html index f3d704f5..710fbbaf 100644 --- a/2020/10/09/x509-ca/index.html +++ b/2020/10/09/x509-ca/index.html @@ -30,8 +30,8 @@ - + @@ -75,7 +75,7 @@ - + @@ -371,10 +371,10 @@

- #X.509 - #certificate + #X.509 + #nginx #openssl diff --git a/2020/11/08/asn-1/index.html b/2020/11/08/asn-1/index.html index 91a02484..9567945d 100644 --- a/2020/11/08/asn-1/index.html +++ b/2020/11/08/asn-1/index.html @@ -69,7 +69,7 @@ - + diff --git a/2021/07/15/2021-7-15-clion-makefile-debug/index.html b/2021/07/15/2021-7-15-clion-makefile-debug/index.html index 189ecff1..6a9d4c25 100644 --- a/2021/07/15/2021-7-15-clion-makefile-debug/index.html +++ b/2021/07/15/2021-7-15-clion-makefile-debug/index.html @@ -71,7 +71,7 @@ - + diff --git a/2021/12/26/pod-resolve-dn/index.html b/2021/12/26/pod-resolve-dn/index.html index d4ceebf2..795ec8a9 100644 --- a/2021/12/26/pod-resolve-dn/index.html +++ b/2021/12/26/pod-resolve-dn/index.html @@ -69,7 +69,7 @@ - + diff --git a/2022/02/06/2022-2-6-leetcode-390/index.html b/2022/02/06/2022-2-6-leetcode-390/index.html index 12e35559..65d483d1 100644 --- a/2022/02/06/2022-2-6-leetcode-390/index.html +++ b/2022/02/06/2022-2-6-leetcode-390/index.html @@ -68,7 +68,7 @@ - + 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 9b2e1448..c95b6b39 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 @@ -71,7 +71,7 @@ - + diff --git a/2023/02/28/2023-2-28-associated-type/index.html b/2023/02/28/2023-2-28-associated-type/index.html index 758122c2..e2f84a0a 100644 --- a/2023/02/28/2023-2-28-associated-type/index.html +++ b/2023/02/28/2023-2-28-associated-type/index.html @@ -70,7 +70,7 @@ - + diff --git a/2023/06/07/2023-6-7-qt5-macos-bundle-app/index.html b/2023/06/07/2023-6-7-qt5-macos-bundle-app/index.html index 0f09a813..ce58840a 100644 --- a/2023/06/07/2023-6-7-qt5-macos-bundle-app/index.html +++ b/2023/06/07/2023-6-7-qt5-macos-bundle-app/index.html @@ -69,7 +69,7 @@ - + diff --git a/2023/06/08/2023-6-8-langchain-openai/index.html b/2023/06/08/2023-6-8-langchain-openai/index.html index 99452041..dd6f9ad8 100644 --- a/2023/06/08/2023-6-8-langchain-openai/index.html +++ b/2023/06/08/2023-6-8-langchain-openai/index.html @@ -71,7 +71,7 @@ - + diff --git a/2023/06/09/2023-6-9-qt5-macos-lldb-debug/index.html b/2023/06/09/2023-6-9-qt5-macos-lldb-debug/index.html index 180694ef..467e4981 100644 --- a/2023/06/09/2023-6-9-qt5-macos-lldb-debug/index.html +++ b/2023/06/09/2023-6-9-qt5-macos-lldb-debug/index.html @@ -71,7 +71,7 @@ - + diff --git a/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html b/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html index 11ccb3f6..cdba5417 100644 --- a/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html +++ b/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html @@ -71,7 +71,7 @@ - + diff --git a/404.html b/404.html index ce8d3c77..ae5e71b2 100644 --- a/404.html +++ b/404.html @@ -55,7 +55,7 @@ - + diff --git a/about/2019-4-2-bt-7.html b/about/2019-4-2-bt-7.html index 9616a6d9..a23b5887 100644 --- a/about/2019-4-2-bt-7.html +++ b/about/2019-4-2-bt-7.html @@ -62,7 +62,7 @@ - + diff --git a/about/index.html b/about/index.html index 1dccfdae..3daa9917 100644 --- a/about/index.html +++ b/about/index.html @@ -65,7 +65,7 @@ - + diff --git a/archives/2016/09/index.html b/archives/2016/09/index.html index 616149d1..dd649900 100644 --- a/archives/2016/09/index.html +++ b/archives/2016/09/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2016/10/index.html b/archives/2016/10/index.html index cb2dea33..e8ac9fbb 100644 --- a/archives/2016/10/index.html +++ b/archives/2016/10/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2016/12/index.html b/archives/2016/12/index.html index 9f1170a5..77960eaf 100644 --- a/archives/2016/12/index.html +++ b/archives/2016/12/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2016/index.html b/archives/2016/index.html index f29d43ec..421b35dd 100644 --- a/archives/2016/index.html +++ b/archives/2016/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/01/index.html b/archives/2017/01/index.html index 8e89dc45..1ff17731 100644 --- a/archives/2017/01/index.html +++ b/archives/2017/01/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/03/index.html b/archives/2017/03/index.html index bf1b1dd5..d858b434 100644 --- a/archives/2017/03/index.html +++ b/archives/2017/03/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/05/index.html b/archives/2017/05/index.html index 3ef5c456..fe7eff8b 100644 --- a/archives/2017/05/index.html +++ b/archives/2017/05/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/07/index.html b/archives/2017/07/index.html index 25e8ce50..c8f6c222 100644 --- a/archives/2017/07/index.html +++ b/archives/2017/07/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/10/index.html b/archives/2017/10/index.html index 688ecf18..d8158f62 100644 --- a/archives/2017/10/index.html +++ b/archives/2017/10/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/11/index.html b/archives/2017/11/index.html index c984aa83..e5aa825c 100644 --- a/archives/2017/11/index.html +++ b/archives/2017/11/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/index.html b/archives/2017/index.html index 7a6d9d6a..64a61c3b 100644 --- a/archives/2017/index.html +++ b/archives/2017/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2017/page/2/index.html b/archives/2017/page/2/index.html index 6c220606..7a8648da 100644 --- a/archives/2017/page/2/index.html +++ b/archives/2017/page/2/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2018/02/index.html b/archives/2018/02/index.html index cd7ab95a..bb72e663 100644 --- a/archives/2018/02/index.html +++ b/archives/2018/02/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2018/08/index.html b/archives/2018/08/index.html index 7c40fd4d..f880be3d 100644 --- a/archives/2018/08/index.html +++ b/archives/2018/08/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2018/11/index.html b/archives/2018/11/index.html index f5c170f2..cc15f7b7 100644 --- a/archives/2018/11/index.html +++ b/archives/2018/11/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2018/12/index.html b/archives/2018/12/index.html index 328833c2..4d195e5c 100644 --- a/archives/2018/12/index.html +++ b/archives/2018/12/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2018/index.html b/archives/2018/index.html index e7330a6a..9eb12a82 100644 --- a/archives/2018/index.html +++ b/archives/2018/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2019/01/index.html b/archives/2019/01/index.html index 7ee42c74..49674cc2 100644 --- a/archives/2019/01/index.html +++ b/archives/2019/01/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2019/02/index.html b/archives/2019/02/index.html index 5164b9dd..b44d4580 100644 --- a/archives/2019/02/index.html +++ b/archives/2019/02/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2019/04/index.html b/archives/2019/04/index.html index 74a7601f..03ab5468 100644 --- a/archives/2019/04/index.html +++ b/archives/2019/04/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2019/11/index.html b/archives/2019/11/index.html index ad235693..b321ffb3 100644 --- a/archives/2019/11/index.html +++ b/archives/2019/11/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2019/index.html b/archives/2019/index.html index b60c9399..0374a9ef 100644 --- a/archives/2019/index.html +++ b/archives/2019/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/03/index.html b/archives/2020/03/index.html index 1600022e..5744b0bf 100644 --- a/archives/2020/03/index.html +++ b/archives/2020/03/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/07/index.html b/archives/2020/07/index.html index 24401c81..7a622fb9 100644 --- a/archives/2020/07/index.html +++ b/archives/2020/07/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html index a408b548..c37af52c 100644 --- a/archives/2020/08/index.html +++ b/archives/2020/08/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/09/index.html b/archives/2020/09/index.html index e262bbdd..afa8c172 100644 --- a/archives/2020/09/index.html +++ b/archives/2020/09/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/10/index.html b/archives/2020/10/index.html index d8c4d4d3..5027bc38 100644 --- a/archives/2020/10/index.html +++ b/archives/2020/10/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/11/index.html b/archives/2020/11/index.html index 62957483..7d165726 100644 --- a/archives/2020/11/index.html +++ b/archives/2020/11/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2020/index.html b/archives/2020/index.html index 1cbfc455..85038b6b 100644 --- a/archives/2020/index.html +++ b/archives/2020/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2021/07/index.html b/archives/2021/07/index.html index 598b1839..388e006f 100644 --- a/archives/2021/07/index.html +++ b/archives/2021/07/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2021/12/index.html b/archives/2021/12/index.html index 01f04a33..062a2753 100644 --- a/archives/2021/12/index.html +++ b/archives/2021/12/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2021/index.html b/archives/2021/index.html index 65cc93c0..afde77e9 100644 --- a/archives/2021/index.html +++ b/archives/2021/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2022/02/index.html b/archives/2022/02/index.html index f507be33..6cdd1a82 100644 --- a/archives/2022/02/index.html +++ b/archives/2022/02/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2022/index.html b/archives/2022/index.html index 2044c723..001038d5 100644 --- a/archives/2022/index.html +++ b/archives/2022/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2023/01/index.html b/archives/2023/01/index.html index 4289cc13..5c683bff 100644 --- a/archives/2023/01/index.html +++ b/archives/2023/01/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2023/02/index.html b/archives/2023/02/index.html index d8d2af76..44ccb0ec 100644 --- a/archives/2023/02/index.html +++ b/archives/2023/02/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html index bdc3623e..fc5d54f4 100644 --- a/archives/2023/06/index.html +++ b/archives/2023/06/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2023/index.html b/archives/2023/index.html index 8db540c1..99852b9e 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2024/07/index.html b/archives/2024/07/index.html index 1f86887e..6c585737 100644 --- a/archives/2024/07/index.html +++ b/archives/2024/07/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/2024/index.html b/archives/2024/index.html index 6293c94f..0281e8eb 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/index.html b/archives/index.html index b87272c7..f37e57fa 100644 --- a/archives/index.html +++ b/archives/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 938a2973..86816b36 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/page/3/index.html b/archives/page/3/index.html index da91762d..c4267e9a 100644 --- a/archives/page/3/index.html +++ b/archives/page/3/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/page/4/index.html b/archives/page/4/index.html index 3ea37b61..19d98152 100644 --- a/archives/page/4/index.html +++ b/archives/page/4/index.html @@ -55,7 +55,7 @@ - + diff --git a/archives/page/5/index.html b/archives/page/5/index.html index fb24df65..5ccfdda8 100644 --- a/archives/page/5/index.html +++ b/archives/page/5/index.html @@ -55,7 +55,7 @@ - + diff --git a/categories/index.html b/categories/index.html index 8e75538f..b807e35e 100644 --- a/categories/index.html +++ b/categories/index.html @@ -55,7 +55,7 @@ - + diff --git a/css/font_4629291_x5qj7u7xj0n/demo_index.html b/css/font_4629291_x5qj7u7xj0n/demo_index.html index 81317d77..00e02241 100644 --- a/css/font_4629291_x5qj7u7xj0n/demo_index.html +++ b/css/font_4629291_x5qj7u7xj0n/demo_index.html @@ -61,7 +61,7 @@ - + diff --git a/en/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html b/en/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html index 16d2eaf7..8d5df7cf 100644 --- a/en/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html +++ b/en/2024/07/18/2024-7-18-slang-ast-hier-tree/index.html @@ -71,7 +71,7 @@ - + diff --git a/en/404.html b/en/404.html index 1a304f1c..588115ee 100644 --- a/en/404.html +++ b/en/404.html @@ -55,7 +55,7 @@ - + diff --git a/en/about/index.html b/en/about/index.html index b1c95146..f56b88c7 100644 --- a/en/about/index.html +++ b/en/about/index.html @@ -65,7 +65,7 @@ - + diff --git a/en/archives/2024/07/index.html b/en/archives/2024/07/index.html index f7d64487..00ef1982 100644 --- a/en/archives/2024/07/index.html +++ b/en/archives/2024/07/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/archives/2024/index.html b/en/archives/2024/index.html index d1a3200a..e7f35abf 100644 --- a/en/archives/2024/index.html +++ b/en/archives/2024/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/archives/index.html b/en/archives/index.html index 70049bf6..105a9598 100644 --- a/en/archives/index.html +++ b/en/archives/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/categories/index.html b/en/categories/index.html index 134c80b7..15a5c0b8 100644 --- a/en/categories/index.html +++ b/en/categories/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/css/font_4629291_x5qj7u7xj0n/demo_index.html b/en/css/font_4629291_x5qj7u7xj0n/demo_index.html index bfb8cc98..52f8764e 100644 --- a/en/css/font_4629291_x5qj7u7xj0n/demo_index.html +++ b/en/css/font_4629291_x5qj7u7xj0n/demo_index.html @@ -61,7 +61,7 @@ - + diff --git a/en/index.html b/en/index.html index 1a60abb3..c6aa27fc 100644 --- a/en/index.html +++ b/en/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/links/index.html b/en/links/index.html index 73fa2a27..e4338e90 100644 --- a/en/links/index.html +++ b/en/links/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/sitemap.xml b/en/sitemap.xml index d1aa29c7..1b14ddaf 100644 --- a/en/sitemap.xml +++ b/en/sitemap.xml @@ -2,7 +2,7 @@ - https://threelambda.com/en/css/font_4629291_x5qj7u7xj0n/iconfont.json + https://threelambda.com/en/css/font_4629291_x5qj7u7xj0n/demo_index.html 2024-07-23 @@ -11,7 +11,7 @@ - https://threelambda.com/en/css/font_4629291_x5qj7u7xj0n/demo_index.html + https://threelambda.com/en/css/font_4629291_x5qj7u7xj0n/iconfont.json 2024-07-23 diff --git a/en/tags/AST/index.html b/en/tags/AST/index.html index 8b38fcfc..6a90bb63 100644 --- a/en/tags/AST/index.html +++ b/en/tags/AST/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/tags/c/index.html b/en/tags/c/index.html index 0546cf46..e57d43a9 100644 --- a/en/tags/c/index.html +++ b/en/tags/c/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/tags/index.html b/en/tags/index.html index 889f65c5..e01e58f0 100644 --- a/en/tags/index.html +++ b/en/tags/index.html @@ -55,7 +55,7 @@ - + diff --git a/en/tags/tree/index.html b/en/tags/tree/index.html index a206480c..5d5da897 100644 --- a/en/tags/tree/index.html +++ b/en/tags/tree/index.html @@ -55,7 +55,7 @@ - + diff --git a/index.html b/index.html index ed488295..dcd104ae 100644 --- a/index.html +++ b/index.html @@ -55,7 +55,7 @@ - + diff --git a/links/index.html b/links/index.html index 1562be92..bee590cf 100644 --- a/links/index.html +++ b/links/index.html @@ -55,7 +55,7 @@ - + diff --git a/local-search.xml b/local-search.xml index 6e783b77..929986a4 100644 --- a/local-search.xml +++ b/local-search.xml @@ -244,10 +244,10 @@ - X.509 - certificate + X.509 + nginx openssl @@ -321,12 +321,12 @@ + docker + k8s ingress - docker - mac @@ -346,12 +346,12 @@ - docker - kafka container + docker + @@ -597,12 +597,12 @@ + python + leetcode 算法 - python - @@ -620,12 +620,12 @@ + python + leetcode 算法 - python - @@ -643,12 +643,12 @@ + python + leetcode 算法 - python - @@ -689,12 +689,12 @@ + python + leetcode 算法 - python - @@ -804,12 +804,12 @@ + python + leetcode 算法 - python - @@ -829,10 +829,10 @@ java - grails - gradle + grails + @@ -850,12 +850,12 @@ + python + leetcode 算法 - python - @@ -915,12 +915,12 @@ + python + leetcode 算法 - python - @@ -938,12 +938,12 @@ + python + leetcode 算法 - python - @@ -982,10 +982,10 @@ - python - R + python + @@ -1003,12 +1003,12 @@ + python + leetcode 算法 - python - @@ -1026,12 +1026,12 @@ + python + leetcode 算法 - python - @@ -1049,12 +1049,12 @@ + python + leetcode 算法 - python - diff --git a/page/2/index.html b/page/2/index.html index fe657be7..cb542748 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -55,7 +55,7 @@ - + @@ -285,10 +285,10 @@

diff --git a/page/3/index.html b/page/3/index.html index cfc69ec4..33e3bdcf 100644 --- a/page/3/index.html +++ b/page/3/index.html @@ -55,7 +55,7 @@ - + @@ -505,12 +505,12 @@

@@ -549,12 +549,12 @@

@@ -593,12 +593,12 @@

@@ -681,12 +681,12 @@

diff --git a/page/4/index.html b/page/4/index.html index b6527fc7..db3b2e19 100644 --- a/page/4/index.html +++ b/page/4/index.html @@ -55,7 +55,7 @@ - + @@ -461,12 +461,12 @@

@@ -507,10 +507,10 @@

#java - #grails - #gradle + #grails + @@ -549,12 +549,12 @@

@@ -677,12 +677,12 @@

diff --git a/page/5/index.html b/page/5/index.html index 4f5132a5..d049f84e 100644 --- a/page/5/index.html +++ b/page/5/index.html @@ -55,7 +55,7 @@ - + @@ -285,12 +285,12 @@

@@ -371,10 +371,10 @@

@@ -413,12 +413,12 @@

@@ -457,12 +457,12 @@

@@ -501,12 +501,12 @@

diff --git a/search.xml b/search.xml index ee638fc4..dc04aa11 100644 --- a/search.xml +++ b/search.xml @@ -1,5 +1,63 @@ + + R和Python里得到传入参数的变量名 + /2016/12/21/get-val-name-and-value/ + 问题

同事写R程序的时候,问我能不能获取一个变量的name, +我说这个好办啊,在R里可以这样写,用quote()

+
> a <- 1
+> quote(a)
+a
+
+

不过我把问题想简单了,他实际需要的是要获得传入 +参数的name。例如定义一个函数foo(c), 给它传入参数a,我能够在函数 +内部知道传入参数的名字a

+ + +

我说可以增加一个参数嘛。把参数的名字直接传入。

+
> foo(a,"a")
+
+

但是我立刻意识到,这里存在重复,而写代码是提倡 +do not repeat yourself的。而且对R这种可以随意获取环境变量的高级语言。 +肯定有办法做到。

+

python可以做到

但是我还是先看看Python吧,毕竟最近用Python +多一些。Google关键字python print variable name and value,立刻可以获得 +答案。这段代码的思想也很直白,就是对输入的obj在环境变量(名称,对象)键值对 +中查找,返回名称。

+
def namestr(obj, namespace=globals()):
+    return [name for name in namespace if namespace[name] is obj]
+
+

这样可以这样使用

+
>>> a = 1
+>>> namestr(a)
+['a']
+
+

R也可以

那么R肯定也有办法做到。Google关键字 +r print variable name and value

+
myfunc <- function(v1) {
+  deparse(substitute(v1))
+}
+
+myfunc(foo)
+[1] "foo"
+
+

真是很不错啊。把结果告诉了同事,他也很高兴。 +但是substitute()deparse()究竟是啥意思呢?在R里输入?substitute,可以 +看到解释。

+
+

substitute returns the parse tree for +the (unevaluated) expression expr, substituting any variables bound in env.

+
+

够晦涩难懂的吧。不过仔细思考一下,也大概明白了。 +在R里,从语法角度来说,所有都是expr,(这跟其他语言都是statement不同) +在R的函数里,可以对传入的未进行求值的expr进行操作。这点非常有趣。 +而deparse(),比较好理解了至少在我看来,与as.character()的作用是一样的。

+]]> + + R + python + + [leetcode 224 & 227]Basic Calculator I & II 原创解法 /2016/10/24/leetcode-224/ @@ -76,9 +134,48 @@ python默认的递归栈深度是10000。

如何把一个调用自身的递归改成非递归。

]]> + python leetcode 算法 + +
+ + 使用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 + 技巧 @@ -130,67 +227,9 @@ not unary) +, -, or * between the digits so they evaluate to the target value.
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)
]]> + python leetcode 算法 - python - -
- - R和Python里得到传入参数的变量名 - /2016/12/21/get-val-name-and-value/ - 问题

同事写R程序的时候,问我能不能获取一个变量的name, -我说这个好办啊,在R里可以这样写,用quote()

-
> a <- 1
-> quote(a)
-a
-
-

不过我把问题想简单了,他实际需要的是要获得传入 -参数的name。例如定义一个函数foo(c), 给它传入参数a,我能够在函数 -内部知道传入参数的名字a

- - -

我说可以增加一个参数嘛。把参数的名字直接传入。

-
> foo(a,"a")
-
-

但是我立刻意识到,这里存在重复,而写代码是提倡 -do not repeat yourself的。而且对R这种可以随意获取环境变量的高级语言。 -肯定有办法做到。

-

python可以做到

但是我还是先看看Python吧,毕竟最近用Python -多一些。Google关键字python print variable name and value,立刻可以获得 -答案。这段代码的思想也很直白,就是对输入的obj在环境变量(名称,对象)键值对 -中查找,返回名称。

-
def namestr(obj, namespace=globals()):
-    return [name for name in namespace if namespace[name] is obj]
-
-

这样可以这样使用

-
>>> a = 1
->>> namestr(a)
-['a']
-
-

R也可以

那么R肯定也有办法做到。Google关键字 -r print variable name and value

-
myfunc <- function(v1) {
-  deparse(substitute(v1))
-}
-
-myfunc(foo)
-[1] "foo"
-
-

真是很不错啊。把结果告诉了同事,他也很高兴。 -但是substitute()deparse()究竟是啥意思呢?在R里输入?substitute,可以 -看到解释。

-
-

substitute returns the parse tree for -the (unevaluated) expression expr, substituting any variables bound in env.

-
-

够晦涩难懂的吧。不过仔细思考一下,也大概明白了。 -在R里,从语法角度来说,所有都是expr,(这跟其他语言都是statement不同) -在R的函数里,可以对传入的未进行求值的expr进行操作。这点非常有趣。 -而deparse(),比较好理解了至少在我看来,与as.character()的作用是一样的。

-]]>
- - python - R
@@ -281,48 +320,9 @@ Return the array [2, 1, 1, 0]. 时间复杂度是O(N*logN),接下来的处理也是O(N*logN)

]]> + python 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 - 技巧
@@ -387,9 +387,9 @@ The three ranges are : [0, 0], [2, 2], [0, 2] and their respective sums are: -2, ]]> + python leetcode 算法 - python @@ -421,9 +421,9 @@ 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))
]]> + python leetcode 算法 - python
@@ -645,9 +645,51 @@ python的list而是使用一个自己创建的链表,那么插入 _heapify_max()但是却没有_heappushpop_max这样的方法。真是无语啊。

]]> + python leetcode 算法 + +
+ + [leetcode 312]Burst Balloons原创解法 + /2017/10/07/leetcode-312/ + 题目概述

312原题链接

+
Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.
+
+Find the maximum coins you can collect by bursting the balloons wisely.
+
+examples:
+nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
+coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167
+
+ + +

简单直接的解法

遍历回溯法,建立一个集合,把遍历过的数字放在该集合里。算法的复杂度是O(n!)。解法如下

+
class Solution1(object):
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
path = []
max_path = [path]
max = [0]
s = [0]
D = set()
self.find(nums, path, D, s, max, max_path)
print(max_path[0])
return max[0]

def find(self, nums, path, D, s, max, max_path):
if len(path) == len(nums):
if s[0] > max[0]:
max[0] = s[0]
max_path[0] = path[:]
else:
for i in range(len(nums)):
if i not in D:
path.append(i)
D.add(i)
tmp = self.compute(i, nums, D)
s[0] += tmp
self.find(nums, path, D, s, max, max_path)
path.pop()
D.remove(i)
s[0] -= tmp

def compute(self, i, nums, D):
left, right = 1, 1
for j in range(i - 1, -1, -1):
if j not in D:
left = nums[j]
break
for k in range(i + 1, len(nums)):
if k not in D:
right = nums[k]
break
return left * right * nums[i]
+ +

回顾矩阵链解法

以上的方法无疑会超时,但是它是我们理解问题,验证其它算法的基础。 +我们希望能够得到一个多项式时间的解法,按照以往的经验,我们如果能够找到一个贪心算法是最好的, +但是如果我们能够有一个贪心算法,那么也一定能够有一个动态规划方法。CRLS对贪心和动态规划做了清楚的 +描述,核心都是要找到最优子结构,并且证明存在最优子结构——这其实也是最难的部分,因为动态规划方法从来只是一个思想,本质上不过是 递归的一种优化而已

+

CRLS中在动态规划一章中用了几个例子来详细展示动态规划方法。其中第二个例子就是矩阵链问题————如果有一个 +矩阵链A1*A2*A3*A4,其中A1是1x5的矩阵,A2是5x1的矩阵,A3是1x5的矩阵,A4是5x1的矩阵。那么先计算A1*A2A3*A4 +则是比较好的,代价较小都是1x5x1=5。而A2*A3则是比较糟糕的,因为A2*A3的计算代价是5x1x5=25

+
+

对给定的两个矩阵相乘A_ik*A_kj, +计算的代价是i*k*j

+
+

等价转换

矩阵链中计算的代价是三个数相乘,而我们这里也是三个数字相乘。对于三个气球(数字)5·1·5,如果戳破第一个气球(数字),则得分(代价)是1x5x1,如果戳破第二个气球(数字) +得分(代价)是5x1x5,戳破第三个气球(数字)得分(代价)是1x5x1。对于1x5x1可以转换为1x5的矩阵和5x1的矩阵的代价。 +5x1x5,可以转换为5x1的矩阵和1x5的矩阵的代价。最终,三个数字5·1·5可以等价转换为求解四个矩阵 +A1*A2*A3*A4的最大计算代价的问题。所以第一步就是把n个数字转换为n+1个矩阵表示,第二步就是把矩阵链问题的递归求解中 +的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]
+]]>
+ python + leetcode + 算法
@@ -698,45 +740,49 @@ python的list而是使用一个自己创建的链表,那么插入 - [leetcode 312]Burst Balloons原创解法 - /2017/10/07/leetcode-312/ - 题目概述

312原题链接

-
Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.
-
-Find the maximum coins you can collect by bursting the balloons wisely.
+    [leetcode 329]Longest Increasing Path in a Matrix 原创解法
+    /2017/03/12/2017-3-12-leetcode-329/
+     题目概述

原题链接

+
Given an integer matrix, find the length of the longest increasing path 
+From each cell, you can either move to four directions: left, right, up or down. You may NOT move diagonally or move outside of the boundary (i.e. wrap-around is not allowed).
 
-examples:
-nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
-coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167
+example:
+nums = [
+  [9,9,4],
+  [6,6,8],
+  [2,1,1]
+]
+return 4
 
-

简单直接的解法

遍历回溯法,建立一个集合,把遍历过的数字放在该集合里。算法的复杂度是O(n!)。解法如下

-
class Solution1(object):
def maxCoins(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
path = []
max_path = [path]
max = [0]
s = [0]
D = set()
self.find(nums, path, D, s, max, max_path)
print(max_path[0])
return max[0]

def find(self, nums, path, D, s, max, max_path):
if len(path) == len(nums):
if s[0] > max[0]:
max[0] = s[0]
max_path[0] = path[:]
else:
for i in range(len(nums)):
if i not in D:
path.append(i)
D.add(i)
tmp = self.compute(i, nums, D)
s[0] += tmp
self.find(nums, path, D, s, max, max_path)
path.pop()
D.remove(i)
s[0] -= tmp

def compute(self, i, nums, D):
left, right = 1, 1
for j in range(i - 1, -1, -1):
if j not in D:
left = nums[j]
break
for k in range(i + 1, len(nums)):
if k not in D:
right = nums[k]
break
return left * right * nums[i]
+

回溯法

首先想到的就是回溯法(backtracking)。思路也很简单,就是遍历每个位置, +并以此位置的数字作为开始,向四个方向进行回溯遍历。最终找到最长的序列。 +以下代码的36行和38行进行回溯。不过毫无疑问超时了。在这个基础上还可以 +进一步优化,比如只对in-degree为0的位置的数字进行考察。但是还是不行。

+
class Solution(object):
def longestIncreasingPath(self, matrix):
"""
回溯法解法。
:type matrix: List[List[int]]
:rtype: int
"""

if matrix:
hight = len(matrix)
width = len(matrix[0])

s = set((u, v) for u in range(hight) for v in range(width))

result = [0]
for item in s:
p = [item]
self.find(matrix, item, p, result, s)

return result[0]

else:
return 0

def find(self, matrix, item, p, result, s):
if self.has_no_choice(matrix, item, p, s):
if (len(p) > result[0]):
result[0] = len(p)
else:
u, v = item
four = [(u + 1, v), (u - 1, v), (u, v + 1), (u, v - 1)]
for one in four:
x, y = one
if (x, y) in s and (x, y) not in p and matrix[x][y] < matrix[u][v]:
p.append((x, y))
self.find(matrix, (x, y), p, result, s)
p.pop()

def has_no_choice(self, matrix, item, p, s):
u, v = item
four = [(u + 1, v), (u - 1, v), (u, v + 1), (u, v - 1)]

for one in four:
x, y = one
if one in s and one not in p and matrix[x][y] < matrix[u][v]:
return False

return True
-

回顾矩阵链解法

以上的方法无疑会超时,但是它是我们理解问题,验证其它算法的基础。 -我们希望能够得到一个多项式时间的解法,按照以往的经验,我们如果能够找到一个贪心算法是最好的, -但是如果我们能够有一个贪心算法,那么也一定能够有一个动态规划方法。CRLS对贪心和动态规划做了清楚的 -描述,核心都是要找到最优子结构,并且证明存在最优子结构——这其实也是最难的部分,因为动态规划方法从来只是一个思想,本质上不过是 递归的一种优化而已

-

CRLS中在动态规划一章中用了几个例子来详细展示动态规划方法。其中第二个例子就是矩阵链问题————如果有一个 -矩阵链A1*A2*A3*A4,其中A1是1x5的矩阵,A2是5x1的矩阵,A3是1x5的矩阵,A4是5x1的矩阵。那么先计算A1*A2A3*A4 -则是比较好的,代价较小都是1x5x1=5。而A2*A3则是比较糟糕的,因为A2*A3的计算代价是5x1x5=25

-
-

对给定的两个矩阵相乘A_ik*A_kj, -计算的代价是i*k*j

-
-

等价转换

矩阵链中计算的代价是三个数相乘,而我们这里也是三个数字相乘。对于三个气球(数字)5·1·5,如果戳破第一个气球(数字),则得分(代价)是1x5x1,如果戳破第二个气球(数字) -得分(代价)是5x1x5,戳破第三个气球(数字)得分(代价)是1x5x1。对于1x5x1可以转换为1x5的矩阵和5x1的矩阵的代价。 -5x1x5,可以转换为5x1的矩阵和1x5的矩阵的代价。最终,三个数字5·1·5可以等价转换为求解四个矩阵 -A1*A2*A3*A4的最大计算代价的问题。所以第一步就是把n个数字转换为n+1个矩阵表示,第二步就是把矩阵链问题的递归求解中 -的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会给我们什么提示呢?点开show tags可以看到这个问题的标签, +Topological SortMemoization。既然使用topsort那么肯定要把这个问题 +转化为DAG问题了。如此一来,令人想到,可以先把矩阵数字使用topsort排成 +一排,然后再利用曾经遇到的求解最长递增子序列的动态规划算法进行求解。 +以下是实现的代码,拓扑排序加动态规划。

+
class Solution(object):
def longestIncreasingPath(self, matrix):
"""
:type matrix: List[List[int]]
:rtype: int
"""

if matrix:
hight = len(matrix)
width = len(matrix[0])

s = set((u, v) for u in range(hight) for v in range(width))
G = self.create_graph(matrix, s)
topsort_list = self.topsort(G)
result = self.find_longest_path(G, topsort_list)
return result

else:
return 0

def create_graph(self, matrix, s):
G = {}
for item in s:
u, v = item
G[item] = set()
center = matrix[u][v]
four = [(u + 1, v), (u - 1, v), (u, v + 1), (u, v - 1)]
for one in four:
if one in s:
x, y = one
adjacent = matrix[x][y]
if center > adjacent:
G[item].add(one)
return G

def tr(self, G):
H = {}
for u in G:
H[u] = 0

for u in G:
for v in G[u]:
H[v] += 1
return H

def topsort(self, G):

H = self.tr(G)
q = deque()
sorted_list = []
for u in H :
if H[u] == 0 :
q.append(u)
while q:
u = q.popleft()
sorted_list.append(u)
for v in G[u]:
H[v] -= 1
if H[v] == 0 :
q.append(v)

return sorted_list

def find_longest_path(self, G, topsort_list):
n = len(topsort_list)

memo = [1] * n
for i in range(1, n):
node = topsort_list[i]
value = 0
for pre in range(0, i):
pre_node = topsort_list[pre]
if node in G[pre_node]:
value = max(value, memo[pre] + 1)
else:
value = max(value, memo[pre])
memo[i] = value

result = max(memo)
#print(memo)
return result
+ +

进一步优化

上边的解法还是超时了,时间不会消耗在topsort上,因为这个方法是O(|V|+|E|)的。 +主要的时间花在了动态规划上,因为这是一个O(N^2)的算法。 +我们仔细看看第72到76行这个内循环,发现遍历所有当前节点之前的节点是无效的。 +因为可以进入当前节点的节点不会超过4个。所以我们可以修改一下这个内循环。

+
def tr2(self, G):
H = {}
for u in G:
H[u] = set()

for u in G:
for v in G[u]:
H[v].add(u)
return H

def find_longest_path(self, G, topsort_list):
n = len(topsort_list)

dic = { node : 1 for node in topsort_list }
H = self.tr2(G)

s = {topsort_list[0]}
for i in range(1, n):
node = topsort_list[i]
s.add(node)
value = 1
for pre_node in H[node] :
if pre_node in s :
value = max(value, dic[pre_node] + 1)
dic[node] = value

result = max(dic.values())
return result
+ +

这里我们在16行使用一个dict来代替之前的memo, +这样就可以把一个O(N^2)改成一个O(N)的了。 +测试一下,幸运的通过了。不过只打败了1.7%

]]>
+ python leetcode 算法 - python @@ -815,49 +861,38 @@ hello world - How to call java code in Grails 3.2.6 - /2017/03/06/2017-3-6-grails-call-java/ - 解决方法

我的Grails的环境是

-
grails -v
-| Grails Version: 3.2.6
-| Groovy Version: 2.4.7
-| JVM Version: 1.8.0_71
-
+ Gradle脚本实现web开发框架的一键构建 + /2017/05/24/2017-5-24-gradle-build-project/ + 问题

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

-

grails 3.2.6是用gradle进行构建的。所以如果要添加java类, -就需要修改build.gradle

-

第一步

创建src/main/java目录。对于com.yanggeorge.XMLtest类, -则要创建src/main/java/com/yanggeorge/目录,并把XMLtest.java放在该 -路径下。

-

第二步

build.gradle文件中添加如下代码

-
apply plugin: "java"

task compileOne (type: JavaCompile) {
source = sourceSets.main.java.srcDirs
include 'com/yanggeorge/XMLtest.java'
classpath = sourceSets.main.compileClasspath
destinationDir = sourceSets.main.output.classesDir
}

compileOne.options.compilerArgs = ["-sourcepath", "$projectDir/src/main/java"]
- -

第三步

编译XMLtest.java。可以用grails compile进行编译。

-
D:\work\grails>grails compile
-:compileJava UP-TO-DATE
-:compileGroovy UP-TO-DATE
-:buildProperties
-:processResources UP-TO-DATE
-:classes UP-TO-DATE
-
-BUILD SUCCESSFUL
-
-Total time: 23.638 secs
-D:\work\grails>
+

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

+

何谓一键构建

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

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

第四步

修改grails-app/conf/spring/resources.groovy

-
import com.yanggeorge.XMLtest

beans = {
myXMLtest(XMLtest)
}
- -

第五步

已经可以使用myXMLtest了,例如创建一个service,grails-app/services/rss/RssService.groovy -第6行就是依赖注入的bean。

-
import grails.transaction.Transactional

@Transactional
class RssService {

def myXMLtest

def serviceMethod(String url, String keyword) {
def items = myXMLtest.getAllItems(url)

}

}
- +

就可以在浏览器里访问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 - grails gradle + build @@ -915,52 +950,6 @@ Replace the swap UUID with the new one (run sudo blkid to find it) after the pri virutalbox - - [leetcode 329]Longest Increasing Path in a Matrix 原创解法 - /2017/03/12/2017-3-12-leetcode-329/ - 题目概述

原题链接

-
Given an integer matrix, find the length of the longest increasing path 
-From each cell, you can either move to four directions: left, right, up or down. You may NOT move diagonally or move outside of the boundary (i.e. wrap-around is not allowed).
-
-example:
-nums = [
-  [9,9,4],
-  [6,6,8],
-  [2,1,1]
-]
-return 4
-
- - -

回溯法

首先想到的就是回溯法(backtracking)。思路也很简单,就是遍历每个位置, -并以此位置的数字作为开始,向四个方向进行回溯遍历。最终找到最长的序列。 -以下代码的36行和38行进行回溯。不过毫无疑问超时了。在这个基础上还可以 -进一步优化,比如只对in-degree为0的位置的数字进行考察。但是还是不行。

-
class Solution(object):
def longestIncreasingPath(self, matrix):
"""
回溯法解法。
:type matrix: List[List[int]]
:rtype: int
"""

if matrix:
hight = len(matrix)
width = len(matrix[0])

s = set((u, v) for u in range(hight) for v in range(width))

result = [0]
for item in s:
p = [item]
self.find(matrix, item, p, result, s)

return result[0]

else:
return 0

def find(self, matrix, item, p, result, s):
if self.has_no_choice(matrix, item, p, s):
if (len(p) > result[0]):
result[0] = len(p)
else:
u, v = item
four = [(u + 1, v), (u - 1, v), (u, v + 1), (u, v - 1)]
for one in four:
x, y = one
if (x, y) in s and (x, y) not in p and matrix[x][y] < matrix[u][v]:
p.append((x, y))
self.find(matrix, (x, y), p, result, s)
p.pop()

def has_no_choice(self, matrix, item, p, s):
u, v = item
four = [(u + 1, v), (u - 1, v), (u, v + 1), (u, v - 1)]

for one in four:
x, y = one
if one in s and one not in p and matrix[x][y] < matrix[u][v]:
return False

return True
- -

拓扑排序和记忆体

看看leetcode会给我们什么提示呢?点开show tags可以看到这个问题的标签, -Topological SortMemoization。既然使用topsort那么肯定要把这个问题 -转化为DAG问题了。如此一来,令人想到,可以先把矩阵数字使用topsort排成 -一排,然后再利用曾经遇到的求解最长递增子序列的动态规划算法进行求解。 -以下是实现的代码,拓扑排序加动态规划。

-
class Solution(object):
def longestIncreasingPath(self, matrix):
"""
:type matrix: List[List[int]]
:rtype: int
"""

if matrix:
hight = len(matrix)
width = len(matrix[0])

s = set((u, v) for u in range(hight) for v in range(width))
G = self.create_graph(matrix, s)
topsort_list = self.topsort(G)
result = self.find_longest_path(G, topsort_list)
return result

else:
return 0

def create_graph(self, matrix, s):
G = {}
for item in s:
u, v = item
G[item] = set()
center = matrix[u][v]
four = [(u + 1, v), (u - 1, v), (u, v + 1), (u, v - 1)]
for one in four:
if one in s:
x, y = one
adjacent = matrix[x][y]
if center > adjacent:
G[item].add(one)
return G

def tr(self, G):
H = {}
for u in G:
H[u] = 0

for u in G:
for v in G[u]:
H[v] += 1
return H

def topsort(self, G):

H = self.tr(G)
q = deque()
sorted_list = []
for u in H :
if H[u] == 0 :
q.append(u)
while q:
u = q.popleft()
sorted_list.append(u)
for v in G[u]:
H[v] -= 1
if H[v] == 0 :
q.append(v)

return sorted_list

def find_longest_path(self, G, topsort_list):
n = len(topsort_list)

memo = [1] * n
for i in range(1, n):
node = topsort_list[i]
value = 0
for pre in range(0, i):
pre_node = topsort_list[pre]
if node in G[pre_node]:
value = max(value, memo[pre] + 1)
else:
value = max(value, memo[pre])
memo[i] = value

result = max(memo)
#print(memo)
return result
- -

进一步优化

上边的解法还是超时了,时间不会消耗在topsort上,因为这个方法是O(|V|+|E|)的。 -主要的时间花在了动态规划上,因为这是一个O(N^2)的算法。 -我们仔细看看第72到76行这个内循环,发现遍历所有当前节点之前的节点是无效的。 -因为可以进入当前节点的节点不会超过4个。所以我们可以修改一下这个内循环。

-
def tr2(self, G):
H = {}
for u in G:
H[u] = set()

for u in G:
for v in G[u]:
H[v].add(u)
return H

def find_longest_path(self, G, topsort_list):
n = len(topsort_list)

dic = { node : 1 for node in topsort_list }
H = self.tr2(G)

s = {topsort_list[0]}
for i in range(1, n):
node = topsort_list[i]
s.add(node)
value = 1
for pre_node in H[node] :
if pre_node in s :
value = max(value, dic[pre_node] + 1)
dic[node] = value

result = max(dic.values())
return result
- -

这里我们在16行使用一个dict来代替之前的memo, -这样就可以把一个O(N^2)改成一个O(N)的了。 -测试一下,幸运的通过了。不过只打败了1.7%

-]]>
- - leetcode - 算法 - python - -
Write a simple parser for MiniLisp by using JavaCC /2017/05/19/2017-5-19-javacc-minilisp/ @@ -1024,38 +1013,49 @@ MiniLisp项目下的life.lisp和nqueens.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的。

+ How to call java code in Grails 3.2.6 + /2017/03/06/2017-3-6-grails-call-java/ + 解决方法

我的Grails的环境是

+
grails -v
+| Grails Version: 3.2.6
+| Groovy Version: 2.4.7
+| JVM Version: 1.8.0_71
+
-

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

-

何谓一键构建

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

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

grails 3.2.6是用gradle进行构建的。所以如果要添加java类, +就需要修改build.gradle

+

第一步

创建src/main/java目录。对于com.yanggeorge.XMLtest类, +则要创建src/main/java/com/yanggeorge/目录,并把XMLtest.java放在该 +路径下。

+

第二步

build.gradle文件中添加如下代码

+
apply plugin: "java"

task compileOne (type: JavaCompile) {
source = sourceSets.main.java.srcDirs
include 'com/yanggeorge/XMLtest.java'
classpath = sourceSets.main.compileClasspath
destinationDir = sourceSets.main.output.classesDir
}

compileOne.options.compilerArgs = ["-sourcepath", "$projectDir/src/main/java"]
+ +

第三步

编译XMLtest.java。可以用grails compile进行编译。

+
D:\work\grails>grails compile
+:compileJava UP-TO-DATE
+:compileGroovy UP-TO-DATE
+:buildProperties
+:processResources UP-TO-DATE
+:classes UP-TO-DATE
+
+BUILD SUCCESSFUL
+
+Total time: 23.638 secs
+D:\work\grails>
 
-

就可以在浏览器里访问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)

}
}

+

第四步

修改grails-app/conf/spring/resources.groovy

+
import com.yanggeorge.XMLtest

beans = {
myXMLtest(XMLtest)
}
+ +

第五步

已经可以使用myXMLtest了,例如创建一个service,grails-app/services/rss/RssService.groovy +第6行就是依赖注入的bean。

+
import grails.transaction.Transactional

@Transactional
class RssService {

def myXMLtest

def serviceMethod(String url, String keyword) {
def items = myXMLtest.getAllItems(url)

}

}
+ ]]> java gradle - build + grails @@ -1169,9 +1169,9 @@ gradle run 我的这个解法会超时呢,结果居然击败的80%的提交。

]]> + python leetcode 算法 - python
@@ -1223,9 +1223,42 @@ Determine the maximum amount of money the thief can rob tonight without alerting

robber3_submit_result

]]> + python leetcode 算法 + +
+ + [leetcode 341]练练手 + /2018/08/18/2018-8-18-leetcode-341/ + 题目概述

原题链接

+
+

Given a nested list of integers, implement an iterator to flatten it.

+

Each element is either an integer, or a list – whose elements may also be integers or other lists. + Input: [[1,1],2,[1,1]] + Output: [1,1,2,1,1]

+
+ + +

解题思路

如果只是遍历一遍,用类似二叉树的中序遍历就好了。但是因为要生成迭代子,那么就得 +考虑用栈来模拟递归了。

+

当然这个题目是比较简单的,压栈的是一个tuple-(list, index)。第一个是list,第二个指向 +list的第几个item。

+

第一次通过的解法

比较慢。

+
class NestedIterator(object):
def __init__(self, nestedList):
"""
Initialize your data structure here.
:type nestedList: List[NestedInteger]
"""
self.q = [(nestedList, 0)]

def next(self):
"""
:rtype: int
"""

val = None
not_find = True
while self.q and not_find:
arr, num = self.q.pop()
if num < len(arr):
ele = arr[num]
if isinstance(ele, int): # 如果提交需要改成 ele.isInteger()
val = ele # ................ ele.getInteger()
not_find = False
self.q.append((arr, num + 1))
else:
self.q.append((arr, num + 1))
self.q.append((ele, 0)) # 如果提交改成,self.q.append((ele.getList(),0))

return None if not_find else val

def hasNext(self):
"""
:rtype: bool
"""
has_next = False
while not has_next and self.q:
arr, num = self.q.pop()
if num < len(arr):
ele = arr[num]
if isinstance(ele, int):
has_next = True
self.q.append((arr, num))
else:
self.q.append((arr, num + 1))
self.q.append((ele, 0))

return has_next

if __name__ == "__main__":
nestedlist = [[1, 2], 3, [4, 5], 6]
i, v = NestedIterator(nestedlist), []
while i.hasNext(): v.append(i.next())
print(v) # [1, 2, 3, 4, 5, 6]
+ +

优化后的解法

之前的解法大概是140ms跑通了44个测试。而大多数的解法则在70ms左右。 +怎么改进呢? +has_next和next方法本质上是一样的,可以只保留一个next就可以实现两个方法了。 +那么我们可以提前跑next方法,缓存两个值。

+

以下是改进后的速度提升了一倍。

+
class NestedIterator2(object):
def __init__(self, nestedList):
"""
Initialize your data structure here.
:type nestedList: List[NestedInteger]
"""
self.q = [(nestedList, 0)]
self.next_val = self.next0()
if self.next_val is not None :
self.has_next = True
else:
self.has_next = False

def next(self):
curr_val = self.next_val
if self.has_next:
self.next_val = self.next0()

if self.next_val is not None:
self.has_next = True
else:
self.has_next = False

return curr_val

def hasNext(self):
"""
:rtype: bool
"""
return self.has_next

def next0(self):
"""
:rtype: int
"""

val = None
not_find = True
while self.q and not_find:
arr, num = self.q.pop()
if num < len(arr):
ele = arr[num]
if isinstance(ele, int):
val = ele
not_find = False
self.q.append((arr, num + 1))
else:
self.q.append((arr, num + 1))
self.q.append((ele, 0))

return None if not_find else val
+ +]]>
+ python + leetcode + 算法
@@ -1294,39 +1327,6 @@ port = 6969 # get from tracker list p2p - - [leetcode 341]练练手 - /2018/08/18/2018-8-18-leetcode-341/ - 题目概述

原题链接

-
-

Given a nested list of integers, implement an iterator to flatten it.

-

Each element is either an integer, or a list – whose elements may also be integers or other lists. - Input: [[1,1],2,[1,1]] - Output: [1,1,2,1,1]

-
- - -

解题思路

如果只是遍历一遍,用类似二叉树的中序遍历就好了。但是因为要生成迭代子,那么就得 -考虑用栈来模拟递归了。

-

当然这个题目是比较简单的,压栈的是一个tuple-(list, index)。第一个是list,第二个指向 -list的第几个item。

-

第一次通过的解法

比较慢。

-
class NestedIterator(object):
def __init__(self, nestedList):
"""
Initialize your data structure here.
:type nestedList: List[NestedInteger]
"""
self.q = [(nestedList, 0)]

def next(self):
"""
:rtype: int
"""

val = None
not_find = True
while self.q and not_find:
arr, num = self.q.pop()
if num < len(arr):
ele = arr[num]
if isinstance(ele, int): # 如果提交需要改成 ele.isInteger()
val = ele # ................ ele.getInteger()
not_find = False
self.q.append((arr, num + 1))
else:
self.q.append((arr, num + 1))
self.q.append((ele, 0)) # 如果提交改成,self.q.append((ele.getList(),0))

return None if not_find else val

def hasNext(self):
"""
:rtype: bool
"""
has_next = False
while not has_next and self.q:
arr, num = self.q.pop()
if num < len(arr):
ele = arr[num]
if isinstance(ele, int):
has_next = True
self.q.append((arr, num))
else:
self.q.append((arr, num + 1))
self.q.append((ele, 0))

return has_next

if __name__ == "__main__":
nestedlist = [[1, 2], 3, [4, 5], 6]
i, v = NestedIterator(nestedlist), []
while i.hasNext(): v.append(i.next())
print(v) # [1, 2, 3, 4, 5, 6]
- -

优化后的解法

之前的解法大概是140ms跑通了44个测试。而大多数的解法则在70ms左右。 -怎么改进呢? -has_next和next方法本质上是一样的,可以只保留一个next就可以实现两个方法了。 -那么我们可以提前跑next方法,缓存两个值。

-

以下是改进后的速度提升了一倍。

-
class NestedIterator2(object):
def __init__(self, nestedList):
"""
Initialize your data structure here.
:type nestedList: List[NestedInteger]
"""
self.q = [(nestedList, 0)]
self.next_val = self.next0()
if self.next_val is not None :
self.has_next = True
else:
self.has_next = False

def next(self):
curr_val = self.next_val
if self.has_next:
self.next_val = self.next0()

if self.next_val is not None:
self.has_next = True
else:
self.has_next = False

return curr_val

def hasNext(self):
"""
:rtype: bool
"""
return self.has_next

def next0(self):
"""
:rtype: int
"""

val = None
not_find = True
while self.q and not_find:
arr, num = self.q.pop()
if num < len(arr):
ele = arr[num]
if isinstance(ele, int):
val = ele
not_find = False
self.q.append((arr, num + 1))
else:
self.q.append((arr, num + 1))
self.q.append((ele, 0))

return None if not_find else val
- -]]>
- - leetcode - 算法 - python - -
BitTorrent协议(四)之bitfield消息 /2019/01/21/2019-1-23-bt-4/ @@ -1415,6 +1415,157 @@ 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).

+
+

翻译为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
+ +

解码

根据eBNF实现的解码代码如下, 把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)

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 + protocol + p2p + +
+ + 使用delve调试K3s + /2019/11/19/debug-k3s/ + k3s是什么

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

+ + +

步骤

    +
  • 建议使用linux 64位操作系统。这样可以native构建。
  • +
  • 建议性能好一些的机器,虚拟机编译会很慢
  • +
  • 建议安装好docker
  • +
  • 配置好go的环境,设置GOPATH,同时把$GOPATH/bin加入到PATH
  • +
  • 安装Goland这个集成开发环境
  • +
  • 从github克隆k3s的代码,加上depth参数则不下载历史,速度会快很多
    $ git clone --depth 1 https://github.com/rancher/k3s.git $GOPATH/src/github.com/rancher/k3s
  • +
  • 安装delve的debug工具。完成之后会生成可执行文件$GOPATH/bin/dlv
    $ git clone --depth 1 https://github.com/go-delve/delve.git $GOPATH/src/github.com/go-delve/delve
    $ cd $GOPATH/src/github.com/go-delve/delve
    $ make install
  • +
  • 构建含有调试信息的可执行文件k3s,所在路径是$GOPATH/src/github.com/rancher/k3s/
    $ cd $GOPATH/src/github.com/rancher/k3s
    $ go build -gcflags "all=-N -l" -o k3s
  • +
  • 用delve执行。这时候线程会监听2345的远程调试接入。
    $ cd $GOPATH/src/github.com/rancher/k3s
    $ dlv --listen=:2345 --headless=true --api-version=2 exec -- ./k3s server --docker --disable-agent
  • +
  • Goland中添加k3s项目。项目根路径为$GOPATH,然后配置增加一个remote调试。在运行之前,在main.go上打一个断点。
  • +
+

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

+]]>
+ + k3s + delve + golang + +
+ + BitTorrent协议(六)之种子嗅探器 + /2019/02/18/2019-2-18-bt-6/ + Sniffer(嗅探器)

实现一个简单的BT种子嗅探器才算是有点实际价值的吧。这样就可以把种子的metadata信息 +缓存下来,提供按照文件名进行检索。

+ + +

不过这里只是实现一个demo,有兴趣的话可以看看github上的dht项目。

+

基本原理

简单的说,就是把嗅探节点加入到其他节点的路由表中,等待其他节点发来的announce_peer请求,然后获取种子的metadata信息。

+

我们根据以下的代码具体说明。

+
    +
  1. 89行代码,向路由节点发送find_node请求。这些路由节点。就是25行代码的3个地址。 +这样做的意义有两个,一方面,路由节点会把我们的节点加入到他们的路由表中。另一方面,路由节点会返回一个好节点列表。

    +
  2. +
  3. 252行代码,启动了6个线程。

    +
  4. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
线程说明
listener代码93行,监听所有接收到的udp数据,并把这些放入到队列recv_q
t_dispatch代码106行,从队列recv_q中取出数据,并进行bdecoding,根据消息类别分别放入对应的队列。一共有三种:应答,请求,错误。
t_hand_reply代码132行,从reply_q中取出数据,解码节点列表。每一个节点都插入到本地的路由表(这里不太好,最好先进行ping_node,确定是好节点再加入),并向其发送find_node消息
t_hand_query代码165行,从query_q中取出请求,分别对四种请求进行应答处理,分别是ping,find_node, get_peers, announce_peer。
t_hand_error错误消息的处理。
t_handle_metadata代码234行,从metadata_q中取出已经获取的种子的metadata,打印并把种子保存再字典表中。
+
    +
  1. 代码165行,t_hand_query线程执行对请求进行处理。
  2. +
+
    +
  • ping: 返回本地节点id
  • +
  • find_node: 从本地路由表中查出与请求节点id最近的8个节点,并返回。(据说把本地id进行返回,有利于本地节点加入其他路由表)
  • +
  • get_peers: 这里进行了简单处理,仅仅返回空节点list。
  • +
  • announce_peer: 这个请求是有节点告诉我们有新的种子文件发布了,并告诉我们info_hash。然后我们拿着info_hash取进行获取metadata的操作。 +代码207行,开启一个独立的获取metadata的线程。如果获取到metadata,则放入metadata_q中。
  • +
+
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__author__ = 'ym'
"""
Date : '2019/2/3'
Description :

"""

from ym.bt.routing_table import RoutingTable, BTNode
from ym.bt.util import id_generator, ip_me, decode_compact_node, encode_compact_node, format_size
from ym.bt.bencoding_bin import BEncode
from ym.bt.bdecoding import BDecode
from ym.bt.request_metadata import request_metadata_top
from queue import Queue
import socket
import threading
import logging

log = logging.getLogger()

NODES = [("router.bittorrent.com", 6881),
("router.utorrent.com", 6881),
("dht.transmissionbt.com", 6881)]

ID = id_generator()
SOURCE_IP = ip_me()
SOURCE_PORT = 6881
TIME_OUT = 1

INFO_HASH_METADATA_DIC = {}
RLOCK = threading.RLock()


def boot_step(sock: socket.socket, local_node: BTNode):
# send find_node query to NODES
try:
dic = {"t": "aa", "y": "q", "q": "find_node",
"a": {"id": local_node.id.hex(), "target": local_node.id.hex()}}
data = BEncode(dic).encode_bin()

for NODE in NODES:
dst_ip, dst_port = NODE
sock.sendto(data, (dst_ip, dst_port))
print("boot_step|send|dst_ip={}|dst_port={}".format(dst_ip, dst_port))
except Exception as e:
log.exception(e)
raise Exception("boot_step error")


def request_metadata_thread(peer_id: bytes, info_hash, ip, port, metadata_q: Queue):
try:
print("request_metadata_thread|info_hash={}|ip={}|port={}".format(info_hash, ip, port))
metadata_dic = request_metadata_top(peer_id, info_hash, ip, port)
if metadata_dic is not None:
metadata_q.put((metadata_dic, info_hash, ip, port))
except Exception as e:
log.exception("request_metadata_thread error")
raise Exception("request metadata error")


def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(True)
sock.bind((SOURCE_IP, SOURCE_PORT))

# recv queue
recv_q = Queue(maxsize=0)
# reply queue
reply_q = Queue(maxsize=0)
# query queue
query_q = Queue(maxsize=0)
# error queue
error_q = Queue(maxsize=0)
# request metadata
metadata_q = Queue(maxsize=0)

# declare routing table
rt = RoutingTable()

local_node = BTNode(id=ID, ip=SOURCE_IP, port=SOURCE_PORT)
print("local_node={}".format(local_node))
rt.insert(local_node)
# 1. boot step
print("boot start.")
boot_step(sock, local_node)
print("boot end.")

# 2. listener
def recv_listener(sock: socket.socket, recv_q: Queue):
while True:
try:
data, addr = sock.recvfrom(2048)
# print("recv_listener|recv|addr={}".format(addr))
recv_q.put((data, addr))
except Exception as e:
print("recv_listener error")

listener = threading.Thread(target=recv_listener, args=(sock, recv_q,))
listener.daemon = True

# 3. handle recv msg
def dispatch_recv(recv_q: Queue, reply_q: Queue, query_q: Queue, error_q: Queue):
while True:
data, addr = recv_q.get()
try:
dic, i, n = BDecode(data).parse()
# print("dispatch_recv|dic={}".format(dic))
if dic.get("y") is not None:
v = dic['y']
if v == 'r':
# reply msg
reply_q.put((dic, addr))
elif v == 'q':
# query msg
query_q.put((dic, addr))
elif v == 'e':
# error msg
error_q.put((dic, addr))
else:
print("unknown type msg.")
except Exception as e:
log.exception('dispatch_recv error|data={}'.format(data))

t_dispatch = threading.Thread(target=dispatch_recv, args=(recv_q, reply_q, query_q, error_q,))
t_dispatch.daemon = True

# 4. handle reply
def handle_reply(sock: socket.socket, reply_q: Queue, rt: RoutingTable):
while True:
dic, addr = reply_q.get()
try:
r = dic['r']
src_id = r['id']
src_ip, src_port = addr
src_node = BTNode(id=src_id, ip=src_ip, port=src_port)
rt.insert(src_node)
print("rt.size={}".format(rt.size()))
# print("handle_reply|src_ip={}|src_port={}".format(src_ip, src_port))
if r.get('nodes') is not None:
# ping every node and put good in rt.
nodes = decode_compact_node(r['nodes'])
dic = {"t": "aa", "y": "q", "q": "find_node",
"a": {"id": local_node.id.hex(), "target": local_node.id.hex()}}
data = BEncode(dic).encode_bin()
for node in nodes:
try:
dst_ip, dst_port, id = node
sock.sendto(data, (dst_ip, dst_port))
except Exception as e:
log.exception("send node error|node={}".format(node))
elif r.get('peers') is not None:
# ping every peer and put good in rt.
print("handle_reply|peers")
except Exception as e:
log.exception("handle_reply error|dic={}".format(dic), e)

t_hand_reply = threading.Thread(target=handle_reply, args=(sock, reply_q, rt,))
t_hand_reply.daemon = True

# 5. handle query
def handle_query(sock: socket.socket, query_q: Queue):
while True:
dic, addr = query_q.get()
dst_ip, dst_port = addr
try:
print("handle_query|addr={}|dic={}".format(addr, dic))
if dic['q'] == 'ping':
# handle ping query
reply = {"t": dic['t'], "y": "r", "r": {"id": local_node.id.hex()}}
data = BEncode(reply).encode_bin()
sock.sendto(data, (dst_ip, dst_port))
# print("handle_query|ping|send_reply|dst_ip={}|dst_port={}|data={}".format(dst_ip, dst_port, data))
elif dic['q'] == 'find_node':
# handle find_node query
nodes = rt.find_closer(bytes.fromhex(dic['a']['id']))
print("local_node.id={}".format(local_node.id.hex()))
print(nodes)
compact_node = encode_compact_node(nodes)
reply = {"t": dic['t'], "y": "r", "r": {"id": local_node.id.hex(), "nodes": compact_node}}
data = BEncode(reply).encode_bin()
sock.sendto(data, (dst_ip, dst_port))
print("handle_query|find_node|send_reply|dst_ip={}|dst_port={}|data={}".format(dst_ip, dst_port,
data))
elif dic['q'] == 'get_peers':
# handle get_peers query
reply = {"t": dic['t'], "y": "r",
"r": {"id": local_node.id.hex(), "token": "alenym".encode("utf8"), "nodes": b""}}
data = BEncode(reply).encode_bin()
sock.sendto(data, (dst_ip, dst_port))
print("handle_query|get_peers|send_reply|dst_ip={}|dst_port={}|data={}".format(dst_ip, dst_port,
data))
elif dic['q'] == 'announce_peer':
# handle announce_peer
print("handle_query|announce_peer|dic={}".format(dic))
info_hash = dic['a'].get('info_hash')
with RLOCK:
if info_hash is None or info_hash in INFO_HASH_METADATA_DIC:
continue

target_ip = dst_ip
target_port = dst_port

t_request_meta = threading.Thread(target=request_metadata_thread,
args=(
local_node.id, info_hash, target_ip, target_port, metadata_q))
t_request_meta.start()


else:
print("handle_query|other|dic={}".format(dic))
except Exception as e:
log.exception("handle_query error|dic={}".format(dic))

t_hand_query = threading.Thread(target=handle_query, args=(sock, query_q,))
t_hand_query.daemon = True

# 6. handle error
def handle_error(error_q: Queue):
while True:
dic, addr = error_q.get()
try:
print("handle_error|addr={}|dic={}".format(addr, dic))
except Exception as e:
log.exception('handle_error error', e)

t_hand_error = threading.Thread(target=handle_error, args=(error_q,))
t_hand_error.daemon = True

# 7. handle requested metadata
def handle_metadata(metadata_q: Queue):
while True:
try:
metadata_dic, info_hash, ip, port = metadata_q.get()
format_s = "handle_metadata|info_hash={}|ip={}|port={}|name={}|size={}"
file_name = metadata_dic['name']
size = format_size(metadata_dic.get('length'))
print(format_s.format(info_hash, ip, port, file_name, size))
with RLOCK:
if info_hash not in INFO_HASH_METADATA_DIC:
INFO_HASH_METADATA_DIC[info_hash] = metadata_dic
except Exception as e:
log.exception("handle_metadata error")


t_handle_metadata = threading.Thread(target=handle_metadata, args=(metadata_q,))
t_handle_metadata.daemon = True

listener.start()
t_dispatch.start()
t_hand_reply.start()
t_hand_query.start()
t_hand_error.start()
t_handle_metadata.start()

listener.join()
t_dispatch.join()
t_hand_reply.join()
t_hand_query.join()
t_hand_error.join()
t_handle_metadata.join()


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

运行日志

我们在一台有公网ip的电脑上运行十几分钟,过滤日志,查看接收到的种子信息如下,

+
root@ubuntu:~# cat log.txt | grep "handle_metadata"
handle_metadata|info_hash=5553330daa12bde6a2f71ab26f7b26688219f276|ip=69.80.12.126|port=11715|name=War for the Planet of the Apes 2017 1080p BluRay x264 DTS 5.1 MSubS-Hon3y|size=None
handle_metadata|info_hash=5556f3a9c605dd009f92fddb91848f565439e4f4|ip=59.169.228.207|port=62215|name=(C94) [くろすこスイッチ (くろすこ)] 冷泉さんといちゃいちゃする本 (ガールズ&パンツァー).zip|size=7.80M
handle_metadata|info_hash=555333edb2519c3aa93db4a150f6e021f5a138ff|ip=96.40.42.32|port=32796|name=Princess Go Round|size=None
handle_metadata|info_hash=572d4df79a7151d9466d371b960cfece91289bf4|ip=1.175.76.59|port=13283|name=(同人ゲーム) [181102][RJ237312][ピンポイント/キングピン] 妻が隠していたビデオ…~元カレ寝取らせ観察記~ DL版 (files).rar|size=1.08G
handle_metadata|info_hash=572edd8670066945538e4fe823294b45f2c2e3a3|ip=14.199.224.142|port=22979|name=0407raw021|size=None
handle_metadata|info_hash=574b27358fc1604e65babf32abe839267d36ba4b|ip=14.199.224.142|port=22979|name=o0Akrios0o@www.sexinsex.com@假裝租屋 事實上在陽台勾引住宅區人妻|size=None
handle_metadata|info_hash=54730eeeb5d74a58f49c6da72eb90be922556f0a|ip=219.98.7.166|port=18586|name=DSAM-29-DVD|size=None
handle_metadata|info_hash=57039c3ef3343c5b288c5ff219c66f837a88e808|ip=14.199.224.142|port=22979|name=tmem|size=None
handle_metadata|info_hash=5687578db92c7df4ae3ab7e33c895f04b1e27d37|ip=14.199.224.142|port=22979|name=GNE-148|size=None
handle_metadata|info_hash=57b832066b2103e29f3a4af256e25175da6dbdc2|ip=14.199.224.142|port=22979|name=[Thz.la]supa-154|size=None
handle_metadata|info_hash=56936a12cdb3a1a837c8faa206a2dea189dadf66|ip=14.199.224.142|port=22979|name=club-022_1.wmv|size=2.96G
handle_metadata|info_hash=570e89058161385b1d7dfadfcdc2d9f276ab829b|ip=14.199.224.142|port=22979|name=avidol.us-PTBI-026.wmv|size=2.71G
handle_metadata|info_hash=57c30a1ddecac71142208425c610837b961f4de6|ip=14.199.224.142|port=22979|name=[HD]sad-039.wmv|size=3.52G
handle_metadata|info_hash=56794273cf092e0b4f671885a16cf2dbe484f559|ip=14.199.224.142|port=22979|name=KRE-002.wmv|size=1.20G
handle_metadata|info_hash=5710c35d85a9a0ed8cadc10bdcd09db5bb0c46fd|ip=14.199.224.142|port=22979|name=0111-xv1088|size=None
handle_metadata|info_hash=561b55ec9a956f208f58b798d781bd5577a47b9e|ip=14.199.224.142|port=22979|name=52.R18-099|size=None
handle_metadata|info_hash=57c262437e7cf0ca5f24bff947757026f9cef9a8|ip=14.199.224.142|port=22979|name=DF-35976|size=None
handle_metadata|info_hash=565bedb7fc17d20f017a668e2931cb1d30b53556|ip=14.199.224.142|port=22979|name=judexkwok@片瀬まこ合集06|size=None
handle_metadata|info_hash=57e93ca1527cd35045f0ca75d0c96579dcf96d2a|ip=14.199.224.142|port=22979|name=SMDV-10-DVD|size=None
handle_metadata|info_hash=56747025a196cb78db9f94d8ca933677f57e54b0|ip=14.199.224.142|port=22979|name=HUNT-759.mp4|size=2.01G
handle_metadata|info_hash=56e45f76194dadb2c799c7f2c9b34bc2fff07cee|ip=14.199.224.142|port=22979|name=0510-sama538|size=None
handle_metadata|info_hash=57fae1a68ee9241a593c1623f64feb7927b40469|ip=14.199.224.142|port=22979|name=[thz.la]chunta-219|size=None
handle_metadata|info_hash=56b89964da08b923b20722b89ccfc6ae4928aca3|ip=14.199.224.142|port=22979|name=HUNT-665_2.mp4|size=1.60G
handle_metadata|info_hash=57528d8633ee8b823aea17e47def172082072dd5|ip=14.199.224.142|port=22979|name=HUNT|size=None
handle_metadata|info_hash=57b14ca4cf260763e170e515b1f235ae5dca487f|ip=14.199.224.142|port=22979|name=SAMA-477|size=None
handle_metadata|info_hash=57bc38a6f182dbd6be5b42371d645cc43cfa12e1|ip=14.199.224.142|port=22979|name=HUNT-710.mp4|size=1.82G
handle_metadata|info_hash=57f2b65672462ff81834a928cf8e9863687dc220|ip=14.199.224.142|port=22979|name=3208|size=None
...
+ +

有些种子文件为什么没有文件大小呢?例如size=None,

+
handle_metadata|info_hash=54730eeeb5d74a58f49c6da72eb90be922556f0a|ip=219.98.7.166|port=18586|name=DSAM-29-DVD|size=None
+ +

用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 @@ -2012,250 +2163,47 @@ Process finished with exit code 0
- BitTorrent协议(一)之解析种子文件 - /2019/01/09/2019-1-9-bt-1/ - bt种子文件

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

-

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

+ Java语言中bytes convert to string and back not equal + /2019/04/28/2019-4-28-byte-to-string-and-back/ + 问题

用google搜索关键词”java bytes to string and back not equal”,第一个就是我说的这个问题。 +什么意思呢?就是在java中,bytes转化为string之后,再转换回bytes的时候,发现不相同了。 +但是Go语言就没有这个问题哦

-

种子文件(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
+

运行如下java代码

+
byte[] a = new byte[]{(byte) 0xc0, (byte) 0xa8, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x38};
System.out.println(ByteBufUtil.hexDump(a));
String s = new String(a);
System.out.println(ByteBufUtil.hexDump(s.getBytes()));
-

如果想要找到下载源,就要通过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).

-
-

翻译为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
+

输出的结果如下:

+
c0a800010438
efbfbdefbfbd00010438
-

解码

根据eBNF实现的解码代码如下, 把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)

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

分析

bytes转化为string类型,本质上要选择一种编码。那么选择的是什么呢? +我们看看new String()执行的代码。通过跟踪,可以看到,使用了默认的编码。 +csn为UTF-8

+
static char[] decode(byte[] ba, int off, int len) {
String csn = Charset.defaultCharset().name();
try {
// use charset name decode() variant which provides caching.
return decode(csn, ba, off, len);
} catch (UnsupportedEncodingException x) {
warnUnsupportedCharset(csn);
}
try {
return decode("ISO-8859-1", ba, off, len);
} catch (UnsupportedEncodingException x) {
// If this code is hit during VM initialization, MessageUtils is
// the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 charset not available: "
+ x.toString());
// If we can not find ISO-8859-1 (a required encoding) then things
// are seriously wrong with the installation.
System.exit(1);
return null;
}
}
-

运行结果

用最近的毒液电影的种子进行解析,打印如下, -其中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

+

因为通常java编译的时候的默认编码是UTF-8。 +那么如何保证转化为字符串还能够转换回来呢?

+

一种方法是使用ISO-8859-1。例如,

+
byte[] a = new byte[]{(byte) 0xC0, (byte) 0xa8, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x38};
System.out.println(ByteBufUtil.hexDump(a));
String s = new String(a, Charset.forName("ISO-8859-1"));
System.out.println(ByteBufUtil.hexDump(s.getBytes(Charset.forName("ISO-8859-1"))));
+

结果是相等的。

+
c0a800010438
c0a800010438
+ +

Go语言没有这个问题

Go语言则对这个问题解决的非常好。

+
a := []byte{0xc0, 0xa8, 0x00, 0x01, 0x04, 0x38}
fmt.Println(a)
s := string(a)
b := []byte(s)
fmt.Println(b)
+

运行结果是相等的。

+
[192 168 0 1 4 56]
[192 168 0 1 4 56]
]]>
- bitTorrent - protocol - p2p + java + golang
- 使用delve调试K3s - /2019/11/19/debug-k3s/ - k3s是什么

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

- - -

步骤

    -
  • 建议使用linux 64位操作系统。这样可以native构建。
  • -
  • 建议性能好一些的机器,虚拟机编译会很慢
  • -
  • 建议安装好docker
  • -
  • 配置好go的环境,设置GOPATH,同时把$GOPATH/bin加入到PATH
  • -
  • 安装Goland这个集成开发环境
  • -
  • 从github克隆k3s的代码,加上depth参数则不下载历史,速度会快很多
    $ git clone --depth 1 https://github.com/rancher/k3s.git $GOPATH/src/github.com/rancher/k3s
  • -
  • 安装delve的debug工具。完成之后会生成可执行文件$GOPATH/bin/dlv
    $ git clone --depth 1 https://github.com/go-delve/delve.git $GOPATH/src/github.com/go-delve/delve
    $ cd $GOPATH/src/github.com/go-delve/delve
    $ make install
  • -
  • 构建含有调试信息的可执行文件k3s,所在路径是$GOPATH/src/github.com/rancher/k3s/
    $ cd $GOPATH/src/github.com/rancher/k3s
    $ go build -gcflags "all=-N -l" -o k3s
  • -
  • 用delve执行。这时候线程会监听2345的远程调试接入。
    $ cd $GOPATH/src/github.com/rancher/k3s
    $ dlv --listen=:2345 --headless=true --api-version=2 exec -- ./k3s server --docker --disable-agent
  • -
  • Goland中添加k3s项目。项目根路径为$GOPATH,然后配置增加一个remote调试。在运行之前,在main.go上打一个断点。
  • -
-

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

-]]>
- - k3s - delve - golang - -
- - BitTorrent协议(六)之种子嗅探器 - /2019/02/18/2019-2-18-bt-6/ - Sniffer(嗅探器)

实现一个简单的BT种子嗅探器才算是有点实际价值的吧。这样就可以把种子的metadata信息 -缓存下来,提供按照文件名进行检索。

- - -

不过这里只是实现一个demo,有兴趣的话可以看看github上的dht项目。

-

基本原理

简单的说,就是把嗅探节点加入到其他节点的路由表中,等待其他节点发来的announce_peer请求,然后获取种子的metadata信息。

-

我们根据以下的代码具体说明。

-
    -
  1. 89行代码,向路由节点发送find_node请求。这些路由节点。就是25行代码的3个地址。 -这样做的意义有两个,一方面,路由节点会把我们的节点加入到他们的路由表中。另一方面,路由节点会返回一个好节点列表。

    -
  2. -
  3. 252行代码,启动了6个线程。

    -
  4. -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
线程说明
listener代码93行,监听所有接收到的udp数据,并把这些放入到队列recv_q
t_dispatch代码106行,从队列recv_q中取出数据,并进行bdecoding,根据消息类别分别放入对应的队列。一共有三种:应答,请求,错误。
t_hand_reply代码132行,从reply_q中取出数据,解码节点列表。每一个节点都插入到本地的路由表(这里不太好,最好先进行ping_node,确定是好节点再加入),并向其发送find_node消息
t_hand_query代码165行,从query_q中取出请求,分别对四种请求进行应答处理,分别是ping,find_node, get_peers, announce_peer。
t_hand_error错误消息的处理。
t_handle_metadata代码234行,从metadata_q中取出已经获取的种子的metadata,打印并把种子保存再字典表中。
-
    -
  1. 代码165行,t_hand_query线程执行对请求进行处理。
  2. -
-
    -
  • ping: 返回本地节点id
  • -
  • find_node: 从本地路由表中查出与请求节点id最近的8个节点,并返回。(据说把本地id进行返回,有利于本地节点加入其他路由表)
  • -
  • get_peers: 这里进行了简单处理,仅仅返回空节点list。
  • -
  • announce_peer: 这个请求是有节点告诉我们有新的种子文件发布了,并告诉我们info_hash。然后我们拿着info_hash取进行获取metadata的操作。 -代码207行,开启一个独立的获取metadata的线程。如果获取到metadata,则放入metadata_q中。
  • -
-
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__author__ = 'ym'
"""
Date : '2019/2/3'
Description :

"""

from ym.bt.routing_table import RoutingTable, BTNode
from ym.bt.util import id_generator, ip_me, decode_compact_node, encode_compact_node, format_size
from ym.bt.bencoding_bin import BEncode
from ym.bt.bdecoding import BDecode
from ym.bt.request_metadata import request_metadata_top
from queue import Queue
import socket
import threading
import logging

log = logging.getLogger()

NODES = [("router.bittorrent.com", 6881),
("router.utorrent.com", 6881),
("dht.transmissionbt.com", 6881)]

ID = id_generator()
SOURCE_IP = ip_me()
SOURCE_PORT = 6881
TIME_OUT = 1

INFO_HASH_METADATA_DIC = {}
RLOCK = threading.RLock()


def boot_step(sock: socket.socket, local_node: BTNode):
# send find_node query to NODES
try:
dic = {"t": "aa", "y": "q", "q": "find_node",
"a": {"id": local_node.id.hex(), "target": local_node.id.hex()}}
data = BEncode(dic).encode_bin()

for NODE in NODES:
dst_ip, dst_port = NODE
sock.sendto(data, (dst_ip, dst_port))
print("boot_step|send|dst_ip={}|dst_port={}".format(dst_ip, dst_port))
except Exception as e:
log.exception(e)
raise Exception("boot_step error")


def request_metadata_thread(peer_id: bytes, info_hash, ip, port, metadata_q: Queue):
try:
print("request_metadata_thread|info_hash={}|ip={}|port={}".format(info_hash, ip, port))
metadata_dic = request_metadata_top(peer_id, info_hash, ip, port)
if metadata_dic is not None:
metadata_q.put((metadata_dic, info_hash, ip, port))
except Exception as e:
log.exception("request_metadata_thread error")
raise Exception("request metadata error")


def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(True)
sock.bind((SOURCE_IP, SOURCE_PORT))

# recv queue
recv_q = Queue(maxsize=0)
# reply queue
reply_q = Queue(maxsize=0)
# query queue
query_q = Queue(maxsize=0)
# error queue
error_q = Queue(maxsize=0)
# request metadata
metadata_q = Queue(maxsize=0)

# declare routing table
rt = RoutingTable()

local_node = BTNode(id=ID, ip=SOURCE_IP, port=SOURCE_PORT)
print("local_node={}".format(local_node))
rt.insert(local_node)
# 1. boot step
print("boot start.")
boot_step(sock, local_node)
print("boot end.")

# 2. listener
def recv_listener(sock: socket.socket, recv_q: Queue):
while True:
try:
data, addr = sock.recvfrom(2048)
# print("recv_listener|recv|addr={}".format(addr))
recv_q.put((data, addr))
except Exception as e:
print("recv_listener error")

listener = threading.Thread(target=recv_listener, args=(sock, recv_q,))
listener.daemon = True

# 3. handle recv msg
def dispatch_recv(recv_q: Queue, reply_q: Queue, query_q: Queue, error_q: Queue):
while True:
data, addr = recv_q.get()
try:
dic, i, n = BDecode(data).parse()
# print("dispatch_recv|dic={}".format(dic))
if dic.get("y") is not None:
v = dic['y']
if v == 'r':
# reply msg
reply_q.put((dic, addr))
elif v == 'q':
# query msg
query_q.put((dic, addr))
elif v == 'e':
# error msg
error_q.put((dic, addr))
else:
print("unknown type msg.")
except Exception as e:
log.exception('dispatch_recv error|data={}'.format(data))

t_dispatch = threading.Thread(target=dispatch_recv, args=(recv_q, reply_q, query_q, error_q,))
t_dispatch.daemon = True

# 4. handle reply
def handle_reply(sock: socket.socket, reply_q: Queue, rt: RoutingTable):
while True:
dic, addr = reply_q.get()
try:
r = dic['r']
src_id = r['id']
src_ip, src_port = addr
src_node = BTNode(id=src_id, ip=src_ip, port=src_port)
rt.insert(src_node)
print("rt.size={}".format(rt.size()))
# print("handle_reply|src_ip={}|src_port={}".format(src_ip, src_port))
if r.get('nodes') is not None:
# ping every node and put good in rt.
nodes = decode_compact_node(r['nodes'])
dic = {"t": "aa", "y": "q", "q": "find_node",
"a": {"id": local_node.id.hex(), "target": local_node.id.hex()}}
data = BEncode(dic).encode_bin()
for node in nodes:
try:
dst_ip, dst_port, id = node
sock.sendto(data, (dst_ip, dst_port))
except Exception as e:
log.exception("send node error|node={}".format(node))
elif r.get('peers') is not None:
# ping every peer and put good in rt.
print("handle_reply|peers")
except Exception as e:
log.exception("handle_reply error|dic={}".format(dic), e)

t_hand_reply = threading.Thread(target=handle_reply, args=(sock, reply_q, rt,))
t_hand_reply.daemon = True

# 5. handle query
def handle_query(sock: socket.socket, query_q: Queue):
while True:
dic, addr = query_q.get()
dst_ip, dst_port = addr
try:
print("handle_query|addr={}|dic={}".format(addr, dic))
if dic['q'] == 'ping':
# handle ping query
reply = {"t": dic['t'], "y": "r", "r": {"id": local_node.id.hex()}}
data = BEncode(reply).encode_bin()
sock.sendto(data, (dst_ip, dst_port))
# print("handle_query|ping|send_reply|dst_ip={}|dst_port={}|data={}".format(dst_ip, dst_port, data))
elif dic['q'] == 'find_node':
# handle find_node query
nodes = rt.find_closer(bytes.fromhex(dic['a']['id']))
print("local_node.id={}".format(local_node.id.hex()))
print(nodes)
compact_node = encode_compact_node(nodes)
reply = {"t": dic['t'], "y": "r", "r": {"id": local_node.id.hex(), "nodes": compact_node}}
data = BEncode(reply).encode_bin()
sock.sendto(data, (dst_ip, dst_port))
print("handle_query|find_node|send_reply|dst_ip={}|dst_port={}|data={}".format(dst_ip, dst_port,
data))
elif dic['q'] == 'get_peers':
# handle get_peers query
reply = {"t": dic['t'], "y": "r",
"r": {"id": local_node.id.hex(), "token": "alenym".encode("utf8"), "nodes": b""}}
data = BEncode(reply).encode_bin()
sock.sendto(data, (dst_ip, dst_port))
print("handle_query|get_peers|send_reply|dst_ip={}|dst_port={}|data={}".format(dst_ip, dst_port,
data))
elif dic['q'] == 'announce_peer':
# handle announce_peer
print("handle_query|announce_peer|dic={}".format(dic))
info_hash = dic['a'].get('info_hash')
with RLOCK:
if info_hash is None or info_hash in INFO_HASH_METADATA_DIC:
continue

target_ip = dst_ip
target_port = dst_port

t_request_meta = threading.Thread(target=request_metadata_thread,
args=(
local_node.id, info_hash, target_ip, target_port, metadata_q))
t_request_meta.start()


else:
print("handle_query|other|dic={}".format(dic))
except Exception as e:
log.exception("handle_query error|dic={}".format(dic))

t_hand_query = threading.Thread(target=handle_query, args=(sock, query_q,))
t_hand_query.daemon = True

# 6. handle error
def handle_error(error_q: Queue):
while True:
dic, addr = error_q.get()
try:
print("handle_error|addr={}|dic={}".format(addr, dic))
except Exception as e:
log.exception('handle_error error', e)

t_hand_error = threading.Thread(target=handle_error, args=(error_q,))
t_hand_error.daemon = True

# 7. handle requested metadata
def handle_metadata(metadata_q: Queue):
while True:
try:
metadata_dic, info_hash, ip, port = metadata_q.get()
format_s = "handle_metadata|info_hash={}|ip={}|port={}|name={}|size={}"
file_name = metadata_dic['name']
size = format_size(metadata_dic.get('length'))
print(format_s.format(info_hash, ip, port, file_name, size))
with RLOCK:
if info_hash not in INFO_HASH_METADATA_DIC:
INFO_HASH_METADATA_DIC[info_hash] = metadata_dic
except Exception as e:
log.exception("handle_metadata error")


t_handle_metadata = threading.Thread(target=handle_metadata, args=(metadata_q,))
t_handle_metadata.daemon = True

listener.start()
t_dispatch.start()
t_hand_reply.start()
t_hand_query.start()
t_hand_error.start()
t_handle_metadata.start()

listener.join()
t_dispatch.join()
t_hand_reply.join()
t_hand_query.join()
t_hand_error.join()
t_handle_metadata.join()


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

运行日志

我们在一台有公网ip的电脑上运行十几分钟,过滤日志,查看接收到的种子信息如下,

-
root@ubuntu:~# cat log.txt | grep "handle_metadata"
handle_metadata|info_hash=5553330daa12bde6a2f71ab26f7b26688219f276|ip=69.80.12.126|port=11715|name=War for the Planet of the Apes 2017 1080p BluRay x264 DTS 5.1 MSubS-Hon3y|size=None
handle_metadata|info_hash=5556f3a9c605dd009f92fddb91848f565439e4f4|ip=59.169.228.207|port=62215|name=(C94) [くろすこスイッチ (くろすこ)] 冷泉さんといちゃいちゃする本 (ガールズ&パンツァー).zip|size=7.80M
handle_metadata|info_hash=555333edb2519c3aa93db4a150f6e021f5a138ff|ip=96.40.42.32|port=32796|name=Princess Go Round|size=None
handle_metadata|info_hash=572d4df79a7151d9466d371b960cfece91289bf4|ip=1.175.76.59|port=13283|name=(同人ゲーム) [181102][RJ237312][ピンポイント/キングピン] 妻が隠していたビデオ…~元カレ寝取らせ観察記~ DL版 (files).rar|size=1.08G
handle_metadata|info_hash=572edd8670066945538e4fe823294b45f2c2e3a3|ip=14.199.224.142|port=22979|name=0407raw021|size=None
handle_metadata|info_hash=574b27358fc1604e65babf32abe839267d36ba4b|ip=14.199.224.142|port=22979|name=o0Akrios0o@www.sexinsex.com@假裝租屋 事實上在陽台勾引住宅區人妻|size=None
handle_metadata|info_hash=54730eeeb5d74a58f49c6da72eb90be922556f0a|ip=219.98.7.166|port=18586|name=DSAM-29-DVD|size=None
handle_metadata|info_hash=57039c3ef3343c5b288c5ff219c66f837a88e808|ip=14.199.224.142|port=22979|name=tmem|size=None
handle_metadata|info_hash=5687578db92c7df4ae3ab7e33c895f04b1e27d37|ip=14.199.224.142|port=22979|name=GNE-148|size=None
handle_metadata|info_hash=57b832066b2103e29f3a4af256e25175da6dbdc2|ip=14.199.224.142|port=22979|name=[Thz.la]supa-154|size=None
handle_metadata|info_hash=56936a12cdb3a1a837c8faa206a2dea189dadf66|ip=14.199.224.142|port=22979|name=club-022_1.wmv|size=2.96G
handle_metadata|info_hash=570e89058161385b1d7dfadfcdc2d9f276ab829b|ip=14.199.224.142|port=22979|name=avidol.us-PTBI-026.wmv|size=2.71G
handle_metadata|info_hash=57c30a1ddecac71142208425c610837b961f4de6|ip=14.199.224.142|port=22979|name=[HD]sad-039.wmv|size=3.52G
handle_metadata|info_hash=56794273cf092e0b4f671885a16cf2dbe484f559|ip=14.199.224.142|port=22979|name=KRE-002.wmv|size=1.20G
handle_metadata|info_hash=5710c35d85a9a0ed8cadc10bdcd09db5bb0c46fd|ip=14.199.224.142|port=22979|name=0111-xv1088|size=None
handle_metadata|info_hash=561b55ec9a956f208f58b798d781bd5577a47b9e|ip=14.199.224.142|port=22979|name=52.R18-099|size=None
handle_metadata|info_hash=57c262437e7cf0ca5f24bff947757026f9cef9a8|ip=14.199.224.142|port=22979|name=DF-35976|size=None
handle_metadata|info_hash=565bedb7fc17d20f017a668e2931cb1d30b53556|ip=14.199.224.142|port=22979|name=judexkwok@片瀬まこ合集06|size=None
handle_metadata|info_hash=57e93ca1527cd35045f0ca75d0c96579dcf96d2a|ip=14.199.224.142|port=22979|name=SMDV-10-DVD|size=None
handle_metadata|info_hash=56747025a196cb78db9f94d8ca933677f57e54b0|ip=14.199.224.142|port=22979|name=HUNT-759.mp4|size=2.01G
handle_metadata|info_hash=56e45f76194dadb2c799c7f2c9b34bc2fff07cee|ip=14.199.224.142|port=22979|name=0510-sama538|size=None
handle_metadata|info_hash=57fae1a68ee9241a593c1623f64feb7927b40469|ip=14.199.224.142|port=22979|name=[thz.la]chunta-219|size=None
handle_metadata|info_hash=56b89964da08b923b20722b89ccfc6ae4928aca3|ip=14.199.224.142|port=22979|name=HUNT-665_2.mp4|size=1.60G
handle_metadata|info_hash=57528d8633ee8b823aea17e47def172082072dd5|ip=14.199.224.142|port=22979|name=HUNT|size=None
handle_metadata|info_hash=57b14ca4cf260763e170e515b1f235ae5dca487f|ip=14.199.224.142|port=22979|name=SAMA-477|size=None
handle_metadata|info_hash=57bc38a6f182dbd6be5b42371d645cc43cfa12e1|ip=14.199.224.142|port=22979|name=HUNT-710.mp4|size=1.82G
handle_metadata|info_hash=57f2b65672462ff81834a928cf8e9863687dc220|ip=14.199.224.142|port=22979|name=3208|size=None
...
- -

有些种子文件为什么没有文件大小呢?例如size=None,

-
handle_metadata|info_hash=54730eeeb5d74a58f49c6da72eb90be922556f0a|ip=219.98.7.166|port=18586|name=DSAM-29-DVD|size=None
- -

用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 - -
- - Java语言中bytes convert to string and back not equal - /2019/04/28/2019-4-28-byte-to-string-and-back/ - 问题

用google搜索关键词”java bytes to string and back not equal”,第一个就是我说的这个问题。 -什么意思呢?就是在java中,bytes转化为string之后,再转换回bytes的时候,发现不相同了。 -但是Go语言就没有这个问题哦

- - -

运行如下java代码

-
byte[] a = new byte[]{(byte) 0xc0, (byte) 0xa8, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x38};
System.out.println(ByteBufUtil.hexDump(a));
String s = new String(a);
System.out.println(ByteBufUtil.hexDump(s.getBytes()));
- -

输出的结果如下:

-
c0a800010438
efbfbdefbfbd00010438
- -

分析

bytes转化为string类型,本质上要选择一种编码。那么选择的是什么呢? -我们看看new String()执行的代码。通过跟踪,可以看到,使用了默认的编码。 -csn为UTF-8

-
static char[] decode(byte[] ba, int off, int len) {
String csn = Charset.defaultCharset().name();
try {
// use charset name decode() variant which provides caching.
return decode(csn, ba, off, len);
} catch (UnsupportedEncodingException x) {
warnUnsupportedCharset(csn);
}
try {
return decode("ISO-8859-1", ba, off, len);
} catch (UnsupportedEncodingException x) {
// If this code is hit during VM initialization, MessageUtils is
// the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 charset not available: "
+ x.toString());
// If we can not find ISO-8859-1 (a required encoding) then things
// are seriously wrong with the installation.
System.exit(1);
return null;
}
}
- -

因为通常java编译的时候的默认编码是UTF-8。 -那么如何保证转化为字符串还能够转换回来呢?

-

一种方法是使用ISO-8859-1。例如,

-
byte[] a = new byte[]{(byte) 0xC0, (byte) 0xa8, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x38};
System.out.println(ByteBufUtil.hexDump(a));
String s = new String(a, Charset.forName("ISO-8859-1"));
System.out.println(ByteBufUtil.hexDump(s.getBytes(Charset.forName("ISO-8859-1"))));
-

结果是相等的。

-
c0a800010438
c0a800010438
- -

Go语言没有这个问题

Go语言则对这个问题解决的非常好。

-
a := []byte{0xc0, 0xa8, 0x00, 0x01, 0x04, 0x38}
fmt.Println(a)
s := string(a)
b := []byte(s)
fmt.Println(b)
-

运行结果是相等的。

-
[192 168 0 1 4 56]
[192 168 0 1 4 56]
-]]>
- - java - golang - -
- - 在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

-]]>
- - k8s - ingress - docker - mac - -
- - docker容器内访问mac主机的kafka - /2020/03/09/connect-kafka/ - 从容器内访问主机的kafka

我最近遇到这样一个需求,需要从容器内的ClickHouse访问安装在mac主机的kafka。这个问题似乎很简单, -因为在windows上,虚拟机可以和host组成一个局域网,因此kafka只要绑定此网段的ip地址即可。 -但是在我的mac主机下,这个方案行不通。

+ docker容器内访问mac主机的kafka + /2020/03/09/connect-kafka/ + 从容器内访问主机的kafka

我最近遇到这样一个需求,需要从容器内的ClickHouse访问安装在mac主机的kafka。这个问题似乎很简单, +因为在windows上,虚拟机可以和host组成一个局域网,因此kafka只要绑定此网段的ip地址即可。 +但是在我的mac主机下,这个方案行不通。

有几个原因,

@@ -2317,9 +2265,9 @@ csn为UTF-8

^Croot@bb6b85562200:/# kafkacat -b host.docker.internal:9092 -C -t flink-test -o beginning
{seqNo: 1, eventTs: 1583739799986, id: even偶数, value: 4.57}
% Reached end of topic flink-test [0] at offset 1
]]>
- docker kafka container + docker
@@ -2350,6 +2298,58 @@ csn为UTF-8

file
+ + 在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 + +
基于CLion和gdbserver实现远程调试c程序 /2020/09/08/remote-debug-with-clion/ @@ -2399,142 +2399,57 @@ remote path为/home/keyvalue/ym/operation,local path为/Use - k3s的Pod无法解析内网域名 - /2021/12/26/pod-resolve-dn/ - 问题

jetson tx2开发板上安装了dockerk3s,部署了一个pod,发现日志报错

-
"dial tcp: lookup esmp-cloud-sync.dev.ennew.com on 10.43.0.10:53: no such host"
-

其中esmp-cloud-sync.dev.ennew.com是内网域名,说明pod无法解析该域名。

+ 自建根证书,中间证书和Server端X.509证书并搭建nginx验证Server端证书有效性 + /2020/10/09/x509-ca/ + X.509证书的颁发和使用

X.509证书是用来认证身份的,例如在访问一个HTTPS网站的时候,浏览器会首先下载该网站 +的证书,验证是否有效。如果无效,浏览器会提示您的连接不是私密连接,进一步访问网站有风险。 +如果有效则可以直接访问,不会告警。

+

浏览器怎么验证网站证书是否有效呢?简单说就是看网站证书的颁发机构是不是已经被操作系统信任,即看 +颁发机构的身份证书是否已经安装到操作系统里,并被设置为信任。

+

那么我们接下来做的事情就是在mac系统上验证浏览器的上述行为。

-

但是宿主机上能ping通该域名。

-

环境

    -
  • os : ubuntu18.04
  • -
  • k3s: v1.18.9+k3s1
  • -
  • cpu arch : arm64
  • -
  • coreDNS: 1.6.9
  • +

    验证步骤,

    +
      +
    1. 生成根证书,中间证书和Server端证书(即用户证书)
    2. +
    3. 本机搭建nginx服务器,并设置Server端证书(!!注意此时未安装根证书到操作系统)
    4. +
    5. 浏览器访问https://localhost,出现告警提示
    6. +
    7. 把根证书和中间证书安装到操作系统,并设置根证书为永久信任
    8. +
    9. 重新访问https://localhost,正常,没有出现告警。
    10. +
    +

    第一步,生成根证书,中间证书和Server端证书

    颁发机构通常会有根机构和中间机构,根机构的证书自己签发自己,中间机构的证书由根机构签发,而Server端证书由中间机构签发。本人参考以下三篇文章创建了根证书,中间证书和Server端证书。

    + -

    排查过程

    首先检查宿主机的域名服务器设置。 -如果不正常,则需要设置正确的域名服务器。并重启k3s服务和pod。 -如果正常,则进一步检查k3s的DNS设置。而k3s的网络域名解析是由coreDNS控制的。

    -

    查看宿主机的DNS设置

    宿主机的DNS设置,查看/etc/resolv.conf

    -
    $ cat /etc/resolv.conf 
    -# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
    -#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
    -# 127.0.0.53 is the systemd-resolved stub resolver.
    -# run "systemd-resolve --status" to see details about the actual nameservers.
    +

    不过需要注意的是,Server端证书有效期不要太长,否则即使安装了根证书也会告警

    +

    第二步,本机搭建nginx服务器,并设置Server端证书

    mac下安装nginx很简单

    +
    brew install nginx
    +

    然后修改/usr/local/etc/nginx/nginx.conf文件,在server里添加三行

    +
    listen       443    ssl;
    ssl_certificate /Users/ym/tmp/myca/localhost/certs/localhost.cert.pem;
    ssl_certificate_key /Users/ym/tmp/myca/localhost/private/localhost.key.pem;
    -nameserver 127.0.0.53 -search addom.xinaogroup.com -
    -

    运行”systemd-resolve –status”查看实际的nameservers。

    -
    $ systemd-resolve --status
    -...
    -      DNS Servers: 10.36.8.40
    -                    10.36.8.41
    -                    127.0.0.1
    -        DNS Domain: ~.
    -                    addom.xinaogroup.com 
    -...
    -
    -

    可以看到实际的域名服务器。pod里无法ping通的域名esmp-cloud-sync.dev.ennew.com,在宿主机环境是可以的。

    -

    查看pod的DNS设置

    pod的DNS设置是由CoreDNS控制的。但是进入CoreDNS的pod,使用kubectl exec是不行的。需要使用边车模式, -先查看运行CoreDNS的容器的ID,然后用docker再启动一个容器。(因为k3s server是基于dockerd运行的,所以 -可以用docker ps查看CoreDNS pod里的容器)

    -
    docker ps | grep coredns
    -ID=8afb33b91c9f
    -docker run -it --net=container:$ID --pid=container:$ID --volumes-from=$ID alpine sh
    -
    -

    然后就可以查看CoreDNS的corefile配置文件

    -
    # cat /etc/coredns/Corefile 
    -.:53 {
    -    errors
    -    health
    -    ready
    -    kubernetes cluster.local in-addr.arpa ip6.arpa {
    -      pods insecure
    -      upstream
    -      fallthrough in-addr.arpa ip6.arpa
    -    }
    -    hosts /etc/coredns/NodeHosts {
    -      reload 1s
    -      fallthrough
    -    }
    -    prometheus :9153
    -    forward . /etc/resolv.conf
    -    cache 30
    -    loop
    -    reload
    -    loadbalance
    -}
    -
    -

    forward . /etc/resolv.conf 可以看出,由/etc/resolv.conf文件接管DNS设置。进一步查看该文件,

    -
      # cat /etc/resolv.conf 
    -  nameserver 8.8.8.8
    -
    -

    可以发现这里设置的nameserver8.8.8.8,而不是10.36.8.40。因此在pod里虽然可以ping通公网域名 -,例如www.baidu.com,但是无法ping通内网域名esmp-cloud-sync.dev.ennew.com

    -

    另外查看CoreDNS的pod日志也可以看到无法解析内网域名esmp-cloud-sync.dev.ennew.com的错误。

    -

    分析问题

    从排查结果可以看到,主要问题在于CoreDNS的DSN设置与宿主机的DNS设置不同,导致解析内网域名解析失败。这一点比较奇怪,通常k3s默认 -会继承宿主机的DNS设置。

    -

    也就是pod里的/etc/resolv.conf,没有与宿主机的/etc/resolv.conf的内容一致。

    -

    再回头看看我们要解决的问题——pod里解析内网域名,那么最直接的方案就是修改CoreDNS的Corefile, -修改forward . /etc/resolv.confforward . 10.36.8.40

    -

    但是这种方法的缺点是显而易见的,写死了。 我们还是希望找到一个方法能够令/etc/resolv.conf与宿主机的/etc/resolv.conf的内容一致

    -

    解决方法

    宿主机的/etc/resolv.conf文件是一个指向 /run/systemd/resolve/resolv.conf的软链接,查看该文件的内容

    -
    $ cat /run/systemd/resolve/resolv.conf
    -# This file is managed by man:systemd-resolved(8). Do not edit.
    -#
    -# This is a dynamic resolv.conf file for connecting local clients directly to
    -# all known uplink DNS servers. This file lists all configured search domains.
    -#
    -# Third party programs must not access this file directly, but only through the
    -# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
    -# replace this symlink by a static file or a different symlink.
    -#
    -# See man:systemd-resolved.service(8) for details about the supported modes of
    -# operation for /etc/resolv.conf.
    +

    启动nginx

    +
    brew sevices start nginx 
    -nameserver 10.36.8.40 -nameserver 10.36.8.41 -nameserver 127.0.0.1 -search addom.xinaogroup.com -
    -

    可以看到含有正确的nameservers。

    -

    另外 k3s 有启动参数--resolv-conf,可以指定默认的resolv.conf

    -

    修改/etc/systemd/system/k3s.service,增加启动参数--resolv-conf /run/systemd/resolve/resolv.conf

    -
    ...
    -ExecStart=/usr/local/bin/k3s \
    -server \
    -'--docker' \
    -'--write-kubeconfig' \
    -'/home/tx2/.kube/config' \
    -'--write-kubeconfig-mode' \
    -'666' \
    -'--resolv-conf' \
    -'/run/systemd/resolve/resolv.conf'
    -
    -

    然后重新启动k3s,并且删除CoreDNS的pod(kubectl delete pod -n kube-system coredns-xxxx),令其自动创建一个新的pod。

    -

    这时候再查看CoreDNS pod里的/etc/resolv.conf, 内容一致了,

    -
    / # cat /etc/resolv.conf
    -nameserver 10.36.8.40
    -nameserver 10.36.8.41
    -nameserver 127.0.0.1
    -search addom.xinaogroup.com
    -
    -

    问题解决了。

    -

    参考

    +

    第三步,浏览器访问https://localhost

    显示告警信息 +invalid-certificate

    +

    第四步,把根证书和中间证书安装到操作系统,并设置根证书为永久信任

    在mac系统下,点击lauchpad,并搜索钥匙串访问。可以看到登录下安装的证书。 +在finder中打开根证书和中间证书,可以添加到登录账号下。 +只需要设置根证书为永久信任即可。以为中间证书是由根证书签发的。 +以下是已经安装的根证书 +root-ca-crt

    +

    以下是已经安装的中间证书 +mid-ca-crt

    +

    第五步, 重新访问https://localhost

    重新访问https://localhost。正常。

    +

    valid-crt

    ]]> - k3s - docker - pod - coredns + certificate + X.509 + nginx + openssl + ssl @@ -2622,37 +2537,176 @@ search addom.xinaogroup.com

    那么具体看一下以上ca证书的前四个字节是什么含义。

    30 82 05 C1
    -
      -
    1. 首先看是什么type。第一个字节0x30描述了type信息。tagClass = 0x30 >> 6 = 0 ,表示universal isConstructed = 0x30 & 0x20 = True,对于sequence基本都是true, -tagNumber = 0x30 & 0x1F = 0x10 ,因此对应的Sequence

      -
    2. -
    3. 接下来计算长度。 -第二个字节为0x82, -分两种情况,判断表达式,byte & 0x7F == byte 如果为true就是小于127。否则就是大于127。

      -
    4. -
    -
      -
    • 长度是小于127(short form) -那么该该字节就是length,之后就是value的内容。
    • -
    • 长度大于127(long form), -byte & 0x7F 表示length的编码长度。
    • +
        +
      1. 首先看是什么type。第一个字节0x30描述了type信息。tagClass = 0x30 >> 6 = 0 ,表示universal isConstructed = 0x30 & 0x20 = True,对于sequence基本都是true, +tagNumber = 0x30 & 0x1F = 0x10 ,因此对应的Sequence

        +
      2. +
      3. 接下来计算长度。 +第二个字节为0x82, +分两种情况,判断表达式,byte & 0x7F == byte 如果为true就是小于127。否则就是大于127。

        +
      4. +
      +
        +
      • 长度是小于127(short form) +那么该该字节就是length,之后就是value的内容。
      • +
      • 长度大于127(long form), +byte & 0x7F 表示length的编码长度。
      • +
      +

      对于以上的例子,0x82是long form +因为

      +
      ( 0x82 == (0x82 & 0x7F) = False
      +

      那么表示长度的字节数量是

      +
      0x82 & 0x7F = 0x2
      +

      因此’0x82’之后的两个字节’0x05’和’0xC1’组成长度。

      +
      0x05C1 = 1473 
      +

      两个字节表示value的长度1473。

      +

      因此,

      +
      30 82 05 C1
      +

      这个头四个字节的含义是,这个是Sequence类型, +长度是1473,这四节之后的1473个字节就是Sequence类型的值。

      +]]> + + X.509 + ASN.1 + + + + k3s的Pod无法解析内网域名 + /2021/12/26/pod-resolve-dn/ + 问题

      jetson tx2开发板上安装了dockerk3s,部署了一个pod,发现日志报错

      +
      "dial tcp: lookup esmp-cloud-sync.dev.ennew.com on 10.43.0.10:53: no such host"
      +

      其中esmp-cloud-sync.dev.ennew.com是内网域名,说明pod无法解析该域名。

      + + +

      但是宿主机上能ping通该域名。

      +

      环境

        +
      • os : ubuntu18.04
      • +
      • k3s: v1.18.9+k3s1
      • +
      • cpu arch : arm64
      • +
      • coreDNS: 1.6.9
      • +
      +

      排查过程

      首先检查宿主机的域名服务器设置。 +如果不正常,则需要设置正确的域名服务器。并重启k3s服务和pod。 +如果正常,则进一步检查k3s的DNS设置。而k3s的网络域名解析是由coreDNS控制的。

      +

      查看宿主机的DNS设置

      宿主机的DNS设置,查看/etc/resolv.conf

      +
      $ cat /etc/resolv.conf 
      +# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
      +#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
      +# 127.0.0.53 is the systemd-resolved stub resolver.
      +# run "systemd-resolve --status" to see details about the actual nameservers.
      +
      +nameserver 127.0.0.53
      +search addom.xinaogroup.com
      +
      +

      运行”systemd-resolve –status”查看实际的nameservers。

      +
      $ systemd-resolve --status
      +...
      +      DNS Servers: 10.36.8.40
      +                    10.36.8.41
      +                    127.0.0.1
      +        DNS Domain: ~.
      +                    addom.xinaogroup.com 
      +...
      +
      +

      可以看到实际的域名服务器。pod里无法ping通的域名esmp-cloud-sync.dev.ennew.com,在宿主机环境是可以的。

      +

      查看pod的DNS设置

      pod的DNS设置是由CoreDNS控制的。但是进入CoreDNS的pod,使用kubectl exec是不行的。需要使用边车模式, +先查看运行CoreDNS的容器的ID,然后用docker再启动一个容器。(因为k3s server是基于dockerd运行的,所以 +可以用docker ps查看CoreDNS pod里的容器)

      +
      docker ps | grep coredns
      +ID=8afb33b91c9f
      +docker run -it --net=container:$ID --pid=container:$ID --volumes-from=$ID alpine sh
      +
      +

      然后就可以查看CoreDNS的corefile配置文件

      +
      # cat /etc/coredns/Corefile 
      +.:53 {
      +    errors
      +    health
      +    ready
      +    kubernetes cluster.local in-addr.arpa ip6.arpa {
      +      pods insecure
      +      upstream
      +      fallthrough in-addr.arpa ip6.arpa
      +    }
      +    hosts /etc/coredns/NodeHosts {
      +      reload 1s
      +      fallthrough
      +    }
      +    prometheus :9153
      +    forward . /etc/resolv.conf
      +    cache 30
      +    loop
      +    reload
      +    loadbalance
      +}
      +
      +

      forward . /etc/resolv.conf 可以看出,由/etc/resolv.conf文件接管DNS设置。进一步查看该文件,

      +
        # cat /etc/resolv.conf 
      +  nameserver 8.8.8.8
      +
      +

      可以发现这里设置的nameserver8.8.8.8,而不是10.36.8.40。因此在pod里虽然可以ping通公网域名 +,例如www.baidu.com,但是无法ping通内网域名esmp-cloud-sync.dev.ennew.com

      +

      另外查看CoreDNS的pod日志也可以看到无法解析内网域名esmp-cloud-sync.dev.ennew.com的错误。

      +

      分析问题

      从排查结果可以看到,主要问题在于CoreDNS的DSN设置与宿主机的DNS设置不同,导致解析内网域名解析失败。这一点比较奇怪,通常k3s默认 +会继承宿主机的DNS设置。

      +

      也就是pod里的/etc/resolv.conf,没有与宿主机的/etc/resolv.conf的内容一致。

      +

      再回头看看我们要解决的问题——pod里解析内网域名,那么最直接的方案就是修改CoreDNS的Corefile, +修改forward . /etc/resolv.confforward . 10.36.8.40

      +

      但是这种方法的缺点是显而易见的,写死了。 我们还是希望找到一个方法能够令/etc/resolv.conf与宿主机的/etc/resolv.conf的内容一致

      +

      解决方法

      宿主机的/etc/resolv.conf文件是一个指向 /run/systemd/resolve/resolv.conf的软链接,查看该文件的内容

      +
      $ cat /run/systemd/resolve/resolv.conf
      +# This file is managed by man:systemd-resolved(8). Do not edit.
      +#
      +# This is a dynamic resolv.conf file for connecting local clients directly to
      +# all known uplink DNS servers. This file lists all configured search domains.
      +#
      +# Third party programs must not access this file directly, but only through the
      +# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
      +# replace this symlink by a static file or a different symlink.
      +#
      +# See man:systemd-resolved.service(8) for details about the supported modes of
      +# operation for /etc/resolv.conf.
      +
      +nameserver 10.36.8.40
      +nameserver 10.36.8.41
      +nameserver 127.0.0.1
      +search addom.xinaogroup.com
      +
      +

      可以看到含有正确的nameservers。

      +

      另外 k3s 有启动参数--resolv-conf,可以指定默认的resolv.conf

      +

      修改/etc/systemd/system/k3s.service,增加启动参数--resolv-conf /run/systemd/resolve/resolv.conf

      +
      ...
      +ExecStart=/usr/local/bin/k3s \
      +server \
      +'--docker' \
      +'--write-kubeconfig' \
      +'/home/tx2/.kube/config' \
      +'--write-kubeconfig-mode' \
      +'666' \
      +'--resolv-conf' \
      +'/run/systemd/resolve/resolv.conf'
      +
      +

      然后重新启动k3s,并且删除CoreDNS的pod(kubectl delete pod -n kube-system coredns-xxxx),令其自动创建一个新的pod。

      +

      这时候再查看CoreDNS pod里的/etc/resolv.conf, 内容一致了,

      +
      / # cat /etc/resolv.conf
      +nameserver 10.36.8.40
      +nameserver 10.36.8.41
      +nameserver 127.0.0.1
      +search addom.xinaogroup.com
      +
      +

      问题解决了。

      +

      参考

      -

      对于以上的例子,0x82是long form -因为

      -
      ( 0x82 == (0x82 & 0x7F) = False
      -

      那么表示长度的字节数量是

      -
      0x82 & 0x7F = 0x2
      -

      因此’0x82’之后的两个字节’0x05’和’0xC1’组成长度。

      -
      0x05C1 = 1473 
      -

      两个字节表示value的长度1473。

      -

      因此,

      -
      30 82 05 C1
      -

      这个头四个字节的含义是,这个是Sequence类型, -长度是1473,这四节之后的1473个字节就是Sequence类型的值。

      ]]>
      - X.509 - ASN.1 + k3s + docker + pod + coredns
      @@ -2715,60 +2769,6 @@ cmd/dist makefile - - 自建根证书,中间证书和Server端X.509证书并搭建nginx验证Server端证书有效性 - /2020/10/09/x509-ca/ - X.509证书的颁发和使用

      X.509证书是用来认证身份的,例如在访问一个HTTPS网站的时候,浏览器会首先下载该网站 -的证书,验证是否有效。如果无效,浏览器会提示您的连接不是私密连接,进一步访问网站有风险。 -如果有效则可以直接访问,不会告警。

      -

      浏览器怎么验证网站证书是否有效呢?简单说就是看网站证书的颁发机构是不是已经被操作系统信任,即看 -颁发机构的身份证书是否已经安装到操作系统里,并被设置为信任。

      -

      那么我们接下来做的事情就是在mac系统上验证浏览器的上述行为。

      - - -

      验证步骤,

      -
        -
      1. 生成根证书,中间证书和Server端证书(即用户证书)
      2. -
      3. 本机搭建nginx服务器,并设置Server端证书(!!注意此时未安装根证书到操作系统)
      4. -
      5. 浏览器访问https://localhost,出现告警提示
      6. -
      7. 把根证书和中间证书安装到操作系统,并设置根证书为永久信任
      8. -
      9. 重新访问https://localhost,正常,没有出现告警。
      10. -
      -

      第一步,生成根证书,中间证书和Server端证书

      颁发机构通常会有根机构和中间机构,根机构的证书自己签发自己,中间机构的证书由根机构签发,而Server端证书由中间机构签发。本人参考以下三篇文章创建了根证书,中间证书和Server端证书。

      - -

      不过需要注意的是,Server端证书有效期不要太长,否则即使安装了根证书也会告警

      -

      第二步,本机搭建nginx服务器,并设置Server端证书

      mac下安装nginx很简单

      -
      brew install nginx
      -

      然后修改/usr/local/etc/nginx/nginx.conf文件,在server里添加三行

      -
      listen       443    ssl;
      ssl_certificate /Users/ym/tmp/myca/localhost/certs/localhost.cert.pem;
      ssl_certificate_key /Users/ym/tmp/myca/localhost/private/localhost.key.pem;
      - -

      启动nginx

      -
      brew sevices start nginx 
      - -

      第三步,浏览器访问https://localhost

      显示告警信息 -invalid-certificate

      -

      第四步,把根证书和中间证书安装到操作系统,并设置根证书为永久信任

      在mac系统下,点击lauchpad,并搜索钥匙串访问。可以看到登录下安装的证书。 -在finder中打开根证书和中间证书,可以添加到登录账号下。 -只需要设置根证书为永久信任即可。以为中间证书是由根证书签发的。 -以下是已经安装的根证书 -root-ca-crt

      -

      以下是已经安装的中间证书 -mid-ca-crt

      -

      第五步, 重新访问https://localhost

      重新访问https://localhost。正常。

      -

      valid-crt

      -]]>
      - - X.509 - certificate - nginx - openssl - ssl - -
      [leetcode 390]Elimination Game原创解法 /2022/02/06/2022-2-6-leetcode-390/ @@ -2863,6 +2863,52 @@ Given the integer n, return the last number that remains in arr. rust + + 玩一下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 + +
      使用Iced的过程中理解Rust的关联类型 /2023/02/28/2023-2-28-associated-type/ @@ -3042,52 +3088,6 @@ 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 - -
      如何更好的打印语法树结构 /2024/07/18/2024-7-18-slang-ast-hier-tree/ diff --git a/sitemap.xml b/sitemap.xml index d4c5b54b..196018b2 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -56,7 +56,7 @@ - https://threelambda.com/2023/06/09/2023-6-9-qt5-macos-lldb-debug/ + https://threelambda.com/2023/06/08/2023-6-8-langchain-openai/ 2024-07-22 @@ -65,7 +65,7 @@ - https://threelambda.com/2023/06/08/2023-6-8-langchain-openai/ + https://threelambda.com/2023/06/09/2023-6-9-qt5-macos-lldb-debug/ 2024-07-22 @@ -119,7 +119,7 @@ - https://threelambda.com/2021/12/26/pod-resolve-dn/ + https://threelambda.com/2020/11/08/asn-1/ 2024-07-22 @@ -128,7 +128,7 @@ - https://threelambda.com/2020/11/08/asn-1/ + https://threelambda.com/2021/12/26/pod-resolve-dn/ 2024-07-22 @@ -155,7 +155,7 @@ - https://threelambda.com/2020/07/06/run-ingress-example-on-mac/ + https://threelambda.com/2020/03/09/connect-kafka/ 2024-07-22 @@ -164,7 +164,7 @@ - https://threelambda.com/2020/03/09/connect-kafka/ + https://threelambda.com/2020/08/14/write-file-with-spring-integration/ 2024-07-22 @@ -173,7 +173,7 @@ - https://threelambda.com/2020/08/14/write-file-with-spring-integration/ + https://threelambda.com/2020/07/06/run-ingress-example-on-mac/ 2024-07-22 @@ -200,7 +200,7 @@ - https://threelambda.com/2019/01/25/2019-1-25-bt-5/ + https://threelambda.com/2019/01/09/2019-1-9-bt-1/ 2024-07-22 @@ -209,7 +209,7 @@ - https://threelambda.com/2019/01/09/2019-1-9-bt-1/ + https://threelambda.com/2019/11/19/debug-k3s/ 2024-07-22 @@ -218,7 +218,7 @@ - https://threelambda.com/2019/11/19/debug-k3s/ + https://threelambda.com/2019/01/25/2019-1-25-bt-5/ 2024-07-22 @@ -254,7 +254,7 @@ - https://threelambda.com/2019/01/10/2019-1-10-bt-2/ + https://threelambda.com/2018/08/18/2018-8-18-leetcode-341/ 2024-07-22 @@ -263,7 +263,7 @@ - https://threelambda.com/2018/08/18/2018-8-18-leetcode-341/ + https://threelambda.com/2019/01/10/2019-1-10-bt-2/ 2024-07-22 @@ -290,7 +290,7 @@ - https://threelambda.com/2017/07/09/2017-7-9-resize-fixed-image/ + https://threelambda.com/2017/05/24/2017-5-24-gradle-build-project/ 2024-07-22 @@ -299,7 +299,7 @@ - https://threelambda.com/2017/05/24/2017-5-24-gradle-build-project/ + https://threelambda.com/2017/07/09/2017-7-9-resize-fixed-image/ 2024-07-22 @@ -317,7 +317,7 @@ - https://threelambda.com/2017/03/06/2017-3-6-grails-call-java/ + https://threelambda.com/2017/05/19/2017-5-19-javacc-minilisp/ 2024-07-22 @@ -326,7 +326,7 @@ - https://threelambda.com/2017/05/19/2017-5-19-javacc-minilisp/ + https://threelambda.com/2017/03/06/2017-3-6-grails-call-java/ 2024-07-22 @@ -344,7 +344,7 @@ - https://threelambda.com/2017/03/23/2017-3-23-compile-and-run-clojure/ + https://threelambda.com/2017/03/12/2017-3-12-leetcode-329/ 2024-07-22 @@ -353,7 +353,7 @@ - https://threelambda.com/2017/03/12/2017-3-12-leetcode-329/ + https://threelambda.com/2017/03/23/2017-3-23-compile-and-run-clojure/ 2024-07-22 @@ -470,28 +470,28 @@ - https://threelambda.com/tags/leetcode/ + https://threelambda.com/tags/R/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/%E7%AE%97%E6%B3%95/ + https://threelambda.com/tags/python/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/python/ + https://threelambda.com/tags/leetcode/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/R/ + https://threelambda.com/tags/%E7%AE%97%E6%B3%95/ 2024-07-23 weekly 0.2 @@ -554,14 +554,14 @@ - https://threelambda.com/tags/grails/ + https://threelambda.com/tags/gradle/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/gradle/ + https://threelambda.com/tags/build/ 2024-07-23 weekly 0.2 @@ -603,7 +603,7 @@ - https://threelambda.com/tags/build/ + https://threelambda.com/tags/grails/ 2024-07-23 weekly 0.2 @@ -659,14 +659,14 @@ - https://threelambda.com/tags/k8s/ + https://threelambda.com/tags/kafka/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/ingress/ + https://threelambda.com/tags/container/ 2024-07-23 weekly 0.2 @@ -680,49 +680,49 @@ - https://threelambda.com/tags/mac/ + https://threelambda.com/tags/spring/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/kafka/ + https://threelambda.com/tags/integration/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/container/ + https://threelambda.com/tags/write/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/spring/ + https://threelambda.com/tags/file/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/integration/ + https://threelambda.com/tags/k8s/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/write/ + https://threelambda.com/tags/ingress/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/file/ + https://threelambda.com/tags/mac/ 2024-07-23 weekly 0.2 @@ -757,70 +757,70 @@ - https://threelambda.com/tags/pod/ + https://threelambda.com/tags/certificate/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/coredns/ + https://threelambda.com/tags/X-509/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/X-509/ + https://threelambda.com/tags/nginx/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/ASN-1/ + https://threelambda.com/tags/openssl/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/clion/ + https://threelambda.com/tags/ssl/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/makefile/ + https://threelambda.com/tags/ASN-1/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/certificate/ + https://threelambda.com/tags/pod/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/nginx/ + https://threelambda.com/tags/coredns/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/openssl/ + https://threelambda.com/tags/clion/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/ssl/ + https://threelambda.com/tags/makefile/ 2024-07-23 weekly 0.2 @@ -848,56 +848,56 @@ - https://threelambda.com/tags/iced/ + https://threelambda.com/tags/langchain/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/qt5/ + https://threelambda.com/tags/openai/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/cmake/ + https://threelambda.com/tags/iced/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/macos/ + https://threelambda.com/tags/qt5/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/bundle/ + https://threelambda.com/tags/cmake/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/lldb/ + https://threelambda.com/tags/macos/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/langchain/ + https://threelambda.com/tags/bundle/ 2024-07-23 weekly 0.2 - https://threelambda.com/tags/openai/ + https://threelambda.com/tags/lldb/ 2024-07-23 weekly 0.2 diff --git a/tags/ASN-1/index.html b/tags/ASN-1/index.html index fbb0b995..7aa98e9b 100644 --- a/tags/ASN-1/index.html +++ b/tags/ASN-1/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/AST/index.html b/tags/AST/index.html index 790c9fe3..8f7f0b20 100644 --- a/tags/AST/index.html +++ b/tags/AST/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/CLion/index.html b/tags/CLion/index.html index 17d0f3a7..3f2fb5ad 100644 --- a/tags/CLion/index.html +++ b/tags/CLion/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/ChatGPT/index.html b/tags/ChatGPT/index.html index c9c1a5b1..173696e6 100644 --- a/tags/ChatGPT/index.html +++ b/tags/ChatGPT/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/Cython/index.html b/tags/Cython/index.html index 08dc0759..883b16c0 100644 --- a/tags/Cython/index.html +++ b/tags/Cython/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/R/index.html b/tags/R/index.html index 9360c1c4..f211785c 100644 --- a/tags/R/index.html +++ b/tags/R/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/X-509/index.html b/tags/X-509/index.html index 61056580..4fa6d722 100644 --- a/tags/X-509/index.html +++ b/tags/X-509/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/algorithm/index.html b/tags/algorithm/index.html index 0b3ed628..f7afd3e2 100644 --- a/tags/algorithm/index.html +++ b/tags/algorithm/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/bitTorrent/index.html b/tags/bitTorrent/index.html index 2a5cf82b..331b2d03 100644 --- a/tags/bitTorrent/index.html +++ b/tags/bitTorrent/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/build/index.html b/tags/build/index.html index 071cb44c..e5f7aa0e 100644 --- a/tags/build/index.html +++ b/tags/build/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/bundle/index.html b/tags/bundle/index.html index b1d1b4e9..2c37c1ec 100644 --- a/tags/bundle/index.html +++ b/tags/bundle/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/c/index.html b/tags/c/index.html index eeba2e71..a627f430 100644 --- a/tags/c/index.html +++ b/tags/c/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/certificate/index.html b/tags/certificate/index.html index 12ff3772..d81f8e51 100644 --- a/tags/certificate/index.html +++ b/tags/certificate/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/clojure/index.html b/tags/clojure/index.html index 884ccb2e..156cdec4 100644 --- a/tags/clojure/index.html +++ b/tags/clojure/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/cmake/index.html b/tags/cmake/index.html index 9a9d5269..0b99d55a 100644 --- a/tags/cmake/index.html +++ b/tags/cmake/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/container/index.html b/tags/container/index.html index 3cace3a4..d313d23f 100644 --- a/tags/container/index.html +++ b/tags/container/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/coredns/index.html b/tags/coredns/index.html index 57701970..32decd10 100644 --- a/tags/coredns/index.html +++ b/tags/coredns/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/debug/index.html b/tags/debug/index.html index 03885628..fa1d3430 100644 --- a/tags/debug/index.html +++ b/tags/debug/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/delve/index.html b/tags/delve/index.html index d126c3b2..8ded594d 100644 --- a/tags/delve/index.html +++ b/tags/delve/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/docker/index.html b/tags/docker/index.html index 02cbad09..29166626 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/file/index.html b/tags/file/index.html index 499c2d0e..4b669383 100644 --- a/tags/file/index.html +++ b/tags/file/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/gdb/index.html b/tags/gdb/index.html index 1d1b0f92..10a876fd 100644 --- a/tags/gdb/index.html +++ b/tags/gdb/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/go/index.html b/tags/go/index.html index e0d3f3ce..43c878cf 100644 --- a/tags/go/index.html +++ b/tags/go/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/golang/index.html b/tags/golang/index.html index 818a6caa..8dba2758 100644 --- a/tags/golang/index.html +++ b/tags/golang/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/gradle/index.html b/tags/gradle/index.html index 3d2deddd..7a394eec 100644 --- a/tags/gradle/index.html +++ b/tags/gradle/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/grails/index.html b/tags/grails/index.html index 8274eb47..d0c1a188 100644 --- a/tags/grails/index.html +++ b/tags/grails/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/iced/index.html b/tags/iced/index.html index 76480f8f..88dabf7a 100644 --- a/tags/iced/index.html +++ b/tags/iced/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/image/index.html b/tags/image/index.html index 1934bb9b..ac9bd8b0 100644 --- a/tags/image/index.html +++ b/tags/image/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/index.html b/tags/index.html index df4aeff8..ddec7f84 100644 --- a/tags/index.html +++ b/tags/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/ingress/index.html b/tags/ingress/index.html index e3332183..35c60e79 100644 --- a/tags/ingress/index.html +++ b/tags/ingress/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/integration/index.html b/tags/integration/index.html index 961aeda9..8f9d0731 100644 --- a/tags/integration/index.html +++ b/tags/integration/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/java/index.html b/tags/java/index.html index 53d6973c..40ae0d26 100644 --- a/tags/java/index.html +++ b/tags/java/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/javacc/index.html b/tags/javacc/index.html index 07072cbf..c141ca02 100644 --- a/tags/javacc/index.html +++ b/tags/javacc/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/jvm/index.html b/tags/jvm/index.html index a0c192d9..340217d6 100644 --- a/tags/jvm/index.html +++ b/tags/jvm/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/k3s/index.html b/tags/k3s/index.html index 2d4e3f25..c01cbabf 100644 --- a/tags/k3s/index.html +++ b/tags/k3s/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/k8s/index.html b/tags/k8s/index.html index c915040d..7da0165d 100644 --- a/tags/k8s/index.html +++ b/tags/k8s/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/kafka/index.html b/tags/kafka/index.html index cf7cf07b..9da4a7b8 100644 --- a/tags/kafka/index.html +++ b/tags/kafka/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/langchain/index.html b/tags/langchain/index.html index 65bf0d3e..7ff82bcd 100644 --- a/tags/langchain/index.html +++ b/tags/langchain/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/leetcode/index.html b/tags/leetcode/index.html index 1126530a..fac5e466 100644 --- a/tags/leetcode/index.html +++ b/tags/leetcode/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/leetcode/page/2/index.html b/tags/leetcode/page/2/index.html index 0780afec..6f812c4a 100644 --- a/tags/leetcode/page/2/index.html +++ b/tags/leetcode/page/2/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/lisp/index.html b/tags/lisp/index.html index 49ab8da5..68b25f8c 100644 --- a/tags/lisp/index.html +++ b/tags/lisp/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/lldb/index.html b/tags/lldb/index.html index b7b76c84..53ac36ed 100644 --- a/tags/lldb/index.html +++ b/tags/lldb/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/mac/index.html b/tags/mac/index.html index e3c8cbe4..30bf1c02 100644 --- a/tags/mac/index.html +++ b/tags/mac/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/macos/index.html b/tags/macos/index.html index 453cebf3..f82c18eb 100644 --- a/tags/macos/index.html +++ b/tags/macos/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/macro/index.html b/tags/macro/index.html index 0ddc7051..22c4647d 100644 --- a/tags/macro/index.html +++ b/tags/macro/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/makefile/index.html b/tags/makefile/index.html index fbf8b094..68d32080 100644 --- a/tags/makefile/index.html +++ b/tags/makefile/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/nginx/index.html b/tags/nginx/index.html index e544b8f0..8d966838 100644 --- a/tags/nginx/index.html +++ b/tags/nginx/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/openai/index.html b/tags/openai/index.html index edcb731c..4e1372fc 100644 --- a/tags/openai/index.html +++ b/tags/openai/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/openssl/index.html b/tags/openssl/index.html index 7d4cd082..ef8ada9b 100644 --- a/tags/openssl/index.html +++ b/tags/openssl/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/p2p/index.html b/tags/p2p/index.html index 1d6da141..a55601a9 100644 --- a/tags/p2p/index.html +++ b/tags/p2p/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/pod/index.html b/tags/pod/index.html index 6af64d5d..cbb5605a 100644 --- a/tags/pod/index.html +++ b/tags/pod/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/protocol/index.html b/tags/protocol/index.html index 5b004377..f8a72c2e 100644 --- a/tags/protocol/index.html +++ b/tags/protocol/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/python/index.html b/tags/python/index.html index 8b34551d..dcadf731 100644 --- a/tags/python/index.html +++ b/tags/python/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/python/page/2/index.html b/tags/python/page/2/index.html index 514a69f2..9b01cf68 100644 --- a/tags/python/page/2/index.html +++ b/tags/python/page/2/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/qt5/index.html b/tags/qt5/index.html index ab9a1048..5c577bfc 100644 --- a/tags/qt5/index.html +++ b/tags/qt5/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/remote/index.html b/tags/remote/index.html index d25659d2..b20e3c50 100644 --- a/tags/remote/index.html +++ b/tags/remote/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/resize/index.html b/tags/resize/index.html index 424aa1e5..97458d40 100644 --- a/tags/resize/index.html +++ b/tags/resize/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/rust/index.html b/tags/rust/index.html index 63255582..79d9c88a 100644 --- a/tags/rust/index.html +++ b/tags/rust/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/spring/index.html b/tags/spring/index.html index 1f7aa9c4..fff161ce 100644 --- a/tags/spring/index.html +++ b/tags/spring/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/ssl/index.html b/tags/ssl/index.html index 6a09d744..1b8f34fb 100644 --- a/tags/ssl/index.html +++ b/tags/ssl/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/tree/index.html b/tags/tree/index.html index f454c801..0a3122aa 100644 --- a/tags/tree/index.html +++ b/tags/tree/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/trie/index.html b/tags/trie/index.html index e0d8c720..b7878603 100644 --- a/tags/trie/index.html +++ b/tags/trie/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/venv/index.html b/tags/venv/index.html index 3b13f954..0691693f 100644 --- a/tags/venv/index.html +++ b/tags/venv/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/virutalbox/index.html b/tags/virutalbox/index.html index d5c869f7..8d5a230d 100644 --- a/tags/virutalbox/index.html +++ b/tags/virutalbox/index.html @@ -55,7 +55,7 @@ - + diff --git a/tags/write/index.html b/tags/write/index.html index 403d24ac..b0a6d032 100644 --- a/tags/write/index.html +++ b/tags/write/index.html @@ -55,7 +55,7 @@ - + diff --git "a/tags/\346\212\200\345\267\247/index.html" "b/tags/\346\212\200\345\267\247/index.html" index 33255a1a..db9843a0 100644 --- "a/tags/\346\212\200\345\267\247/index.html" +++ "b/tags/\346\212\200\345\267\247/index.html" @@ -55,7 +55,7 @@ - + diff --git "a/tags/\347\256\227\346\263\225/index.html" "b/tags/\347\256\227\346\263\225/index.html" index c6d76310..f7bc8458 100644 --- "a/tags/\347\256\227\346\263\225/index.html" +++ "b/tags/\347\256\227\346\263\225/index.html" @@ -55,7 +55,7 @@ - + diff --git "a/tags/\347\256\227\346\263\225/page/2/index.html" "b/tags/\347\256\227\346\263\225/page/2/index.html" index 5e41d1d8..58d9973b 100644 --- "a/tags/\347\256\227\346\263\225/page/2/index.html" +++ "b/tags/\347\256\227\346\263\225/page/2/index.html" @@ -55,7 +55,7 @@ - +