HtmlAgilityPack + Fizzler
这两天在做个爬虫, 一次任务要下载3万多个页面, 然后从这3万多个页面提取数据.
以前写过两年的类似的东西, 基本都是写正则表达式, 速度快, 就是写正则表达式老费劲了, 目标网页稍微改动一点就要重写正则.
后来我用了 HtmlAgilityPack + Fizzler, 很轻松的就处理了.
昨天, 我找了两个类似 HtmlAgilityPack 的东西:CsQuery 和 AngleSharp
翻了翻它们的API和说明文档, CsQuery 说能实现 jQuery 的 selector 语法, 我试了试,还真是, :even 这个东西也能使用, 但是在 AngleSharp 里就不支持这个了, HtmlAgilityPack 没有试, 应该也不支持.
看 AngleSharp , 它给出了这三个东西的性能对比:
https://github.com/FlorianRappl/AngleSharp/wiki/Performance
从其中列出的对比结果来看, 确实比 CsQuery 和 HtmlAgilityPack 快不少.
于是,我放弃了 CsQuery 的牛逼的 selector 和 HtmlAgilityPack 的口碑, 直接在项目中使用了 AngleSharp.
没想到它是个坑爹货:
今天中午利用吃饭时间, 我运行了这个任务, 回来后发现 这货吃了快 11G 内存, 没看错, 11G! 因为这台电脑总共只有 12G内存!
为留证据, 我又跑了一下, 3分20秒, 占用内存高达 1.8G.
怕冤枉好人, 我把所有自己写的东西都运行了一遍代码分析, 确保没有任何未释放的对象.
结果这货还是这样吃内存.
换成 HtmlAgilityPack 后:
3分20秒的时候, 也不过才 270M 而已. 而且是一边增加,一边释放, 到现在运行了差不多半个小时了, 内存还在 180M 左右徘徊。
以下是用 HtmlAgilityPack 的代码:
1 public override IEnumerable<DIRTY_SCHEDULE> Fetch(string ctx, string url = "") { 2 var doc = new HtmlDocument(); 3 doc.LoadHtml2(ctx); 4 var root = doc.DocumentNode; 5 var trs = root.QuerySelectorAll("#accordion2>.accordion-group>.accordion-heading>table>tbody>tr") 6 .ToList(); 7 for (var i = 0; i < trs.Count(); i = i + 2) { 8 var tr = trs[i]; 9 var tds = tr.QuerySelectorAll("td").ToList(); 10 var entry = new DIRTY_SCHEDULE { 11 CARRIER = tds[0].InnerText.Clear(), 12 ROUTE = tds[1].InnerText.Clear(), 13 VESSEL = tds[2].InnerText.Clear(), 14 VOYAGE = tds[3].InnerText.Clear(), 15 ORGIN = tds[4].InnerText.Clear(), 16 ETD = tds[5].InnerText.Clear().ToDateTime("yyyy-MM-dd", DateTime.Now), 17 DEST = tds[6].InnerText.Clear(), 18 ETA = tds[7].InnerText.Clear().ToDateTime("yyyy-MM-dd", DateTime.Now), 19 TT = tds[8].InnerText.Clear().ToDecimalOrNull(), 20 DIRTY_SCHEDULE_TRANSF = this.FetchTransf(trs[i + 1]).ToList(), 21 SOURCE = url, 22 APP = "Fetcher.Soushipping", 23 }; 24 25 entry.UNQTAG = entry.GetUNQTag(); 26 27 yield return entry; 28 } 29 } 30 31 32 private IEnumerable<DIRTY_SCHEDULE_TRANSF> FetchTransf(HtmlNode tr) { 33 var tbls = tr.QuerySelectorAll("table.widget").ToList(); 34 //第一个列出的是起始地 35 for (var i = 1; i < tbls.Count(); i++) { 36 var rows = tbls[i].QuerySelectorAll("tr").ToList(); 37 if (rows.Count == 3) 38 yield return new DIRTY_SCHEDULE_TRANSF { 39 VESSEL = rows[0].InnerText.Clear(), 40 AT = rows[1].QuerySelector("td").InnerText.Clear(), //rows[1].FirstChild.Text().Trim(), 41 VOYAGE = rows[2].InnerText.Clear(), 42 SEQ = i - 1 43 }; 44 } 45 }
下面是用 AngleSharp 的代码:
1 public override IEnumerable<DIRTY_SCHEDULE> Fetch(string ctx, string url = "") { 2 var dom = DocumentBuilder.Html(ctx); 3 //不支持 even 4 //var trs = dom.QuerySelectorAll("#accordion2 table tbody tr:even"); 5 var trs = dom.QuerySelectorAll("#accordion2>.accordion-group>.accordion-heading>table>tbody>tr"); 6 for (var i = 0; i < trs.Length; i = i + 2) { 7 var tr = trs[i]; 8 var tds = tr.QuerySelectorAll("td"); 9 var entry = new DIRTY_SCHEDULE { 10 CARRIER = tds[0].Text(), 11 ROUTE = tds[1].Text().Trim(), 12 VESSEL = tds[2].Text().Trim(), 13 VOYAGE = tds[3].Text().Trim(), 14 ORGIN = tds[4].Text().Trim(), 15 ETD = tds[5].Text().Trim().ToDateTime("yyyy-MM-dd", DateTime.Now), 16 DEST = tds[6].Text().Trim(), 17 ETA = tds[7].Text().Trim().ToDateTime("yyyy-MM-dd", DateTime.Now), 18 TT = tds[8].Text().Trim().ToDecimalOrNull(), 19 DIRTY_SCHEDULE_TRANSF = this.FetchTransf(trs[i + 1]).ToList(), 20 SOURCE = url, 21 APP = "Fetcher.Soushipping", 22 }; 23 24 entry.UNQTAG = entry.GetUNQTag(); 25 26 yield return entry; 27 } 28 } 29 30 private IEnumerable<DIRTY_SCHEDULE_TRANSF> FetchTransf(IElement tr) { 31 var tbls = tr.QuerySelectorAll("table.widget"); 32 //第一个列出的是起始地 33 for (var i = 1; i < tbls.Length; i++) { 34 var rows = tbls[i].QuerySelectorAll("tr"); 35 if (rows.Length == 3) 36 yield return new DIRTY_SCHEDULE_TRANSF { 37 VESSEL = rows[0].Text().Trim(), 38 AT = rows[1].QuerySelector("td").Text().Trim(), //rows[1].FirstChild.Text().Trim(), 39 VOYAGE = rows[2].Text().Trim(), 40 SEQ = i - 1 41 }; 42 } 43 }
基本一模一样.
看一下 IElement , 这货跟本就没有继承 IDisposable接口, 所以, 也就没有释放不释放这一说.