如何从字符串内容中删除一些特定的单词?

19

我有一些包含表情符号代码的字符串,例如:grinning:, :kissing_heart:, 或者:bouquet:。我想处理它们以删除表情符号代码。

例如,给定以下字符串:

Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet:

我希望得到这个结果:

Hello , how are you? Are you fine?

我知道可以使用此代码:

richTextBox2.Text = richTextBox1.Text.Replace(":kissing_heart:", "").Replace(":bouquet:", "").Replace(":grinning:", "").ToString();

然而,我需要删除856个不同的表情符号(如果使用这种方法,需要调用856次Replace()函数)。是否有其他方法可以完成这项任务?


2
查找表能解决你的问题吗? - Quality Catalyst
2
@demonplus:你提供的链接是关于删除表情符号,而这个问题是关于表情符号序列。 - Quality Catalyst
1
如果您只想用string.empty替换表情符号,您可以定义一个包含所有表情符号字符串的数组,然后将它们替换为string.empty。 - Iman Nemati
9个回答

27

您可以使用正则表达式来匹配:anything:之间的单词。使用Replace函数,您可以进行其他验证。

string pattern = @":(.*?):";
string input = "Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet: Are you super fan, for example. :words not to replace:";
string output = Regex.Replace(input, pattern, (m) =>
{
    if (m.ToString().Split(' ').Count() > 1) // more than 1 word and other validations that will help preventing parsing the user text
    {
        return m.ToString();
    }
    return String.Empty;
}); // "Hello , how are you? Are you fine? Are you super fan, for example. :words not to replace:"

如果您不想使用利用lambda表达式的Replace,则可以使用\w,正如@yorye-nathan所提到的,仅匹配单词。

string pattern = @":(\w*):";
string input = "Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet: Are you super fan, for example. :words not to replace:";
string output = Regex.Replace(input, pattern, String.Empty); // "Hello , how are you? Are you fine? Are you super fan, for example. :words not to replace:"

2
:(\w*): 可能更合适。 - SimpleVar
2
你认为这个足够安全吗? - Tolga Evcimen
2
你最终会不允许用户在冒号之间写任何内容。 - Rohit Vipin Mathews
2
这个怎样处理 Tricky test:123:grinning:? (应该变成 Tricky test:123 因为 123 不是表情符号。) - Dorus
2
思考了几天,可能不会比使用HashSet更好,因为已经有人建议过了。fubo已经提出了稍微简单一点的方法。此外,您可以通过为表情符号添加最小和最大长度来使正则表达式更简单。例如 :(\w{4,15}):(其中4和15应该是最短和最长表情符号代码的长度)。 - Dorus
显示剩余10条评论

16
string Text = "Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet:";

我会用那种方法解决它。

List<string> Emoj = new List<string>() { ":kissing_heart:", ":bouquet:", ":grinning:" };
Emoj.ForEach(x => Text = Text.Replace(x, string.Empty));

更新 - 参考 Detail 的评论

另一种方法:仅替换现有的表情符号

List<string> Emoj = new List<string>() { ":kissing_heart:", ":bouquet:", ":grinning:" };
var Matches = Regex.Matches(Text, @":(\w*):").Cast<Match>().Select(x => x.Value);
Emoj.Intersect(Matches).ToList().ForEach(x => Text = Text.Replace(x, string.Empty));

但我不确定对于这么短的聊天字符串来说是否有很大的区别,更重要的是要有易于阅读/维护的代码。原帖的问题是如何减少冗余的 Text.Replace().Text.Replace(),而不是寻求最有效的解决方案。


在我看来,OP 执行此操作时可能会处理一个包含所有 856 个表情符号的大文件,因此您的第二种方法会更慢。如果他计划仅在短行上使用它,则性能增加永远无法证明减少可读性的价值。 - Dorus
那是错误的,第一篇帖子说:“我从一个信使中读取了一些字符串内容”。 - fubo

8
我会使用一些已经提出的技术结合起来使用。首先,我会将800多个表情符字符串存储在数据库中并在运行时加载它们。使用HashSet将它们存储在内存中,这样我们就可以获得O(1)的查找时间(非常快)。使用正则表达式从输入中提取所有可能的模式匹配,然后将每个与我们的哈希表情符进行比较,删除有效的表情符并保留用户输入的任何非表情符模式...
public class Program
{
    //hashset for in memory representation of emoji,
    //lookups are O(1), so very fast
    private HashSet<string> _emoji = null;

    public Program(IEnumerable<string> emojiFromDb)
    {
        //load emoji from datastore (db/file,etc)
        //into memory at startup
        _emoji = new HashSet<string>(emojiFromDb);
    }

