更新OpenCC.NET

这两天有点空,把OpenCC.NET重构和优化了一下,版本号也正式来到了1.0。本来是想加入异步API,结果测试发现可能因为本来转换就是查字典速度很快,加上异步的话还需要Task的创建开销,导致性能还不如同步版本。然后尝试把原来用StringBuilder改成栈上的Span<char>,结果性能也没啥区别。不过原来的内部转换的实现复用性不强而且乱,还是决定重写成OpenCC原本那种链式处理。简单来说就是分词完后得到IEnumerable<string>格式的词组,给IEnumerable<string>写一个扩展方法:

private static IEnumerable<string> ConvertBy(this IEnumerable<string> phrases,
    params IDictionary<string, string>[] dictionaries)
{
    return phrases.Select(phrase =>
    {
        ...    // 根据字典进行转换
    }
}

一批词组先用一些字典进行转换,得到中间结果,然后可以再用其他字典继续转换,直到得到结果。这样就可以等效出原OpenCC中的转换链。比如原OpenCC中的s2twp.json(简=>繁台+词汇):

{
  "name": "Simplified Chinese to Traditional Chinese (Taiwan standard, with phrases)",
  "segmentation": {
    "type": "mmseg",
    "dict": {
      "type": "ocd2",
      "file": "STPhrases.ocd2"
    }
  },
  "conversion_chain": [{
    "dict": {
      "type": "group",
      "dicts": [{
        "type": "ocd2",
        "file": "STPhrases.ocd2"
      }, {
        "type": "ocd2",
        "file": "STCharacters.ocd2"
      }]
    }
  }, {
    "dict": {
      "type": "ocd2",
      "file": "TWPhrases.ocd2"
    }
  }, {
    "dict": {
      "type": "ocd2",
      "file": "TWVariants.ocd2"
    }
  }]
}

根据conversion_chain部分,利用ConvertBy()可以等效出:

public static string HansToTWWithPhrase(string text)
{
    // 分词
    var phrases = ZhSegment.Segment(text);
    // 链式转换
    return phrases.ConvertBy(ZhDictionary.STPhrases, ZhDictionary.STCharacters)
            .ConvertBy(ZhDictionary.TWPhrases)
            .ConvertBy(ZhDictionary.TWVariants)
            // Join()是我写的扩展方法,其实就是调用string.Join()把词组重新合并成句子
            .Join()
}

于是我就把原来的API全部重写成这种转换链的形式了,另外还加了日语汉字新旧字体转换的API(因为原OpenCC带了相应的字典,干脆也就实现了),虽然感觉也就图一乐用。

顺带做了一下性能测试。程序的第一次转换,就算是转换空文本都要差不多花费500ms的时间,后续就不用了,可能是因为要加载Jieba分词的模型。然后测试了一本15.5M大小的网络小说,共14万行,同步的话大概要总共花费9s;改用异步方式,每行都Task.Run()调用转换方法然后await,可以缩短到4.5s左右。

另外还尝试了一下GitHub上的Action,用了别人写好的脚本,可以自动把最新的工程构建和发布到Nuget上,很是方便。

Nuget打包的一个坑

先说结论,我在Visual Studio 2022 17.1.0上设置C#项目自动生成Nuget包时,如果在ItemGroupContent上配置了PackageCopyToOutput属性,且同时又配置了PackagePath属性,那么PackageCopyToOutput会被无效。

OpenCC.NET是我第一次打Nuget包,所以啥也不懂。其实打包都是自动化的,只要在工程配置文件里设置一下库的信息比如名字版本号啥的就好。但问题是这个库需要用到OpenCC字典和Jieba.NET的资源文件,所以按理说应该也要打进Nuget包里,然后其他程序如果引用了这个包,编译的时候会自动把这些文件复制到输出目录。
一开始我实在是不知道咋操作,所以只能在readme上注明需要手动下载这两个文件夹放到程序目录。

一开始我的配置类似:

<ItemGroup>
  <Content Include="Dictionary\*.txt">
    <Pack>True</Pack>
    <PackagePath>content\Dictionary\</PackagePath>
  </Content> 
  <Content Include="JiebaResource\*.*">
    <Pack>True</Pack>
    <PackagePath>content\JiebaResource\</PackagePath>
  </Content>
</ItemGroup>

文件确实是打进包里了,可是还是无法输出。困扰了我很久,我后面试过什么CopyToOutputDirectory属性之类的,都无效,只能放弃,然后在readme上写需要手工将两个文件夹下载放入程序目录下。

这次我又重新经过一番搜索,发现是我想要的这个功能原来是靠PackageCopyToOutput属性实现的,于是我尝试:

<ItemGroup>
  <Content Include="Dictionary\*.txt">
    <Pack>True</Pack>
    <PackageCopyToOutput>True</PackageCopyToOutput>
  </Content>
  <Content Include="JiebaResource\*.*">
    <Pack>True</Pack>
    <PackageCopyToOutput>True</PackageCopyToOutput>
  </Content>
</ItemGroup>

发现终于能输出了!但是生成的.nupkg却大了一倍,打开包结构一看,文件夹给我放进去一模一样的两份。

一顿搜索之后发现官方文档上确实是介绍了这一点

默认情况下,所有内容都添加到包中 contentcontentFiles\any\<target_framework> 文件夹根目录,并保留相对文件夹结构,除非指定包路径

但这样不是白白浪费了空间,于是我就在上面的基础上,像最开始一样添加PackagePath属性:

<ItemGroup>
  <Content Include="Dictionary\*.txt">
    <Pack>True</Pack>
    <PackageCopyToOutput>True</PackageCopyToOutput>
    <PackagePath>content\Dictionary\</PackagePath>
  </Content>
  <Content Include="JiebaResource\*.*">
    <Pack>True</Pack>
    <PackageCopyToOutput>True</PackageCopyToOutput>
    <PackagePath>content\JiebaResource\</PackagePath>
  </Content>
</ItemGroup>

这次确实只打包一份资源文件了。

但搞笑的是又不能自动把两个文件夹复制到程序输出目录了。所以最后我还是只能选择去掉指定路径,让它自动给我打包两份资源文件上去。虽然大小成了两倍,但至少能用不是嘛。只能说属实是有点坑了。

如果觉得我的文章对你有用,请随意赞赏