Code Project 论坛分析器:找出您有多少不必要的生活!






4.92/5 (39投票s)
这是一个非官方的 Code Project 应用程序,可以分析一系列帖子来检索单个成员的发帖统计信息。
引言
这是一个非官方的 Code Project 应用程序,可以分析一系列帖子来检索单个成员的发帖统计信息。与我其他的 Code Project 应用程序(以及 John 和 Luc 等人的应用程序)一样,此应用程序使用 HTML 抓取和解析来提取所需信息。因此,站点布局/CSS 的任何更改都可能破坏此应用程序的功能。在 Code Project 提供允许公开此数据的官方 Web 服务之前,没有解决方法。
使用应用程序
我有一个硬编码的论坛列表,显示在一个组合框中。我选择了更重要的论坛以及发帖量相对可观的论坛。您还可以选择要获取和分析的帖子数量。该应用程序目前支持获取 1,000 篇、5,000 篇或 10,000 篇帖子。超过 10,000 篇是不可靠的,因为您会开始遇到高负载的 Code Project 数据库服务器的影响,这意味着超时和丢失页面。这不会破坏应用程序,但应用程序将被迫跳过页面,从而导致统计准确性降低。即使是 10,000 篇帖子,在像 Bugs/Suggestions 或 C++/CLI 这样的论坛上,您仍然会遇到这种情况,因为一些旧页面中的帖子包含格式错误的 HTML,这会破坏 HTML 解析器。用户界面底部有一个状态日志,会列出此类解析错误,并在分析结束时告诉您跳过了多少帖子。
CP 对允许帖子中的 HTML 进行了更严格的检查,因此随着时间的推移,这种情况应该会减少。分析完成后,您可以使用“**导出**”功能将结果保存为 CSV 文件。现在可以在 Excel 中打开此 CSV 文件进行进一步的分析和统计图表绘制。
实用技巧
如果将鼠标悬停在显示名称上,它会突出显示该显示名称,并且鼠标光标会变成一只手。这意味着您可以单击显示名称在默认浏览器中打开用户的 CP 个人资料,即使在分析进行中也可以这样做。
导出到 CSV 和外国语言的成员名称
它应该会根据您当前的区域设置正确选择逗号分隔符(感谢 Mika Wendelius 帮助我正确处理这一点),但我没有将文件保存为 Unicode。因为 Excel 在处理它时似乎遇到了问题,将其视为一个大单列(而不是 3 个独立的列)。所以目前我使用的是 Encoding.Default
,这比不使用任何编码要好一点,但如果任何成员显示名称包含 Unicode 字符,您可能会在 Excel 中遇到奇怪的显示。我还没有找到解决方法,目前也不知道是否要花时间研究修复。如果您知道如何解决这个问题,我将非常感谢您的任何建议。
实现细节
获取网站所有数据的主要类是 ForumAnalyzer
类。它使用了出色的 **HtmlAgilityPack** 进行 HTML 解析。以下是该类中一些更有趣的 METH ODS。
private void InitMaxPosts()
{
string html = GetHttpPage(GetFetchUrl(1), this.timeOut);
HtmlDocument document = new HtmlDocument();
document.LoadHtml(html);
HtmlNode trNode = document.DocumentNode.SelectNodes(
"//tr[@class='forum-navbar']").FirstOrDefault();
if (trNode != null)
{
if (trNode.ChildNodes.Count > 2)
{
var node = trNode.ChildNodes[2];
string data = node.InnerText;
int start = data.IndexOf("of");
if (start != -1)
{
int end = data.IndexOf('(', start);
if (end != -1)
{
if (end - start - 2 > 0)
{
var extracted = data.Substring(start + 2, end - start - 2);
Int32.TryParse(extracted.Trim(),
NumberStyles.AllowThousands,
CultureInfo.InvariantCulture, out maxPosts);
}
}
}
}
}
}
这用于确定论坛中帖子的最大数量。由于页面是动态的,如果您尝试获取超过最后一页的页面,不会出错,但会浪费时间和带宽,并会扰乱统计数据。因此,确保我们知道可以安全获取的最大页数非常重要。
public ICollection<Member> FetchPosts(int from)
{
if (from > maxPosts)
{
throw new ArgumentOutOfRangeException("from");
}
string html = GetHttpPage(GetFetchUrl(from), this.timeOut);
HtmlDocument document = new HtmlDocument();
document.LoadHtml(html);
Collection<Member> members = new Collection<Member>();
foreach (HtmlNode tdNode in document.DocumentNode.SelectNodes(
"//td[@class='Frm_MsgAuthor']"))
{
if (tdNode.ChildNodes.Count > 0)
{
var aNode = tdNode.ChildNodes[0];
int id;
if (aNode.Attributes.Contains("href")
&& TryParse(aNode.Attributes["href"].Value, out id))
{
members.Add(new Member(id, aNode.InnerText));
}
}
}
return members;
}
这是提取帖子数据的地方。它利用了 Code Project 使用的 **Frm_MsgAuthor** CSS 类。现在既然我在这里提到了这一点,我敢打赌墨菲定律将会显现,Chris 会随意重命名该类。请注意,此类只会以大块的形式返回帖子数据,具体分析则取决于调用者。我在应用程序的视图模型中执行此操作。这个决定可能会被一些人质疑,他们可能会认为应该有一个单独的包装器来完成计算,而视图模型只需访问该数据。如果是这样,是的,他们可能是对的,但对于这样一个简单的应用程序,我选择了简洁性而不是设计上的纯粹性。
使用 ForumAnalyzer
类的代码是从后台工作线程调用的,完成后,会生成第二个后台工作线程来对结果进行排序。我还使用了 Task Parallel Library 的 Parallel.For
,这给了我显著的速度提升。最初运行在我的连接上(15 Mbps,提升到 24 Mbps)需要 8-9 分钟,但一旦我添加了 Parallel.For
,这个时间就缩短到一分钟多一点。从大约 8 分钟缩短到 1 分钟,这非常令人印象深刻!不过,有一些副作用,我将在下面的代码列表之后讨论。
private void Fetch()
{
canFetch = false;
canExport = false;
this.logs.Clear();
var dispatcher = Application.Current.MainWindow.Dispatcher;
Stopwatch stopWatch = new Stopwatch();
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += (sender, e) =>
{
ForumAnalyzer analyzer = new ForumAnalyzer(this.SelectedForum);
dispatcher.Invoke((Action)(() =>
{
this.TimeElapsed = TimeSpan.FromSeconds(0).ToString(timeSpanFormat);
this.Total = 0;
this.results.Clear();
AddLog(new LogInfo("Started fetching posts..."));
}));
Dictionary<int, MemberPostInfo> results =
new Dictionary<int, MemberPostInfo>();
stopWatch.Start();
ParallelOptions options = new ParallelOptions()
{ MaxDegreeOfParallelism = 8 };
Parallel.For(0, Math.Min((int)(PostCount)this.PostsToFetch,
analyzer.MaxPosts) / postsPerPage, options, (i) =>
{
ICollection<Member> members = null;
int trials = 0;
while (members == null && trials < 5)
{
try
{
members = analyzer.FetchPosts(i * postsPerPage + 1);
}
catch
{
trials++;
}
}
if (members == null)
{
dispatcher.Invoke((Action)(() =>
{
AddLog(new LogInfo(
"Http connection failure", i, postsPerPage));
}));
return;
}
if (members.Count < postsPerPage)
{
dispatcher.Invoke((Action)(() =>
{
AddLog(new LogInfo(
"Html parser failure", i, postsPerPage - members.Count));
}));
}
lock (results)
{
foreach (var member in members)
{
if (results.ContainsKey(member.Id))
{
results[member.Id].PostCount++;
}
else
{
results[member.Id] = new MemberPostInfo()
{ Id = member.Id, DisplayName = member.DisplayName,
PostCount = 1 };
dispatcher.Invoke((Action)(() =>
{
this.results.Add(results[member.Id]);
}));
}
}
dispatcher.Invoke((Action)(() =>
{
this.Total += members.Count;
this.TimeElapsed = stopWatch.Elapsed.ToString(timeSpanFormat);
}));
}
});
};
worker.RunWorkerCompleted += (s, e) =>
{
stopWatch.Stop();
BackgroundWorker sortWorker = new BackgroundWorker();
sortWorker.DoWork += (sortSender, sortE) =>
{
var temp = this.results.OrderByDescending(
ks => ks.PostCount).ToArray();
dispatcher.Invoke((Action)(() =>
{
AddLog(new LogInfo("Sorting results..."));
foreach (var item in temp)
{
this.results.Remove(item);
this.results.Add(item);
}
}));
};
sortWorker.RunWorkerCompleted += (sortSender, sortE) =>
{
AddLog(new LogInfo("Task completed!"));
canFetch = true;
canExport = true;
CommandManager.InvalidateRequerySuggested();
};
sortWorker.RunWorkerAsync();
};
worker.RunWorkerAsync();
}
当我添加 Parallel.For
时,我注意到的第一件事是错误和超时数量显著增加,以至于结果几乎没有用。发生的情况是我在与 CP 内置的洪水保护系统对抗,我意识到如果我并行启动太多连接,这根本行不通。经过反复试验,我最终将并发的最大级别降低到 8,这给了我最好的结果。巧合的是,我有 4 个核心带有超线程,也就是 8 个虚拟 CPU - 所以这对我来说很完美。请注意,这纯属巧合,我不得不降低并行度的原因是 CP 的洪水预防系统,而不是我拥有的核心数量。
使用 Parallel.For
的一个主要副作用是,我失去了按顺序获取页面的能力。如果我没有使用并行循环,我可以检测到 HTML 解析错误,然后跳过那个帖子,继续处理下一个帖子。但对于并发循环,如果遇到错误,我被迫跳过该页面的其余部分。当然,并非不可能通过启动一个侧任务来正确处理这种情况,该任务将仅获取被跳过的帖子(不包括格式错误的帖子),但这大大增加了代码的复杂性。我决定可以接受 10,000 篇帖子中丢失 50-100 篇。这低于 1% 的准确性偏差,我认为对于该应用程序来说是可以接受的。
好了,就是这些了。感谢阅读本文并尝试该应用程序。一如既往,我非常感谢任何和所有的反馈、批评和评论。
参考文献
- Html Agility Pack - 太棒的库了!无与伦比!
致谢
感谢以下 CP 用户帮助测试应用程序!非常非常感激。我只列出了前 3 位,但还有很多人也提供了帮助。所以感谢他们所有人,我为没有一一列出所有人表示歉意(我没想到会有这么多人如此乐于助人)!
历史
- 2011 年 3 月 26 日 - 文章首次发布