分类 默认分类 下的文章

urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

最近一个项目把 python 从 3.9 升级到3.11, 于是把 3.9 删除, 使用 brew install python@3.11, 其它都正常, 就是使用 requests 访问各个https 地址的时候, 就报错:

Failed to send a request to Slack API server: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)>

因为出错的是访问 slack api(其实跟访问那个url没关系), 所以找到了这个帖子: https://github.com/slackapi/bolt-python/issues/673.

可是里面提到的 Certificates.command 对于我的安装路径根本就不存在. 于是怀疑 brew 安装有问题, 重新安装, 没看到任何错误, 运行程序还少一样的错.

最终根据一篇帖子的描述, 找到了答案.

启动python, 运行下面的脚本:

supra@host tools % python -V
Python 3.11.9
supra@host tools % python
Python 3.11.9 (main, Apr  2 2024, 08:25:04) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import _ssl; _ssl.get_default_verify_paths()
('SSL_CERT_FILE', '/usr/local/etc/openssl@3/cert.pem', 'SSL_CERT_DIR', '/usr/local/etc/openssl@3/certs')

可以看到它用的证书在/usr/local/etc/openssl@3/cert.pem, 发现这个是一个软链接, 链接到 ../ca-certificates/cert.pem, 可是这个文件夹都不存在.

于是新建这个文件夹, 并且把正确的证书放到那里, 然后就工作了.

IEEE 754 浮点表示

IEEE 754 定义了计算机如何表示和存储非整数的数据, 如: 12.084, 0.3, -0.0043. 对于整数, 我们只要把十进制的整数转换成二进制, 并且在最前面设置它的正数/负数的符号, 就很容易的存储.

浮点数转成二进制

浮点数转成二进制分成2部份: 整数部分和小数部份.

  1. 对于整数部分, 直接转成二进制, 比如 12(十进制) = 1100 (二进制).
  2. 对于小数部份, 0.5(十进制) = 0.1(二进制), 0.25(十进制) = 0.01(二进制), 0.125(十进制) = 0.001(二进制), 所以可以看到对于小数部份是通过把1整除的方式获得的. 所以0.375(十进制) = 1/4 + 1/8 = 0.25 + 0.125 = 0.01(二进制) + 0.001(二进制) = 0.011(二进制).

所以:

12.125
   12 = 8 + 4 = 1100
   0.125 = 1/8 = 0.001
12.125 = 1100.001

科学计数法

科学记数法是一种记数的方法。 把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学记数法。
例如:12.125 = 1.2125×10^1, 19971400000000=1.99714×10^13, 0.00345 = 3.45×10^-3.
所以它把一个数分成大于等于1小于10的科学计数部份和指数部分.

对于二进制, 同样适用科学计数法, 只不过二进制表示成科学计数法整数部分除了0之外, 只能有1. 比如:

12.125 = 1100.001 = 1.100001×2^3
0.375 = 0.011 = 1.1×2^-2

IEEE754 浮点表示

IEEE754 规定了单精度(32位)浮点数和双精度(64位)浮点数以及其他浮点数. 我们这里主要看单精度, 其它原理类似.

对于32位的浮点数, 32位分成 1位符号位(正负) + 8位指数 + 23位 科学计数数值.
符号位 0 表示正, 1 表示负.
比如:
12.125 = 1100.001 = 1.100001×2^3, 它的符号位0(正数), 指数部分是3 (二进制11), 科学计数数值是1.100001(二进制).
-0.375 = 0.011 = 1.1×2^-2, 它的符号位数1(负数), 指数部分是-2(-10二进制), 科学计数数值是1.1(二进制).

需要进一步明确的地方:

  1. 8位指数从 00000000 ~ 11111111, 即0 ~ 255, 但是这样对于是负数的指数无法表示, 所以需要调整这个值从0 ~ 255表示成-127 ~ 128. 即0表示 0-127 = -127(指数), 127 - 127 = 0(指数为0), 255 - 127 = 128. 但实际使用的时候, -127128被用作特殊值处理, 实际可能的值只能是 -126 ~ 127.
  2. 对于数值部分, 因为是科学计数法, 所以除了0之外, 其它时候都是1, 所以这个1可以去掉, 所以23位全部用来表示点之后的部份. 即 1.001101 只要使用 001101 它, 1.101 只要使用 101. 0表示成 23个0.

所以:
12.125 = 1100.001 = 1.100001×2^3 -> 符号是0, 指数部分原本是3,转成127+3=130,即二进制 10000010, 科学计数部分去掉点之前的1, 即是100001.
-0.375 = 0.011 = 1.1×2^-2 -> 符号是1, 指数部分是-2, 转成127-2=125, 即二进制 1111101, 科学计数部分去掉点之前的1, 即是 1.

