登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

搜索关键字高亮显示,就比微信多个多音字搜索 [复制链接]

2017-12-18 10:36
sergiochanTest 阅读:847 评论:0 赞:1
Tag:  

1.gif

首先看下demo效果,下载地址

一. 需求要求实现的效果

  • 汉字支持汉字直接搜索、拼音全拼搜索、拼音简拼搜索

  • 搜索匹配到的关键字高亮显示

  • 搜索结果优先显示全部匹配、其次是拼音全拼匹配、拼音简拼匹配;关键字在结果字符串中位置越靠前,优先显示

  • 支持搜索英文、汉字、电话号码及混合搜索

二. 需求分析

  • 英文名称及电话号码的搜索直接使用完全匹配的方式即可

  • 重难点是汉字的拼音相关的拼音全拼、简拼搜索,比如 “刘亦菲” 对应的搜索关键字有且只有以下三大类总计 25 种匹配

    汉字:“刘”、“亦”、“菲”、“刘亦”、“亦菲”、“刘亦菲”

    简拼相关:"l"、"y"、"f"、"ly"、"yf"、"lyf"

    全拼相关:"li"、"liu"、"liuy"、"liuyi"、"liuyif"、"liuyife"、"liuyifei"、"yi"、"yif"、"yife"、"yifei"、"fe"、"fei"

  • 拼音的重难点还包括:比如搜索关键字为“xian”,既要匹配出“先”,也要匹配出“西安”

三. 代码设计

1. 整体流程

