在微信群里看到有同学对.NET 9的贡献者数量有质疑,.NET 这样的一个全场景的应用开发平台,他的生态是很庞大的,自然一起参与开源贡献的开发者也是很大的,但是很多人都不知道一直有这么一个地址是统计了.NET各个版本的开发者数量的,这篇文章就是给大家统计显示一下各个版本的.NET贡献者人数.

.NET  Core 1.0 一共有12870 贡献:
https://dotnet.microsoft.com/en-us/thanks/1.0

image

.NET Core 2.0 一共有618 贡献:
https://dotnet.microsoft.com/en-us/thanks/2.0

image

.NET Core 3.0 一共有34108贡献:
https://dotnet.microsoft.com/en-us/thanks/3.0

image

.NET Core 3.1一共有9491 贡献:
https://dotnet.microsoft.com/en-us/thanks/3.1

image

.NET 5.0一共有49900 贡献:
https://dotnet.microsoft.com/en-us/thanks/5.0

image

.NET 6.0一共有243366 贡献:
https://dotnet.microsoft.com/en-us/thanks/6.0

image

.NET 7.0 一共有155976 贡献:
https://dotnet.microsoft.com/en-us/thanks/7.0

image

.NET 8.0 一共有78000 贡献:
https://dotnet.microsoft.com/en-us/thanks/8.0

image

.NET 9一共有49946 贡献:
https://dotnet.microsoft.com/en-us/thanks/9.0

image

数据展示组件在
Streamlit
各类组件中占据了至关重要的地位,

它的核心功能是以直观、易于理解的方式展示数据。

本次介绍的数据展示组件
st.dataframe

st.table
,能够将复杂的数据集以表格、图表等形式清晰地呈现出来,使得用户能够快速把握数据的整体情况和细节特征。

1. st.dataframe

st.dataframe
以易读且美观的方式展示
pandas

DataFrame

无论是处理小型数据集还是庞大的数据表,
st.dataframe
都能轻而易举展示数据。

st.dataframe
适用于需要在Web应用中展示复杂数据集的场景。

首先,它能够自动适应屏幕宽度,并支持水平或垂直滚动,确保用户能方便地浏览整个数据集。

此外,
st.dataframe
还支持对数据进行排序、筛选和搜索等操作,增强了数据的可读性和交互性。

2. st.table

st.table
也是用于在Web应用中显示表格数据,

它可以显示交互式表格,并提供多种自定义设置来满足各类需求。


st.dataframe
相比,
st.table
更适用于当
数据集不是特别庞大
且需要保持清晰可读性的场景。

它允许用户通过简单的配置来调整表格的显示方式,如列宽、行高等。

3. 两者区别

这两个组件都用于展示数据,都支持多种类型的数据对象作为输入,比如
pandas.DataFrame

numpy.ndarray

Iterable

dict
等等。

但是在
交互性

显示方式

功能丰富度
上面是有区别的,

下面通过一个示例来演示两者在使用上的区别,

先使用
st.dataframe
显示一个包含用户信息的静态
DataFrame
,如姓名、年龄和邮箱。

DataFrame
将显示为可滚动、可排序和可搜索的表格。还可以将数据保存为
CSV
文件。

同样使用 st.table 显示相同的用户信息数据集,但表格样式会更加简洁,功能相对较少(例如,不支持搜索)。

import streamlit as st
import pandas as pd

# 创建静态数据集
data = {
    "姓名": ["张三", "李四", "王五"],
    "年龄": [25, 30, 35],
    "邮箱": ["zhangsan@example.com", "lisi@example.com", "wangwu@example.com"],
}
df = pd.DataFrame(data)

st.header("st.dataframe")
# 使用st.dataframe显示
st.dataframe(df)

st.header("st.table")
# 使用st.table显示
st.table(df)

除了功能比较丰富以外,
st.dataframe
对于展示千上万行的大型数据集时,可以调整其高度和宽度,可以搜索过滤和排序,因此更方便遇查看数据。


st.table
由于功能相对简单,会将所有数据直接展示出来,浏览和分析大量数据不那么方便。

比如,下面模拟了一个一万条数据的场景。

st.dataframe
展示时,可以固定一块位置;而
st.table
将所有数据平铺下去展示,加装时间也明显长很多。

# 创建大数据集
np.random.seed(0)
data = {
    "ID": np.arange(1, 10001),
    "值1": np.random.rand(10000),
    "值2": np.random.rand(10000),
    # ... 可以添加更多列
}
df = pd.DataFrame(data)

