一、Linux / Shell 进阶
今日新知识点:sed 文本替换 + 重定向
任务:
对之前的 access.log 做如下操作
1.将所有 200 替换为 OK
2.将所有 404 替换为 ERROR
3.将结果保存到新文件 access_new.log
# 替换并输出到新文件
sed 's/200/OK/g; s/404/ERROR/g' access.log > access_new.log
语法:
sed [选项] '命令' 文件名
sed [选项] -f 脚本文件 文件名 #从脚本文件读取命令
1.替换操作:
# 语法:s/查找内容/替换内容/标志
# 将每行第一个 foo 替换为 bar
sed 's/foo/bar/' file.txt
# 替换所有 foo(g 表示全局替换)
sed 's/foo/bar/g' file.txt
# 替换第2个匹配
sed 's/foo/bar/2' file.txt
# 只替换第3行
sed '3s/foo/bar/' file.txt
# 替换第3-5行
sed '3,5s/foo/bar/' file.txt
# 当查找内容包含 / 时,可以使用其他分隔符
sed 's/\/path\/to\/dir/\/new\/path/' file.txt
# 更清晰的方式:
sed 's|/path/to/dir|/new/path|' file.txt
sed 's#/path/to/dir#/new/path#' file.txt
# g:全局替换
sed 's/old/new/g' file.txt
# i:忽略大小写
sed 's/old/new/i' file.txt
# p:打印替换的行
sed -n 's/old/new/p' file.txt
# w:将替换的行写入文件
sed 's/old/new/w output.txt' file.txt
# 数字:只替换第n个匹配
sed 's/old/new/2' file.txt
# 组合使用
sed 's/old/new/gi' file.txt # 全局+忽略大小写
2.删除操作:
# 删除第2行
sed '2d' file.txt
# 删除第2-5行
sed '2,5d' file.txt
# 删除最后一行
sed '$d' file.txt
# 删除空行
sed '/^$/d' file.txt
# 删除包含"error"的行
sed '/error/d' file.txt
# 删除不包含"error"的行
sed '/error/!d' file.txt
# 删除第2行到最后一行
sed '2,$d' file.txt
3.打印操作(配合-n: -n 禁止自动打印):
# 打印第2行
sed -n '2p' file.txt
# 打印第2-5行
sed -n '2,5p' file.txt
# 打印包含"error"的行
sed -n '/error/p' file.txt
# 打印行号
sed -n '=' file.txt
# 打印行号和内容
sed -n '=;p' file.txt
4.插入、追加、修改:
# i:在当前行之前插入
sed '2i\这是插入的内容' file.txt
# a:在当前行之后追加
sed '2a\这是追加的内容' file.txt
# c:替换整行
sed '2c\这是替换后的内容' file.txt
# 多行插入
sed '2i\
第一行\
第二行\
第三行' file.txt
5.行范围操作:
# 从第2行到第5行
sed '2,5s/old/new/g' file.txt
# 从第2行到最后一行
sed '2,$s/old/new/g' file.txt
# 从匹配"start"的行到匹配"end"的行
sed '/start/,/end/s/old/new/g' file.txt
# 从第2行到匹配"end"的行
sed '2,/end/s/old/new/g' file.txt
6.实战:
# 文本替换
# 替换IP地址
sed 's/192.168.1.1/10.0.0.1/g' file.txt
# 替换多个模式
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt
# 使用分号分隔多个命令
sed 's/foo/bar/g; s/baz/qux/g' file.txt
# 替换行首和行尾
sed 's/^/前缀 /; s/$/ 后缀/' file.txt
# 注释掉配置文件中的行(在行首添加#)
sed 's/^/#/' config.conf
# 取消注释
sed 's/^#//' config.conf
# 格式化文本
# 在每行末尾添加逗号
sed 's/$/,/' file.txt
# 删除行首空格
sed 's/^[[:space:]]*//' file.txt
# 删除行尾空格
sed 's/[[:space:]]*$//' file.txt
# 删除所有空格
sed 's/[[:space:]]//g' file.txt
# 将多个连续空格替换为单个空格
sed 's/[[:space:]]\+/ /g' file.txt
# 日志处理
# 替换状态码
sed 's/ 200 / OK /g; s/ 404 / ERROR /g' access.log
# 提取日期(假设日期在行首)
sed -n 's/^\([0-9-]*\).*/\1/p' log.txt
# 删除时间戳之前的行
sed '/2024-01-01/,$!d' log.txt
7.原地编辑:
# 直接修改文件(危险操作,建议先备份)
sed -i 's/old/new/g' file.txt
# 修改前自动备份
sed -i.bak 's/old/new/g' file.txt
# 批量修改多个文件
sed -i 's/old/new/g' *.txt
8.实用案例:
# 案例1:处理CSV文件
# 将CSV文件中的逗号替换为制表符
sed 's/,/\t/g' file.csv > file.tsv
# 删除CSV中的引号
sed 's/"//g' file.csv
# 只修改第2列(复杂情况用awk更好)
# 修改配置文件
# 修改nginx配置中的端口
sed -i 's/listen 80;/listen 8080;/' /etc/nginx/nginx.conf
# 取消注释特定行
sed -i '/AllowOverride/s/^#//' httpd.conf
# 修改数据库连接字符串
sed -i 's/db_password=oldpass/db_password=newpass/' config.ini
# 文本统计
# 统计非空行数
sed -n '/^$/!p' file.txt | wc -l
# 或
sed '/^$/d' file.txt | wc -l
# 删除注释和空行
sed -e '/^#/d' -e '/^$/d' file.txt
# 处理HTML
# 提取所有链接
sed -n 's/.*href="\([^"]*\)".*/\1/p' page.html
# 删除HTML标签
sed 's/<[^>]*>//g' page.html
二、SQL
分组聚合 + 子查询 + 排名综合
分组 TopN 标准模板:
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY 分组字段 ORDER BY 排序字段 DESC) AS rn
FROM 表
) t
WHERE rn <= N
关联子查询 vs 非关联子查询
• 非关联:子查询里没有外层表的列 → 先内后外
• 关联:子查询里有外层表的列 → 边外跑,边内查
COUNT(IF(…)) 条件计数
这是一种在 SQL 中实现条件聚合的常见写法,用于统计满足特定条件的行数
COUNT(IF(条件, 1, NULL))
-- 或
COUNT(IF(条件, 1, NULL)) AS 别名
原理:COUNT() 只统计非 NULL 值。当条件为 TRUE 时返回 1(非 NULL),FALSE 时返回 NULL(被忽略),等价于 SUM(CASE WHEN 条件 THEN 1 ELSE 0 END),适合需要一次扫描完成多维度统计的场景
185.部门工资前三高的所有员工

