Маска / замена внутренней части строкового столбца в Pyspark - PullRequest
1 голос
/ 29 октября 2019

У меня есть столбец электронной почты в кадре данных, и я хочу заменить его часть звездочками. Я не могу понять это с помощью функций PySpark.

Мой столбец электронной почты может выглядеть примерно так:

1004

Чего я хочу добиться, так это:

mod_email_col
ab**23@gmail.com
12*****23@yahoo.com

Таким образом, по существу, кроме первых 2-х символов и последних 2-х символов, я хочу, чтобы оставшаяся часть была заменена звездочками.

Это то, что я пробовал

from pyspark.sql import functions as F

split_email = F.split(df.email_address, "@")
df = df.withColumn('email_part', split_email.getItem(0))
df = df.withColumn('start', df.email_part.substr(0,2))
df = df.withColumn('end', df.email_part.substr(-2,2))

df.withColumn(
    'masked_part', 
     F.expr("regexp_replace(email_part, email_part[email_part.index(start)+len(start):email_part.index(end)], '*')")
).show(n=5)

Ответы [ 2 ]

2 голосов
/ 29 октября 2019

Я думаю, что вы можете достичь этого с помощью следующего регулярного выражения: (?<=.{2})\w+(?=.{2}@)

  • (?<=.{2}): положительный взгляд назад для 2 символов
  • \w+: любое словосимволы
  • (?=.{2}@): положительный прогноз для 2 символов, за которым следует литерал @

Первое использование regexp_extract для извлечения этого шаблона из вашей строки,

from pyspark.sql.functions import regexp_extract, regexp_replace

df = df.withColumn(
    "pattern", 
    regexp_extract("email", r"(?<=.{2})\w+(?=.{2}@)", 0)
)
df.show()
#+-------------------+-------+
#|              email|pattern|
#+-------------------+-------+
#|   abc123@gmail.com|     c1|
#|123abc123@yahoo.com|  3abc1|
#|      abcd@test.com|       |
#+-------------------+-------+

Затем используйте regexp_replace, чтобы создать замену * такой же длины.

df = df.withColumn(
    "replacement",
    regexp_replace("pattern", r"\w", "*")
)
df.show()
#+-------------------+-------+-----------+
#|              email|pattern|replacement|
#+-------------------+-------+-----------+
#|   abc123@gmail.com|     c1|         **|
#|123abc123@yahoo.com|  3abc1|      *****|
#|      abcd@test.com|       |           |
#+-------------------+-------+-----------+

Затем снова используйте regexp_replace в исходном столбце email, используя производные столбцы pattern и replacement.

Чтобы быть в безопасности, concat смотрят назад / смотрят в сторону от исходного шаблона при выполнении замены. Для этого нам нужно будет использовать expr, чтобы передать значения столбца в качестве параметров .

from pyspark.sql.functions import concat, expr, lit

df = df.withColumn(
    "mod_email_col",
    expr("regexp_replace(email, concat('(?<=.{2})', pattern, '(?=.{2}@)'), replacement)")
)
df.show()
#+-------------------+-------+-----------+-------------------+
#|              email|pattern|replacement|      mod_email_col|
#+-------------------+-------+-----------+-------------------+
#|   abc123@gmail.com|     c1|         **|   ab**23@gmail.com|
#|123abc123@yahoo.com|  3abc1|      *****|12*****23@yahoo.com|
#|      abcd@test.com|       |           |      abcd@test.com|
#+-------------------+-------+-----------+-------------------+

Наконец, отбросьте промежуточные столбцы:

df = df.drop("pattern", "replacement")
df.show()
#+-------------------+-------------------+
#|              email|      mod_email_col|
#+-------------------+-------------------+
#|   abc123@gmail.com|   ab**23@gmail.com|
#|123abc123@yahoo.com|12*****23@yahoo.com|
#|      abcd@test.com|      abcd@test.com|
#+-------------------+-------------------+

Примечание : я добавил один тестовый пример, чтобы показать, что это ничего не делает, если часть адреса электронной почты состоит из 4 символов или менее.


Обновление . Вот несколько способов обработки крайних случаев, когда часть адреса электронной почты содержит менее 4 символов.

Используемые правила:

  • Длина адреса электронной почты составляетбольше 5: сделать выше
  • Длина адреса электронной почты составляет 3, 4 или 5: оставить первый и последний символы, маскируя остальные с помощью *
  • Адрес электронной почты имеет длину 1 или2: маска одиночного символа перед @