st.header("st.dataframe", width=400, height=600)
# 使用st.dataframe显示大数据集
st.dataframe(df)

st.header("st.table")
# 使用st.table显示大数据集(可能性能不佳)
# 对于大数据集,st.table可能不是最佳选择
st.table(df)

4. 总结

总得来看,
st.dataframe
更适合需要高级功能和动态交互的场景,


st.table
则更适合简单、快速的表格展示。

基本概念

基于磁盘的B+树

为什么使用B+数进行数据访问(Access Method):

image-20241109143725399

  • 天然有序,支持范围查找
  • 支持遍历所有数据,利用顺序IO
  • 时间复杂度为
    \(O(logn)\)
    ,满足性能需求
  • 相比于B树,数据访问都在叶子结点:磁盘空间利用率高;并发冲突减少

一个基础的B+树:

  • 三类借点:根结点,中间结点,叶子结点
  • 数据分布:根结点和中间结点只存储索引,叶子结点存储数据
  • 指针关系:父子指针,兄弟指针

image-20241113104944036

基于磁盘的B+树映象:

一个结点存储在一个堆文件(Heap File)页中;页ID(PageId)代替指针的作用。

  • 键值联合存储

    image-20241113105746186

  • 键值分别存储

    image-20241113105801713

B+树的叶子结点存储实际数据,这个数据如何理解,取决于不同的数据库实现:有些存储RecordID,有些基于索引组织(Index-Organized Storage)的数据库则直接存储元组(Tuple)数据。

image-20241113110032259

如果不了解RecordID,数据组织方式,可以参看
这篇博文

查询与索引

最左前缀匹配

有联合索引
<a,b,c>
,支持如下查询条件

  • (a=1 AND b=2 AND c=3)
  • (a=1 AND b=2)

image-20241113111703944

如果所有不满足最左前缀匹配原则,需要全表扫描。

如何处理重复键

  • 加上RecordID使其变成唯一键

    image-20241113111945385

  • 叶子结点溢出(没有实际系统采用)

    image-20241113111956652

聚簇索引

  • 一个表只能有一个聚簇索引
  • 索引键和值存储在一起
  • 数据按照索引的键排序
  • 操作数据时要同步操作索引

聚簇索引是非必须的,取决于数据库具体实现,Mysql和SQLite中数据直接用聚簇索引组织。

用B+树实现聚簇索引可以很方便地实现范围查询和便利,充分利用顺序IO。

image-20241113112518741

对于非聚簇索引,虽然索引的键有序,但是对应的数据在磁盘上不一定是顺序存储的,所以很有效的方式是先得到PageID,后根据PageID进行排序,最后获取数据,充分利用顺序IO。
image-20241113112630957

设计选择

结点大小(Node Size)

存储设备读取数据越慢,越需要利用顺序IO,结点就越大;

存储设备读取数据越快,越需要减少冗余数据读取,结点就越小。

  • HDD:~1MB
  • SSD:~10KB
  • In-Memory:~512B

合并阈值(Merge Thredshold)

结点中的键数量低于半满的时候,不会立刻进行合并,而是允许小结点存在,然后再周期性地重建整棵树。

PostgreSQL中称其为不平衡的B+树("non-balanced" B+Tree, nbtree)。

变长键(Variable-length Keys)

  • 指针:键存储指向实际数据的指针【无法利用顺序IO,因为要跳转去读取指针内容】
  • 变长结点
  • 填充数据(Padding)

实际系统中的索引数据和堆文件数据一样,是能存结点就存结点中,是在存不下存指针。

  • 线性查找:由于SIMD指令集存在,实际顺序查询,其实可以是批处理


    image
  • 二分查找

  • 插值法:键没有间隙的时候(自增),可以直接计算出偏移


    image

优化手段

Pointer Swizzling

基本思想:当一个对象从磁盘加载到内存时,将其磁盘地址转换成内存地址(
swizzling
),以便程序在内存中直接通过指针访问。

例子:比如主键索引的B+树根结点读取到Buffer Pool后,会被pin住,不被置换出去,所以此时可以直接用内存指针访问根结点,省略用PageID问Buffer Pool要内存地址的步骤。

图示:

image
image

Bε-trees

一种B+树的写优化。

基本思想:更新时不直接修改数据 ,而是记录日志(类似于log-structured data storage)。

日志记录在结点上,当结点日志记录满以后,该结点的日志下推到孩子结点。

image-20241113133509231

image-20241113133530151

Bulk Insert

基本思想:由底至顶创建B+树,而不是由顶至底。

