65.9K
CodeProject 正在变化。 阅读更多。
Home

Rhymes - 易于使用的押韵应用程序,适用于不擅长拼写的人

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2022年1月30日

CPOL

10分钟阅读

viewsIcon

6381

downloadIcon

98

一种基于三叉树的算法,将用户输入分解为语音元音,然后将其用作搜索键,在树的每个搜索级别查找押韵词。即使你拼写不正确,也能找到单词。

引言

我一直在使用我的创意写作文字处理器应用程序编写小说,并在此过程中不断改进它。然而,我从Rhymer.com网站上抓取的押韵词典并不够用。我能够从他们的服务器下载79,635个文件,然后为其创建了一个搜索引擎,该搜索引擎已集成到Words应用程序中,但尽管他们提供了数千个与常用后缀押韵的单词,但它们都被聚集在一起,没有考虑多音节押韵。

例如:uncomfortablebowel 以及其他10,000个只有末尾相似的“el”音的词条押韵,需要仔细挑选才能找到合理的内容

仅仅因为你在一个单词上添加了“-ed”后缀,并不意味着当你也添加相同的“-ed”后缀时,你需要将字典中的每个动词都包含在你与该词押韵的单词列表中。

它仍然是一个有用的词典,但你必须做一些工作来过滤掉所有不符合你正在寻找的示例。有时有成千上万个。这就像在暴风雪中寻找一片特定的雪花。

然而,这个应用程序使用每个单词的语音指纹来分离不仅仅是最后一个音节的相似性。

使用应用程序

新手注意事项:如果你从未编写过任何软件,并且只是在寻找一个押韵词典,你仍然可以在Windows 10上运行此应用程序。你只需下载上面的软件并将其解压缩到硬盘驱动器上。然后你需要找到可执行文件。

C:\-你解压文件的位置-\Rhymes\Rhymes\bin\Debug\Rhymes.exe

你可能希望创建一个快捷方式并将其放在桌面上。

因为 CodeProject 将可下载文件大小限制为10MB,所以应用程序在首次启动时需要构建其数据库。它将需要大约10-15分钟才能准备好,并且在它工作时,你将在表单的左上方看到一列单词闪烁。一旦完成,它就可以随意押韵了。

只需在文本框中输入你想押韵的单词,然后按回车键。由于它使用语音算法,你的拼写不会像从预定义列表中查找单词那样重要。即使你拼错了你想押韵的单词(我经常这样做),搜索结果也会给你一个正确拼写的列表,这可能会在将来帮助你。你不必担心美式或英式拼写,也不必在创作时费力地争论。

选项

MaxEntries - 你可以使用此选项限制显示的条目数量。如果你的搜索找到的单词列表超过此限制,则该搜索“级别”(匹配结尾音的数量)中的任何单词都不会显示。搜索像“this”这样的短词会给你很多答案,这些答案会四处散落,所以我质疑你的诗歌是否需要帮助押韵“this”或“that”,但如果你必须……只需增加MaxEntries,你就会找到一些东西。

通过增加 MaxEntries 值,你可能会得到你正在寻找的结果

UseClipBoard - 勾选此框后,当应用程序运行时,Microsoft的“剪贴板”将每秒测试一次。如果你将文本“复制”或“剪切”到剪贴板中,应用程序将使用你复制的文本作为搜索参数。这在你使用单独的文字处理器工作而不想切换应用程序时很方便。

TopMost - 勾选此框将强制应用程序保持在你正在运行的其他应用程序前面。你仍然可以使用你的文字处理器进行写作,但勾选此选项和 UseClipBoard 选项后,你将快速看到押韵出现,并且只需“复制”你想押韵的单词而无需切换到应用程序本身。

代码

数据树是一个三叉树,树中的每个叶子都附有单词的链表。由于所有数据在创建时都附加到文件的末尾,并且每个数据项(树叶和链表项)都通过它们在同一文件上的地址引用,因此无需繁琐的索引系统,并且不同的单词大小不影响存储和检索。Insert/Search 方法都从树的根开始,并向下遍历构成被搜索单词签名的搜索键的数量。

树搜索键本身是按倒序排列的声音标签,从单词的末尾到前面。相似的声音具有相同的标签,三叉树搜索的每个级别对应于从单词末尾开始计数的“声音”数量。由于每个树键比较都是任意分配的唯一数字ID的比较,这些ID对应于字母组合的集合,因此这些比较不是按字母顺序排列的,而是严格的数字性质。