Код:

patA = "regexp_replace(email, concat('(?<=.{2})', pattern, '(?=.{2}@)'), replacement)"
patB = "regexp_replace(email, concat('(?<=.{1})', pattern, '(?=.{1}@)'), replacement)"

from pyspark.sql.functions import regexp_extract, regexp_replace
from pyspark.sql.functions import concat, expr, length, lit, split, when

df.withColumn("address_part", split("email", "@").getItem(0))\
.withColumn(
    "pattern", 
    when(
        length("address_part") > 5, 
        regexp_extract("email", r"(?<=.{2})\w+(?=.{2}@)", 0)
    ).otherwise(
        regexp_extract("email", r"(?<=.{1})\w+(?=.{1}@)", 0)
    )
).withColumn(
    "replacement", regexp_replace("pattern", r"\w", "*")
).withColumn(
    "mod_email_col",
    when(
        length("address_part") > 5, expr(patA)
    ).when(
        length("address_part") > 3, expr(patB)
    ).otherwise(regexp_replace('email', '\w(?=@)', '*'))
).drop("pattern", "replacement", "address_part").show()
#+-------------------+-------------------+
#|              email|      mod_email_col|
#+-------------------+-------------------+
#|   abc123@gmail.com|   ab**23@gmail.com|
#|123abc123@yahoo.com|12*****23@yahoo.com|
#|     abcde@test.com|     a***e@test.com|
#|      abcd@test.com|      a**d@test.com|
#|        ab@test.com|        a*@test.com|
#|         a@test.com|         *@test.com|
#+-------------------+-------------------+
0 голосов
/ 30 октября 2019

Ваша проблема может быть упрощена с помощью некоторых строковых манипуляций (функции Spark SQL: instr , concat, left , repeat , substr ):

Сначала найдите позицию @ в строке электронного письма: pos_at = instr(email_col, '@'), затем длина части имени пользователя составляет pos_at - 1. Если в качестве числа сохраняемых символов взять N=2, то число маскируемых символов должно быть pos_at - 1 - 2*N, в коде мы имеем:

from pyspark.sql.functions import instr, expr

df = spark.createDataFrame(
        [(e,) for e in ['abc123@gmail.com', '123abc123@yahoo.com', 'abd@gmail.com']]
      , ['email_col']
)

# set N=2 as a parameter in the SQL expression
N = 2

df.withColumn('pos_at', instr('email_col', '@')) \
  .withColumn('new_col', expr("""
        CONCAT(LEFT(email_col,{0}), REPEAT('*', pos_at-1-2*{0}), SUBSTR(email_col, pos_at-{0}))
   """.format(N))).show(truncate=False)
#+-------------------+------+-------------------+
#|email_col          |pos_at|new_col            |
#+-------------------+------+-------------------+
#|abc123@gmail.com   |7     |ab**23@gmail.com   |
#|123abc123@yahoo.com|10    |12*****23@yahoo.com|
#|abd@gmail.com      |4     |abbd@gmail.com     |
#+-------------------+------+-------------------+

Обратите внимание на проблему споследняя строка при pos_at - 1 <= 2*N, которая должна обрабатываться отдельно. Если я определю следующую логику:

if `pos_at - 1 <= 2*N`:   keep the first char and mask the rest
otherwise: keep the original processing routine

вся обработка может быть заключена в лямбда-функцию с двумя аргументами (column_name и N)

# in the SQL expression, {0} is column_name and {1} is N
mask_email = lambda col_name, N: expr("""

  IF(INSTR({0}, '@') <= {1}*2+1
    , CONCAT(LEFT({0},1), REPEAT('*', INSTR({0}, '@')-2), SUBSTR({0}, INSTR({0}, '@')))
    , CONCAT(LEFT({0},{1}), REPEAT('*', INSTR({0}, '@')-1-2*{1}), SUBSTR({0}, INSTR({0}, '@')-{1}))
  ) as `{0}_masked`

""".format(col_name, N))

df.select('*', mask_email('email_col', 2)).show()
#+-------------------+-------------------+
#|          email_col|   email_col_masked|
#+-------------------+-------------------+
#|   abc123@gmail.com|   ab**23@gmail.com|
#|123abc123@yahoo.com|12*****23@yahoo.com|
#|      abd@gmail.com|      a**@gmail.com|
#+-------------------+-------------------+
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...