首先初始化原始的数据(包含汉语、英文、数字及随意组合),主要是将一个汉语字符串转化为汉语全拼拼音及每个拼音字母所对应汉字的位置 和 汉语简拼拼音和每个拼音字母对应汉字的位置,将初始化之后的信息缓存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
+ (instancetype)personWithName:(NSString *)name hanyuPinyinOutputFormat:(HanyuPinyinOutputFormat *)pinyinFormat {
    WPFPerson *person = [[WPFPerson alloc] init];
     
    /** 将汉字转化为拼音的类方法
     *  name : 需要转换的汉字
     *  pinyinFormat : 拼音的格式化器
     *  @"" :  seperator 分隔符
     */
    NSString *completeSpelling = [PinyinHelper toHanyuPinyinStringWithNSString:name withHanyuPinyinOutputFormat:pinyinFormat withNSString:@""];
     
    // 首字母所组成的字符串
    NSString *initialString = @"";
    // 全拼拼音数组
    NSMutableArray *completeSpellingArray = [[NSMutableArray alloc] init];
    // 拼音首字母的位置数组
    NSMutableArray *pinyinFirstLetterLocationArray = [[NSMutableArray alloc] init];
     
    // 遍历每一个字符
    for (NSInteger x =0; x<name.length; x++) {         nsrange range =" NSMakeRange(x, 1);"         =""  获取字符=""         nsstring* hanyucharstring =" [name substringWithRange:range];"  如果该字符是中文=""         if ([wpfpinyintools ischinese:hanyucharstring]) {=""             =""  获取该字符的第一个拼音字母,如 wang 的 firstletter 就是 w=""             nsstring *firstletter =" [WPFPinYinTools firstCharactor:hanyuCharString withFormat:pinyinFormat];"  获取该字符的拼音全拼,如 王 的 pinyinstring就是 wang=""             nsstring *pinyinstring =" [PinyinHelper toHanyuPinyinStringWithNSString:hanyuCharString withHanyuPinyinOutputFormat:pinyinFormat withNSString:@""];" ** 获取该字符的拼音在整个字符串中的位置,如 "wang peng fei" =""  =""              *   "wang" 对应的四个拼音字母是 0,0,0,0,     =""              *   "peng" 对应的四个拼音字母是 1,1,1,1,     =""              *   "fei"  对应的三个拼音字母是 2,2,2,       =""              *=""             for (nsinteger j=" 0 ;j<pinyinString.length ; j++) {"                 [completespellingarray addobject:@(x)];=""             }=""  拼接首字母字符串,如 "王鹏飞" 对应的首字母字符串就是 "wpf"=""             initialstring =" [initialString stringByAppendingString:firstLetter];"    拼接首字母位置字符串,如 "王鹏飞" 对应的首字母位置就是 "0,1,2" =""             [pinyinfirstletterlocationarray addobject:@(x)];=""         else {=""             [completespellingarray addobject:@(x)];=""         }=""     }=""     person.name =" name;"     person.completespelling =" completeSpelling;"     person.initialstring =" initialString;"     person.pinyinlocationstring =" [completeSpellingArray componentsJoinedByString:@","];"     person.initiallocationstring =" [pinyinFirstLetterLocationArray componentsJoinedByString:@","];"     =""     return person;="" }<="" pre=""><ul class=" list-paddingleft-2"><li><p>根据 UISearchResultsUpdating 代理方法 - (void)updateSearchResultsForSearchController:(UISearchController *)searchController 来实时获取输入的最新关键字,并遍历数据源,将匹配到的结果显示出来</p></li></ul><pre class="brush:as3;toolbar:false">// 更新搜索结果
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
    NSLog(@"%@", searchController.searchBar.text);
     
    [self.searchResultVC.resultDataSource removeAllObjects];
     
    for (WPFPerson *person in self.dataSource) {
        WPFSearchResultModel *resultModel = [WPFPinYinTools
                                             searchEffectiveResultWithSearchString:searchController.searchBar.text.lowercaseString
                                             nameString:person.name
                                             completeSpelling:person.completeSpelling
                                             initialString:person.initialString
                                             pinyinLocationString:person.pinyinLocationString
                                             initialLocationString:person.initialLocationString];
         
        if (resultModel.highlightRang.length) {
            person.highlightLoaction = resultModel.highlightRang.location;
            person.textRange = resultModel.highlightRang;
            person.matchType = resultModel.matchType;
            [self.searchResultVC.resultDataSource addObject:person];
        }
    };
    // 将匹配结果按照产品规则进行排序
    [self.searchResultVC.resultDataSource sortUsingDescriptors:[WPFPinYinTools sortingRules]];
    // 刷新tableView
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.searchResultVC.tableView reloadData];
    });
}</pre><ul class=" list-paddingleft-2"><li><p>匹配的过程是一个重难点,分别进行汉字直接匹配、拼音全拼匹配、拼音简拼匹配</p></li></ul><pre class="brush:as3;toolbar:false">+ (WPFSearchResultModel *)searchEffectiveResultWithSearchString:(NSString *)searchStrLower
                                                     nameString:(NSString *)nameStrLower
                                               completeSpelling:(NSString *)completeSpelling
                                                  initialString:(NSString *)initialString
                                           pinyinLocationString:(NSString *)pinyinLocationString
                                          initialLocationString:(NSString *)initialLocationString {
     
    WPFSearchResultModel *searchModel = [[WPFSearchResultModel alloc] init];
     
    NSArray *completeSpellingArray = [pinyinLocationString componentsSeparatedByString:@","];
    NSArray *pinyinFirstLetterLocationArray = [initialLocationString componentsSeparatedByString:@","];
     
    // 完全中文匹配范围
    NSRange chineseRange = [nameStrLower rangeOfString:searchStrLower];
    // 拼音全拼匹配范围
    NSRange complateRange = [completeSpelling rangeOfString:searchStrLower];
    // 拼音首字母匹配范围
    NSRange initialRange = [initialString rangeOfString:searchStrLower];
     
    // 汉字直接匹配
    if (chineseRange.length!=0) {
        searchModel.highlightedRange = chineseRange;
        searchModel.matchType = MatchTypeChinese;
        return searchModel;
    }
     
    NSRange highlightedRange = NSMakeRange(00);
     
    // MARK: 拼音全拼匹配
    if (complateRange.length != 0) {
        if (complateRange.location == 0) {
            // 拼音首字母匹配从0开始,即搜索的关键字与该数据源第一个汉字匹配到,所以高亮范围从0开始
            highlightedRange = NSMakeRange(0, [completeSpellingArray[complateRange.length-1] integerValue] +1);
             
        else {
            /** 如果该拼音字符是一个汉字的首个字符,如搜索“g”,
             *  就要匹配出“gai”、“ge”等“g”开头的拼音对应的字符,
             *  而不应该匹配到“wang”、“feng”等非”g“开头的拼音对应的字符
             */
            NSInteger currentLocation = [completeSpellingArray[complateRange.location] integerValue];
            NSInteger lastLocation = [completeSpellingArray[complateRange.location-1] integerValue];
            if (currentLocation != lastLocation) {
                // 高亮范围从匹配到的第一个关键字开始
                highlightedRange = NSMakeRange(currentLocation, [completeSpellingArray[complateRange.length+complateRange.location -1] integerValue] - currentLocation +1);
            }
        }
        searchModel.highlightedRange = highlightedRange;
        searchModel.matchType = MatchTypeComplate;
        if (highlightedRange.length!=0) {
            return searchModel;
        }
    }
     
    // MARK: 拼音首字母匹配
    if (initialRange.length!=0) {
        NSInteger currentLocation = [pinyinFirstLetterLocationArray[initialRange.location] integerValue];
        NSInteger highlightedLength;
        if (initialRange.location ==0) {
            highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length-1] integerValue]-currentLocation +1;
            // 拼音首字母匹配从0开始,即搜索的关键字与该数据源第一个汉字匹配到,所以高亮范围从0开始
            highlightedRange = NSMakeRange(0, highlightedLength);
        else {
            highlightedLength = [pinyinFirstLetterLocationArray[initialRange.length+initialRange.location-1] integerValue]-currentLocation +1;
            // 高亮范围从匹配到的第一个关键字开始
            highlightedRange = NSMakeRange(currentLocation, highlightedLength);
        }
        searchModel.highlightedRange = highlightedRange;
        searchModel.matchType = MatchTypeInitial;
        if (highlightedRange.length!=0) {
            return searchModel;
        }
    }
     
    searchModel.highlightedRange = NSMakeRange(00);
    searchModel.matchType = NSIntegerMax;
    return searchModel;
}</pre><p><strong>2. 第三方依赖</strong></p><ul class=" list-paddingleft-2"><li><p>首先筛选出一个比较全的第三方库 PinYin4Objc用于汉语转拼音,拼音的 unicode 库比较全,一些新的汉字也都能转成拼音</p></li><li><p>但是由于该库好久没有更新,获取拼音文件部分代码不适合组件化的直接开发,因此我直接合到源文件里面了</p></li><li><p>汉语转拼音的格式</p></li></ul><pre class="brush:as3;toolbar:false">// 获取格式化器
+ (HanyuPinyinOutputFormat *)getOutputFormat {
    HanyuPinyinOutputFormat *pinyinFormat = [[HanyuPinyinOutputFormat alloc] init];
    /** 设置大小写
     *  CaseTypeLowercase : 小写
     *  CaseTypeUppercase : 大写
     */
    [pinyinFormat setCaseType:CaseTypeLowercase];
    /** 声调格式 :如 王鹏飞
     * ToneTypeWithToneNumber : 用数字表示声调 wang2 peng2 fei1
     * ToneTypeWithoutTone    : 无声调表示 wang peng fei
     * ToneTypeWithToneMark   : 用字符表示声调 wáng péng fēi
     */
    [pinyinFormat setToneType:ToneTypeWithoutTone];
    /** 设置特殊拼音ü的显示格式:
     * VCharTypeWithUAndColon : 以U和一个冒号表示该拼音,例如:lu:
     * VCharTypeWithV         : 以V表示该字符,例如:lv
     * VCharTypeWithUUnicode  : 以ü表示
     */
    [pinyinFormat setVCharType:VCharTypeWithV];
    return pinyinFormat;
}</pre><p><strong>3. 其他细节</strong></p><ul class=" list-paddingleft-2"><li><p>排序规则</p></li></ul><pre class="brush:as3;toolbar:false">+ (NSArray *)sortingRules {
    // 按照 matchType 顺序排列,即优先展示 中文,其次是全拼匹配,最后是拼音首字母匹配
    NSSortDescriptor *desType = [NSSortDescriptor sortDescriptorWithKey:@"matchType" ascending:YES];
    // 优先显示 高亮位置索引靠前的搜索结果
    NSSortDescriptor *desLocation = [NSSortDescriptor sortDescriptorWithKey:@"highlightLoaction" ascending:YES];
    return @[desType,desLocation];
}</pre><p><span style="font-size: 18px;"><strong>四. 循环方法测试及优化选择过程</strong></span></p><p>在优化遍历方法的过程中,测试了几种遍历方法,这里以输入关键字“wang”为测试数据,测试真机机型为iPhone SE 10.3</p><ul class=" list-paddingleft-2"><li><p>常规 for 循环</p></li></ul><pre class="brush:as3;toolbar:false">/**
 2017-12-06 12:02:51.943006 HighlightedSearch[4459:1871193] w
 2017-12-06 12:02:51.943431 HighlightedSearch[4459:1871193] 开始匹配,开始时间:2017-12-06 04:02:51 +0000
 2017-12-06 12:02:51.980588 HighlightedSearch[4459:1871193] 匹配结束,结束时间:2017-12-06 04:02:51 +0000,耗时:0.0372
 2017-12-06 12:02:52.284488 HighlightedSearch[4459:1871193] wa
 2017-12-06 12:02:52.284771 HighlightedSearch[4459:1871193] 开始匹配,开始时间:2017-12-06 04:02:52 +0000
 2017-12-06 12:02:52.316536 HighlightedSearch[4459:1871193] 匹配结束,结束时间:2017-12-06 04:02:52 +0000,耗时:0.0318
 2017-12-06 12:02:52.516826 HighlightedSearch[4459:1871193] wan
 2017-12-06 12:02:52.517121 HighlightedSearch[4459:1871193] 开始匹配,开始时间:2017-12-06 04:02:52 +0000
 2017-12-06 12:02:52.545542 HighlightedSearch[4459:1871193] 匹配结束,结束时间:2017-12-06 04:02:52 +0000,耗时:0.0285
 2017-12-06 12:02:52.838220 HighlightedSearch[4459:1871193] wang
 2017-12-06 12:02:52.838602 HighlightedSearch[4459:1871193] 开始匹配,开始时间:2017-12-06 04:02:52 +0000
 2017-12-06 12:02:52.880200 HighlightedSearch[4459:1871193] 匹配结束,结束时间:2017-12-06 04:02:52 +0000,耗时:0.0417
 */