整个押韵词典及其三叉树算法可以通过单个文件 classRD_TernaryTree.cs 集成到任何应用程序中。链表元素和树叶都有独立的类,它们处理访问文件中数据所需的 WriteRead 方法,使用 Addr 长整型来定位 FileStream。这些是记录为树的所有组件叶子和列表的指针的相同地址。树的插入和搜索方法都接收要插入/搜索的 Word 作为 string 参数。Search() 方法返回的不仅仅是与输入参数押韵的单词列表,而是一系列渐进相似的单词列表,其中第一个列表包含只有一个音节与搜索词押韵的单词,而下一个列表将有两个与请求的押韵相似的末尾音节。

Word 首先被“分解”成其组成音素,声音列表用作树的搜索键。在构建阶段,当每个单词被添加到树叶的每个链表时,它通过 前端插入 方式“落”到树叶的链表上,这些链表包含树中所有具有相同语音签名的单词,直到该搜索级别(你正在查看的叶子的任何级别)。

例如 - 单词 proposaldisposal 将在连续的“al”(1) 和 “pos”(2) 级别叶子中,但在下一个级别将分化为两个独立的“pro”(3) 和 “dis”(3) 叶子。

用于对每个单词进行语音指纹识别的不同声音存储在 classSounds 中。其完整内容如下所示。

public class classSounds
{
    public List<string> lstText = new List<string>();  // similar sounding word snips
    static int intIDCounter = 0;                       // static ID counter
    int intID = intIDCounter++;                        // unique ID used as search 'key'
    public int ID { get { return intID; } }
    public classSounds() { }
    public classSounds(string strSound)
    {
        lstText.Add(strSound);
    }

    public classSounds(string[] strSounds)
    {
        lstText.AddRange(strSounds.ToArray<string>());
    }
}

搜索中实际使用的“键”是 classSound 实例的 ID。当它通过使用 static 计数器整型变量首次创建时,此值被分配一个唯一的整数。此类的每个实例都包含 lstText 变量中的一个或多个字母组合。此类的所有实例集合分为三类:PrefixClusterSuffix

每个单词都由 DissectWord() 方法转换为语音签名。前缀和后缀组首先被处理,并按照它们创建的顺序从分解单词的头部和尾部取下。“cluster”类型列表取自单词的任何部分,不限于后缀/前缀列表那样在单词的尾部/头部。

public static List<int> DissectWord(string strWord)

为了创建单词的语音签名,每个在 classSoundlstText 中定义的字符系列都替换为方括号标签。

例如,一个 classSound 实例具有

  • ID = 34
  • lstText 中包含 string 变量 'ea' 和 'ee'

将用于将单词中每个 'ea' 和 'ee' 字母替换为相应的 'key-string' [34],即它们在被分解单词中的位置。当分解完成时,最初由方法接收的整个 string strWord 将转换为其等效的 'key-strings' 系列(按它们在单词中出现的顺序排列的方括号 ID 号),并且不再包含任何字母,只有方括号和数字。然后它们在方括号处 Split 成一个字符串数组,其中包含 classSoundsID 号,然后这些 ID 号转换为整数值,并以它们被发现的相反顺序返回给调用方法(三叉树的 SearchInsert 方法都使用 DissectWord())并用于遍历三叉树。

下面是该方法

