基于JieBaNet+Lucene.Net实现全文搜索
实现效果:
上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图:
基本风格是模仿的百度搜索结果,绿色的分页略显小清新。
目前已采集并创建索引的文章约3W多篇,索引文件不算太大,查询速度非常棒。
刀不磨要生锈,人不学要落后。每天都要学一些新东西。
基本技术介绍:
还记得上一次做全文搜索是在2013年,主要核心设计与代码均是当时的架构师写的,自己只能算是全程参与。
当时使用的是经典搭配:盘古分词+Lucene.net。
前几篇文章有说到,盘古分词已经很多年不更新了,我在SupportYun系统一直引用的JieBaNet来做分词技术。
那么是否也有成型的JieBaNet+Lucene.Net的全文搜索方案呢?
经过多番寻找,在GitHub上面找到一个简易的例子:https://github.com/anderscui/jiebaForLuceneNet
博主下面要讲的实现方案就是从这个demo得到的启发,大家有兴趣可以去看看这个demo。
博主使用的具体版本:Lucene.net 3.0.3.0 ,JieBaNet 0.38.3.0(做过简易的调整与扩展,前面文章有讲到)
首先我们对Lucene.Net的分词器Tokenizer、分析器Analyzer做一个基于JieBaNet的扩展。
1.基于LuceneNet扩展的JieBa分析器JiebaForLuceneAnalyzer


1 /// <summary>
2 /// 基于LuceneNet扩展的JieBa分析器
3 /// </summary>
4 public class JiebaForLuceneAnalyzer : Analyzer
5 {
6 protected static readonly ISet<string> DefaultStopWords = StopAnalyzer.ENGLISH_STOP_WORDS_SET;
7
8 private static ISet<string> StopWords;
9
10 static JiebaForLuceneAnalyzer()
11 {
12 StopWords = new HashSet<string>();
13 var stopWordsFile = Path.GetFullPath(JiebaNet.Analyser.ConfigManager.StopWordsFile);
14 if (File.Exists(stopWordsFile))
15 {
16 var lines = File.ReadAllLines(stopWordsFile);
17 foreach (var line in lines)
18 {
19 StopWords.Add(line.Trim());
20 }
21 }
22 else
23 {
24 StopWords = DefaultStopWords;
25 }
26 }
27
28 public override TokenStream TokenStream(string fieldName, TextReader reader)
29 {
30 var seg = new JiebaSegmenter();
31 TokenStream result = new JiebaForLuceneTokenizer(seg, reader);
32 result = new LowerCaseFilter(result);
33 result = new StopFilter(true, result, StopWords);
34 return result;
35 }
36 }


2.基于LuceneNet扩展的JieBa分词器:JiebaForLuceneTokenizer


1 /// <summary>
2 /// 基于Lucene的JieBa分词扩展
3 /// </summary>
4 public class JiebaForLuceneTokenizer:Tokenizer
5 {
6 private readonly JiebaSegmenter segmenter;
7 private readonly ITermAttribute termAtt;
8 private readonly IOffsetAttribute offsetAtt;
9 private readonly ITypeAttribute typeAtt;
10
11 private readonly List<Token> tokens;
12 private int position = -1;
13
14 public JiebaForLuceneTokenizer(JiebaSegmenter seg, TextReader input):this(seg, input.ReadToEnd()) { }
15
16 public JiebaForLuceneTokenizer(JiebaSegmenter seg, string input)
17 {
18 segmenter = seg;
19 termAtt = AddAttribute<ITermAttribute>();
20 offsetAtt = AddAttribute<IOffsetAttribute>();
21 typeAtt = AddAttribute<ITypeAttribute>();
22
23 var text = input;
24 tokens = segmenter.Tokenize(text, TokenizerMode.Search).ToList();
25 }
26
27 public override bool IncrementToken()
28 {
29 ClearAttributes();
30 position++;
31 if (position < tokens.Count)
32 {
33 var token = tokens[position];
34 termAtt.SetTermBuffer(token.Word);
35 offsetAtt.SetOffset(token.StartIndex, token.EndIndex);
36 typeAtt.Type = "Jieba";
37 return true;
38 }
39
40 End();
41 return false;
42 }
43
44 public IEnumerable<Token> Tokenize(string text, TokenizerMode mode = TokenizerMode.Search)
45 {
46 return segmenter.Tokenize(text, mode);
47 }
48 }


