
本文介绍如何将针对多个文本列(如 ["text1", "text2", "text3", "text4"])的 HTML 标签检测、分组计数与横向求和,完全内联集成于单条 Polars 方法链中,避免 Python 循环、多次扫描或外部 reduce,显著提升可读性与执行效率。
本文介绍如何将针对多个文本列(如 `["text1", "text2", "text3", "text4"]`)的 html 标签检测、分组计数与横向求和,**完全内联集成于单条 polars 方法链中**,避免 python 循环、多次扫描或外部 `reduce`,显著提升可读性与执行效率。
在 Polars 中处理多列相似聚合任务(如对多个文本字段分别统计 HTML 标签出现次数),关键在于避免显式循环 + 多次 .collect() + 外部合并——这不仅破坏惰性计算优势,还引入冗余 I/O 和内存开销。理想方案是:在单次 group_by().agg() 中批量处理所有目标列,并利用 Polars 原生横向聚合函数完成后续汇总。
✅ 推荐方案:pl.col(fields).str.contains_any(...).sum() + pl.sum_horizontal()
以下代码完整替代原始多步循环逻辑,全程在一条惰性链中完成:
import polars as pl
# 预定义 HTML 标签列表(同问题中)
html_tags = [
"<html>", "</html>", "<head>", "</head>", "<title>", "</title>", "<meta>", "</meta>",
"<link>", "</link>", "<style>", "</style>", "<body>", "</body>", "<header>", "</header>",
"<footer>", "</footer>", "<nav>", "</nav>", "<main>", "</main>", "<section>", "</section>",
"<article>", "</article>", "<aside>", "</aside>", "<h1>", "</h1>", "<h2>", "</h2>",
"<h3>", "</h3>", "<h4>", "</h4>", "<h5>", "</h5>", "<h6>", "</h6>", "<p>", "</p>",
"<ul>", "</ul>", "<ol>", "</ol>", "<li>", "</li>", "<div>", "</div>", "<span>", "</span>",
"<a>", "</a>", "<img alt="如何在 Polars 方法链中高效实现多列 HTML 标签计数与横向聚合" >", "</img>", "<table>", "</table>", "<thead>", "</thead>",
"<tbody>", "</tbody>", "<tr>", "</tr>", "<td>", "</td>", "<th>", "</th>", "<form>", "</form>",
"<input>", "</input>", "<textarea>", "</textarea>", "<button>", "</button>",
"<select>", "</select>", "<option>", "</option>", "<script>", "</script>",
"<noscript>", "</noscript>", "<iframe>", "</iframe>", "<canvas>", "</canvas>",
"<source>", "</source>"
]
# 主处理链:支持任意字段列表,零外部循环
def contains_html_batch(parquet_file: str, fields: list[str]) -> pl.DataFrame:
return (
pl.scan_parquet(parquet_file)
.select("publisher", "date", *fields) # 仅加载必要列
.drop_nulls(subset=["publisher", "date"]) # 提前过滤空 key
.group_by("publisher", "date")
.agg(
# 对 fields 中每一列:检测任意 HTML 标签 → 转布尔 → 求和(即计数)
pl.col(fields).str.contains_any(html_tags).sum()
)
.with_columns(
# 新增 sum 列:横向求和,自动跳过 null(等价于 fill_null(0).sum())
sum=pl.sum_horizontal(fields)
)
.filter(pl.col("sum") > 0) # 只保留至少含一个标签的组合
.sort("sum", descending=True)
.collect()
)
# 使用示例
result = contains_html_batch("/tmp/test.parquet", ["text1", "text2", "text3", "text4"])✅ 输出结构说明
结果 DataFrame 包含:
立即学习“前端免费学习笔记(深入)”;
-
publisher,date:分组键; -
text1,text2,text3,text4:各列独立的 HTML 标签计数(u32类型,null 表示该列全为 null 或无匹配); -
sum:横向求和列(pl.sum_horizontal自动将null视为0,无需手动fill_null(0))。
? 关键优势:
- 单次扫描:
.scan_parquet()仅执行一次;- 向量化聚合:
pl.col(fields)批量应用str.contains_any().sum(),底层高度优化;- 零 Python 循环:全部逻辑由 Polars 查询引擎调度;
- null 安全:
pl.sum_horizontal()默认忽略null,行为等价于pl.sum_horizontal(pl.col(fields).fill_null(0)),更简洁。
? 替代方案:按字段展开(宽表 → 长表)
若需分析「哪个字段贡献最多」,而非总和,可用 unpivot() 将结果转为长格式:
result_long = (
pl.scan_parquet(parquet_file)
.select("publisher", "date", *fields)
.drop_nulls(subset=["publisher", "date"])
.group_by("publisher", "date")
.agg(pl.col(fields).str.contains_any(html_tags).sum())
.unpivot(
index=["publisher", "date"],
on=fields,
variable_name="field_name",
value_name="tag_count"
)
.filter(pl.col("tag_count") > 0)
.sort(["publisher", "date", "tag_count"], descending=[False, False, True])
.collect()
)此输出每行代表 (publisher, date, 字段名, 计数),便于后续按字段名分组统计或可视化。
⚠️ 注意事项与最佳实践
-
字段存在性检查:生产环境建议在
select()前验证fields中所有列均存在于 Parquet schema,可加assert set(fields).issubset(set(df.columns)); -
性能提示:
str.contains_any()在 Polars ≥ 0.20.30 中已深度优化,但若html_tags极大(>1000 项),可考虑预编译正则(pl.col().str.contains(r"", literal=False))或分批处理; -
内存友好:始终优先使用
.scan_parquet()+.collect()而非.read_parquet(),尤其在大数据集上; -
类型安全:
pl.sum_horizontal()要求所有输入列为数值型;若某列含非数值数据,需先.cast(pl.UInt32, strict=False)并处理null。
通过上述方法,你不仅能将“map-reduce”式逻辑无缝嵌入 Polars 链式调用,还能获得媲美手写 Rust 的执行效率与远超 Pandas 的可维护性。



