for (NSInteger i = 0; i < self.dataSource.count; i++) {</pre><ul class=" list-paddingleft-2"><li><p>GCD 多线程循环</p></li></ul><pre class="brush:as3;toolbar:false">/**
 2017-12-06 11:56:55.565738 HighlightedSearch[4419:1869486] w
 2017-12-06 11:56:55.566287 HighlightedSearch[4419:1869486] 开始匹配,开始时间:2017-12-06 03:56:55 +0000
 2017-12-06 11:56:55.626184 HighlightedSearch[4419:1869486] 匹配结束,结束时间:2017-12-06 03:56:55 +0000,耗时:0.0601
 2017-12-06 11:56:55.937535 HighlightedSearch[4419:1869486] wa
 2017-12-06 11:56:55.937842 HighlightedSearch[4419:1869486] 开始匹配,开始时间:2017-12-06 03:56:55 +0000
 2017-12-06 11:56:55.983074 HighlightedSearch[4419:1869486] 匹配结束,结束时间:2017-12-06 03:56:55 +0000,耗时:0.0452
 2017-12-06 11:56:56.344808 HighlightedSearch[4419:1869486] wan
 2017-12-06 11:56:56.347350 HighlightedSearch[4419:1869486] 开始匹配,开始时间:2017-12-06 03:56:56 +0000
 2017-12-06 11:56:56.414215 HighlightedSearch[4419:1869486] 匹配结束,结束时间:2017-12-06 03:56:56 +0000,耗时:0.0690
 2017-12-06 11:56:56.711174 HighlightedSearch[4419:1869486] wang
 2017-12-06 11:56:56.712013 HighlightedSearch[4419:1869486] 开始匹配,开始时间:2017-12-06 03:56:56 +0000
 2017-12-06 11:56:56.774761 HighlightedSearch[4419:1869486] 匹配结束,结束时间:2017-12-06 03:56:56 +0000,耗时:0.0632
 */
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(self.dataSource.count, queue, ^(size_t index) {</pre><ul class=" list-paddingleft-2"><li><p>enumerateObjectsWithOptions 多线程循环</p></li></ul><pre class="brush:as3;toolbar:false">/**
 2017-12-06 11:58:12.716606 HighlightedSearch[4428:1869917] w
 2017-12-06 11:58:12.717005 HighlightedSearch[4428:1869917] 开始匹配,开始时间:2017-12-06 03:58:12 +0000
 2017-12-06 11:58:12.780168 HighlightedSearch[4428:1869917] 匹配结束,结束时间:2017-12-06 03:58:12 +0000,耗时:0.0633
 2017-12-06 11:58:13.058590 HighlightedSearch[4428:1869917] wa
 2017-12-06 11:58:13.058841 HighlightedSearch[4428:1869917] 开始匹配,开始时间:2017-12-06 03:58:13 +0000
 2017-12-06 11:58:13.116964 HighlightedSearch[4428:1869917] 匹配结束,结束时间:2017-12-06 03:58:13 +0000,耗时:0.0581
 2017-12-06 11:58:13.397052 HighlightedSearch[4428:1869917] wan
 2017-12-06 11:58:13.397338 HighlightedSearch[4428:1869917] 开始匹配,开始时间:2017-12-06 03:58:13 +0000
 2017-12-06 11:58:13.460298 HighlightedSearch[4428:1869917] 匹配结束,结束时间:2017-12-06 03:58:13 +0000,耗时:0.0630
 2017-12-06 11:58:13.763888 HighlightedSearch[4428:1869917] wang
 2017-12-06 11:58:13.764263 HighlightedSearch[4428:1869917] 开始匹配,开始时间:2017-12-06 03:58:13 +0000
 2017-12-06 11:58:13.833888 HighlightedSearch[4428:1869917] 匹配结束,结束时间:2017-12-06 03:58:13 +0000,耗时:0.0697
 */
dispatch_queue_t queue = dispatch_queue_create("wpf.updateSearchResults.test", DISPATCH_QUEUE_SERIAL);
[self.dataSource enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {</pre><ul class=" list-paddingleft-2"><li><p>forin 循环</p></li></ul><pre class="brush:as3;toolbar:false">/**
 2017-12-06 12:00:38.217187 HighlightedSearch[4439:1870645] w
 2017-12-06 12:00:38.217575 HighlightedSearch[4439:1870645] 开始匹配,开始时间:2017-12-06 04:00:38 +0000
 2017-12-06 12:00:38.253997 HighlightedSearch[4439:1870645] 匹配结束,结束时间:2017-12-06 04:00:38 +0000,耗时:0.0364
 2017-12-06 12:00:38.616430 HighlightedSearch[4439:1870645] wa
 2017-12-06 12:00:38.616807 HighlightedSearch[4439:1870645] 开始匹配,开始时间:2017-12-06 04:00:38 +0000
 2017-12-06 12:00:38.654969 HighlightedSearch[4439:1870645] 匹配结束,结束时间:2017-12-06 04:00:38 +0000,耗时:0.0383
 2017-12-06 12:00:38.948700 HighlightedSearch[4439:1870645] wan
 2017-12-06 12:00:38.949453 HighlightedSearch[4439:1870645] 开始匹配,开始时间:2017-12-06 04:00:38 +0000
 2017-12-06 12:00:38.986892 HighlightedSearch[4439:1870645] 匹配结束,结束时间:2017-12-06 04:00:38 +0000,耗时:0.0378
 2017-12-06 12:00:39.280979 HighlightedSearch[4439:1870645] wang
 2017-12-06 12:00:39.281563 HighlightedSearch[4439:1870645] 开始匹配,开始时间:2017-12-06 04:00:39 +0000
 2017-12-06 12:00:39.317743 HighlightedSearch[4439:1870645] 匹配结束,结束时间:2017-12-06 04:00:39 +0000,耗时:0.0365
 */
for (WPFPerson *person in self.dataSource) {</pre><blockquote><p>最终选择的是forin循环,因为一般情况下 enumerateObjectsWithOptions 多线程是最快的,并且稍快于 dispatch_apply 方法,但是因为这个方法需要操作数组,因此必须将操作数据的那行代码加锁或者在指定线程进行,进行这个操作后效率反而不如其他单线程循环,考虑到搜索结果本来还要再次根据规则排序,就选择了 forin 循环</p></blockquote><p><span style="font-size: 18px;"><strong>五. 为什么没有选择hash</strong></span></p><ul class=" list-paddingleft-2"><li><p>首先最重要的一条是当前循环的方式也能满足需求(线上大概四千多条数据,使用过程中基本实时展现)</p></li><li><p>上文在需求分析中已举例,一个三个字的汉字对应的key值就有20多个甚至更多,在解析过程中是十分耗时的,但需求往往还存在类似微信的“群名称”匹配,每多一个字,对应的key值就多几个数量级</p></li><li><p>MapTable在高并发情况下,需要不断进行Resize(扩容 & Rehash),并且在Rehash 并发的情况下还可能形成链表环</p></li><li><p>有个优化的思路,考虑到遍历的方式解析快,搜索匹配慢;hash的方式解析慢,搜索匹配快</p><p>通过遍历的方式先快速解析数据,此时搜索使用遍历的方式</p><p>然后再用hash的方式再次解析数据(考虑到hash表的扩容会使得瞬时效率的降低,为了避免频繁的扩容,先使用桶排序的方法将10个数字、26个英文字母、以及特殊符号开头的key分别放在37个字典里面,整体是一个数组。每个字典里面存放对应key和value),解析完成之后做个标记就采用hash的方式直接使用输入的key值去查询</p><p>配合DB缓存,效果应该是很棒的</p></li></ul><p><span style="font-size: 18px;"><strong>六. 多音字</strong></span></p><p>简单测了一下拥有该功能的产品:</p><ul class=" list-paddingleft-2"><li><p>微信搜索(就是文中讲的该类型搜索)是在本地做的,不支持多音字</p></li><li><p>钉钉的搜索是服务器做的,支持多音字(但是简单测了一下一些基本的多音字存在bug)</p></li></ul><p><span style="font-size: 18px;"><strong>七. 实际项目还要做哪些工作?</strong></span></p><ul class