首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >解析10万个字符串并将它们匹配到不同的类

解析10万个字符串并将它们匹配到不同的类
EN

Code Review用户
提问于 2019-09-25 15:18:14
回答 2查看 147关注 0票数 5

我有一个应用程序,它以strings的形式接受“命令”,这些命令被解析,然后根据字符串中的第一个单词传递给一个特定的类。

每个string命令都由由空格分隔的两个或三个单词组成,还可以有一个'=‘,后面跟着用逗号分隔的更多单词/数字。

下面是一些例子:

add layer layerName=path,style -带有=的命令将遵循这种格式。=前有三个“部件”,后面有任意数量的“部件”,用逗号隔开。

assign database C:\temp folder\temp.mdb -没有=的命令将遵循这种格式。最多三个“部分”。

现在,我有一个List,它由所有可能的“第一”单词组成,它们是动作动词。

我将string命令解析为一个string[],并打开第一个元素以确定要调用的正确函数。

下面是包含可能的动作动词的List (为了更好的可读性而缩短):

代码语言:javascript
复制
//list of all types of commands. Add to this list if more commands are created.
private static readonly List<string> Commands = new List<string>
    {"add", "assign", "cancel", "reload"}; //the actual list has ~50 strings

private static readonly string[] EmptyString = { string.Empty };

下面是我的解析函数:

代码语言:javascript
复制
/// <summary>
/// Given a command string, will parse it
/// </summary>
public static void StartParse(string command)
{
    if (string.IsNullOrWhiteSpace(command))  //make sure the command isn't empty
    {
        Log("Command is empty.");
        return;
    }

    string textToParse = command;

    if (textToParse.Contains('='))
    {
        string[] parts = textToParse.Split(new[] { '=' }, 2); //split the command into two parts
        string[] secondPart;

        //special case, only one command will have this format: 
        //ex: add layer layerName=filepath| ( OBJECTID = 10 ) OR ( OBJECTID = 20),styleName
        if (parts[1].Contains('|') && parts[1].Contains('(')) 
        {
            string secondParts = "";

            for (int i = 1; i < parts.Length; i++)
            {
                secondParts += parts[i];
            }

            secondPart = secondParts.Replace(@"""", "").Split(',');
        }
        else
        {
            secondPart = parts.Last().Replace(@"""", "").Split(',');
        }

        string[] firstPart = parts.First().Replace(@"""", "").Split(' '); //get rid of any quotes and split by spaces

        //if a filepath is in the first part of a command and contains spaces, it could be split into more than three parts
        //Note: if the above is true, the filepath will always start at the THIRD parameter 
        //ex: use database c:\users\test folder\temppath.mdb=...
        if (firstPart.Length > 3) 
        {
            StringBuilder sb = new StringBuilder();

            for (int i = 2; i < firstPart.Length; i++) //file path will always start at the third parameter
            {
                sb.Append(firstPart[i] + " "); //make the different parts of the filepath one string
            }

            firstPart = new[] { firstPart[0], firstPart[1], sb.ToString() };
        }

        string[] allParts = MergeArrays(firstPart, secondPart); //make one array out of the two parts of the command
        allParts[0] = allParts[0].ToLower(); //make everything lower

        //If first element is not in the command list
        if (!Commands.Contains(allParts.First()))
        {
            Log(allParts.First() + " is not a proper command.");
        }

        CallCommand(allParts);
    }
    else //if there is no '=' in the command
    {
        //creates a string array out of the command string, split by spaces
        string[] parts = TextParser.ParseText(textToParse, ' ', '"').ToArray(); 
        parts[0] = parts[0].ToLower();

        //If first element is not in the command list
        if (!Commands.Contains(parts.First()))
        {
            Log(parts.First() + " is not a proper command.");
        }

        CallCommand(parts);
    }
}

下面是使用switch语句确定要调用哪个函数的函数:

代码语言:javascript
复制
/// <summary>
/// Calls the correct command function or sends off a message based on the command verb. 
/// The command verb is always the first argument.
/// </summary>
/// <param name="command">An array of strings representing all the arguments of a commmand.</param>
public static void CallCommand(string[] command)
{
    string commandVerb = command.First().ToLower();

    //shortened for better readability, actual switch has ~50 cases
    switch (commandVerb)
    {
        case "add":
            AddCommand.AddObj(command);
            break;
        case "assign":
            AssignCommand.ParseCommand(command);
            break;
        case "cancel":
            CancelCommand.ParseCommand(command);
            break;
        case "reload":
            ReloadCommand.ParseCommand(command);
            break;
        default:
            Log("This command doesn't exist.");
            break;
    }
}

/// <summary>
/// Merges two arrays into one
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="first"></param>
/// <param name="second"></param>
/// <returns>Returns the merged array</returns>
public static T[] MergeArrays<T>(T[] first, T[] second)
{
    T[] result = new T[first.Length + second.Length];
    Array.Copy(first, result, first.Length);
    Array.Copy(second, 0, result, first.Length, second.Length);
    return result;
}

下面是我使用的帮助解析器类:

代码语言:javascript
复制
public static class TextParser
{
    /// <summary>
    /// Parses text given a delimiter and a text qualifier
    /// </summary>
    public static IEnumerable<string> ParseText(string line, char delimiter, char textQualifier)
    {

        if (line == null)
            yield break;

        bool inString = false;

        StringBuilder token = new StringBuilder();

        for (int i = 0; i < line.Length; i++)
        {
            var currentChar = line[i];

            var prevChar = '\0';
            prevChar = i > 0 ? line[i - 1] : '\0';

            var nextChar = '\0';
            nextChar = i + 1 < line.Length ? line[i + 1] : '\0';

            if (currentChar == textQualifier && (prevChar == '\0' || prevChar == delimiter) && !inString)
            {
                inString = true;
                continue;
            }

            if (currentChar == textQualifier && (nextChar == '\0' || nextChar == delimiter) && inString)
            {
                inString = false;
                continue;
            }

            if (currentChar == delimiter && !inString)
            {
                yield return token.ToString();
                token = token.Remove(0, token.Length);
                continue;
            }

            token = token.Append(currentChar);

        }

        yield return token.ToString();
    }
}

下面是其中一个命令类的示例(它们都是这样设置的):

代码语言:javascript
复制
public static class AssignCommand
{
    /// <summary>
    /// Starts parsing the command and assigns a tag to a feature object if one is found with the given name
    /// </summary>
    public static void ParseCommand(string[] command)
    {
        if (command.Length < 4)
        {
            Log("Assign command failed: must have at least four arguments total", null)
            return;
        }

        var objectName = command[2];

        var tag = command[3];

        var feature = GlobalObjects.Map.FindFeatureByName(objectName); //Map is a third party object. GlobalObjects is a global class in the app containing the Map object. I didn't make it, it's been there forever.
        if (feature != null)
        {
            feature.Tag = tag;
        }
    }
}

优先考虑的是尽可能提高速度,因为会有成千上万的这些命令进入。

需要注意的一点是,对于每个动作动词,都有一个类来匹配它来处理命令,这就是在switch语句中看到的。我不包括这些,因为它们很长,但是每个函数都有一个ParseCommand函数,它只是将字符串数组解析成一个对象。我正在考虑为它们创建一个基类,并使用某种泛型将switch语句简化为一行。但不确定这是否真的会提高性能。

另一个想法是使用Dictionary而不是开关语句,Key是动作动词,Value是委托。

EN

回答 2

Code Review用户

回答已采纳

发布于 2019-09-25 16:14:40

看起来,您给我们的是一个系统,它接受一个字符串并将其解析为一个语义命令。

关于你展示的代码,我建议你自己建立更少的东西。整个解析系统可能只是一个带有捕获组的(已编译的)正则表达式。我确实建议用字典代替开关语句。所有这些可能会使这个系统变得更小、更容易阅读,并可能使它更具表现力。

尽管如此,这个路由系统(希望如此)并不是性能瓶颈,您需要问的是如何提高性能。首先您需要查看获取命令字符串列表的系统,还需要查看运行这些命令的系统(S)。

您的最终目标可能是建立一个流读取器,为每个循环提供一个异步的、与外部资源连接的共享池。但是我们无法从这里看到大部分的系统,如果只有100 k项需要处理,那就太过分了。

票数 5
EN

Code Review用户

发布于 2019-09-26 04:39:36

一般观测

  • 方法名StartParse表示也会有它的补充EndParse。但没有。所以这个名字很不幸。
  • 方法StartParse没有返回值。这使得它对消费者来说有点像黑匣子。我希望是返回您解析的命令。“我正在考虑为它们创建一个基类”,如果命令共享足够的状态,->将创建一个基类,但是如果它们共享一些状态/操作,则更倾向于创建一个接口ICommand
  • 由于方法StartParse是无效的,所以对参数检查的无声捕获不会引起消费者的注意。更喜欢抛出ArgumentExceptionArgumentNullException
  • 您有太多的内联评论。如果您认为需要这些注释来容纳代码,那么您可能需要重新考虑代码是否是以可读的方式编写的。作为.ToLower(); // make everything lower的注释是完全多余的。
  • 方法StartParse调用CallCommand。虽然我认为它应该返回一个命令,但是在解析器中调用方法名CallCommand感觉非常奇怪。在我看来,CallCommand意味着您正在执行一个命令,而不是将一些输入数据解析为一个命令。
  • StartParse的方法体是冗长的。您应该考虑编写更紧凑的代码。使用regex在另一个答案中提供的替代解决方案是一个很好的选择,因为输入语言相当简单。
  • 如果方法List<string> Commands中的开关大小写不使用该列表来验证,则使用允许谓词CallCommand的上下文没有任何好处。你对commandVerb交换机进行了硬编码。同样,在错误输入时,您会在使用者不知道该命令未被识别的情况下,悄然忽略该错误输入。
  • TextParser也可以使用一些重构。如果您有像currentChar == textQualifier && (prevChar == '\0'这样的循环代码,这应该是一个信号,可以重写代码,只编写一行代码一次。
  • AssignCommand内部使用对全局共享静态数据GlobalObjects.Map的引用。这使得测试和重用类变得困难。考虑使用IoC。与静态解析方法不同,您可以为ICommandParser接口提供一个或多个特定的解析器实现。然后,可以将它们注册到IoC容器中。
票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/229643

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档