对于指数部分不够8位只要前面补0, 对于科学计数部分, 由于是小数点后的, 所以后面补0. 于是:
12.125 = 1100.001 = 1.100001×2^3 => 0 10000010 10000100000000000000000`.
-0.375 = 0.11 = 1.1×2^-1 => 1 01111101 1000000000000000000000

验证

让AI写了一段 C 代码, 来验证一把:

#include <stdio.h>

int main() {
    float num1 = 12.125f;
    float num2 = -0.375f;

    // 将浮点数的二进制表示转换为字节表示
    unsigned char *bytePtr1 = (unsigned char *)&num1;
    unsigned char *bytePtr2 = (unsigned char *)&num2;

    printf("12.125 的二进制表示为:\n");
    for (int i = sizeof(float) - 1; i >= 0; i--) {
        for (int j = 7; j >= 0; j--) {
            printf("%d", (bytePtr1[i] >> j) & 1);
        }
        printf(" ");
    }
    printf("\n");

    printf("-0.375 的二进制表示为:\n");
    for (int i = sizeof(float) - 1; i >= 0; i--) {
        for (int j = 7; j >= 0; j--) {
            printf("%d", (bytePtr2[i] >> j) & 1);
        }
        printf(" ");
    }
    printf("\n");

    return 0;
}

跑一台机器

12.125 的二进制表示为:
01000001 01000010 00000000 00000000
-0.375 的二进制表示为:
10111110 11000000 00000000 00000000

通过已有网站内容借助GPT来回答问题

最近学习 chatGPT 文档的时候,看到这么一篇文章 How to build an AI that can answer questions about your website. 它讲的是如何用你现有的网站来做一个AI bot 来回答有关你网站的问题. 这种场景很常见: 比如你公司有很多很有用的文档存放在某个站点, 或者你有一个专门针对某个主题的blog网站,又或者某个产品的详细使用说明在线文档. 当有了GPT的工具后, 我们把这些站点的内容作为context送给GPT,然后GPT以这些context为基础来回答用户的问题.

下面我们就以我的个人网站为例,以 openai 的chatGPT API为工具, 构建这么一个问答程序.

步骤概括

总的来看, 我们要做下面的一些步骤:

  1. 把整个网站下载下来.
  2. 把有用的网页文档里面的核心内容取出来.
  3. 把这些取出来的核心文本内容做 text embeding, 并放入向量数据库.
  4. 当有问题的时候, 先使用问题的内容去搜索向量数据库, 把搜索的结果做为 context, 连同问题一并送给 chatGPT 获取答案.
    下面我们就给出具体的代码和步骤.

把整个网站下载下来

使用 wget 命令很容易去下载一个网站.

$ wget -r https://www.tianxiaohui.com 
        ...省略...
FINISHED --2024-01-06 19:42:12--
Total wall clock time: 11m 28s
Downloaded: 3611 files, 133M in 3m 37s (625 KB/s)

通过 -r 参数, wget会把整个网站都下载下来, 并且按照网页的路径分类. 可以看到这里下载了3611个文件, 133M. 但是我的网站明显没有这么多文章, 这里面包含很多图片的链接, 有些分类的页面.

把有用的网页文档里面的核心内容取出来.

通过人工浏览这些页面, 我们可以看到我们只需要特定的含有整篇文章的html页面, 有些分类页面(比如2023年3月的文章合集)是不需要的, 一篇文章的html被加载之后, 我们只需要取其中文章的部分, 不需要菜单链接和旁边的分类链接. 所以我们有下面的代码:

import os

from bs4 import BeautifulSoup


def fetch_docs(folder: str = None):
    # 遍历并过滤以 .html 结尾的文档
    html_docs = [f for f in os.listdir(folder) if f.endswith('.html')]

    txts = []
    for html_doc in html_docs:
        with open(folder + "/" + html_doc, 'r') as file:
            # 使用BeautifulSoup 解析html
            soup = BeautifulSoup(file, 'html.parser')
            # 只取其中文章的部分, 有些分类页面没有文章部分, 这里就会放弃
            post = soup.find('div', class_='post-content')
            if post:
                # 替换掉很多分隔符
                txt = post.get_text().replace("\n", "").replace("\t", " ").replace("\r", "");
                # print(txt) 查看具体文本, 方便跟多加工
                txts.append(txt)
            else:
                print("not find article from " + html_doc)
    print(len(txts))
    return txts
    

fetch_docs(“/Users/eric/Downloads/blogs/www.tianxiaohui.com/index.php/Troubleshooting”)

把这些取出来的核心文本内容做 text embeding, 并放入向量数据库.

我们使用openAI 的 embedding, 并使用 FAISS 做为向量库来进行相似性搜索.

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS


embeddings_model = OpenAIEmbeddings()
vectorstore = FAISS.from_texts(
    fetch_docs("/Users/eric/Downloads/blogs/www.tianxiaohui.com/index.php/Troubleshooting"), embedding=embeddings_model
)

从向量数据库获取相关内容调用 GPT 生成答案

首先我们把向量库FAISS设置为 retriever, 然后检索相关文档, 然后把问题和相关文档组成的context 给chatGPT, 获取答案.

from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI


question = "如何分析Java应用OOM问题?"
retriever = vectorstore.as_retriever(search_kwargs={"k": 3, "score_threshold": .5})
docs = retriever.get_relevant_documents(question)
doc_contents = [doc.page_content for doc in docs]

prompt = PromptTemplate.from_template("here is the question: {query}, this is the contexts: {context}")
chain = prompt | ChatOpenAI()
result = chain.invoke({"query": question, "context": doc_contents})
print(result)

总结

通过上面几十行代码, 我们就能把一个现有的知识网站, 做成了一个初级版本的可以回答问题的智能AI了.

静态代码分析工具 Spoon 使用

官网: https://spoon.gforge.inria.fr/launcher.html
它是基于 Eclipse JDT 打造的静态代码分析工具. 它把原代码拆解成包,模块,类,方法,语句,表达式,各种语法单元, 各个语法单元又形成了父子包含等关系. 可以对原代码编译, 检查, 分析, 过滤, 替换, 转换 等.

AST 语法树的元素

语法树包含的各类语法基本单元: https://spoon.gforge.inria.fr/structural_elements.html
详细的代码块形成的各个基本单元: https://spoon.gforge.inria.fr/code_elements.html

setup project

可以分析一个基本的 Java 项目, 一个 Maven 项目, 或者一个 Jar包(通过反编译).

# 分析一个 Maven project 的 source code
MavenLauncher launcher = new MavenLauncher("/Users/tianxiaohui/codes/myProj", SOURCE_TYPE.APP_SOURCE,"/Users/tianxiaohui/apache-maven-3.9.1/");

launcher.getEnvironment().setComplianceLevel(17);
launcher.getEnvironment().setNoClasspath(true); //有些类没提供, 比如 servlet jar 里面的类
launcher.buildModel();

CtModel model = launcher.getModel();

3种情况:

  1. 有源代码 reference.getDeclaration() = reference.getTypeDeclaration()
  2. 没有源代码, 只有binary(jar). reference.getDeclaration() = null, reference.getTypeDeclaration() 反射得来, isShadow = true.
  3. 没有源代码, 也没有binary. reference.getDeclaration() = reference.getTypeDeclaration() = false.
    上面的 getTypeDeclaration 适用于 getFieldDeclaration, getExecutableDeclaration.

常见的代码分析

返回原代码中所有的包和类

for(CtPackage p : model.getAllPackages()) {
  System.out.println("package: " + p.getQualifiedName());
}
// list all classes of the model
for(CtType<?> s : model.getAllTypes()) {
  System.out.println("class: " + s.getQualifiedName());
}

找到一个方法的定义

当你知道一个方法名的时候, 你要查看这个类具体的定义, 可以通过下面的查找方法.

    public static void findMethodDefinition(CtModel ctModel, String clazzName, String methodName) {
        CtClass<?> foundClass = ctModel.getElements(new TypeFilter<CtClass<?>>(CtClass.class) {
            @Override
            public boolean matches(CtClass<?> clazz) {
                return clazz.getQualifiedName().equals(clazzName);
            }
        }).stream().findFirst().orElse(null);

        if (foundClass != null) {
            //System.out.println("Found class definition: " + foundClass);
            foundClass.getMethodsByName(methodName).forEach(m -> {
                System.out.println("Found method definition: " + m.getSignature());
                System.out.println(m.toString());
            });
        } else {
            System.out.println("Class definition not found for: " + clazzName);
        }
    }

    public static void findInvocationPoints(CtModel ctModel, String className, String methodName) {
        System.out.println(" Method " + className + "." + methodName + " is called by:");
        findInvocation(ctModel, className, methodName, 0, false);
    }

查找一个类实例是在哪里构造的

有时候我们要查找某个类是在哪里被初始化的, 可以通过下面的代码获得.

    public static void findNewClassConstruct(CtModel ctModel, String clazzName) {
        long start = System.currentTimeMillis();
        ctModel.getRootPackage().getElements(new TypeFilter<>(CtConstructorCall.class)).forEach(e -> {
            if (e.getExecutable().getDeclaringType().getQualifiedName().equals(clazzName)) {
                System.out.println(clazzName + "is created at: " + e.getPosition());
            }
        });
        System.out.println("time0: " + (System.currentTimeMillis() - start));
    }
    public static void findNewClassConstruct1(CtModel ctModel, String clazzName) {
        long start = System.currentTimeMillis();
        ctModel.getRootPackage().getElements(new TypeFilter<>(CtConstructorCall.class)).forEach(e -> {
            System.out.println(e.getType());
            String type = e.getType().toString();
            if (type.equals(clazzName)) {
                System.out.println(clazzName + "is created at: " + e.getPosition());
            }
        });
        System.out.println("time1: " + (System.currentTimeMillis() - start));
    }

查找一个方法的调用点

一个方法被调用的时候, 它声明的类型可能是它本身的类型, 或者它实现的接口类型, 或者直接/非直接父类的类型. 为了查看完整的可能性, 要能要去每个父类, 实现的接口都去查看一遍. 下面的方法只是查看当前类型的直接调用.

    /**
     *  here we only find the invocation with the exactly class name and method name, not the declared method in
     *  Interface and parent class.
     *  Sometimes you want to find all the invocation points of a method, include the declared method in Interface and parent class.
     *  ex:
     *    IHello hello = new Hello();
     *    hello.sayHello();
     *    In this case, the invocation point of sayHello() is in IHello, not in Hello.
     * @param ctModel
     * @param className
     * @param methodName
     * @param depth
     * @param recursive
     */
    private static void findInvocation(CtModel ctModel, String className, String methodName, int depth, boolean recursive) {

        List<CtInvocation<?>> invocations = ctModel.getElements(new TypeFilter<CtInvocation<?>>(CtInvocation.class) {
            @Override
            public boolean matches(CtInvocation<?> element) {
                return element.getExecutable().getSignature().toString().equals(methodName) && containsRefType(element.getReferencedTypes(), className);
            }
        });

        for (CtInvocation<?> invocation : invocations) {
            CtExecutable<?> caller = invocation.getParent(CtExecutable.class);
            if (caller != null) {
                for (int i = 0; i < depth; i++) {
                    System.out.print("\t");
                }
                System.out.print(" - " + caller.getParent(CtClass.class).getPackage() + "." + caller.getParent(CtClass.class).getSimpleName() + "." + caller.getSignature());
                if (caller.getThrownTypes().size() > 0) {
                    System.out.print(" throws ");
                    System.out.print(caller.getThrownTypes().stream().map(t -> t.toString()).collect(Collectors.joining( ", ")));
                }
                System.out.println(" at line " + invocation.getPosition().getLine());
                if (recursive) {
                    findInvocation(ctModel, caller.getParent(CtClass.class).getQualifiedName(), caller.getSignature(), 1 + depth, recursive);
                }
            }
        }
    }

找到所有抛出异常的代码

下面的代码找出所有抛出异常的代码点. 当然你可以根据异常的类型去过滤.

    public static void findThrowStatements(CtModel ctModel) {
        List<CtThrow> throwStatements = ctModel.getElements(new TypeFilter<>(CtThrow.class));

        // Process each throw statement
        for (CtThrow throwStatement : throwStatements) {
            CtExecutable<?> executable = throwStatement.getParent(CtExecutable.class);
            if (executable != null) {
                System.out.println(executable.getParent(CtClass.class).getPackage() + " - " + executable.getParent(CtClass.class).getSimpleName() + "." + executable.getSimpleName()
                        + " at line " + throwStatement.getPosition().getLine());
            }
        }
    }

找到包含特定注解的类或方法

有时候你想找到特定注解的类, 比如有些注解定义了系统的所有API, 有些注解标注了系统将要废弃的API.

findWithAnnotation(ctModel, javax.ws.rs.ApplicationPath.class).forEach(clazz -> {
            System.out.println(clazz.getAnnotation(javax.ws.rs.ApplicationPath.class).value());
        });


public static List<CtClass> findWithAnnotation(CtModel ctModel, Class<? extends Annotation> annotationType) {
        return ctModel.getRootPackage().getElements(new AnnotationFilter<>(annotationType));

列出某个函数调用的其它函数列表

给出特定函数, 我们可以列出当前函数使用了其他哪些函数

public static void findNextCalls(CtModel ctModel, String pkg, String clazz, String methodName) {
        //find this method
        ctModel.getElements(new TypeFilter<CtMethod<?>>(CtMethod.class) {
            @Override
            public boolean matches(CtMethod<?> method) {
                if (!(method.getParent() instanceof CtClass)) {
                    return false;
                }

                CtClass cz = (CtClass) method.getParent();
                String curClazz = cz.getSimpleName();
                String curPkg = null != cz.getPackage() ? cz.getPackage().getQualifiedName() : "-";

                if (method.getSimpleName().equals(methodName)) {
                    System.out.println(curPkg + " - " + curClazz + " - " + method.getSimpleName());
                }
                return pkg.equals(curPkg) && clazz.equals(curClazz) && method.getSimpleName().equals(methodName);
            }
        }).forEach(m -> {
            System.out.println("Method found: " + m.getSimpleName());
            //find next calls
            List<CtInvocation<?>> invocations = m.getElements(new TypeFilter<>(CtInvocation.class));
            for (CtInvocation<?> invocation : invocations) {
                // Check if the invocation is a client call
                CtExecutableReference exec = invocation.getExecutable();
                System.out.println(m.getSimpleName() + " -> " + invocation.getExecutable().getDeclaringType() + "."
                        + invocation.getExecutable().getSignature());
            }
        });
    }

关于 PromQL 的 histogram_quantile 的算法

关于 PromQL 的 histogram_quantile 的算法. 很长一段时间内, 我都以为是使用正态分布算的, 其实并没有那么复杂.

假如我们有11个桶, 我们的数据是从0开始的, 第一个桶是接收落在(0~10]区间的数量, 第二个桶是接收落在(10 ~ 20]区间的数量, 以此类推, 第10个桶接收落在(90~100]区间的数量, 那么第11个桶接收(100 ~ +Inf) 的区间, 通常最后一个区间应该没有值或非常少.

那么如果要求第 95分位等值, 我们就要统计这时总共有多少samples, 其中第95th/100 落在那个桶. 假如我们收到100个samples, 那么第95th/100 就是第95个sample, 看它落在哪个桶. 如果我们收到10000个samples, 那就是第9500个sample, 看它落在哪个桶.

当我们看到它落到那个桶之后, 就在数一下这个桶共有多少个数, 然后算一下这个桶占据了第多少分位(低)到第多少分位(高), 所以就知道了这个桶占据了从多少分位到多少分位.

然后按照这个桶内的数据是平均分布的, 然后算出第nth(95th)是到底处于哪个值.

这篇问答很好的解释了这个算法是如何工作的:
https://stackoverflow.com/questions/55162093/understanding-histogram-quantile-based-on-rate-in-prometheus/69938239#69938239

官方的开源代码: https://github.com/prometheus/prometheus/blob/main/promql/quantile.go

其中要注意的是:

  1. 分位数值在桶内假定为线性分布进行插值计算
  2. 如果分位数落在最高的桶内,将返回第二高桶的上界值
  3. 如果最低桶的上界大于0,则假定自然下界为0

设计好桶是很关键的一步:

  1. 尽量在较低的桶内平均分布;
  2. 最大值不要超过第二高桶的上界;

对于上面提到的 如果分位数落在最高的桶内,将返回第二高桶的上界值, 下面是一个演示: 其中从 10240 到正无穷也有一些samples, 但是不论我们使用多少9999, 这里最多返回10240.

http_server_requests_duration_ms_bucket{le="5",method="GET",commandName="SigninLegacyView"} 0
http_server_requests_duration_ms_bucket{le="20",method="GET",commandName="SigninLegacyView"} 0
http_server_requests_duration_ms_bucket{le="80",method="GET",commandName="SigninLegacyView"} 30
http_server_requests_duration_ms_bucket{le="320",method="GET",commandName="SigninLegacyView"} 5881
http_server_requests_duration_ms_bucket{le="640",method="GET",commandName="SigninLegacyView"} 8567
http_server_requests_duration_ms_bucket{le="1280",method="GET",commandName="SigninLegacyView"} 8831
http_server_requests_duration_ms_bucket{le="2560",method="GET",commandName="SigninLegacyView"} 8865
http_server_requests_duration_ms_bucket{le="5120",method="GET",commandName="SigninLegacyView"} 8865
http_server_requests_duration_ms_bucket{le="10240",method="GET",commandName="SigninLegacyView"} 8867
http_server_requests_duration_ms_bucket{le="+Inf",method="GET",commandName="SigninLegacyView"} 8885

histogram.png