select name Department, employee Employee, salary Salary
from(select a.name employee, a.salary, a.departmentid, b.id, b.name,
dense_rank() over(partition by departmentid order by salary desc) salary_rank
from employee a left join department b on a.departmentid = b.id
) tem
where salary_rank <= 3;
574.当选者(子查询 + 分组综合)

直接联表 + 分组排序:
SELECT name
FROM Candidate
JOIN Vote ON Candidate.id = Vote.candidateId
GROUP BY Candidate.id, name -- 按候选人分组
ORDER BY COUNT(*) DESC -- 票数从高到低
LIMIT 1; -- 取第1名
窗口函数:
WITH vote_count AS (
SELECT
c.name,
RANK() OVER (ORDER BY COUNT(v.id) DESC) AS rnk
FROM Candidate c
LEFT JOIN Vote v ON c.id = v.candidateId
GROUP BY c.id, c.name
)
SELECT name
FROM vote_count
WHERE rnk = 1;
580.统计各专业学生人数(分组 + 条件计数)

SELECT
d.department_id,
d.department_name,
COUNT(s.student_id) AS student_number
FROM Department d
LEFT JOIN Student s
ON d.department_id = s.department_id
GROUP BY d.department_id, d.department_name
ORDER BY student_number DESC, department_name ASC;
三、PySpark 核心进阶
承接 Day4:Join + SparkSQL,今日新内容:数据倾斜前置、UDF、函数扩展
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
from pyspark.sql.types import IntegerType
# 1. 构建SparkSession(承接前序)
spark = SparkSession.builder \
.master("local[*]") \
.appName("day5") \
.getOrCreate()
# 造数据
data = [
(1, "小明", 23),
(2, "小红", 25),
(3, "小李", 30),
(4, "小张", 17)
]
df = spark.createDataFrame(data, ["id", "name", "age"])
# ==================== 新知识点1:自定义UDF ====================
def is_adult(age):
return 1 if age >= 18 else 0
# 注册UDF
is_adult_udf = F.udf(is_adult, IntegerType())
df = df.withColumn("is_adult", is_adult_udf(F.col("age")))
df.show()
# ==================== 新知识点2:常用函数 ====================
df.withColumn("age_square", F.pow(F.col("age"), 2)) \
.withColumn("name_upper", F.upper(F.col("name"))) \
.show()
# ==================== 新知识点3:处理可能的数据倾斜(面试高频) ====================
# 加盐思想:给key加随机前缀
df_salt = df.withColumn("salt", F.floor(F.rand() * 3))
df_salt.show()
# ==================== 新知识点4:写入本地文件 ====================
# df.write.mode("overwrite").csv("./output", header=True)
spark.stop()
今日必须掌握:
1.UDF 定义与注册
2. PySpark 常用函数:pow、upper、rand
3. 数据倾斜加盐预处理思路
4. withColumn 链式调用
PySpark UDF 性能较差,优先使用 Spark 内置函数,必要时用 Pandas UDF
1. UDF 定义与注册