理想如果不向现实做一点点屈服,那么理想也将归于尘土。
实现方案设计:
我们做全文搜索的设计时一定会考虑的一个问题就是:我们系统是分很多模块的,不同模块的字段差异很大,怎么才能实现同一个索引,既可以单个模块搜索又可以全站搜索,甚至按一些字段做条件来搜索呢?
这些也是SupportYun系统需要考虑的问题,因为目前的数据就天然的拆分成了活动、文章两个类别,字段也大有不同。博主想实现的是一个可以全站搜索(结果包括活动、文章),也可以在文章栏目/活动栏目分别搜索,并且可以按几个指定字段来做搜索条件。
要做一个这样的全文搜索功能,我们需要从程序设计上来下功夫。下面就介绍一下博主的设计方案:
一、索引创建
1.我们设计一个IndexManager来处理最基本的索引创建、更新、删除操作。
2.创建、更新使用到的标准数据类:IndexContent。
我们设计TableName(对应DB表名)、RowId(对应DB主键)、CollectTime(对应DB数据创建时间)、ModuleType(所属系统模块)、Title(检索标题)、IndexTextContent(检索文本)等六个基础字段,所有模块需要创建索引必须构建该6个字段(大家可据具体情况扩展)。
然后设计10个预留字段Tag1-Tag10,用以兼容各大模块其他不同字段。
预留字段的存储、索引方式可独立配置。
其中BaseIndexContent含有六个基础字段。
3.创建一个子模块索引构建器的接口:IIndexBuilder。
各子模块通过继承实现IIndexBuilder,来实现索引的操作。
4.下面我们以活动模块为例,来实现索引创建。
a)首先创建一个基于活动模块的数据类:ActivityIndexContent,可以将我们需要索引或存储的字段都设计在内。
b)我们再创建ActivityIndexBuilder并继承IIndexBuilder,实现其创建、更新、删除方法。
代码就不解释了,很简单。主要就是调用IndexManager来执行操作。
我们只需要在需要创建活动数据索引的业务点,构建ActivityIndexBuilder对象,并构建ActivityIndexContent集合作为参数,调用BuildIndex方法即可。
二、全文搜索
全文搜索我们采用同样的设计方式。
1.设计一个抽象的搜索类:BaseIndexSearch,所有搜索模块(包括全站)均需继承它来实现搜索效果。


