Windows Phone 7的代码项目帖子分析器






4.96/5 (28投票s)
这是一个 WP7 应用程序,可以分析您最近的帖子,并为您提供有关您在论坛中发帖分布的汇总统计信息。
引言
CP 帖子分析器是一个 Windows Phone 7 应用程序,可以分析任何 CP 成员发布的最后 200 条帖子(涵盖所有论坛、文章、调查等)。您需要开发者许可证才能在 WP7 设备上部署和运行此程序,但如果您没有许可证,仍然可以在模拟器上运行。我已在模拟器和三星 Focus 手机上测试过此应用程序。
警告
由于该应用程序会抓取 CP 网站的 HTML,因此如果网站布局发生变化(在抓取的页面中),该应用程序可能会停止工作。希望这种情况不会太频繁,以免应用维护起来很麻烦。
此应用程序使用极佳且强烈推荐的 HTML Agility Pack 来解析 HTML。
使用方法
我在下方包含了一些截图供您参考,但该应用程序非常简单易用。运行应用程序时,您会看到一个全景视图,第一个页面会提示您输入会员 ID(例如,我的会员 ID 是 20248)。您可以从您的个人资料页面获取会员 ID。由于该应用程序不需要您登录,因此您可以输入任何您想查看的会员 ID。该应用程序会维护您最近使用过的 10 个会员 ID 的历史记录,从而为您节省一些输入(在带有虚拟键盘的这种小型设备上输入并不愉快)。最近的会员 ID 使用隔离存储进行存储,因此会一直保留,直到您卸载该应用程序。
运行获取后,全景视图中的其他三个页面将发挥作用。第一个页面提供您最后 200 条帖子的分类分布。Lounge 属于“Page”类别,但大多数其他论坛,包括技术论坛和一些非技术论坛(如印度论坛),都属于“Forum”。您在文章中发布的任何帖子都会被分类为“Article”,然后您还有“Survey”和“Member”类别(不言而喻)。可能还有我遗漏或未遇到的其他类别,它们暂时会显示为“Unknown”。
下一页是您帖子在论坛中的分布情况,这样您就可以看到您在特定论坛发了多少帖子。由于 Chris 选择只暴露您发布的最后 200 条消息,所以此应用程序的限制也在此。因此,该应用程序基本上会分析您最近的帖子活动。在调试/测试应用程序时,我发现我的大部分帖子都在 Lounge 论坛,直到印度对南非的 ODI 比赛发生,然后我在印度论坛的帖子就像疯了一样占了主导地位。我运行了 Chris 的数据,发现他大部分时间都在 Site Bugs / Suggestions 论坛回答问题(我想他做了一项基本上吃力不讨好的工作)。
全景视图中的最后一页将列出所有 200 条帖子,为您提供近期帖子内容的快速摘要。仅显示主题标题(不显示消息内容)。
该应用程序支持休眠,因此您可以切换到另一个应用程序再回来,您的状态都会被保留。好吧,我想就这些了。如果您能想到一些有用的功能,并且考虑到我所有输入数据都来自 HTML 抓取这一限制,我很乐意尝试为您进行更改(前提是我有时间)。查看更多截图,然后是一个关于技术实现细节的简短部分。
更多截图
实现细节
该应用程序是用 SilverLight 为 WP7 编写的,并且项目尝试使用基本的 MVVM 模型。为了帮助进行数据绑定,我使用了以下几种类型来表示返回的数据。
namespace CPPostsAnalyzerWP7.Models
{
public class PostInfo
{
public string ThreadName { get; set; }
public string DisplayName { get; set; }
public string TimeString { get; set; }
public string ForumName { get; set; }
public ForumType ForumType { get; set; }
}
}
namespace CPPostsAnalyzerWP7.Models
{
public class PostSummaryInfo
{
public string ForumName { get; set; }
public int Count { get; set; }
public ForumType ForumType { get; set; }
}
}
namespace CPPostsAnalyzerWP7.Models
{
public enum ForumType
{
Unknown = 0,
Forum = 1,
Page = 2,
Article = 3,
Survey = 4,
Member = 5
}
}
有一个 PostsFetcher
类负责 HTML 获取和解析,它使用了出色的 HTML Agility Pack 库。
// The code has been artificially line-wrapped to fit within the
// CP article width limits. The source code in the project is formatted
// far more elegantly.
namespace CPPostsAnalyzerWP7.Models
{
public class PostsFetcher
{
private string memberId;
public PostsFetcher(string memberId)
{
this.memberId = memberId.Trim();
}
public event EventHandler<PostInfoEventArgs> PostFetched;
public event EventHandler<FetchCompletedEventArgs> FetchCompleted;
private void FirePostFetched(PostInfoEventArgs e)
{
var handler = this.PostFetched;
if (handler != null)
{
handler(this, e);
}
}
private void FireFetchCompleted(FetchCompletedEventArgs e)
{
var handler = this.FetchCompleted;
if (handler != null)
{
handler(this, e);
}
}
private int nextPage = 1;
private const int maxPage = 4;
public void Fetch()
{
int temp;
if (Int32.TryParse(this.memberId, out temp))
{
LoadNextPageAsync();
}
else
{
FireFetchCompleted(new FetchCompletedEventArgs()
{ Error = new ArgumentException("Invalid memberId.") });
}
}
private void LoadNextPageAsync()
{
HtmlWeb.LoadAsync(String.Format(
"https://codeproject.org.cn/script/Forums/Messages.aspx?fmid={0}&fid=0&pgnum={1}",
this.memberId, nextPage++), HtmlLoaded);
}
private void HtmlLoaded(object sender, HtmlDocumentLoadCompleted e)
{
if (e.Error != null)
{
FireFetchCompleted(new FetchCompletedEventArgs() { Error = e.Error });
return;
}
try
{
ParseHtml(e);
}
catch (Exception ex)
{
FireFetchCompleted(new FetchCompletedEventArgs() { Error = ex });
}
if (nextPage > maxPage)
{
var args = new FetchCompletedEventArgs();
foreach (var item in forumTypeCountMap)
{
args.ForumTypeSummaries.Add(new PostSummaryInfo()
{ ForumType = item.Key, Count = item.Value, ForumName = String.Empty });
}
foreach (var item in forumPostsCountMap)
{
args.PostSummaries.Add(new PostSummaryInfo()
{ ForumType = ForumType.Unknown, Count = item.Value, ForumName = item.Key });
}
FireFetchCompleted(args);
}
else
{
LoadNextPageAsync();
}
}
private Dictionary<ForumType, int> forumTypeCountMap = new Dictionary<ForumType, int>();
private Dictionary<string, int> forumPostsCountMap = new Dictionary<string, int>();
private void ParseHtml(HtmlDocumentLoadCompleted e)
{
var tableNode = e.Document.DocumentNode.DescendantNodes().Where(
n => n.Name.ToLower() == "table"
&& n.Attributes["cellspacing"] != null
&& n.Attributes["cellspacing"].Value == "4").FirstOrDefault();
if (tableNode == null)
return;
var trNodes = tableNode.Descendants("tr");
foreach (var tdNode in trNodes)
{
var aNode = tdNode.Descendants("a").FirstOrDefault();
if (aNode == null)
continue;
PostInfo postInfo = new PostInfo();
postInfo.ThreadName = aNode.InnerText.Trim();
var divNodes = tdNode.Descendants("div").Where(
n => n.Attributes["class"] != null
&& n.Attributes["class"].Value == "small-text subdue");
if (divNodes.Count() == 2)
{
var divNodesArray = divNodes.ToArray();
string nameAndTime = divNodesArray[0].InnerText.Trim();
int byPos = nameAndTime.IndexOf("by");
int atPos = nameAndTime.LastIndexOf("at");
if (byPos == -1 || atPos == -1)
continue;
postInfo.DisplayName = nameAndTime.Substring(byPos + 2, atPos - byPos - 2).Trim();
postInfo.TimeString = nameAndTime.Substring(atPos + 2).Trim();
string[] forumLines = divNodesArray[1].InnerText.Trim().Split(
'\r', '\n').Where(s => !String.IsNullOrEmpty(s.Trim('\r', '\n'))).ToArray();
if (forumLines.Length < 1 || forumLines.Length > 2)
continue;
string forumNameInput = forumLines.Length == 1 ? "(Untitled)" : forumLines[0];
string forumTypeInput = forumLines.Length == 1 ? forumLines[0] : forumLines[1];
postInfo.ForumName = forumNameInput.Trim();
int leftBracketPost = forumTypeInput.IndexOf('(');
int rightBracketPost = forumTypeInput.IndexOf(')');
if (leftBracketPost == -1 || rightBracketPost == -1)
continue;
ForumType forumType = ForumType.Unknown;
try
{
string enumLine = forumTypeInput.Substring(
leftBracketPost + 1, rightBracketPost - leftBracketPost - 1).Trim();
forumType = (ForumType)Enum.Parse(typeof(ForumType), enumLine.Trim(), true);
}
catch (ArgumentException)
{
}
postInfo.ForumType = forumType;
}
if (!forumTypeCountMap.ContainsKey(postInfo.ForumType))
{
forumTypeCountMap[postInfo.ForumType] = 1;
}
else
{
forumTypeCountMap[postInfo.ForumType]++;
}
if (!forumPostsCountMap.ContainsKey(postInfo.ForumName))
{
forumPostsCountMap[postInfo.ForumName] = 1;
}
else
{
forumPostsCountMap[postInfo.ForumName]++;
}
FirePostFetched(new PostInfoEventArgs() { PostInfo = postInfo });
}
}
}
}
只是基本的 HTML 解析而已——正如您所见,我不得不做出一些高风险的假设(因为其中一些表格没有关联可识别的 ID)。另外,我使用 Enum.Parse
的原因是 WP7 的 mscorlib 没有 TryParse
!
Tombstoning
正如我之前提到的,该应用程序支持休眠,我需要做的一件事就是确保所有我想恢复的类型都完全兼容序列化(这基本上意味着它们必须具有公共属性等)。这是处理休眠的代码。
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
App.MainViewModel.SaveState();
base.OnNavigatedFrom(e);
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
App.MainViewModel.RetrieveState();
base.OnNavigatedTo(e);
}
这是视图模型中的实现。
public void SaveState()
{
var appService = PhoneApplicationService.Current;
appService.State.Clear();
appService.State["MemberId"] = this.MemberId;
appService.State["CanFetch"] = this.CanFetch;
if (this.CanFetch)
{
appService.State["MemberName"] = this.MemberName;
appService.State["Results"] = this.Results;
appService.State["ForumTypeSummaries"] = this.ForumTypeSummaries;
appService.State["PostSummaries"] = this.PostSummaries;
}
}
public void RetrieveState()
{
var appService = PhoneApplicationService.Current;
if (appService.State.ContainsKey("MemberId"))
{
this.MemberId = (string)appService.State["MemberId"];
}
if (appService.State.ContainsKey("CanFetch"))
{
this.CanFetch = (bool)appService.State["CanFetch"];
}
if (!this.CanFetch)
{
this.CanFetch = true;
return;
}
if (appService.State.ContainsKey("MemberName"))
{
this.MemberName = (string)appService.State["MemberName"];
}
if (appService.State.ContainsKey("Results"))
{
this.Results.Clear();
foreach (PostInfo item in (IEnumerable<PostInfo>)appService.State["Results"])
{
this.Results.Add(item);
}
}
if (appService.State.ContainsKey("ForumTypeSummaries"))
{
this.ForumTypeSummaries.Clear();
foreach (PostSummaryInfo item in
(IEnumerable<PostSummaryInfo>)appService.State["ForumTypeSummaries"])
{
this.ForumTypeSummaries.Add(item);
}
}
if (appService.State.ContainsKey("PostSummaries"))
{
this.PostSummaries.Clear();
foreach (PostSummaryInfo item in
(IEnumerable<PostSummaryInfo>)appService.State["PostSummaries"])
{
this.PostSummaries.Add(item);
}
}
}
我只是顺带提一下,我曾尝试序列化整个视图模型但都失败了,从一开始就注定要失败。我最终放弃了,决定只序列化我具体想要的东西。我想我本可以成功,但这会浪费太多精力而没有任何回报,除了可能一些虚假提升的自尊,而我对此并不在意!
隔离存储
收件人列表通过隔离存储进行保存,因此您最近的 10 次搜索将被记住。
public MainViewModel()
{
IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication();
if (!store.FileExists(configPath))
return;
using (StreamReader reader = new StreamReader(
new IsolatedStorageFileStream(configPath, FileMode.Open, store)))
{
var lines = reader.ReadToEnd().Trim().Split('\r', '\n').Where(
s => !String.IsNullOrEmpty(s.Trim('\r', '\n'))).ToList();
lines.ForEach(line => recentMemberIds.Add(line));
}
}
private void SaveConfig()
{
IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication();
string baseDirectory = System.IO.Path.GetDirectoryName(configPath);
if (!store.DirectoryExists(baseDirectory))
{
store.CreateDirectory(baseDirectory);
}
using (StreamWriter writer = new StreamWriter(
new IsolatedStorageFileStream(configPath, FileMode.Create, store)))
{
foreach (var entry in recentMemberIds)
{
writer.WriteLine(entry);
}
}
}
内置支持使得使用隔离存储非常容易。最初,我确实考虑过保存帖子然后累积它们,以便我可以分析超过 200 条帖子,但这假设用户会足够频繁地运行它,以至于不会丢失帖子(这很难强制执行,实际上是不可能的)。所以我就放弃了,认为 200 条足够了。有一天,如果 Chris 将该限制增加到 1000 条,我将在那时更新该应用程序。
结论
好了,这就是全部内容。它已经经过了相当彻底的测试,但可能存在问题,尤其是在其他手机型号上。错误处理有点隐蔽,所以如果遇到任何错误,它不会崩溃,但也不会告知您。您可以尝试再次运行 Fetch,或者关闭并重新运行应用程序(尽管我从未不得不这样做)。
请随时提交您的反馈、批评和建议。
历史
- 2011 年 1 月 23 日 - 文章首次发布。