news 2026/10/16 12:32:57

Pandas进阶实战:分组聚合、多表合并与数据清洗一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pandas进阶实战:分组聚合、多表合并与数据清洗一次讲透

做数据分析的朋友应该都有这种感觉:DataFrame的基础操作(切片、筛选、排序、列运算)用顺手之后,真正让工作效率上一个台阶的,反而是那些“一学就会、一用就多”的组合型方法。上一篇我把创建、索引、筛选这类基本功梳理了一遍,这一篇继续往下走,聚焦分组聚合、多表合并、缺失值清洗和宽长表转换这四个方向,这几个方向覆盖了日常数据处理的绝大部分场景。内容会比较密集,但每段都配有可以直接跑通的代码示例,适合已经会基础的DataFrame操作、想进阶到“能独立处理一份脏数据”的朋友。

1. 分组聚合:groupby、agg与apply的各自边界

分组聚合是DataFrame最常用的操作之一,几乎任何带维度的汇总需求都得用到它。很多人从Excel转过来之后觉得groupby难,其实不是难,是一上来就被GroupBy对象搞晕了——明明打印出来能看,但直接取列、直接赋值都不行。这一节把groupby、agg、apply三件套拆开讲清楚,顺便把哪些场景该用谁、哪些场景别用谁说明白。

1.1 groupby的基本形态:先分组,再聚合

先看一段最普通的代码:

import pandas as pd df = pd.DataFrame({ "区域": ["华东", "华南", "华东", "华北", "华南", "华东"], "销售": [1200, 800, 1500, 900, 1100, 1300], "成本": [900, 600, 1100, 700, 850, 950], }) result = df.groupby("区域")["销售"].sum() print(result)

输出结果是一个Series,索引是“区域”,值是各区域销售合计。这个写法里有两个关键点:

第一,df.groupby("区域")返回的是一个DataFrameGroupBy对象,它本身不计算结果,只是记录了“按哪一列分组”这个规则。你可以把它理解成把一摞卡片按颜色分成几叠,但还没数每叠有多少张。只有后面接了聚合方法(.sum()、.mean()、.count()),结果才真正出来。

第二,["销售"]放在.groupby()之后、聚合函数之前,表示“我只关心销售这一列”。如果不加这一层,.sum()会对所有数值列都做求和,结果是一个DataFrame。这个差异在实际工作中经常影响下一步代码怎么写。

如果你想让聚合结果重新变成一份规整的DataFrame,而不是带索引的Series,记得加reset_index():

result = df.groupby("区域")["销售"].sum().reset_index()

这步看起来没啥技术含量,但很多人处理多级索引时被搞到头大,根因就是忘了reset。多个分组键、多列数值的写法也很直观:

result = df.groupby(["区域", "月份"])[["销售", "成本"]].sum()

这里得到的DataFrame包含两层索引,第一层是区域,第二层是月份。继续reset_index()就能得到完整的表格。我的习惯是,只要后续还要跟其他表merge、或者要写入数据库,就尽早reset,减少索引带来的麻烦。

1.2 agg:一次汇总多个统计量

单看sum()和mean()确实够用,可现实中经常遇到“既要总和、又要均值、还要订单数”的报表需求。一个个写过去也不是不行,但代码会变得又长又碎。这时候agg()就派上用场了。

result = df.groupby("区域").agg( 销售合计=("销售", "sum"), 销售均值=("销售", "mean"), 成本合计=("成本", "sum"), )

这种写法叫命名聚合,等号左边是你要的列名,右边是“源列名+聚合函数”的元组。跟老式写法比起来,最大的优势是一眼能看出新列的含义,不用再回头猜('销售', 'sum')到底叫什么。

如果你的需求更灵活,还可以在agg()里传列表:

result = df.groupby("区域")[["销售", "成本"]].agg(["sum", "mean"])

这样返回的列会是多层索引,比如("销售", "sum")、("销售", "mean")。我自己的经验是:这种多层索引好看但不好用,尤其是后面还要画图、筛选、rename的时候,层级处理费神。所以更推荐命名聚合或字典写法,保持列名的扁平结构。