1 public abstract class BaseIndexSearch<TIndexSearchResultItem>
2 where TIndexSearchResultItem : IndexSearchResultItem
3 {
4 /// <summary>
5 /// 索引存储目录
6 /// </summary>
7 private static readonly string IndexStorePath = ConfigurationManager.AppSettings["IndexStorePath"];
8 private readonly string[] fieldsToSearch;
9 protected static readonly SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
10 private static IndexSearcher indexSearcher = null;
11
12 /// <summary>
13 /// 索引内容命中片段大小
14 /// </summary>
15 public int FragmentSize { get; set; }
16
17 /// <summary>
18 /// 构造方法
19 /// </summary>
20 /// <param name="fieldsToSearch">搜索文本字段</param>
21 protected BaseIndexSearch(string[] fieldsToSearch)
22 {
23 FragmentSize = 100;
24 this.fieldsToSearch = fieldsToSearch;
25 }
26
27 /// <summary>
28 /// 创建搜索结果实例
29 /// </summary>
30 /// <returns></returns>
31 protected abstract TIndexSearchResultItem CreateIndexSearchResultItem();
32
33 /// <summary>
34 /// 修改搜索结果(主要修改tag字段对应的属性)
35 /// </summary>
36 /// <param name="indexSearchResultItem">搜索结果项实例</param>
37 /// <param name="content">用户搜索内容</param>
38 /// <param name="docIndex">索引库位置</param>
39 /// <param name="doc">当前位置内容</param>
40 /// <returns>搜索结果</returns>
41 protected abstract void ModifyIndexSearchResultItem(ref TIndexSearchResultItem indexSearchResultItem, string content, int docIndex, Document doc);
42
43 /// <summary>
44 /// 修改筛选器(各模块)
45 /// </summary>
46 /// <param name="filter"></param>
47 protected abstract void ModifySearchFilter(ref Dictionary<string, string> filter);
48
49 /// <summary>
50 /// 全库搜索
51 /// </summary>
52 /// <param name="content">搜索文本内容</param>
53 /// <param name="filter">查询内容限制条件,默认为null,不限制条件.</param>
54 /// <param name="fieldSorts">对字段进行排序</param>
55 /// <param name="pageIndex">查询结果当前页,默认为1</param>
56 /// <param name="pageSize">查询结果每页结果数,默认为20</param>
57 public PagedIndexSearchResult<TIndexSearchResultItem> Search(string content
58 , Dictionary<string, string> filter = null, List<FieldSort> fieldSorts = null
59 , int pageIndex = 1, int pageSize = 20)
60 {
61 try
62 {
63 if (!string.IsNullOrEmpty(content))
64 {
65 content = ReplaceIndexSensitiveWords(content);
66 content = GetKeywordsSplitBySpace(content,
67 new JiebaForLuceneTokenizer(new JiebaSegmenter(), content));
68 }
69 if (string.IsNullOrEmpty(content) || pageIndex < 1)
70 {
71 throw new Exception("输入参数不符合要求(用户输入为空,页码小于等于1)");
72 }
73
74 var stopWatch = new Stopwatch();
75 stopWatch.Start();
76
77 Analyzer analyzer = new JiebaForLuceneAnalyzer();
78 // 索引条件创建
79 var query = MakeSearchQuery(content, analyzer);
80 // 筛选条件构建
81 filter = filter == null ? new Dictionary<string, string>() : new Dictionary<string, string>(filter);
82 ModifySearchFilter(ref filter);
83 Filter luceneFilter = MakeSearchFilter(filter);
84
85 #region------------------------------执行查询---------------------------------------
86
87 TopDocs topDocs;
88 if (indexSearcher == null)
89 {
90 var dir = new DirectoryInfo(IndexStorePath);
91 FSDirectory entityDirectory = FSDirectory.Open(dir);
92 IndexReader reader = IndexReader.Open(entityDirectory, true);
93 indexSearcher = new IndexSearcher(reader);
94 }
95 else
96 {
97 IndexReader indexReader = indexSearcher.IndexReader;
98 if (!indexReader.IsCurrent())
99 {
100 indexSearcher.Dispose();
101 indexSearcher = new IndexSearcher(indexReader.Reopen());
102 }
103 }
104 // 收集器容量为所有
105 int totalCollectCount = pageIndex*pageSize;
106 Sort sort = GetSortByFieldSorts(fieldSorts);
107 topDocs = indexSearcher.Search(query, luceneFilter, totalCollectCount, sort ?? Sort.RELEVANCE);
108
109 #endregion
110
111 #region-----------------------返回结果生成-------------------------------
112
113 ScoreDoc[] hits = topDocs.ScoreDocs;
114 var start = (pageIndex - 1)*pageSize + 1;
115 var end = Math.Min(totalCollectCount, hits.Count());
116
117 var result = new PagedIndexSearchResult<TIndexSearchResultItem>
118 {
119 PageIndex = pageIndex,
120 PageSize = pageSize,
121 TotalRecords = topDocs.TotalHits
122 };
123
124 for (var i = start; i <= end; i++)
125 {
126 var scoreDoc = hits[i - 1];
127 var doc = indexSearcher.Doc(scoreDoc.Doc);
128
129 var indexSearchResultItem = CreateIndexSearchResultItem();
130 indexSearchResultItem.DocIndex = scoreDoc.Doc;
131 indexSearchResultItem.ModuleType = doc.Get("ModuleType");
132 indexSearchResultItem.TableName = doc.Get("TableName");
133 indexSearchResultItem.RowId = Guid.Parse(doc.Get("RowId"));
134 if (!string.IsNullOrEmpty(doc.Get("CollectTime")))
135 {
136 indexSearchResultItem.CollectTime = DateTime.Parse(doc.Get("CollectTime"));
137 }
138 var title = GetHighlighter(formatter, FragmentSize).GetBestFragment(content, doc.Get("Title"));
139 indexSearchResultItem.Title = string.IsNullOrEmpty(title) ? doc.Get("Title") : title;
140 var text = GetHighlighter(formatter, FragmentSize)
141 .GetBestFragment(content, doc.Get("IndexTextContent"));
142 indexSearchResultItem.Content = string.IsNullOrEmpty(text)
143 ? (doc.Get("IndexTextContent").Length > 100
144 ? doc.Get("IndexTextContent").Substring(0, 100)
145 : doc.Get("IndexTextContent"))
146 : text;
147 ModifyIndexSearchResultItem(ref indexSearchResultItem, content, scoreDoc.Doc, doc);
148 result.Add(indexSearchResultItem);
149 }
150 stopWatch.Stop();
151 result.Elapsed = stopWatch.ElapsedMilliseconds*1.0/1000;
152
153 return result;
154
155 #endregion
156 }
157 catch (Exception exception)
158 {
159 LogUtils.ErrorLog(exception);
160 return null;
161 }
162 }
163
164 private Sort GetSortByFieldSorts(List<FieldSort> fieldSorts)
165 {
166 if (fieldSorts == null)
167 {
168 return null;
169 }
170 return new Sort(fieldSorts.Select(fieldSort => new SortField(fieldSort.FieldName, SortField.FLOAT, !fieldSort.Ascend)).ToArray());
171 }
172
173 private static Filter MakeSearchFilter(Dictionary<string, string> filter)
174 {
175 Filter luceneFilter = null;
176 if (filter != null && filter.Keys.Any())
177 {
178 var booleanQuery = new BooleanQuery();
179 foreach (KeyValuePair<string, string> keyValuePair in filter)
180 {
181 var termQuery = new TermQuery(new Term(keyValuePair.Key, keyValuePair.Value));
182 booleanQuery.Add(termQuery, Occur.MUST);
183 }
184 luceneFilter = new QueryWrapperFilter(booleanQuery);
185 }
186 return luceneFilter;
187 }
188
189 private Query MakeSearchQuery(string content, Analyzer analyzer)
190 {
191 var query = new BooleanQuery();
192 // 总查询参数
193 // 属性查询
194 if (!string.IsNullOrEmpty(content))
195 {
196 QueryParser parser = new MultiFieldQueryParser(Version.LUCENE_30, fieldsToSearch, analyzer);
197 Query queryObj;
198 try
199 {
200 queryObj = parser.Parse(content);
201 }
202 catch (ParseException parseException)
203 {
204 throw new Exception("在FileLibraryIndexSearch中构造Query时出错。", parseException);
205 }
206 query.Add(queryObj, Occur.MUST);
207 }
208 return query;
209 }
210
211 private string GetKeywordsSplitBySpace(string keywords, JiebaForLuceneTokenizer jiebaForLuceneTokenizer)
212 {
213 var result = new StringBuilder();
214
215 var words = jiebaForLuceneTokenizer.Tokenize(keywords);
216
217 foreach (var word in words)
218 {
219 if (string.IsNullOrWhiteSpace(word.Word))
220 {
221 continue;
222 }
223
224 result.AppendFormat("{0} ", word.Word);
225 }
226
227 return result.ToString().Trim();
228 }
229
230 private string ReplaceIndexSensitiveWords(string str)
231 {
232 str = str.Replace("+", "");
233 str = str.Replace("+", "");
234 str = str.Replace("-", "");
235 str = str.Replace("-", "");
236 str = str.Replace("!", "");
237 str = str.Replace("!", "");
238 str = str.Replace("(", "");
239 str = str.Replace(")", "");
240 str = str.Replace("(", "");
241 str = str.Replace(")", "");
242 str = str.Replace(":", "");
243 str = str.Replace(":", "");
244 str = str.Replace("^", "");
245 str = str.Replace("[", "");
246 str = str.Replace("]", "");
247 str = str.Replace("【", "");
248 str = str.Replace("】", "");
249 str = str.Replace("{", "");
250 str = str.Replace("}", "");
251 str = str.Replace("{", "");
252 str = str.Replace("}", "");
253 str = str.Replace("~", "");
254 str = str.Replace("~", "");
255 str = str.Replace("*", "");
256 str = str.Replace("*", "");
257 str = str.Replace("?", "");
258 str = str.Replace("?", "");
259 return str;
260 }
261
262 protected Highlighter GetHighlighter(Formatter formatter, int fragmentSize)
263 {
264 var highlighter = new Highlighter(formatter, new Segment()) { FragmentSize = fragmentSize };
265 return highlighter;
266 }
267 }