    public string RemoveEmoji(string input)
    {
        //pattern to search for
        string pattern = @":(\w*):";
        string output = input;

        //use regex to find all potential patterns in the input
        MatchCollection matches = Regex.Matches(input, pattern);

        //only do this if we actually find the 
        //pattern in the input string...
        if (matches.Count > 0)
        {
            //refine this to a distinct list of unique patterns 
            IEnumerable<string> distinct = 
                matches.Cast<Match>().Select(m => m.Value).Distinct();

            //then check each one against the hashset, only removing
            //registered emoji. This allows non-emoji versions 
            //of the pattern to survive...
            foreach (string match in distinct)
                if (_emoji.Contains(match))
                    output = output.Replace(match, string.Empty);
        }

        return output;
    }
}

public class MainClass
{
    static void Main(string[] args)
    {
        var program = new Program(new string[] { ":grinning:", ":kissing_heart:", ":bouquet:" });
        string output = program.RemoveEmoji("Hello:grinning: :imadethis:, how are you?:kissing_heart: Are you fine?:bouquet: This is:a:strange:thing :to type:, but valid :nonetheless:");
        Console.WriteLine(output);
    }
}

这将导致:
“大家好 :imadethis:,你怎么样?你还好吗?这是一个:a:strange:thing :to type:, 但仍然有效。”

1
这不是非常有效的。output.Replace(...)必须再次搜索整个字符串。Replace本身已经替换了所有的表情符号,但是您的匹配器可能会第二次匹配相同的表情符号,导致Replace再次运行而不需要。 - Dorus
你说得完全正确,我在循环之前将匹配列表精炼成了一组不同(唯一)的字符串集合。 - Detail
你做得非常出色。我真的很难在你的回答和@Dorus的回答之间做出选择,你们两个都值得我奖励。 - adricadar

7
您无需替换所有的 856 个表情符号,只需替换出现在字符串中的表情符号。因此,请查看以下内容:Finding a substring using C# with a twist。基本上,您需要提取所有标记(即 : 和 : 之间的字符串),然后用 string.Empty() 替换它们。如果您担心搜索结果返回的字符串不是表情符号,例如 :some other text:,那么您可以使用哈希表查找来确保替换所找到的标记是适当的。

5
最终终于写了些东西。我将几个先前提到的想法结合起来,并且我们应该只循环一次字符串这一事实。基于这些要求,这听起来是Linq的完美工作。
你应该缓存HashSet。除此之外,它具有O(n)的性能,只需遍历列表一次。进行基准测试会很有趣,但这很可能是最有效的解决方案。
方法相当简单。
  • 首先在HashSet中加载所有表情符号,以便我们可以快速查找。
  • 使用input.Split(':'):处拆分字符串。
  • 决定是否保留当前元素。
    • 如果上一个元素匹配,则保留当前元素。
    • 如果上一个元素不匹配,请检查当前元素是否匹配。
      • 如果匹配,则忽略它。(这实际上从输出中删除了子字符串)。
      • 如果不匹配,请添加:并保留它。
  • 使用StringBuilder重建我们的字符串。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    static class Program
    {
        static void Main(string[] args)
        {
            ISet<string> emojiList = new HashSet<string>(new[] { "kissing_heart", "bouquet", "grinning" });

            Console.WriteLine("Hello:grinning: , ho:w: a::re you?:kissing_heart:kissing_heart: Are you fine?:bouquet:".RemoveEmoji(':', emojiList));
            Console.ReadLine();
        }

        public static string RemoveEmoji(this string input, char delimiter, ISet<string> emojiList)
        {
            StringBuilder sb = new StringBuilder();
            input.Split(delimiter).Aggregate(true, (prev, curr) =>
            {
                if (prev)
                {
                    sb.Append(curr);
                    return false;
                }
                if (emojiList.Contains(curr))
                {
                    return true;
                }
                sb.Append(delimiter);
                sb.Append(curr);
                return false;
            });
            return sb.ToString();
        }
    }
}

编辑:我使用Rx library做了一些很酷的事情,但后来意识到Aggregate是Rx中Scan的对应物,因此进一步简化了代码。


你非常专注于让它快速运行,而你成功了。 - adricadar
@adricadar 这确实是一种痴迷 :-) - Dorus

3
如果效率是一个问题,并且要避免处理“假阳性”,请考虑使用StringBuilder重写字符串,同时跳过特殊的表情符号标记。
static HashSet<string> emojis = new HashSet<string>()
{
    "grinning",
    "kissing_heart",
    "bouquet"
};