- UDF 定义:在 Python 中定义 UDF 只需创建普通函数,通过装饰器或 API 转换为 Spark 可识别的用户函数:
# 基础 UDF(一进一出)
def str_upper(s: str) -> str:
return s.upper()
# 复杂 UDF(多列输入)
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
""" 计算经纬度距离 (Haversine公式) """
import math
R = 6371 # 地球半径(km)
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * \
math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
- 注册方法(以 Hive 和 Spark 为例)
临时注册(会话级有效)
from pyspark.sql import SparkSession
from pyspark.sql.functions import udf
from pyspark.sql.types import FloatType, StringType
spark = SparkSession.builder.appName("UDF Demo").getOrCreate()
# 方法1: 使用 udf() 函数注册
distance_udf = udf(calculate_distance, FloatType())
spark.udf.register("calc_distance", distance_udf)
# 方法2: 装饰器注册 (Spark 3.0+)
@udf(returnType=StringType())
def format_name(name: str) -> str:
return name.strip().title()
# 调用示例
df = spark.createDataFrame([(31.23, 121.47, 30.25, 120.15)], ["lat1","lon1","lat2","lon2"])
df.select(distance_udf("lat1","lon1","lat2","lon2").alias("distance")).show()
永久注册(跨会话生效),需将函数打包为 JAR 或通过 Hive Metastore 注册:
# 注册到 Hive Metastore
spark.sql("""
CREATE FUNCTION geo_distance
AS 'com.example.udf.GeographyUDF' -- Python类路径
USING JAR 'hdfs:///udfs/geography.jar' -- 包含Python实现的JAR
""")
# 调用示例
spark.sql("SELECT geo_distance(31.2, 121.5, 30.3, 120.2)").show()
- 类方法中定义 UDF(解决序列化问题)
通过嵌套函数避免类对象传递问题:
class UserProcessor:
@staticmethod
def _format_age(age: int) -> str:
return f"Age: {age} years"
@classmethod
def get_age_udf(cls):
# 嵌套函数解决序列化
def _inner(age: int) -> str:
return cls._format_age(age)
return udf(_inner, StringType())
# 注册使用
processor = UserProcessor()
spark.udf.register("format_age", processor.get_age_udf())
- 关键注意事项
数据类型映射
Python 类型需与 Spark 类型匹配(如 FloatType() 对应 float)
性能优化
优先使用内置函数
复杂计算使用 Pandas UDF(向量化计算)
from pyspark.sql.functions import pandas_udf
@pandas_udf(FloatType())
def vectorized_udf(v: pd.Series) -> pd.Series:
return v * 0.8 # 向量化操作
依赖管理,通过 spark.submit.pyFiles 上传依赖:
spark-submit --py-files utils.py main.py
完整示例:
# 上传依赖到 HDFS
hdfs dfs -put geography_udf.py hdfs:///udfs/
# 在 Spark 中注册
spark.udf.register("hdfs_udf",
udf(calculate_distance, FloatType()),
"hdfs:///udfs/geography_udf.py"
)
2. PySpark 常用函数:pow、upper、rand