agg()其实是个很朴素的“分发器”,它把不同列交给不同的聚合函数处理。凡是能在groupby之后做的批量计算,都建议优先用agg()收敛成一条语句,别在循环里逐个做。

1.3 apply的适用场景和性能边界

apply()大概是groupby系列里被误解最深的一个方法。很多人以为它就是“更高级的agg”,什么都能干,结果一跑就把性能拖垮了。实际上它的定位是:当内置聚合函数和agg都表达不了你的逻辑时,才轮到apply出场。

一个典型场景是“分组之后,每组的输出不只是一个标量,而是一个Series或DataFrame”,比如按区域分组后,每组的销售值减去该组的均值:

df["销售_去均值"] = df.groupby("区域")["销售"].transform(lambda x: x - x.mean())

这个例子用transform()比apply()更合适,因为transform要求输出形状与输入一致,刚好符合“逐行填充”的需求。

那什么时候真的要用apply()?比如你要按组拟合一个简单线性回归、按组生成一个字符串摘要、或者对每组的多个列做一套复杂运算,这时候apply()可以把整组数据作为一个块传进来:

def 区域摘要(group): return f"销售额{group['销售'].sum()},订单数{len(group)}" df.groupby("区域").apply(区域摘要)

apply很灵活,但每次调用Python函数都有开销,数据量一大性能会明显下降。我给团队的建议是:能向量化就向量化,能用内置聚合用内置聚合,apply是最后手段。处理百万行以上数据时,这个选择差别可能从几秒钟变成几分钟。

1.4 分组聚合中我踩过的三个坑

第一个坑:分组键里有NaN。默认情况下NaN会被当作一个独立组参与聚合,这会导致结果里出现一行不知道哪来的数据。写出来的报表一旦没检查,对接的人就会问“这个空值组是什么鬼”。如果你明确知道NaN不参与统计,可以用dropna=False或dropna=True控制,也可以提前fillna("未分配")。

第二个坑:聚合后没有重置索引,直接去透视或merge,结果行列错乱。这个问题新手特别容易碰到,现象是数据看起来没问题,但索引是重复的,一merge就出现笛卡尔积式的翻倍。

第三个坑:用apply后返回的列名和预期对不上。apply的结果经常保留原索引或原列结构,如果处理不当,后续代码里要么rename,要么reset_index,总是要多出几步。我的排查方法很简单,每做完一步就打印.shape和.head()看看,别等全套跑完再回头查。

2. 多表合并:merge、join、concat的关键差异与选型逻辑

现实中数据很少整整齐齐躺在一张表里,多数情况是明细表、维度表、月度报表各存各的,处理的第一步就是把这些表拼起来。DataFrame里负责拼接的方法主要有三个:merge()、join()、concat()。它们表面功能有重叠,但设计目的完全不同,用错了轻则报错,重则数据翻倍还不自知。

2.1 merge是SQL式关联的移植

merge()的逻辑跟SQL里的JOIN基本一致,核心参数是on、how、left_on/right_on。它干的事情是:根据一个或多个键,把左边表(left)和右边表(right)的行对应起来,按需合并列。

看一个最常见的场景,业务明细表和产品维表按产品编号关联:

orders = pd.DataFrame({ "订单号": ["A001", "A002", "A003"], "产品编号": ["P01", "P02", "P01"], "金额": [100, 200, 150], }) products = pd.DataFrame({ "产品编号": ["P01", "P02", "P03"], "产品分类": ["数码", "家居", "服饰"], }) merged = orders.merge(products, on="产品编号", how="left")

把how设为left,表示以左边的订单表为主,尽量保留全部订单行,产品维表有匹配就带过来,没有匹配就置空。这应该是业务分析里最常用的关联方式。

为什么不是默认的inner?因为inner只保留两边都匹配上的行,一旦维表缺数据,订单明细就会悄悄减少。很多人查“数据为什么变少”时,第一反应都是分组或过滤写错了,很少有人往merge的how上想。

如果两个表用来关联的列名不一样,就要用left_on和right_on分别指定:

merged = orders.merge(products, left_on="产品编号", right_on="商品编码", how="left")

合并完成后,如果两边有重复列名,DataFrame会自动加后缀。默认是_x和_y,看起来丑且容易混淆,我建议主动用suffixes参数手动命名:

merged = orders.merge(products, on="产品编号", how="left", suffixes=("_订单", "_产品"))

2.2 join:基于索引的关联,省一步set_index

join()和merge()最大的区别在于关联键的位置——join默认以索引为关联键,不要求你先把列设为索引。举个例子:

region_df = df.groupby("区域")[["销售"]].sum() target_df = pd.DataFrame({ "目标": [3000, 2800], "负责人": ["张三", "李四"], }, index=["华东", "华南"]) combined = region_df.join(target_df, how="left")

适合用join的场景通常是:两个表都已经按同一套索引整理好了,比如按年月索引的月度汇总表、按部门索引的人事表。直接join最省事,不用额外set_index再merge。

但有个细节要留意:join本质上是merge的索引版,匹配时同样受how影响,也会产生一对多的行膨胀。如果两边索引有重复,照样会出现笛卡尔积。所以我在用join之前都会先检查一下索引的is_unique,避免数据悄悄翻倍。

2.3 concat:顺着方向拼接,才是它的主业

concat()在处理“多个结构相同的表堆在一起”时最顺手。比如三个月各有一份销售明细,列结构完全一样,你要做的就是把它们纵向叠起来:

df_q1 = pd.DataFrame({"区域": ["华东"], "销售": [1000]}) df_q2 = pd.DataFrame({"区域": ["华南"], "销售": [800]}) all_df = pd.concat([df_q1, df_q2], ignore_index=True)

ignore_index=True的意思是:不要保留原始每个表的索引,生成一套全新的连续索引。这是纵向拼接最常见的诉求,不加的话索引会重复,后续用起来很难受。

concat也可以横向拼接,只要指定axis=1:

pd.concat([df_q1.reset_index(drop=True), df_q2.reset_index(drop=True)], axis=1)

但这种横向拼接要求两侧的行位置一一对应,否则数据就对错了行。它做的是“并排摆放”,不是“按逻辑匹配”,这一点和merge有本质区别。实战中我更建议:横向要按某种键对齐时用merge或join,只有纯粹按位置并排时才用concat(axis=1)。

2.4 合并前的数据体检:避免行数膨胀和静默丢失

合并操作最怕的不是报错,而是不报错却得到错误结果。我处理这类问题有个固定流程:

第一步,检查关联键是否唯一。用df["键"].duplicated().sum()看一眼,如果有重复,合并时就可能出现一对多。第二步,记录合并前后的行数。合并后行数如果比左边表多,多半是右表键重复;行数如果变少,多半是how选成了inner。第三步,抽样比对。取几行合并前和合并后的结果验证一下关联关系是否符合预期。

我之前帮一位朋友排查过这样的问题:他拿订单表去merge产品表,结果订单行数几乎翻倍,查了半天才发现产品表里有十几条产品编码完全重复的记录。这类问题根本防不胜防,只能靠合并前查重复来规避。

3. 缺失值与重复值:清洗流程中的判定与顺序

数据处理里最花时间的不是建模,不是写SQL,而是清洗。DataFrame的isna、fillna、dropna、duplicated、replace这套方法,就是要解决“脏数据”的问题。但方法本身不难,难的是什么时候该填、什么时候该删、什么时候该替换。

3.1 先摸清缺失情况再动手

任何清洗动作之前,第一步永远是统计缺失情况。你至少要知道三件事:哪些列有缺失、每列缺多少、缺失值到底是什么含义。

print(df.isna().sum()) print(df.isna().mean())

isna().sum()得到每列缺失个数,isna().mean()得到每列缺失比例。比例低于1%的列,直接删除缺失行通常无伤大雅;比例超过50%的列,你就要慎重考虑——删除整列可能损失大量信息,填充则可能引入较大偏差。

还有一种情况经常被忽略:缺失值并不都是NaN。很多系统导出数据时,会用"NULL"、"-"、"未知"、"0"来填充空值。如果直接用isna()判断,这些值根本不会被视为缺失。所以清洗之前,应该先看一眼每一列的取值分布,确认有没有这类“伪装值”。有的话用replace()先转成NaN,再做后续处理:

df = df.replace({"NULL": pd.NA, "-": pd.NA, "未知": pd.NA})