几个protected abstract方法,是需要继承的子类来实现的。
其中为了实现搜索结果对命中关键词进行高亮显示,特引用了盘古分词的Highlighter。原则是此处应该是参照盘古分词的源码,自己使用JieBaNet来做实现的,由于工期较紧,直接引用了盘古。
2.我们设计一个IndexSearchResultItem,表示搜索结果的基类。
3.我们来看看具体的实现,先来看全站搜索的SearchService


1 public class IndexSearch : BaseIndexSearch<IndexSearchResultItem>
2 {
3 public IndexSearch()
4 : base(new[] { "IndexTextContent", "Title" })
5 {
6 }
7
8 protected override IndexSearchResultItem CreateIndexSearchResultItem()
9 {
10 return new IndexSearchResultItem();
11 }
12
13 protected override void ModifyIndexSearchResultItem(ref IndexSearchResultItem indexSearchResultItem, string content,
14 int docIndex, Document doc)
15 {
16 //不做修改
17 }
18
19 protected override void ModifySearchFilter(ref Dictionary<string, string> filter)
20 {
21 //不做筛选条件修改
22 }
23 }


是不是非常简单。由于我们此处搜索的是全站,结果展示直接用基类,取出基本字段即可。
4.再列举一个活动的搜索实现。
a)我们首先创建一个活动搜索结果类ActivityIndexSearchResultItem,继承自结果基类IndexSearchResultItem
b)然后创建活动模块的搜索服务:ActivityIndexSearch,同样需要继承BaseIndexSearch,这时候ActivityIndexSearch只需要相对全站搜索修改几个参数即可。