public static List<int> DissectWord(string strWord)
{
    if (lstSounds.Count == 0)  // if the list of sounds is still empty -> build it
        SoundsInit();

    string strDebugCopy = strWord;          // keep a copy for debugging purposes
    bool bolDebug = false;
    if (bolDebug)
        strWord = strDebugCopy;

    List<int> lstRetVal = new List<int>();
    strWord = Deaccent(strWord).ToLower();  //replaces accented letters 
                                            //with unaccented version

    // replace non-alpha char with enum.NULL
    classSounds cNULL = lstSounds[0];
    string strNULL = EnumReplacement(ref cNULL);

    for (int intLetterCounter = strWord.Length - 1; intLetterCounter >= 0; intLetterCounter--)
    {
        char chrTest = strWord[intLetterCounter];
        if (!char.IsLetter(chrTest))
        {
            string strLeft = strWord.Substring(0, intLetterCounter);
            string strRight = intLetterCounter < strWord.Length - 1
                                                ? strWord.Substring(intLetterCounter + 1)
                                                : "";
            strWord = strLeft + strNULL + strRight;
        }
    }

    //              prefixes - sound-tag prefix at the front of the word
    for (int intPrefixCounter = Prefixes_Start; 
         intPrefixCounter <= Prefixes_End; intPrefixCounter++)
    {
        classSounds cPrefix = lstSounds[intPrefixCounter];
        string strEnumReplacement = EnumReplacement(ref cPrefix);

        for (int intTextCounter = 0; 
             intTextCounter < cPrefix.lstText.Count; intTextCounter++)
        {
            string strPrefix = cPrefix.lstText[intTextCounter];

            if (strWord.Length > strPrefix.Length)
            {
                if (string.Compare(strWord.Substring(0, strPrefix.Length), strPrefix) == 0)
                {
                    // prefix matches word
                    strWord = strEnumReplacement + strWord.Substring(strPrefix.Length);
                    goto exitPrefix; // exit because there can only be one prefix
                }
            }
        }
    }
exitPrefix:

    // suffixes - sound-tag suffix at the end of the word
    for (int intSuffixCounter = Suffixes_Start; 
             intSuffixCounter <= Suffixes_End; intSuffixCounter++)
    {
        classSounds cSuffix = lstSounds[intSuffixCounter];
        string strEnumReplacement = EnumReplacement(ref cSuffix);

        for (int intTextCounter = 0; intTextCounter < cSuffix.lstText.Count; intTextCounter++)
        {
            string strSuffix = cSuffix.lstText[intTextCounter];

            if (strWord.Length > strSuffix.Length)
            {
                string strWordEnd = strWord.Substring(strWord.Length - strSuffix.Length);
                if (string.Compare(strWordEnd, strSuffix) == 0)
                {
                    // Suffix matches word
                    strWord = strWord.Substring
                              (0, strWord.Length - strSuffix.Length) + strEnumReplacement;
                    goto exitSuffixes; // exit because there can only bee one suffix
                }
            }
        }
    }
exitSuffixes:

    //              consonantal
    for (int intClusterCounter = Clusters_Start; 
         intClusterCounter <= Clusters_End; intClusterCounter++)
    {  // for every classSound in Cluster list
        classSounds cSound = lstSounds[intClusterCounter];
        string strEnumReplacement = EnumReplacement(ref cSound);

        for (int intTextCounter = 0; intTextCounter < cSound.lstText.Count; intTextCounter++)
        { // for every letter-combination defined in this cSound's list 
            string strCluster = cSound.lstText[intTextCounter];
            if (strWord.Length >= strCluster.Length)
            {  
                if (strWord.Contains(strCluster))
                { 
                    // Cluster matches word -> replace it with its unique sound-tag
                    DissectWord_ReplaceEnum(ref strWord, ref cSound);
                }
            }
        }
    }

//  split the string sequence of soundtags into a string array
    char[] chrSplit = { ']', '[' };
    string[] strEnumList = strWord.Split(chrSplit, StringSplitOptions.RemoveEmptyEntries);
    for (int intCounter = strEnumList.Length - 1; intCounter >= 0; intCounter--)
    {    // proceed through the list of strings in reverse order
         // convert each string representation of the sound-tag's unique ID back to integer
        string strEnum = strEnumList[intCounter];
        try
        {
            int intEnum = Convert.ToInt32(strEnum);
            int eItem = (int)intEnum;
            lstRetVal.Add(eItem);
        }
        catch (Exception)
        {
        }
    }

    return lstRetVal;
}

创建语音列表

语音列表本身可能需要微调。当我使用这个写作工具时,我将纠正我发现的任何问题,并逐步提高其已经强大的性能。你可以通过编辑 SoundsInit() 方法中的现有示例,轻松地用你自己的开源程序副本完成此操作。

