记一次企业级爬虫系统升级改造:文本分析与数据建模规则化处理

Posted 彩色铅笔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次企业级爬虫系统升级改造:文本分析与数据建模规则化处理相关的知识,希望对你有一定的参考价值。

 

SupportYun当前状况:

  博主的SupportYun系统基本已经完成第一阶段预期的底层服务开发啦~~~自己小嘚瑟一下。

  有对该系统历史背景与功能等不明白的可先看该系列的第1/2篇文章:

    1.记一次企业级爬虫系统升级改造(一)

    2.记一次企业级爬虫系统升级改造(二):基于AngleSharp实现的抓取服务

  再贴一次博主对这个系统的简要整体规划图:

  博主第一阶段主要会做独立的爬虫服务+数据规则化引擎以及内容归类处理这一块。

  再简单粗暴一点就是把大量类似下图的网页抓取,然后分析数据,得到活动城市、活动地址、热线、活动日期、活动分类以及内容纯文本的数据提供给业务部门。 

  

 

  本篇文章将主要讲博主是怎么在抓取到网页数据后做通用数据规则处理的。

  先来看看当前的项目代码结构图:

  

  不错,你没有看错,短短三周时间,博主一个人已经写了这么多代码了~~想想手都累啊!心也累~

其实,每一天的生活状态,就决定了五年甚至十年后的状态,日子是一天一天过的,积累的多了,就是一生。

 