pow函数
功能:计算数值列的幂运算
公式:pow(x,y)=x^y
输入类型:数值类型(IntegerType, FloatType, DoubleType等)
返回类型:DoubleType
from pyspark.sql import SparkSession
from pyspark.sql.functions import pow
spark = SparkSession.builder.appName("pow_example").getOrCreate()
data = [(2, 3), (5, 2), (10, 0.5)]
df = spark.createDataFrame(data, ["base", "exponent"])
# 计算幂运算
result = df.select(
"base",
"exponent",
pow("base", "exponent").alias("result")
)
result.show()
输出示例:
+----+--------+------------------+
|base|exponent| result|
+----+--------+------------------+
| 2| 3| 8.0|
| 5| 2| 25.0|
| 10| 0.5|3.1622776601683795|
+----+--------+------------------+
upper 函数
功能:将字符串列转换为全大写
公式:upper(s)=str.toUpperCase()
输入类型:StringType
返回类型:StringType
from pyspark.sql.functions import upper
data = [("spark",), ("PySpark",), ("DataFrame",)]
df = spark.createDataFrame(data, ["text"])
# 转换为大写
result = df.select(
"text",
upper("text").alias("uppercase")
)
result.show(truncate=False)
输出示例:
+--------+---------+
|text |uppercase|
+--------+---------+
|spark |SPARK |
|PySpark |PYSPARK |
|DataFrame|DATAFRAME|
+--------+---------+
rand 函数
功能:生成 [0, 1) 区间内的随机浮点数
公式:rand(seed)∼U(0,1)
输入参数:可选随机种子(LongType)
返回类型:DoubleType
from pyspark.sql.functions import rand
# 创建包含随机数的DataFrame
df = spark.range(5).select(rand(seed=42).alias("random"))
df.show()
输出示例:
+-------------------+
| random|
+-------------------+
| 0.7275631410328446|
|0.30783211384997455|
| 0.1799956432689908|
| 0.4278578143953135|
| 0.9971062793385363|
+-------------------+
对比:

进阶:复合使用示例(生成带随机数的用户名)
from pyspark.sql.functions import concat, lit
df = spark.createDataFrame([("john",), ("emma",)], ["name"])
df.select(
upper(concat("name", lit("_"), (rand()*100).cast("int"))).alias("user_id")
).show()
+--------+
| user_id|
+--------+
|JOHN_42|
|EMMA_17|
+--------+
- 数据倾斜加盐预处理思路

加盐(Salting)是解决数据倾斜的核心技术之一,本质是通过人工引入随机因子打散热点Key,使数据均匀分布到各计算节点。以下是系统化的加盐预处理思路:
-
加盐核心原理(数学表达)
-
设原始数据分布为:

-
其中存在热点Key kh 满足

-
加盐操作通过随机映射函数:

-
将原始Key空间映射到新空间:

-
使 ∀ k ′ ∈ D ′ \forall k' \in D' ∀k′∈D′ 满足均匀分布:

-
-
加盐预处理实施步骤
识别热点Key(关键预处理)
# 统计Key频率分布
key_dist = df.groupBy("key").count().orderBy("count", ascending=False)
# 识别倾斜阈值 (如Top 1%数据量占比超过总数据30%)
skew_threshold = key_dist.approxQuantile("count", [0.99], 0.01)[0]
hot_keys = key_dist.filter(col("count") > skew_threshold)
动态盐值分配
salt_buckets = 100 # 根据集群规模确定盐桶数量
# 仅对热点Key加盐,非热点Key保留原值
salted_df = df.withColumn(
"salted_key",
when(col("key").isin(hot_keys),
concat(col("key"), lit("_"), (rand() * salt_buckets).cast("int")))
.otherwise(col("key"))
)
两阶段聚合(解决Group By倾斜)

# 第一阶段:盐值分桶聚合
stage1 = salted_df.groupBy("salted_key").agg(sum("value").alias("partial_sum"))
# 第二阶段:还原Key全局聚合
stage2 = stage1.withColumn("original_key", split(col("salted_key"), "_")[0])
result = stage2.groupBy("original_key").agg(sum("partial_sum").alias("total_sum"))
Join场景加盐扩展
# 大表热点Key加盐
big_table_salted = big_table.withColumn(
"salted_join_key",
when(is_hot_key(col("join_key")),
concat(col("join_key"), lit("_"), (rand() * salt_buckets).cast("int")))
.otherwise(col("join_key"))
)
# 小表广播盐值扩展
salt_df = spark.range(salt_buckets).withColumn("salt", col("id"))
small_table_salted = small_table.crossJoin(broadcast(salt_df)) \
.withColumn("salted_join_key", concat(col("join_key"), lit("_"), col("salt")))
# 盐值Join
joined_df = big_table_salted.join(small_table_salted, "salted_join_key")
-
关键优化策略
-
盐桶数量计算