static void SoundsInit()
{
    lstSounds.Add(new classSounds("NULL"));

/////////////////////// prefix start value  //////////////////////////////////////////////////// 
    _intPrefixes_Start = lstSounds.Count;   // set prefix START value here
////////////////////////////////////////////////////////////////////////////////////////////////
                                            // as prefixes lead a word their importance 
                                            // in a rhyming dictionary are negligible
                                            // here are examples of classSound instances 
                                            // with only 1 Text combination each
    lstSounds.Add(new classSounds("extra"));
    lstSounds.Add(new classSounds("hyper"));
    lstSounds.Add(new classSounds("inter"));
    lstSounds.Add(new classSounds("trans"));
    lstSounds.Add(new classSounds("ultra"));
    lstSounds.Add(new classSounds("under"))
    lstSounds.Add(new classSounds("super"));;
    lstSounds.Add(new classSounds("anti"));
    lstSounds.Add(new classSounds("auto"));
    lstSounds.Add(new classSounds("down"));
    lstSounds.Add(new classSounds("mega"));
    lstSounds.Add(new classSounds("over"));
    lstSounds.Add(new classSounds("post"));
    lstSounds.Add(new classSounds("semi"));
    lstSounds.Add(new classSounds("tele"));
    lstSounds.Add(new classSounds("con"));
    lstSounds.Add(new classSounds("dis"));
    lstSounds.Add(new classSounds("mid"));
    lstSounds.Add(new classSounds("mis"));
    lstSounds.Add(new classSounds("non"));
    lstSounds.Add(new classSounds("out"));
    lstSounds.Add(new classSounds("pre"));
    lstSounds.Add(new classSounds("pro"));
    lstSounds.Add(new classSounds("sub"));
    lstSounds.Add(new classSounds("de"));
    lstSounds.Add(new classSounds("il"));
    lstSounds.Add(new classSounds("im"));
    lstSounds.Add(new classSounds("ir"));
    lstSounds.Add(new classSounds("in"));
    lstSounds.Add(new classSounds("re"));
    lstSounds.Add(new classSounds("un"));
    lstSounds.Add(new classSounds("up"));
///////////////////////////// prefix END value ////////////////////////////////////////////////
    _intPrefixes_End = lstSounds.Count - 1;    // set prefix END value here
///////////////////////////////////////////////////////////////////////////////////////////////

    //  tripthongs      -       diphthongs      -       Consonantal Clusters        -       letters
///////////////////////////// Cluster start value /////////////////////////////////////////////
    _intClusters_Start = lstSounds.Count;     // set cluster START value here
///////////////////////////////////////////////////////////////////////////////////////////////
    classSounds cSound = new classSounds();   // create an instance of a classSound
    cSound.lstText.Add("ayer");               // add multiple equivalent or similar sounds 
                                              // to the same classSound object
    cSound.lstText.Add("ower");               // these different groups of letters will 
                                              // ALL have the phonetic value
    cSound.lstText.Add("oyer");
    cSound.lstText.Add("our");
    cSound.lstText.Add("ure");
    lstSounds.Add(cSound);                    // insert the new instance of the classSound 
                                              // into the existing list

    cSound = new classSounds();
    cSound.lstText.Add("ord");
    cSound.lstText.Add("ard");
    cSound.lstText.Add("urd");
    lstSounds.Add(cSound);

由于这些声音分组按照它们插入列表的顺序进行处理,因此应首先测试较长的字符串值,否则拼写相似的较短字符串组合可能会排除较长的字符串组合,并可能给用户体验带来意外结果。由于 SuffixPrefixCluster 声音列表都在同一个列表中,并且仅由整数变量区分,这些整数变量用于在 DissectWord() 方法中将它们彼此区分开来,因此你需要确保这些分隔整数值反映它们全部出现的顺序。

static int _intPrefixes_Start = -1;
static public int Prefixes_Start
{
    get { return _intPrefixes_Start; }
}
static int _intPrefixes_End = -1;
static public int Prefixes_End
{
    get { return _intPrefixes_End; }
}
static int _intClusters_Start = -1;
static public int Clusters_Start
{
    get { return _intClusters_Start; }
}
static int _intClusters_End = -1;
static public int Clusters_End
{
    get { return _intClusters_End; }
}
static int _intSuffixes_Start = -1;
static public int Suffixes_Start
{
    get { return _intSuffixes_Start; }
}
static int _intSuffixes_End = -1;
static public int Suffixes_End
{
    get { return _intSuffixes_End; }
}

你可以在 SoundsInit() 方法中看到这些整数值正在使用 lstSounds.Count Property 进行分配,并且你对该方法所做的任何更改都应考虑到它们。将用于 Cluster(不是单词的头部/尾部,而是中间的任何位置)的声音包含在前缀或后缀列表中都不会给你想要的结果。

注意:要更改你的数据树,你需要删除它在重新启动应用程序之前构建和依赖的 CK_RhymingDictionary.tree 文件,在你对 SoundInit() 做出任何更改之后。只需再次启动应用程序,它就会像你第一次启动时一样构建数据树。我用来包含到这个押韵词典中的单词是我为另一个项目抓取的 Rhyming.Com 网站的文件名。由于不可能向你提供所有这些文件(笨重且不必要),我将它们的文件名整理成位于你下载的源代码的 Debug/Bin 子目录中的26个单独的文本文件。从该列表中添加/删除单词并修改你自己的个人押韵词典是一件简单的事情。如果你想用不同的语言(例如波兰语、法语或德语)制作押韵词典。你必须更改 SoundInit() 方法,以使其反映你打算押韵的语言。这可能需要你进行一些修补,但这真的并不痛苦。

关注点

这不是我建造的第一个三叉树。它们往往占用太多内存,不值得在RAM内存中使用,所以我通常会为我的许多搜索方法选择二叉树,但由于这个项目依赖于三叉树的独特属性来通过重复声音(而不是像在二叉树中找到的唯一树叶)的语音签名进行跟踪,我不知道有任何更好的替代方案,尽管很可能存在。我最初每个字母组合只有一个ID,所以“ph”是独一无二的,不同于“ff”甚至“f”,这有点无用。由于这只花了我几个小时来编写,我又花了一些时间,并进行了必要的更改以构建当前版本。

历史

  • 2022年1月30日 - 首次发布
© . All rights reserved.