1 public class ActivityIndexSearch: BaseIndexSearch<ActivityIndexSearchResultItem>
2 {
3 public ActivityIndexSearch()
4 : base(new[] { "IndexTextContent", "Title" })
5 {
6 }
7
8 protected override ActivityIndexSearchResultItem CreateIndexSearchResultItem()
9 {
10 return new ActivityIndexSearchResultItem();
11 }
12
13 protected override void ModifyIndexSearchResultItem(ref ActivityIndexSearchResultItem indexSearchResultItem, string content,
14 int docIndex, Document doc)
15 {
16 indexSearchResultItem.ActivityTypes = doc.Get("Tag1");
17 indexSearchResultItem.Url = doc.Get("Tag2");
18 indexSearchResultItem.SourceName = doc.Get("Tag3");
19 indexSearchResultItem.SourceOfficialHotline = doc.Get("Tag4");
20 indexSearchResultItem.SourceUrl = doc.Get("Tag5");
21 indexSearchResultItem.CityId=new Guid(doc.Get("Tag6"));
22 indexSearchResultItem.Address = doc.Get("Tag7");
23 indexSearchResultItem.ActivityDate = doc.Get("Tag8");
24 }
25
26 protected override void ModifySearchFilter(ref Dictionary<string, string> filter)
27 {
28 filter.Add("ModuleType", "活动");
29 }
30 }