-
复合盐值设计
- 多维热点:concat(key1, " _ ", key2, " _ ", salt)
- 分层盐值:salt1 % 10 + salt2 % 100(二级分散)
-
动态盐值调整
-
# 根据运行时数据分布动态调整盐桶
if executor_skew_detected():
salt_buckets = min(salt_buckets * 2, MAX_BUCKETS)
- 适用场景对比

实践建议:在数据湖存储层预先对已知热点Key添加盐值字段,从源头避免计算倾斜
- withColumn 链式调用

withColumn是Spark DataFrame API的核心方法之一,支持链式调用(method chaining),允许在单个数据转换流程中连续执行多个列操作。这种编程模式是Spark DSL风格的核心优势
- 核心特性
不可变性原则:每次调用withColumn都返回新的DataFrame,原始DataFrame保持不变,符合函数式编程思想
# 原始DF不会改变
new_df = (original_df
.withColumn("new_col1", ...)
.withColumn("new_col2", ...) # 基于前一步的结果
)
惰性执行机制:所有转换操作在触发行动操作(如show(), count())前仅构建逻辑计划,Catalyst优化器会自动优化整个调用链
类型安全(Scala):在Scala API中编译器会验证列数据类型,而PySpark在运行时检查
- 使用场景与示例
基础列操作链
from pyspark.sql import functions as F
# 连续添加/修改多列
transformed_df = (salesDF
.withColumn("discount", F.when(F.col("amount") > 3000, 0.1).otherwise(0))
.withColumn("net_amount", F.col("amount") * (1 - F.col("discount")))
.withColumn("purchase_year", F.year("purchaseDate"))
)
条件分支处理
# 链式条件分支
categorized_df = (salesDF
.withColumn("category_type",
F.when(F.col("productCategory") == "家电", "electronics")
.when(F.col("productCategory").isin(["服装","食品"]), "fast_moving")
.otherwise("other")
)
.withColumn("priority",
F.when(F.col("net_amount") > 4000, "high")
.otherwise("normal")
)
)
窗口函数集成
from pyspark.sql.window import Window
window_spec = Window.partitionBy("productCategory").orderBy(F.col("amount").desc())
ranked_df = (salesDF
.withColumn("rank", F.dense_rank().over(window_spec))
.withColumn("category_avg", F.avg("amount").over(window_spec))
.filter(F.col("rank") <= 2) # 继续链式过滤
)
- 性能优化技巧
列裁剪优先原则:先使用select()过滤不需要的列,减少后续处理数据量
optimized_df = (salesDF
.select("orderId", "productCategory", "amount", "purchaseDate")
.withColumn(...) # 仅在必要列上操作
)
避免重复计算:对重复使用的表达式创建临时列
(salesDF
.withColumn("is_electronics", F.col("productCategory") == "家电")
.withColumn("adjusted_amount",
F.when(F.col("is_electronics"), F.col("amount")*0.9)
.otherwise(F.col("amount"))
)
)
UDF使用规范:将多个UDF调用合并为单个处理单元
# 不推荐:多次调用UDF
.withColumn("feature1", udf1("col"))
.withColumn("feature2", udf2("col"))
# 推荐:单次UDF返回struct
@F.udf(StructType(...))
def combined_udf(col):
return (calc1(col), calc2(col))
.withColumn("features", combined_udf("col"))
.select("col", "features.*")
- 与SQL对比优势

- 最佳实践
单链长度控制:超过10步操作时拆分为多个逻辑单元,使用cache()缓存中间结果
# 阶段1:数据清洗
cleaned = (raw_df
.withColumn(...)
.withColumn(...)
.cache() # 缓存中间结果
)
# 阶段2:特征工程
features = (cleaned
.withColumn(...)
.withColumn(...)
)
错误处理模式:使用transform()函数实现更安全的链式调用
from pyspark.sql.utils import AnalysisException
try:
df.transform(lambda d: d.withColumn(...))
.transform(lambda d: d.withColumn(...))
except AnalysisException as e:
print(f"列操作错误: {e}")
关键提醒:链式调用时注意操作顺序,类似withColumnRenamed会影响后续列名引用
四、算法(双指针 + 链表)
LeetCode 142. 环形链表 II
要求:
理解快慢指针(龟兔赛跑)思想
能手写找到环入口
对比 Day3 的 141 环形链表 I,理解区别
思路要点:
快指针走 2 步,慢指针走 1 步
相遇后,慢指针回到起点,两者同步走 1 步,再次相遇即为入口
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 第一次相遇
if slow == fast:
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
923

被折叠的 条评论
为什么被折叠?