减少了插入时树的结构变化,前提是需要预先排序数据。

Keys: 3, 7, 9, 13, 6, 1
Sorted Keys: 1, 3, 6, 7, 9, 13

image-20241113133953890

image-20241113134001752

Prefix Compression

基本思想:字典序压缩前缀。

Deduplication

基本思想:非唯一索引中避免重复存储相同键。

Suffix Truncation

基本思想:中间结点只是起引路作用,所以存储能辨识的最小前缀即可。

image
image

问题情景

混淆群内的小伙伴遇到这么个问题,Mailivery 这个网站登录后,明明提交的表单(邮箱和密码也正确)、请求头等等都没问题,为啥一直重定向到登录页面呢?唉,该出手时就出手啊,我也看看咋回事吧!

截图_20241113133726

登录参数分析

显而易见,需要:邮箱(有邮箱校验)、密码

打开开发者工具,随意输入邮箱和密码登录,查看登录接口的请求和方法:

截图_20241113133929

请求网址:https://app.mailivery.io/login
请求方法:POST
状态代码:302 Found

302
?!重定向吗?在登录不成功后重定向到了登录网站,要求重新登录。目测前后端不分离项目(阿巴阿巴),如果登录成功,肯定会携带登录成功时设置的 Cookie 重定向到主页或者相关主面板。

查看一下登录接口提交头:

POST /login HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 341
Content-Type: application/x-www-form-urlencoded
Cookie: XSRF-TOKEN=省略; mailivery_session=省略; ......
DNT: 1
Host: app.mailivery.io
Origin: https://app.mailivery.io
Pragma: no-cache
Referer: https://app.mailivery.io/login
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
sec-ch-ua: "Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

没有啥加密参数,emm……,很简单,的确是看着很简单,注意
Content-Type

application/x-www-form-urlencoded

再看看提交的参数:

_token=lCsu2Ruuw33uHlHkRlKwZG3C2tw7TQBjUoTo1yjz&paid_user_Khe2xqZLA4Tkq3py=&submitted_in_seconds=eyJpdiI6IjJ4OXdFK3ZPZklKUnNadXVtRTk0L3c9PSIsInZhbHVlIjoiNHg5NzJmUjM3UnlDOU1tanlnUHpWdz09IiwibWFjIjoiMDYxYmRkODU0YmY1ZjY0MDk4OWMzNmM5YWU5MjNmZDM4NTg5NzQ1MmM3MzBjNzQ3YjYxNTg0MjliYjFjYzM3OCIsInRhZyI6IiJ9&email=xxx%40foxmail.com&password=123

格式化看一下:

_token: lCsu2Ruuw33uHlHkRlKwZG3C2tw7TQBjUoTo1yjz
paid_user_Khe2xqZLA4Tkq3py: 
submitted_in_seconds: eyJpdiI6IjJ4OXdFK3ZPZklKUnNadXVtRTk0L3c9PSIsInZhbHVlIjoiNHg5NzJmUjM3UnlDOU1tanlnUHpWdz09IiwibWFjIjoiMDYxYmRkODU0YmY1ZjY0MDk4OWMzNmM5YWU5MjNmZDM4NTg5NzQ1MmM3MzBjNzQ3YjYxNTg0MjliYjFjYzM3OCIsInRhZyI6IiJ9
email: xxx%40foxmail.com
password: 123

表单提交嘛,多多少少也是带点验证参数,比如这里的:
_token

paid_user_Khe2xqZLA4Tkq3py

submitted_in_seconds

经过多次登录提交,发现
_token

submitted_in_seconds
的值是变动的,而
paid_user_Khe2xqZLA4Tkq3py
变动的是
paid_user_
之后的部分。并且这些参数与窗口的
Cookie
相关,如果不一致,将发生 416 错误,提示页面过期。看来这就是个反爬点了,还记得我说的吗,这个网站是前后端不分离,那么这些参数肯定也隐藏于表单,在元素中搜索特色浓重的
submitted_in_seconds

截图_20241113135213

非常的好,可以看到我们这三个参数的来源了,捋一捋:

访问 login 网站(GET) 得到三个参数,此时响应头所设置的 Cookie 无登录效力
对 login 网站(POST) 携带三个参数,根据结果重定向:
	如果登录失败,此时响应头所设置的 Cookie 也是无登录效力的,携带 Cookie 要求咱们继续登录
	如果登录成功,此时响应头所设置的 Cookie 就是具备登录效力的,携带 Cookie 直接上主页

接下来在 Python 中实现一下,创建
login.py

dashboard.py

