You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PySpark中基于Spark SQL函数实现两列最长公共子串

在PySpark中用Spark SQL实现两列的最长公共子串对比

我需要在PySpark中对两列执行最长公共子串(Longest Common Substring)对比。理论上可以用以下Python函数实现:

def longest_common_substring(input_string, string_list):
    longest_substrings = []
    for string in string_list:
        longest_substring = ""
        for i in range(len(input_string)):
            for j in range(i, len(input_string)):
                if string.startswith(input_string[i:j+1]):
                    if len(input_string[i:j+1]) > len(longest_substring):
                        longest_substring = input_string[i:j+1]
        longest_substrings.append(longest_substring)
    return longest_substrings

longest_common_substring("Node 1 - 2643", 
                         ['Node 1 - 2643', 'Node ', 'Node 1 - 2643 - Node 1 A032 - 32432'])

输出:

['Node 1 - 2643', 'Node ', 'Node 1 - 2643']

但实际数据量有1亿至数十亿行,性能至关重要,UDF不是最优方案。请问能否仅使用Spark SQL函数实现?

以下是示例数据及目标列lcs (goal):

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("MWE - longest common substring") \
    .getOrCreate()

data = [
    ["AB1234|BC24412|DE34322", "AB1234|BC24412|DE9687", "AB1234|BC24412|DE"],
    ["AA2222|BB3333|CC6666", "AA2222|BD23123|EE12|ZZ929292", "AA2222|B"]
]
schema = ["pathA", "pathB", "lcs (goal)"]

df = spark.createDataFrame(data, schema=schema)

数据展示:

+----------------------+----------------------------+-----------------+
|pathA                 |pathB                       |lcs (goal)       | 
+----------------------+----------------------------+-----------------+
|AB1234|BC24412|DE34322|AB1234|BC24412|DE9687       |AB1234|BC24412|DE|
|AA2222|BB3333|CC6666  |AA2222|BD23123|EE12|ZZ929292|AA2222|B         |
+----------------------+----------------------------+-----------------+

解决方案:Spark SQL实现最长公共子串

针对大数据量场景,完全可以用Spark SQL内置函数实现,避免UDF的性能开销。以下是适配不同场景的方案:

方案1:前缀式公共子串(适配你的示例场景)

你的示例中最长公共子串是从字符串开头的连续匹配部分,用递归CTE可以高效计算:

WITH recursive_lcs AS (
    SELECT 
        pathA,
        pathB,
        LEAST(LENGTH(pathA), LENGTH(pathB)) AS max_len,
        LEAST(LENGTH(pathA), LENGTH(pathB)) AS current_len
    FROM df
    UNION ALL
    SELECT 
        pathA,
        pathB,
        max_len,
        current_len - 1 AS current_len
    FROM recursive_lcs
    WHERE current_len > 0 
      AND SUBSTRING(pathA, 1, current_len) != SUBSTRING(pathB, 1, current_len)
)
SELECT 
    pathA,
    pathB,
    CASE 
        WHEN current_len = 0 THEN ''
        ELSE SUBSTRING(pathA, 1, current_len)
    END AS lcs
FROM recursive_lcs
WHERE current_len = 0 
   OR SUBSTRING(pathA, 1, current_len) = SUBSTRING(pathB, 1, current_len)
QUALIFY ROW_NUMBER() OVER (PARTITION BY pathA, pathB ORDER BY current_len DESC) = 1

说明

  • 从两个字符串的最短长度开始,逐步缩短前缀长度,直到找到第一个匹配项
  • 所有操作都是Spark原生向量化计算,性能远优于UDF
  • 递归次数最多等于最短字符串的长度,资源消耗可控

方案2:通用最长公共子串(支持任意位置匹配)

如果公共子串可能出现在字符串任意位置,可以用以下方案:

WITH string_lengths AS (
    SELECT 
        pathA,
        pathB,
        LENGTH(pathA) AS lenA,
        LENGTH(pathB) AS lenB
    FROM df
),
possible_lengths AS (
    SELECT 
        pathA,
        pathB,
        lenA,
        lenB,
        EXPLODE(SEQUENCE(LEAST(lenA, lenB), 1, -1)) AS candidate_len
    FROM string_lengths
),
substring_matches AS (
    SELECT 
        pathA,
        pathB,
        candidate_len,
        SUBSTRING(pathA, pos, candidate_len) AS subA,
        SUBSTRING(pathB, pos, candidate_len) AS subB
    FROM possible_lengths
    LATERAL VIEW SEQUENCE(1, lenA - candidate_len + 1) pos AS pos
    WHERE lenA >= candidate_len
    UNION ALL
    SELECT 
        pathA,
        pathB,
        candidate_len,
        SUBSTRING(pathA, pos, candidate_len) AS subA,
        SUBSTRING(pathB, pos, candidate_len) AS subB
    FROM possible_lengths
    LATERAL VIEW SEQUENCE(1, lenB - candidate_len + 1) pos AS pos
    WHERE lenB >= candidate_len
),
valid_matches AS (
    SELECT 
        pathA,
        pathB,
        candidate_len,
        subA AS lcs_candidate
    FROM substring_matches
    WHERE subA = subB
)
SELECT 
    pathA,
    pathB,
    COALESCE(FIRST(lcs_candidate), '') AS lcs
FROM valid_matches
QUALIFY ROW_NUMBER() OVER (PARTITION BY pathA, pathB ORDER BY candidate_len DESC) = 1

说明

  • 先生成从最长到最短的所有可能子串长度
  • 遍历所有对应长度的子串,筛选出两个字符串中相同的子串
  • 最后取每个分组中最长的匹配结果
  • 注意:长字符串会生成大量子串,建议在字符串长度可控时使用

方案3:针对|分隔路径的优化方案

你的数据是|分隔的路径片段,可先拆分片段再匹配,性能最优:

WITH split_paths AS (
    SELECT 
        pathA,
        pathB,
        SPLIT(pathA, '\\|') AS partsA,
        SPLIT(pathB, '\\|') AS partsB
    FROM df
),
full_part_matches AS (
    SELECT 
        pathA,
        pathB,
        CONCAT_WS('|', SLICE(partsA, 1, part_idx)) AS lcs_candidate,
        LENGTH(CONCAT_WS('|', SLICE(partsA, 1, part_idx))) AS match_len
    FROM split_paths
    LATERAL VIEW SEQUENCE(1, LEAST(SIZE(partsA), SIZE(partsB))) part_idx AS part_idx
    WHERE CONCAT_WS('|', SLICE(partsA, 1, part_idx)) = CONCAT_WS('|', SLICE(partsB, 1, part_idx))
),
partial_part_matches AS (
    SELECT 
        pathA,
        pathB,
        CONCAT(
            CONCAT_WS('|', SLICE(partsA, 1, LEAST(SIZE(partsA), SIZE(partsB)))),
            SUBSTRING(partsA[LEAST(SIZE(partsA), SIZE(partsB))], 1, pos)
        ) AS lcs_candidate,
        LENGTH(CONCAT(
            CONCAT_WS('|', SLICE(partsA, 1, LEAST(SIZE(partsA), SIZE(partsB)))),
            SUBSTRING(partsA[LEAST(SIZE(partsA), SIZE(partsB))], 1, pos)
        )) AS match_len
    FROM split_paths
    LATERAL VIEW SEQUENCE(1, LEAST(LENGTH(partsA[LEAST(SIZE(partsA), SIZE(partsB))]), LENGTH(partsB[LEAST(SIZE(partsA), SIZE(partsB))])) - 1) pos AS pos
    WHERE partsA[LEAST(SIZE(partsA), SIZE(partsB))] != partsB[LEAST(SIZE(partsA), SIZE(partsB))]
      AND SUBSTRING(partsA[LEAST(SIZE(partsA), SIZE(partsB))], 1, pos) = SUBSTRING(partsB[LEAST(SIZE(partsA), SIZE(partsB))], 1, pos)
),
all_matches AS (
    SELECT * FROM full_part_matches
    UNION ALL
    SELECT * FROM partial_part_matches
)
SELECT 
    pathA,
    pathB,
    COALESCE(FIRST(lcs_candidate), '') AS lcs
FROM all_matches
QUALIFY ROW_NUMBER() OVER (PARTITION BY pathA, pathB ORDER BY match_len DESC) = 1

说明

  • 先按|拆分路径,匹配完整的前缀片段
  • 再处理最后一个片段的部分匹配情况
  • 大幅减少需要检查的子串数量,完全适配你的示例数据

内容的提问来源于stack exchange,提问作者Marco_CH

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 07:12:06