static string RemoveEmojis(string input)
{
    StringBuilder sb = new StringBuilder();

    int length = input.Length;
    int startIndex = 0;
    int colonIndex = input.IndexOf(':');

    while (colonIndex >= 0 && startIndex < length)
    {
        //Keep normal text
        int substringLength = colonIndex - startIndex;
        if (substringLength > 0)
            sb.Append(input.Substring(startIndex, substringLength));

        //Advance the feed and get the next colon
        startIndex = colonIndex + 1;
        colonIndex = input.IndexOf(':', startIndex);

        if (colonIndex < 0) //No more colons, so no more emojis
        {
            //Don't forget that first colon we found
            sb.Append(':');
            //Add the rest of the text
            sb.Append(input.Substring(startIndex));
            break;
        }
        else //Possible emoji, let's check
        {
            string token = input.Substring(startIndex, colonIndex - startIndex);

            if (emojis.Contains(token)) //It's a match, so we skip this text
            {
                //Advance the feed
                startIndex = colonIndex + 1;
                colonIndex = input.IndexOf(':', startIndex);
            }
            else //No match, so we keep the normal text
            {
                //Don't forget the colon
                sb.Append(':');

                //Instead of doing another substring next loop, let's just use the one we already have
                sb.Append(token);
                startIndex = colonIndex;
            }
        }
    }

    return sb.ToString();
}

static void Main(string[] args)
{
    List<string> inputs = new List<string>()
    {
        "Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet:",
        "Tricky test:123:grinning:",
        "Hello:grinning: :imadethis:, how are you?:kissing_heart: Are you fine?:bouquet: This is:a:strange:thing :to type:, but valid :nonetheless:"
    };

    foreach (string input in inputs)
    {
        Console.WriteLine("In  <- " + input);
        Console.WriteLine("Out -> " + RemoveEmojis(input));
        Console.WriteLine();
    }

    Console.WriteLine("\r\n\r\nPress enter to exit...");
    Console.ReadLine();
}

输出:

In  <- Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet:
Out -> Hello , how are you? Are you fine?

In  <- Tricky test:123:grinning:
Out -> Tricky test:123

In  <- Hello:grinning: :imadethis:, how are you?:kissing_heart: Are you fine?:bouquet: This is:a:strange:thing :to type:, but valid :nonetheless:
Out -> Hello :imadethis:, how are you? Are you fine? This is:a:strange:thing :to type:, but valid :nonetheless:

3
使用我下面提供的代码,我认为使用这个函数可以解决你的问题。
        string s = "Hello:grinning: , how are you?:kissing_heart: Are you fine?:bouquet:";

        string rmv = ""; string remove = "";
        int i = 0; int k = 0;
    A:
        rmv = "";
        for (i = k; i < s.Length; i++)
        {
            if (Convert.ToString(s[i]) == ":")
            {
                for (int j = i + 1; j < s.Length; j++)
                {
                    if (Convert.ToString(s[j]) != ":")
                    {
                        rmv += s[j];
                    }
                    else
                    {
                        remove += rmv + ",";
                        i = j;
                        k = j + 1;
                        goto A;
                    }
                }
            }
        }

        string[] str = remove.Split(',');
        for (int x = 0; x < str.Length-1; x++)
        {
            s = s.Replace(Convert.ToString(":" + str[x] + ":"), "");
        }
        Console.WriteLine(s);
        Console.ReadKey();

3
我会使用扩展方法,像这样:

public static class Helper
{
   public static string MyReplace(this string dirty, char separator)
    {
        string newText = "";
        bool replace = false;

        for (int i = 0; i < dirty.Length; i++)
        {
            if(dirty[i] == separator) { replace = !replace ; continue;}
            if(replace ) continue;
            newText += dirty[i];
        }
        return newText;
    }

用法:

richTextBox2.Text = richTextBox2.Text.MyReplace(':');

这种方法的性能比使用正则表达式的性能更好。


确实,您可以节省一些毫秒,但它会删除“:everything:”之间的所有内容。 - adricadar
正确,但这正是它应该做的;如果您使用匹配以“:”开头和结尾的所有文本的模式进行Regex.Replace,则结果将相同;但有时Regex太慢了...(特别是如果一个人不知道如何正确使用它) - Fabjan

0
我会用冒号将文本分割,然后构建字符串,排除找到的表情符号名称。
        const char marker = ':';
        var textSections = text.Split(marker);

        var emojiRemovedText = string.Empty;

        var notMatchedCount = 0;
        textSections.ToList().ForEach(section =>
        {
            if (emojiNames.Contains(section))
            {
                notMatchedCount = 0;
            }
            else
            {
                if (notMatchedCount++ > 0)
                {
                    emojiRemovedText += marker.ToString();

                }
                emojiRemovedText += section;
            }
        });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接