login.py 实现登录的接口:

import requests

url = "https://app.mailivery.io/login"

_token = ''
paid_user = ''
submitted_in_seconds = ''
email = 'xxx%40163.com'
password = 'xxxxxxx'

payload=f'_token={_token}&{paid_user}=&submitted_in_seconds={submitted_in_seconds}&email={email}&password={password}'

headers = {
   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
   'Accept-Language': 'zh-CN,zh;q=0.9',
   'Host': 'app.mailivery.io',
   'Origin': 'https://app.mailivery.io',
   'Referer': 'https://app.mailivery.io/login',
   'Sec-Fetch-Dest': 'document',
   'Sec-Fetch-Mode': 'navigate',
   'Sec-Fetch-Site': 'same-origin',
   'Sec-Fetch-User': '?1',
   'Upgrade-Insecure-Requests': '1',
   'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
   'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
   'sec-ch-ua-mobile': '?0',
   'sec-ch-ua-platform': '"Windows"',
   'Cookie': 'XSRF-TOKEN=eyJpdiI6Ikk1WjFjMFRsdVhKdzBuMjBWamNVVEE9PSIsInZhbHVlIjoiblAzS0ZBOEUyK1lXWGV0ZXhuT1Y5MldLc290d2ZzL0E3dTQxT3BDMmNGR3d6aG0vamhUekozeVFCUVVVczRJSWxxQTM1ZGpvOU5KTm11bFp4NEsvWGlObUJ6V1A3WWc1WFJXcUlPYWYzYTgrSGNMZ2VtM0s1R0tGUlJ4Z0ZMSy8iLCJtYWMiOiI2NGUxYmNhMjEwMmE3ZDNmOTc4OTcxMWVlZGY3ODIyNDZhODBiYzUxZjVhMWE2YWZkMWVhOGM2YjA4MmQzYmY0IiwidGFnIjoiIn0%3D; mailivery_session=eyJpdiI6IjVwL1QwUHNhMTlTdGUyZ0ozUzY3aGc9PSIsInZhbHVlIjoiYlNuSXI2d2tKWjMrYjFpVmx1Ym5uTEVOUGhXZjFKRGhVV1VMeXRHQ1BpRWFsV0ZnYVFkNDd4Vm9wdXY1ZElqVWpVL2xhSytNdnBDYS9NNHRBWmNzRDF4ZjJtWFhPTHFJRFBLVnNYSmFPMW9HSkEweVVpQTZZVjhJU2k5WSswR0oiLCJtYWMiOiJkYzg0ZmY2ODEwZmEyMzczNzU5NGU4YzMwYjA2MDRlZTc0ZWJiNDc4ZDBhMDU4OTgyM2E3NDMzZDM3NmRmNTcxIiwidGFnIjoiIn0%3D',
   'Content-Type': 'application/x-www-form-urlencoded',
   'Connection': 'keep-alive'
}

response = requests.request("POST", url, headers=headers, data=payload, allow_redirects=False, proxies={
    'http': None,
    'https': None
})

print(response.headers)
print(response.status_code)

注意,我说过,Cookie 和三个参数是有紧密联系的,上述 Cookie 是通过对登录页面 GET 时得到的,三个参数同理。

三个参数的值我已省略,特别特别要注意的是:请求时,
allow_redirects
这个值最好设置为 False,因为重定向底层和浏览器不同,如果为 True,你会发现你的 Cookie 一直都是失效的。因为产生了如下的重定向过程:

邮箱密码正确的情况:
login -> dashboard -> login
邮箱密码错误的情况:
login -> login
参数未校验的情况:
login -> 页面过期

为什么最终又到 login 了???

我猜测可能是反爬点,动态加载未加载好,太快了,Cookie 或者三个参数在后端都还是未准备好的状况。

我们只需要取得跳转到 dashboard 前响应头中的 Set-Cookie 的值,也就是
XSRF-TOKEN

mailivery_session

因此我们停止使用 requests 的重定向。

接下来看一下 dashboard.py 这个文件,主要测试一下取得 Cookie 是否有效:

import requests

url = "https://app.mailivery.io/campaign/dashboard"

payload={}
xsrf_token = 'xxx'
mailivery_session = 'xxx'

headers = {
   'Cookie': f'XSRF-TOKEN={xsrf_token}; mailivery_session={mailivery_session}',
   'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
   'Accept': '*/*',
   'Host': 'app.mailivery.io',
   'Connection': 'keep-alive'
}

response = requests.request("GET", url, headers=headers, data=payload, proxies={
    'http': None,
    'https': None
})