3.2 dropna和fillna:删和填的取舍逻辑

先说dropna()。它最简单的用法是删掉有任意缺失值的行:

df_clean = df.dropna()

但在真实场景里,整行所有字段都齐全的数据往往很少,无脑dropna()会把样本删到惨不忍睹。此时thresh参数很有用,它表示一行里至少要有多少个非空值才保留:

df_clean = df.dropna(thresh=5)

意思是,一行只要少于5个有效值就删除掉。这样能保留更多信息量足够的行,而不是因为某一个次要字段为空就整行丢掉。

再说fillna()。它最大的价值是让填充方式贴合业务含义。如果你的数据是时间序列,比如某天的销售额缺失,用前向填充可能更合理:

df["销售额"] = df["销售额"].fillna(method="ffill")

如果是数值型字段缺失,且分布呈现明显的中位数特征,很多人喜欢填均值或中位数。这里我多说一句:填中位数比填均值更稳健,因为有极端值的时候均值会被拉偏。

另一个容易被忽略的点是:fillna不只是填特定值,还可以按列填不同的值:

df = df.fillna({ "销售额": 0, "区域": "未分配", "负责人": "待补充", })

清洗逻辑和业务含义是对应的——“没有销售额”可以理解成0,“没有区域”可能代表数据没录全,填“未分配”比填0更有业务说服力。

3.3 duplicated和drop_duplicates:重复也要分情况处理

重复值问题比缺失值更隐蔽,因为它的判定完全依赖于“哪几列重复算重复”这个业务定义。duplicated()返回一个布尔序列,标记每一行是否重复;drop_duplicates()则直接删除重复行。

关键参数有三个:subset、keep、inplace。

  • subset指定按哪些列判断重复,默认是全列一致才判重。比如订单表里,同一用户同一时间下的单,主键通常是订单号,但按“用户ID + 下单日期”判断,一天内一人的多次下单就算重复。
  • keep控制保留哪一条:"first"保留第一次出现的行,"last"保留最后一次出现的行,False则删除所有重复出现的行。

实际排查时,我常用duplicated().sum()统计有多少重复行,再用布尔序列筛出来看看重复的具体形态:

dup_mask = df.duplicated(subset=["用户ID", "下单日期"], keep=False) df[dup_mask].sort_values(["用户ID", "下单日期"]).head(20)

注意,这里keep=False的作用是“把所有重复行都标True”,包括第一行。这样你能看到重复组的全貌,不至于只看到一条而猜不出规律。

还有一个决策点是:判断重复时选错列,会把真正需要保留的数据误删。我见过一个挺典型的例子,某人按“姓名+城市”判断重复,结果两位同名的员工被删掉了一条记录。所以重复值的业务定义一定要先确认好。

3.4 清洗流程的组合顺序

清洗方法之间是有顺序讲究的。我的习惯是:先处理重复值,再处理缺失值,最后再转类型、建新列。为什么不反过来?因为重复值会影响缺失值统计的准确性。假如1000行数据里藏着200行重复,先统计缺失比例,很可能高估缺失严重程度,后面做出的删除或填充决策也跟着偏向。

一个相对完整的清洗链路大概是:

# 1. 先看整体形状和字段类型 print(df.info()) print(df.shape) # 2. 替换伪装空值 df = df.replace({"NULL": pd.NA, "-": pd.NA, "未知": pd.NA}) # 3. 去重 df = df.drop_duplicates(subset=["订单号"], keep="first") # 4. 按列填充缺失 df["销售额"] = df["销售额"].fillna(df["销售额"].median()) df["区域"] = df["区域"].fillna("未分配") # 5. 最后检查一遍 print(df.isna().sum()) print(df.shape)

这套流程不是万能的,但跑完基本能做到“数据不脏、边界可控”。尤其最后一步的print检查,可以说是整个清洗过程中最容易被忽略也最救命的一步。

4. 宽表与长表的切换:pivot_table、melt、stack的实战场景

第一次接触“宽表”、“长表”这两个词的人,多半是在做透视、画图或建模型的时候。简单说,宽表是每一行包含多个时间点或类别的多个列,比如“区域”一行后面跟1月、2月、3月三列;长表则是一行对应一个“区域-月份-数值”的三元组。Excel里的透视表用的就是宽表逻辑,统计建模大多要长表。DataFrame里负责这两者转换的,是pivot_table和melt,以及不那么常用但关键时刻很顶用的stack/unstack。