唯美的偶遇:JieBa.Net

  博主首先想实现的一个功能是,分析文章或活动文本的内容语义,将其按我们设定的几大模块分类。

  具体怎么从抓取的html片段中提取纯文本,博主将在下面讲到。

  比如说这么一篇文章:为什么美国精英中学距离中国孩子越来越远?

  我们一读或者只看title,基本就能确定,它应该属于【海外教育】这个大类,当然,也可以牵强的给他一个【海外生活】的标签。这是我们人工很容易处理的逻辑。那么要是爬取到大量的这种网页数据,我们的代码应该怎么准确的来进行归类处理呢?

  来看看博主的多番思考后的最终实现方案:

    1.基础规则化服务涉及到数据清理等内容,后面介绍。

    2.xml规则集是楼主找业务部门以及自己整理的一个数据集,存储各大类别的主要热门、经典关键词。

    3.关键词提取算法,这儿博主分析了TF-IDF算法与TextRank算法,最终采用的TextRank算法,基于JieBa.Net实现。

  先来说说为什么要是用JieBa分词吧。

  博主的下一阶段就会涉及到全文索引,必然会引用分词技术,以前都是轻车熟路的使用Lucene.Net+盘古分词,代码都不用多写...毕竟存货多多!

  早说了,这一次打算吃点素的,不能老啃老本...我们是软件工程师,不是码农!!!

  鬼使神差的就搜到了JieBa.Net,还发现几个bug,提了GitHub...说来也是心累,但是作者开源也是多辛苦的,得多多支持!

  由于当时作者不能及时的更新NuGet上的包,所以自己本机改了一些就开始用了。  

 1     /// <summary>
 2     /// 关键词提取
 3     /// </summary>
 4     public class KeyWordExtractor
 5     {
 6         /// <summary>
 7         /// TF-IDF算法 关键词提取
 8         /// </summary>
 9         /// <returns></returns>
10         public List<string> TFIDFExtractor(string content, int count = 10)
11         {
12             var extractor = new TFIDFKeywordExtractor();
13             var keywords = extractor.ExtractTags(content, count, Constants.NounAndVerbPos);
14             return keywords.ToList();
15         }
16 
17         /// <summary>
18         /// TextRank算法 关键词提取
19         /// </summary>
20         /// <returns></returns>
21         public List<string> TextRankExtractor(string content, int count = 10)
22         {
23             var extractor = new TextRankKeywordExtractor();
24             var keywords = extractor.ExtractTags(content, count, Constants.NounAndVerbPos);
25             return keywords.ToList();
26         }
27     }

  第12与23行本来直接使用JieBa封装的方法就可以的,上面说了由于NuGet未能及时更新,自己把有bug的逻辑实现了一遍:

 1     /// <summary>
 2     /// TF-IDF算法 关键词提取器
 3     /// </summary>
 4     public class TFIDFKeywordExtractor : BaseKeywordExtractor
 5     {
 6         private static readonly string DefaultIdfFile = Path.Combine(ConfigurationManager.AppSettings["JiebaConfigFileDir"] ?? HttpContext.Current.Server.MapPath("/Resources/"), "idf.txt");
 7        private static readonly int DefaultWordCount = 20;
 8 
 9         private JiebaSegmenter Segmenter { get; set; }
10         private PosSegmenter PosSegmenter { get; set; }
11         private IDFLoader Loader { get; set; }
12 
13         private IDictionary<string, double> IdfFreq { get; set; }
14         private double MedianIdf { get; set; }
15 
16         public TFIDFKeywordExtractor(JiebaSegmenter segmenter = null)
17         {
18             Segmenter = segmenter.IsNull() ? new JiebaSegmenter() : segmenter;
19             PosSegmenter = new PosSegmenter(Segmenter);
20             SetStopWords(StopWordsIdfFile);
21             if (StopWords.IsEmpty())
22             {
23                 StopWords.UnionWith(DefaultStopWords);
24             }
25 
26             Loader = new IDFLoader(DefaultIdfFile);
27             IdfFreq = Loader.IdfFreq;
28             MedianIdf = Loader.MedianIdf;
29         }
30 
31         public void SetIdfPath(string idfPath)
32         {
33             Loader.SetNewPath(idfPath);
34             IdfFreq = Loader.IdfFreq;
35             MedianIdf = Loader.MedianIdf;
36         }
37 
38         private IEnumerable<string> FilterCutByPos(string text, IEnumerable<string> allowPos)
39         {
40             var posTags = PosSegmenter.Cut(text).Where(p => allowPos.Contains(p.Flag));
41             return posTags.Select(p => p.Word);
42         }
43 
44         private IDictionary<string, double> GetWordIfidf(string text, IEnumerable<string> allowPos)
45         {
46             IEnumerable<string> words = null;
47             if (allowPos.IsNotEmpty())
48             {
49                 words = FilterCutByPos(text, allowPos);
50             }
51             else
52             {
53                 words = Segmenter.Cut(text);
54             }
55 
56             var freq = new Dictionary<string, double>();
57             foreach (var word in words)
58             {
59                 var w = word;
60                 if (string.IsNullOrEmpty(w) || w.Trim().Length < 2 || StopWords.Contains(w.ToLower()))
61                 {
62                     continue;
63                 }
64                 freq[w] = freq.GetDefault(w, 0.0) + 1.0;
65             }
66             var total = freq.Values.Sum();
67             foreach (var k in freq.Keys.ToList())
68             {
69                 freq[k] *= IdfFreq.GetDefault(k, MedianIdf) / total;
70             }
71 
72             return freq;
73         }
74 
75         public override IEnumerable<string> ExtractTags(string text, int count = 20, IEnumerable<string> allowPos = null)
76         {
77             if (count <= 0) { count = DefaultWordCount; }
78 
79             var freq = GetWordIfidf(text, allowPos);
80             return freq.OrderByDescending(p => p.Value).Select(p => p.Key).Take(count);
81         }
82 
83         public override IEnumerable<WordWeightPair> ExtractTagsWithWeight(string text, int count = 20, IEnumerable<string> allowPos = null)
84         {
85             if (count <= 0) { count = DefaultWordCount; }
86 
87             var freq = GetWordIfidf(text, allowPos);
88             return freq.OrderByDescending(p => p.Value).Select(p => new WordWeightPair()
89             {
90                 Word = p.Key,
91                 Weight = p.Value
92             }).Take(count);
93         }
94     }
  1     /// <summary>
  2     /// TextRank算法 关键词提取器
  3     /// </summary>
  4     public class TextRankKeywordExtractor : BaseKeywordExtractor
  5     {
  6         private static readonly IEnumerable<string> DefaultPosFilter = new List<string>()
  7         {
  8             "n", "ng", "nr", "nrfg", "nrt", "ns", "nt", "nz", "v", "vd", "vg", "vi", "vn", "vq"
  9         };
 10 
 11         private JiebaSegmenter Segmenter { get; set; }
 12         private PosSegmenter PosSegmenter { get; set; }
 13 
 14         public int Span { get; set; }
 15 
 16         public bool PairFilter(Pair wp)
 17         {
 18             return DefaultPosFilter.Contains(wp.Flag)
 19                    && wp.Word.Trim().Length >= 2
 20                    && !StopWords.Contains(wp.Word.ToLower());
 21         }
 22 
 23         public TextRankKeywordExtractor()
 24         {
 25             Span = 5;
 26 
 27             Segmenter = new JiebaSegmenter();
 28             PosSegmenter = new PosSegmenter(Segmenter);
 29             SetStopWords(StopWordsIdfFile);
 30             if (StopWords.IsEmpty())
 31             {
 32                 StopWords.UnionWith(DefaultStopWords);
 33             }
 34         }
 35 
 36         public override IEnumerable<string> ExtractTags(string text, int count = 20, IEnumerable<string> allowPos = null)
 37         {
 38             var rank = ExtractTagRank(text, allowPos);
 39             if (count <= 0) { count = 20; }
 40             return rank.OrderByDescending(p => p.Value).Select(p => p.Key).Take(count);
 41         }
 42 
 43         public override IEnumerable<WordWeightPair> ExtractTagsWithWeight(string text, int count = 20, IEnumerable<string> allowPos = null)
 44         {
 45             var rank = ExtractTagRank(text, allowPos);
 46             if (count <= 0) { count = 20; }
 47             return rank.OrderByDescending(p => p.Value).Select(p => new WordWeightPair()
 48             {
 49                 Word = p.Key, Weight = p.Value
 50             }).Take(count);
 51         }
 52 
 53         #region Private Helpers
 54 
 55         private IDictionary<string, double> ExtractTagRank(string text, IEnumerable<string> allowPos)
 56         {
 57             if (allowPos.IsEmpty())
 58             {
 59                 allowPos = DefaultPosFilter;
 60             }
 61 
 62             var g = new UndirectWeightedGraph();
 63             var cm = new Dictionary<string, int>();
 64             var words = PosSegmenter.Cut(text).ToList();
 65 
 66             for (var i = 0; i < words.Count(); i++)
 67             {
 68                 var wp = words[i];
 69                 if (PairFilter(wp))
 70                 {
 71                     for (var j = i + 1; j < i + Span; j++)
 72                     {
 73                         if (j >= words.Count)
 74                         {
 75                             break;
 76                         }
 77                         if (!PairFilter(words[j]))
 78                         {
 79                             continue;
 80                         }
 81 
 82                         var key = wp.Word + "$" + words[j].Word;
 83                         if (!cm.ContainsKey(key))
 84                         {
 85                             cm[key] = 0;
 86                         }
 87                         cm[key] += 1;
 88                     }
 89                 }
 90             }
 91 
 92             foreach (var p in cm)
 93             {
 94                 var terms = p.Key.Split(\'$\');
 95                 g.AddEdge(terms[0], terms[1], p.Value);
 96             }
 97 
 98             return g.Rank();
 99         }
100 
101         #endregion
102     }
 1     /// <summary>
 2     /// 关键词提取器基类
 3     /// 基于JieBa分词
 4     /// </summary>
 5     public abstract class BaseKeywordExtractor
 6     {
 7         protected static readonly string StopWordsIdfFile = Path.Combine(ConfigurationManager.AppSettings["JiebaConfigFileDir"] ?? HttpContext.Current.Server.MapPath("/Resources/"), "stopwords.txt");
 8 
 9         protected static readonly List<string> DefaultStopWords = new List<string>()
10         {
11             "the", "of", "is", "and", "to", "in", "that", "we", "for", "an", "are",
12             "by", "be", "as", "on", "with", "can", "if", "from", "which", "you", "it",
13             "this", "then", "at", "have", "all", "not", "one", "has", "or", "that"
14         };
15 
16         protected virtual ISet<string> StopWords { get; set; }
17 
18         public void SetStopWords(string stopWordsFile)
19         {
20             StopWords = new HashSet<string>();
21             var path = Path.GetFullPath(stopWordsFile);
22             if (File.Exists(path))
23             {
24                 var lines = File.ReadAllLines(path);
25                 foreach (var line in lines)
26                 {
27                     StopWords.Add(line.Trim());
28                 }
29             }
30         }
31 
32         public abstract IEnumerable<string> ExtractTags(string text, int count = 20, IEnumerable<string> allowPos = null);
33         public abstract IEnumerable<WordWeightPair> ExtractTagsWithWeight(string text, int count = 20, IEnumerable<string> allowPos = null);
34     }

  绝大部分都是JieBa.NET的源码,大家也可以去github支持一下作者~~~ 

作为一枚北漂,如果要选择平平淡淡的生活,确实不如早点回老家谋求一份稳定的饭碗,还能常伴家人左右。

 

细说数据规则化引擎服务:

  1.创建一个windows服务,设置5分钟自动轮询启动执行数据规则化引擎。

  

 1     public partial class Service1 : ServiceBase
 2     {
 3         private RuleEngineService.RuleEngineService ruleEngineService = new RuleEngineService.RuleEngineService();
 4 
 5         public Service1()
 6         {
 7             InitializeComponent();
 8         }
 9 
10         protected override void OnStart(string[] args)
11         {
12             try
13             {
14                 EventLog.WriteEntry("【Support云数据规则化引擎服务启动】");
15                 CommonTools.WriteLog("【Support云数据规则化引擎服务启动】");
16 
17                 XmlConfigurator.Configure();
18 
19                 Timer timer = new Timer();
20                 // 循环间隔时间(默认5分钟)
21                 timer.Interval = StringHelper.StrToInt(ConfigurationManager.AppSettings["TimerInterval"].ToString(), 300) * 1000;
22                 // 允许Timer执行
23                 timer.Enabled = true;
24                 // 定义回调
25                 timer.Elapsed += new ElapsedEventHandler(TimedTask);
26                 // 定义多次循环
27                 timer.AutoReset = true;
28             }
29             catch (Exception ex)
30             {
31                 CommonTools.WriteLog("【服务运行 OnStart:Error" + ex + "");
32             }
33         }
34 
35         private void TimedTask(object source, ElapsedEventArgs e)
36         {
37             System.Threading.ThreadPool.QueueUserWorkItem(delegate
38 记一次WMS的系统改造-分析问题

项目实践记一次对后端服务进行跨域改造和HTTPS升级的探究和实践

项目实践记一次对后端服务进行跨域改造和HTTPS升级的探究和实践

项目实践记一次对后端服务进行跨域改造和HTTPS升级的探究和实践

用例建模 Use Case Modeling

记一次 .NET 某企业 ERP网站系统 崩溃分析