print(response.text)

全套模拟

我们最终的效果是:自动获取三个参数和初始的 Cookie,然后根据邮箱和密码登录,再携带 Cookie 访问 Dashboard。

  1. 自动获取这个很简单,通过 lxml 中的 etree 解析,Cookie 通过 requests 响应头得到。

  2. 然后去登录,也很简单,携带好参数和上一级得到的 Cookie,如果登录密码这些没错,将得到有效的 Cookie。

  3. 最后携带上一级得到的有效 Cookie 进行登录。

如果说你觉得到此结束了,那很抱歉,你再进行上述的实现后,将发现 Cookie 无效!!!

略带一笔,自动获取的表单参数和初始 Cookie 如果有问题,请求时将响应 416。如果没问题,你在取到后立马进行登录,请求将响应 200。200 就一定是好事吗?我已经说过了,要重定向到 dashboard 页面,那么应该是 302 才对!所以我们在取参数到登录这个请求中进行延时,等待后端缓过神,比如 3 秒?4秒?都是可以的。或者你根据响应状态码增加重试机制,尝试到出现 302 的情况。

完整代码就不提供啦,自行琢磨吧~

前言

在工作我们经常会出现有多个文件,为了节省资源会将多个文件放在一起进行压缩处理;为了让大家进一步了解我先将springboot处理的方法总结如下,有不到之处敬请大家批评指正!

一、文件准备:

https://qnsc.oss-cn-beijing.aliyuncs.com/crmebimage/public/product/2024/11/12/be353210028a3da732c8ba34073fb4ca.jpeg
https://qnsc.oss-cn-beijing.aliyuncs.com/crmebimage/public/product/2024/11/13/5bbf579109db2641249deab4be4340f6.jpeg
https://qnsc.oss-cn-beijing.aliyuncs.com/crmebimage/public/product/2024/11/13/1808773678128361474.xlsx

二、处理步骤:

1.创建一个springboot web项目 这一步在此省略.....

2.需要的方法及类的编写

(1)业务方法-TestService
public interfaceTestService {void compressFiles(List<String>fileUrls, HttpServletResponse response);
}
(2)业务方法实现类-TestServiceImpl
@Service
@Slf4j
public class TestServiceImpl implementsTestService {

@Override
public void compressFiles(List<String>fileUrls, HttpServletResponse response) {try (ZipOutputStream zipOut = newZipOutputStream(response.getOutputStream())) {for(String fileUrl : fileUrls) {//1.从网络下载文件并写入 ZIP try{
URL url
= newURL(fileUrl);
HttpURLConnection connection
=(HttpURLConnection) url.openConnection();
connection.setRequestMethod(
"GET");
connection.connect();
//2.检查响应码 if (connection.getResponseCode() !=HttpURLConnection.HTTP_OK) {throw new IOException("Failed to download file: " +fileUrl);
}
//3.从 URL 中提取文件名 String pathStr = fileUrl.substring(fileUrl.lastIndexOf('/') + 1);//4.创建 ZIP 条目 ZipEntry zipEntry = newZipEntry(pathStr);
zipOut.putNextEntry(zipEntry);
//5.读取文件的输入流 try (InputStream inputStream = newBufferedInputStream(connection.getInputStream())) {byte[] buffer = new byte[1024];intlength;while ((length = inputStream.read(buffer)) >= 0) {
zipOut.write(buffer,
0, length);
}
}
zipOut.closeEntry();
}
catch(IOException e) {
log.error(
"Error processing file URL: " +fileUrl, e);throw newRuntimeException(e);
}
}
       // 6.响应信息设置处理
response.setContentType(
"application/octet-stream");
response.setHeader(
"Content-Disposition", "attachment;filename=test.zip");
response.flushBuffer();
}
catch(IOException e) {
log.error(
"Error compressing files", e);throw newRuntimeException(e);
}
}
}
(3)控制器类的编写-TestController
/*** @Project:
* @Description:
*
@author: songwp
* @Date: 2024/11/13 14:50
*
*/@RequestMapping("test")
@RestController
@Slf4j
public classTestController {

@Autowired
privateTestService testService;/*** 文件压缩
*
*
@paramfileUrls 要压缩的文件 URL 列表
*
@paramresponse 响应对象*/@GetMapping("/fileToZip")public void zip(@RequestParam("fileUrls") List<String>fileUrls, HttpServletResponse response) {
testService.compressFiles(fileUrls, response);
}
}

三、方法调用展示

(1)存放到桌面

(2)解压response.zip文件