4.1 pivot_table:宽表的制造机器

pivot_table可以把“一列多值”的数据摊开,变成“多列各值”的宽表。最经典的场景是把某个月的销售明细变成“区域×月份”的矩形表:

sales = pd.DataFrame({ "区域": ["华东", "华东", "华南", "华南"], "月份": ["1月", "2月", "1月", "2月"], "销售额": [1200, 1300, 800, 1100], }) pivot = sales.pivot_table( index="区域", columns="月份", values="销售额", aggfunc="sum", )

这里index决定了行维度,columns决定了列维度,values是要聚合的数值列,aggfunc是聚合函数。加一个margins=True还能顺便得到“总计”行和列:

pivot = sales.pivot_table( index="区域", columns="月份", values="销售额", aggfunc="sum", margins=True, )

很多人都分不清pivot()和pivot_table()的区别。一句话就能说清:pivot()是格式转换,不涉及聚合,如果同一个“区域-月份”组合出现两条记录,它会直接报错;pivot_table()是“先分组再摊开”,自带聚合逻辑,同一个组合出现多次会被aggfunc合并掉。所以日常业务里,你几乎永远该用pivot_table而不是pivot。

4.2 melt:把宽表变成长表,越早掌握越好

melt的作用刚好和pivot_table相反,它把“一行的多列值”拆成“多行的一列值”。还是上面那份宽表,现在想转回长表用于画图或者建模:

long_df = pivot.reset_index().melt( id_vars="区域", value_vars=["1月", "2月"], var_name="月份", value_name="销售额", )

参数含义很直接:id_vars是保留不变的标识列,value_vars是要由宽变长的那些列,var_name和value_name分别是新生成列的列名。

为什么要转成长表?因为大多数可视化库和统计分析函数都期望“一行一个观测值”的结构。宽表人看着舒服,但机器处理起来不如长表方便。分组聚合、多表合并、透视,这些操作在长表结构下也更加顺手。

我在实际项目中用melt最多的场景是:多个来源的数据导出为宽表格式,我需要先统一转成长表,才能和其他明细表合并、过滤、透视。如果你经常处理报表数据,建议把这套“宽转长、长转宽”的互换练熟,后面会非常省事。

4.3 stack和unstack:在索引层面做旋转

stack/unstack和pivot_table/melt作用类似,但操作对象不同——它们操作的是索引层级。

例如上面生成的透视表,如果用stack(),列索引会被折叠成行索引的一部分,宽表自动变成长表:

long_from_stack = pivot.stack().reset_index().rename(columns={0: "销售额"})

unstack()则是反方向,把部分行索引展开成列索引。它们的优点是“不用指定列名”,直接靠索引结构旋转;缺点是输出结果往往带着多层索引,若不及时reset_index()和重命名,可读性会比较差。

实际工作中,stack/unstack用得不如前两个频繁,但在处理多层索引的复杂表格时,它们往往能给出非常简洁的转换效果。我的看法是:不必背用法,但要知道有这么两个工具存在,遇到“莫名其妙多了一层索引”时能想起来用它们解围。

4.4 透视和逆透视时常见的报错与对策

我遇到的用户提问里,出现频率最高的是下面这条报错:

ValueError: Index contains duplicate entries, cannot reshape

这句话翻译过来就是:透视时出现了同一个“行标签+列标签”对应多行的局面,而你又用了不支持聚合的pivot()。解决办法很简单,换成pivot_table(aggfunc="sum"),或者在透视前先对原数据groupby聚合一次。

还有一个小坑:pivot_table默认会把列名和索引名带进结果,reset_index()之后会出现类似("区域", "")的多层列。应对方法是用.columns重新赋值为扁平列表:

pivot.columns = [col for col in pivot.columns]

或者直接用.rename_axis清理轴名称。这类问题不是技术难,而是细节繁琐,容易让人卡住。

5. 一条完整的处理链路:从原始明细到可汇报数据