筛选条件加上模块=活动,返回结果数据类指定,活动特有字段返回赋值。
业务调用就非常简单了。
全站全文搜索:我们直接new IndexSearch(),然后调用其Search()方法
活动全文搜索:我们直接new ActivityIndexSearch(),然后调用其Search()方法
Search()方法几个参数:
///<param name="content">搜索文本内容</param>
/// <param name="filter">查询内容限制条件,默认为null,不限制条件.</param>
/// <param name="fieldSorts">对字段进行排序</param>
/// <param name="pageIndex">查询结果当前页,默认为1</param>
/// <param name="pageSize">查询结果每页结果数,默认为20</param>
如果我们用软能力而不是用技术能力来区分程序员的好坏 – 是不是有那么点反常和变态。
基于JieBaNet+Lucene.Net实现全文搜索的更多相关文章
- 记一次企业级爬虫系统升级改造(五):基于JieBaNet+Lucene.Net实现全文搜索
实现效果: 上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图: 基本风格是模仿的百度搜索结果,绿色的分页略显小清新. 目前已采集并创建索引的文章约3W多篇,索引文件不算太大,查询速度 ...
- JieBaNet+Lucene.Net
基于JieBaNet+Lucene.Net实现全文搜索 实现效果: 上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图: 基本风格是模仿的百度搜索结果,绿色的分页略显小清新. 目前 ...
- C# 全文搜索Lucene
全文出自:https://blog.csdn.net/huangwenhua5000/article/details/9341751 1 lucene简介1.1 什么是luceneLucene是一个全 ...
- OSChina 的全文搜索设计说明 —— 索引过程
http://www.oschina.net/question/12_71591 言: OSChina 的搜索做得并不好,很久之前一直想在细节方面进行改造,一直也没什么好的思路.但作为整体的结构或许对 ...
- Apache Solr采用Java开发、基于Lucene的全文搜索服务器
http://docs.spring.io/spring-data/solr/ 首先介绍一下solr: Apache Solr (读音: SOLer) 是一个开源.高性能.采用Java开发.基于Luc ...
- lucene全文搜索之三:生成索引字段,创建索引文档(给索引字段加权)基于lucene5.5.3
前言:上一章中我们已经实现了索引器的创建,但是我们没有索引文档,本章将会讲解如何生成字段.创建索引文档,给字段加权以及保存文档到索引器目录 luncene5.5.3集合jar包下载地址:http:// ...
- lucene全文搜索之二:创建索引器(创建IKAnalyzer分词器和索引目录管理)基于lucene5.5.3
前言: lucene全文搜索之一中讲解了lucene开发搜索服务的基本结构,本章将会讲解如何创建索引器.管理索引目录和中文分词器的使用. 包括标准分词器,IKAnalyzer分词器以及两种索引目录的创 ...
- lucene全文搜索之一:lucene的主要功能和基本结构(基于lucene5.5.3)
前言:lucene并不是像solr或elastic那样提供现成的.直接部署可用的系统,而是一套jar包,提供了一些常见语言分词.构建索引和创建搜索器等等功能的API,我们常用到的也就是分词器.索引目录 ...
- lucene全文搜索之四:创建索引搜索器、6种文档搜索器实现以及搜索结果分析(结合IKAnalyzer分词器的搜索器)基于lucene5.5.3
前言: 前面几章已经很详细的讲解了如何创建索引器对索引进行增删查(没有更新操作).如何管理索引目录以及如何使用分词器,上一章讲解了如何生成索引字段和创建索引文档,并把创建的索引文档保存到索引目录,到这 ...
随机推荐
- Flask request接口获取参数
Flask request接口获取参数 request.form.get("key", type=str, default=None) 获取表单数据, request.args ...
- Codeforces Round #554 (Div. 2) D 贪心 + 记忆化搜索
https://codeforces.com/contest/1152/problem/D 题意 给你一个n代表合法括号序列的长度一半,一颗有所有合法括号序列构成的字典树上,选择最大的边集,边集的边没 ...
- POJ3685Matrix(二分套二分)
传送门 题目大意:N*N的矩阵,a[i][j]=i*i+100000*i+j*j-100000*j+i*j,求矩阵中第K小. N<=5*10^4 题解: 打个表,发现每一列从上往下单调递增. 在 ...
- Luogu P4585 [FJOI2015]火星商店问题
颓文化课作业到很晚写篇博客清醒一下 首先我们仔细阅读并猜测了题意之后,就会想到一个暴力的线段树套可持久化0/1Trie的做法,但是它显然是过不去的 由于最近再做线段树分治的题,我们可以想到用线段树分治 ...
- ARC082E ConvexScore(神奇思路)
这题就是拼拼凑凑就出来了. 可能看英文题面容易题意杀(小写大写 \(n,N\)),这里复述一遍:对于每个构成凸多边形的点集(每个点恰好都是凸多边形的顶点,必须是严格的凸多边形,内角严格小于 180 度 ...
- [算法模版]Tarjan爷爷的几种图论算法
[算法模版]Tarjan爷爷的几种图论算法 前言 Tarjan爷爷发明了很多图论算法,这些图论算法有很多相似之处(其中一个就是我都不会).这里会对这三种算法进行简单介绍. 定义 强连通(strongl ...
- js生成条形码
生成条形码 <body> <div> <img id="ma"/> </div> </body> </html&g ...
- 博云 x 某农商行 | 银行信息化运维系统升级的最佳实践
随着银行新一代信息化运维系统建设的推进,应用系统更新换代速度明显提升.数字化转型的发展对银行业务需求的敏捷性提出了越来越高的要求,促进敏捷开发和资源敏捷部署成为大势所趋. 背景 江苏某农村商业银行成立 ...
- 修改设置notepad++默认保存文件格式
设置notepad++保存文件的默认格式 在使用notepad++新建文件保存后,有的时候会有一个all type 然后没有文件扩展名,很不方便 网上有很多帖子都是如下图所示的提示,但是却不生效
- Visual Studio 语法高亮插件推荐
编辑器 GItHub:https://github.com/Art-Stea1th/Enhanced-Syntax-Highlighting Visual Studio Marketplace:htt ...