方法讲了一堆,最后用一个带一点真实复杂度的例子把它们串起来。假设现在需要生成一份“区域×月份×品类”的销售汇总表,原始数据分别来自三张表:订单明细表、产品维表、区域目标表。订单表里存在重复订单和缺失金额,产品表里有少量品类缺失。

第一步,读取并预览三张表。这一步看的是整体结构和缺失情况,确认处理方向。

orders = pd.read_csv("orders.csv") products = pd.read_csv("products.csv") targets = pd.read_csv("targets.csv") print(orders.shape, products.shape, targets.shape) print(orders.head()) print(orders.isna().sum())

第二步,合并订单表和产品表。以订单表为主表,用left方式关联产品信息;如果产品表关联键有重复,先检查再合并:

products = products.drop_duplicates(subset="产品编号") df = orders.merge(products, on="产品编号", how="left")

第三步,清洗订单明细。默认把销量为空的记录删除或填0,把品类缺失的记录填“待分类”,同时对“订单号”去重:

df["销售额"] = df["销售额"].fillna(0) df["品类"] = df["品类"].fillna("待分类") df = df.drop_duplicates(subset=["订单号"], keep="first")

第四步,生成月份字段。如果订单时间是字符串,先转成datetime,再取出月份:

df["订单日期"] = pd.to_datetime(df["订单日期"]) df["月份"] = df["订单日期"].dt.month

第五步,做汇总透视。先按区域、月份、品类做一次长表聚合,再把月份转成列方向,形成宽表:

summary = df.groupby(["区域", "月份", "品类"])["销售额"].sum().reset_index() pivot_summary = summary.pivot_table( index=["区域", "品类"], columns="月份", values="销售额", aggfunc="sum", fill_value=0, ).reset_index()

跑完这条链路后,你会得到一份可以直接交给业务方或写进报表的干净数据。整个处理流程里最重要的工作习惯只有一个:每完成一步,就用print(shape)和head()确认一遍,再进下一步。别等所有步骤跑完才检查,那时候排查成本已经高出太多。

数据清洗和整理就是这样,方法本身学起来很快,真正拉开效率差距的,是每一步是否想清楚“这个操作会带来什么后果、有什么前置条件需要确认”。把常用方法背后的选择逻辑弄明白了,后面遇到新数据集时,你自然会知道该用什么、不该用什么。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 12:32:37

钉钉到底是什么?从组织在线到企业数字化的底层逻辑拆解

很多人对钉钉的第一反应是“那个天天打卡的软件”,甚至有人管它叫“考勤工具”。这个印象不算错,但如果你只把它当成打卡软件,那你大概率还没看懂钉钉到底在做什么。我在企业数字化这块摸爬滚打了十来年,经手过大大小小几十家公司…

作者头像 李华
网站建设 2026/10/16 12:31:55

OCAD鞋楦多档变形系统:参数化驱动母楦快速衍生全系列

1. 多档变形系统的需求原点做鞋类设计的朋友都知道,楦头是整个鞋款从图纸走向量产的第一道关卡。设计一款新鞋,不是只做一个码,而是要一整套从34到44甚至更大跨度的高矮、宽窄、胖瘦配比齐全的楦型系列。老办法是一个码一个码地手工修&#x…

作者头像 李华
网站建设 2026/10/16 12:28:09

Python+OpenCV相机标定与3D坐标转换:RGB/红外全流程实战

简介:这是一套面向计算机视觉初学者与工程开发者的相机标定与3D坐标转换实战代码包,围绕RGB相机标定、红外相机标定、图像去畸变以及2D到3D坐标转换等核心任务展开,覆盖平面直线算法、PnP算法、8点算法等经典求解思路,可应用于机器…

作者头像 李华
网站建设 2026/10/16 12:27:53

海康人脸识别C#开发实战:从SDK集成到设备报警回调

简介:这是基于C#开发的海康人脸识别Demo,面向需要在DS-K56系列人脸终端上做二次开发的工程师,直接弥补了官方SDK包缺少C#版示例、接口文档杂乱的不足。压缩包共83个文件,整体仅13.59MB,其中34个dll为海康SDK运行依赖&a…

作者头像 李华
网站建设 2026/10/16 12:24:45

基于springboot的美容院预约管理系统-附源码

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华