libHN 4.2.0

libHN 4.2.0

测试已测试
Lang语言 Obj-CObjective C
许可证 MIT
发布最后发布2016 年 10 月

Ben Gordon 维护。



libHN 4.2.0

  • Ben Gordon

为您的 iOS/Mac 应用添加 HackerNews 的最权威的 Cocoa 框架。这个精巧库包括抓取帖子(包括按热门、提问、新帖子、工作、最佳排序)、评论、登录和提交新帖子/评论等功能!


目录

入门指南

安装 libHN 非常简单。首先,将这个仓库中的顶级 libHN 类 文件夹中的所有类添加到您的应用中。完成了吗?很好。现在,只需在您的任何控制器、类或视图中使用 libHN 的代码中将 #import "libHN.h"

要添加的类

  • libHN.h
  • HNManager.{h,m}
  • HNUtilities.{h,m}
  • HNWebService.{h,m}
  • HNPost.{h,m}
  • HNComment.{h,m}
  • HNUser.{h,m}
  • HNCommentLink.{h,m}

CocoaPods

如果您喜欢使用 CocoaPods 进行依赖管理,那么这里也有一个 .podspec。只需在 Podfile 中添加以下行,并安装您的 pods,即可开始轻松使用 libHN。

pod 'libHN'

JSON 配置

4.0.1+ 版本可用

hn.json 是为了解决两个大烦恼而开发的。第一个是 HN 倾向于频繁更改其标记语言,这对这个库来说是个坏消息,因为这是一个爬虫。所以,以前的方法是更新扫描器中要查找的文本,将其序列化到帖子、评论等的属性中。另一个主要烦恼是更改后,苹果平均需要一周的时间才能将更新上线。这意味着处理一个标记更改就需要一周的时间来更新商店中的应用。这是不可维护的。所以这里是解决方案。

进入 JSON 配置!

JSON配置是一个JSON文件,它描述了如何根据HTML标记构建帖子和评论对象,以及如何处理回复和提交新帖子以及其他网络调用所需的额外信息。此JSON文件随库预先打包,但每次当HNManager初始化时也会从互联网上获取。这意味着每次HN更改标记时,只需对hn.json文件进行简单的pull-request就可以修复所有使用此库的应用程序的问题——无需更新App Store。

变更通知

当从互联网上获取新的JSON配置并将其与当前应用程序正在使用的配置进行比较时,它将保存新的配置并发送一个NSNotification,任何使用帖子或评论的任何内容都应该重新加载数据以使用新的配置。通知的名称为kHNShouldReloadDataFromConfiguration

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(someSelector) name:kHNShouldReloadDataFromConfiguration object:nil];

分解

hn.json配置包含4个顶级键值对

  • 帖子
  • 评论
  • 回复
  • 提交

帖子处理如何构建HNPost对象的数组。以下是该对象内的每个键值对所代表的意义

  • CS:将帖子对象与总HTML分开的字符串(组件分离)
  • 投票:
    • R:如果存在投票箭头,扫描器查找的内容
    • S:开始扫描的位置
    • E:结束扫描的位置
  • Parts:一个对象数组,包含了构建帖子所需的所有部分,按顺序排列。
    • S:开始扫描的位置
    • E:结束扫描的位置
    • I:将结果放入的位置。如果它是TRASH,则不会保存。使用TRASH来获取必要属性间存在的垃圾。

评论处理如何构建HNComment对象的数组,但它在查找方面要更加健壮

  • CS:将帖子对象与总HTML分开的字符串(组件分离)
  • 赞成和反对
    • R:如果存在投票箭头,扫描器查找的内容
    • S:开始扫描的位置
    • E:结束扫描的位置
  • Level:确定该评论对象嵌套方式的办法
    • S:开始扫描的位置
    • E:结束扫描的位置
  • ASKJOBSREG确定查找所需部分构建评论的顺序
    • S:开始扫描的位置
    • E:结束扫描的位置
    • I:将结果放入的位置。如果它是TRASH,则不会保存。使用TRASH来获取必要属性间存在的垃圾。

回复处理如何构建表示回复提交或另一条评论的POSTed操作。

  • Action:回复表单操作的路径
  • Parts:隐藏在用户输入中但构建回复对象必成的额外表单值。
    • S:开始扫描的位置
    • E:结束扫描的位置
    • I:将结果放入的位置。如果它是TRASH,则不会保存。使用TRASH来获取必要属性间存在的垃圾。

提交处理如何构建代表向HN提交新提交的POSTed操作。

  • Action:提交表单操作的路径
  • Parts:隐藏在用户输入中但构建回复对象必成的额外表单值。
    • S:开始扫描的位置
    • E:结束扫描的位置
    • I:将结果放入的位置。如果它是TRASH,则不会保存。使用TRASH来获取必要属性间存在的垃圾。
  • Url:提交的URL属性名。
  • Title:提交的标题属性名。
  • Text:提交的正文字体属性名。

HNManager

HNManager将成为你使用libHN的首选类。所有操作都通过该类进行流——所有网络调用、会话生成等。它是你连接到HackerNews功能的渠道。HNManager是一个Singleton类,并且有一个sharedManager初始化,你应该使用它以确保一切都能通过Manager正确路由。

启动会话

将代码片段[[HNManager sharedManager] startSession]添加到以开始HNManager的sharedManager对象的会话。此方法将登录一个用户,如果用户以前在应用程序中登录过,将使用设备上存储的Cookie进行登录。为了最佳实践,将此方法添加到你的AppDelegate.m类中的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中,在方法返回之前。这将确保在应用程序启动时首先设置管理器,并且任何已登录的用户将立即登录。


获取帖子

由于HackerNews的设置方式,获取帖子的方法有两种。第一种是loadPostsWithFilter:completion:方法,这是您开始根据筛选器获取帖子的方法。所以,如果您访问HN主页,调用此方法并使用作为PostFilterType参数的PostFilterTypeTop,您将得到如下结果。

如果注意到主页的底部有一个"More"按钮。点击它,然后查看网址栏。注意网址末尾看起来像这样:"fnid=kS3LAcKvtXPC85KnoQszPW"吗?HackerNews通过为一个分配 fnid,或者基本上是一个SessionKey,来确定您要访问哪个页面以及请求/响应的真实性。这用于网站上除获取任何类型帖子的前30个链接之外的所有操作。这就是第二种方法的用武之地,即loadPostsWithUrlAddition:completion:方法,该方法接受一个URL附加字符串来确定应该显示哪些帖子。

loadPostsWithFilter

此方法接受一个PostFilterType参数,并返回一个HNPost对象的NSArray。下面列出了各种PostFilterTypes以及您收到的帖子类型。

  • PostFilterTypeTop - 主页
  • PostFilterTypeAsk - Ask HN
  • PostFilterTypeNew - 最新帖子
  • PostFilterTypeJobs - HN Jobs
  • PostFilterTypeBest - 最近评分最高的帖子

以下是使用方法

[[HNManager sharedManager] loadPostsWithFilter:(PostFilterType)filter completion:(NSArray *posts){
  if (posts) {
    // Posts were successfuly retrieved
  }
  else {
    // No posts retrieved, handle the error
  }
}];

loadPostsWithUrlAddition

在获取了第一组帖子之后,使用此方法继续获取同一筛选器中更多的帖子。FNID参数主要是由HNManager的postUrlAddition属性管理的。如果您想自定义操作,可以输入一个任意的字符串,但我建议您坚持使用默认的postUrlAddition属性。每次使用这两种方法中的任何一种加载帖子时,都会在sharedManager上更新postUrlAddition参数。

[[HNManager sharedManager] loadPostsWithUrlAddition:[[HNManager sharedManager] postUrlAddition] completion:(NSArray *posts){
  if (posts && posts.count > 0) {
    // Posts were successfuly retrieved
  }
  else {
    // No posts retrieved, handle the error
  }
}];

HNpost.{h,m}

实际的HNPost对象相当简单,它仅包含帖子的元数据,如标题和网址。这里有一个类方法,用于扫描传入的HTML以返回上述两种网络方法返回的帖子数组的帖子。这是您不应涉及的低级内容,但如果您想了解其工作原理或自行实现更改,可能会有所帮助。

// HNPost.h

// Enums
typedef NS_ENUM(NSInteger, PostType) {
    PostTypeDefault,
    PostTypeAskHN,
    PostTypeJobs
};

// Properties
@property (nonatomic, assign) PostType *Type;
@property (nonatomic,retain) NSString *Username;
@property (nonatomic, retain) NSURL *Url;
@property (nonatomic, retain) NSString *UrlDomain;
@property (nonatomic, retain) NSString *Title;
@property (nonatomic, assign) int Points;
@property (nonatomic, assign) int CommentCount;
@property (nonatomic, retain) NSString *PostId;
@property (nonatomic, retain) NSString *TimeCreatedString;

// Methods
+ (NSArray *)parsedPostsFromHTML:(NSString *)html FNID:(NSString **)fnid;

只有一种方法可以加载评论,它自然而然地跟随帖子的加载。在加载帖子后,您可以将其传递给以下方法,以返回一个包含HNComment对象的数组。如果您访问一个Ask HN帖子,您会注意到文本与评论中的其余部分内联(通过一个文本区域区分回复),所以我决定将自发表作为返回数组中的第一个评论。您可以通过使用HNComment的Type属性来识别这一点。对于HNJobs帖子也是如此。有时,一个工作帖会是一个指向Hacker News的内部链接,而不是外部链接,所以您可以用完全相同的方式捕获此数据。如果Type == HNCommentTypeJobs,则表示您有一个内部工作帖。

我之所以这样设计Ask HN和Jobs,是为了获取帖子中的任何链接数据,并将它们与我自己的个人应用中的任何其他评论一起内联显示给用户。

[[HNManager sharedManager] loadCommentsFromPost:(HNPost *)post completion:(NSArray *comments){
  if (comments) {
    // Comments retrieved.
  }
  else {
    // No comments retrieved, handle the error
  }
}];

HNComment.{h,m}

类似于HNPost对象,HNComment具有一个方便的类方法,可以解析HTML本身以生成HNComments数组。同样,我会浏览一下,以了解它是如何工作的。

// HNComment.h

// Enums
typedef NS_ENUM(NSInteger, HNCommentType) {
    HNCommentTypeDefault,
    HNCommentTypeAskHN,
    HNCommentTypeJobs
};

// Properties
@property (nonatomic, assign) HNCommentType *Type;
@property (nonatomic, retain) NSString *Text;
@property (nonatomic, retain) NSString *Username;
@property (nonatomic, retain) NSString *CommentId;
@property (nonatomic, retain) NSString *ParentID;
@property (nonatomic, retain) NSString *TimeCreatedString;
@property (nonatomic, retain) NSString *ReplyURLString;
@property (nonatomic, assign) int Level;
@property (nonatomic, retain) NSArray *Links;

// Methods
+ (NSArray *)parsedCommentsFromHTML:(NSString *)html;

用户相关的操作是成为 HackerNews 社区一员的重要方面。我的意思是,如果你不能积极参与讨论或提交有趣的链接,那只能算是个旁观者。不幸的是,大多数 HN Reader for iOS/Mac 应用忽略了这部分社区,而更关注有趣的链接。这是有原因的——实现它并不简单;你需要考虑 Cookie 以及只为了提交或评论通过,就需要进行两次网络调用。这很烦人,我决定自己干这些烦人事,并抽象化处理,这样你就无需再次考虑。这一切都从登录开始。

HN 在浏览器中的操作是在 HTTP Cookie 的基础上进行的。这个 Cookie 在登录时生成,并保留较长时间。在不同的计算机上登录将使该用户的所有 Cookie 无效。因此,在进行登录尝试之前,检查是否有 Cookie 并验证它是必要的。这将在 HNManager 使用 startSession 方法初始化时自动完成。它会找到设备上的 Cookie 并尝试验证。如果验证通过,它将把 Cookie 设置为 HNManager 的 SessionCookie 参数,并获取正确的 HNUser 以填充 SessionUser 属性。如果找不到 Cookie 或 Cookie 已不再有效,你需要使用以下方法以传统方式登录。首先确保像这样检查用户是否已登录

// Check to make sure no user is logged in
if (![[HNManager sharedManager] userIsLoggedIn]) {
 // No user is logged in; attempt to login
 [[HNManager sharedManager] loginWithUsername:@"user" password:@"pass" completion:(HNUser *user){
   if (user) {
     // Login was successful!
   }
   else {
     // Login failed, handle the error
   }
 }];
}

登出仅将 SessionCookie 属性和 SessionUser 属性从内存中删除,以及从 [NSHTTPCookieStorage sharedStorage] 中删除实际的 cookie,因此你不能再使用它们来提交特定于用户的需求,如提交和评论。实现登出非常简单。

[[HNManager sharedManager] logout];

以下为 HNUser 对象的外观,仅供参考

// HNUser.h

// Properties
@property (nonatomic, retain) NSString *Username;
@property (nonatomic, assign) int Karma;
@property (nonatomic, assign) int Age;
@property (nonatomic, retain) NSString *AboutInfo;


// Methods
+(HNUser *)userFromHTML:(NSString *)html;

提交新帖子

提交帖子是维持社区活力的重要方面之一。不幸的是,大多数 iOS/Mac 客户端都没有这个功能——因此我们只能是无声的旁观者,虽然美丽,但终究是旁观者。但不再是这种情况了。在 HackerNews 上,你可以提交一个链接或文本帖子(Ask HN),因此我们在这个单一的网络服务调用中模拟了这个功能。要执行文本帖子,只需让链接参数为 nil。如果链接参数和文本参数都填写了,则文本将被忽略。如果两者都为 nil,则完成块将以 NO 作为布尔值触发。以下是实现方式

// Submit a Link!
[[HNManager sharedManager] submitPostWithTitle:@"Hello World!" link:@"www.helloworld.com" text:nil completion:(BOOL success){
 if (success) {
  // Post was submitted
 }
 else {
  // Post was not submitted
 }
}];

// Submit a text post!
[[HNManager sharedManager] submitPostWithTitle:@"Hello World!" link:nil text:@"Hello World!" completion:(BOOL success){
 if (success) {
  // Post was submitted
 }
 else {
  // Post was not submitted
 }
}];

/////////////////

// This will use the LINK and not the text
[[HNManager sharedManager] submitPostWithTitle:@"Hello World!" link:@"www.helloworld.com" text:@"Hello World!" completion:(BOOL success){
 if (success) {
  // Post was submitted
 }
 else {
  // Post was not submitted
 }
}];

/////////////////

// These requests won't work!
[[HNManager sharedManager] submitPostWithTitle:@"Hello World!" link:nil text:nil completion:(BOOL success){
 //
}];

// Must have a title!
[[HNManager sharedManager] submitPostWithTitle:nil link:nil text:@"Hello World!" completion:(BOOL success){
 //
}];

回复帖子/评论

在 HackerNews 中,不论回复的对象是帖子还是其他评论,回复的方式都是一样的。这使得我们的工作变得很容易。正因为如此,当你想回复一个对象时,只需调用一个方法——你只需提供一个 HNPost 或 HNComment 就可以了。这里有一个方法,你必须传递一个 HNPost 或 HNComment 以及你想要评论的文本。如果你不这样做,完成块将返回 NO,表示失败。

[[HNManager sharedManager] replyToPostOrComment:(HNPost *)post withText:@"Comment to a post" completion:(BOOL success){
 if (success) {
  // Comment was submitted
 }
 else {
  // Comment failed submitting
 }
}];

// And of course, if you want to post a comment to a comment
[[HNManager sharedManager] replyToPostOrComment:(HNComment *)comment withText:@"Comment to a Comment" completion:(BOOL success){
 if (success) {
  // Comment was submitted
 }
 else {
  // Comment failed submitting
 }
}];

/////////////////

// This request won't work!
[[HNManager sharedManager] replyToPostOrComment:nil withText:@"Comment to a post" completion:(BOOL success){
 //
}];

// And neither will this one!
[[HNManager sharedManager] replyToPostOrComment:(HNComment *)comment withText:nil completion:(BOOL success){
 //
}];

对帖子/评论进行投票

在进行帖子投票时,有几个考虑的事项。对于帖子,您只能进行向上投票。在评论上,您只能进行向上投票,直到您作为用户拥有>= 500 好评为止。因此,这个方法可能不会根据您的登录状态和您在 HN 上的好评数量让您进行上下投票。如果您在这个方法的完成块中收到 NO 反馈,表示它没有成功让您进行投票。

[[HNManager sharedManager] voteOnPostOrComment:(HNPost *)post direction:VoteDirectionUp completion:(BOOL success){
 if (success) {
  // Voting worked!
 }
 else {
  // Voting was not successful!
 }
}];

获取用户的全部帖子

获取用户帖子比较像基于过滤条件获取主页帖子。一个用户前 30 条帖子的获取可以通过这种方式访问网站:[https://news.ycombinator.com/submitted?id=pg](https://news.ycombinator.com/submitted?id=pg),但如果想获取更多帖子(假设他们有超过 30 个),则必须再次使用 FNID。因此,我们将重用前面提到的方法以获取前 30 条帖子之后的帖子并保存 FNID 作为 HNManager 下的一个属性,称为 userSubmissionUrlAddition。下面是如何使用这个方法:

// Fetch the first 30 posts for a User
[[HNManager sharedManager] fetchSubmissionsForUser:@"pg" completion:(NSArray *posts){
 if (posts) {
    // Posts were successfuly retrieved
  }
  else {
    // No posts retrieved, handle the error
  }
}];

// Fetch posts 31 - n
[[HNManager sharedManager] loadPostsWithUrlAddition:[[HNManager sharedManager] userSubmissionUrlAddition] completion:(NSArray *posts){
  if (posts) {
    // Posts were successfuly retrieved
  }
  else {
    // No posts retrieved, handle the error
  }
}];

辅助方法

HNManager 还处理一些与 HN 相关的事务,例如能够记录您访问过的帖子 - 允许您在不自行实现的情况下标记帖子为已读。以下是你能做的事情列表。

标记帖子为已读

要将帖子标记为已读,HNManager 内部有一个字典会跟踪所有信息。使用这个管理器,您可以检查帖子是否已被阅读,并可以向已读帖子的字典中添加 HNPost。下面是如何使用这个功能:

// Methods
- (BOOL)hasUserReadPost:(HNPost *)post;
- (void)setMarkAsReadForPost:(HNPost *)post;

// Has User Read A Post?
BOOL hasRead = [[HNManager sharedManager] hasUserReadPost:(HNPost *)post];
if (hasRead) {
 // User has read the post.
}

// Add a post
[[HNManager sharedManager] setMarkAsReadForPost:post];

记录用户的投票情况

HackerNews 在确定可以或不可以进行投票上是有点搞怪的 - 因此,如果您自己去完成这个工作,跟踪这些信息可能会很耗时。然而,如果与投票帖子/评论的方法结合使用,并且如果收到成功的响应,可以将帖子/评论扔到这个方法里,这个方法会处理您的投票。

// Methods
- (BOOL)hasVotedOnObject:(id)hnObject;
- (void)addHNObjectToVotedOnDictionary:(id)hnObject direction:(VoteDirection)direction;

// Find out if a post has been voted on
BOOL hasVoted = [[HNManager sharedManager] hasVotedOnObject:(HNPost *)post];
if (hasVoted) {
 // User has voted on the Post/Comment
}

// Add to the record
[[HNManager sharedManager] addHNObjectToVotedOnDictionary:(HNComment *)comment direction:VoteDirectionDown];

终止 Web 请求

有时加载评论或帖子可能会花费一段时间,您可能想在应用中导航到别的地方并停止加载。为此,您可以调用一个简单的方法来结束所有正在通过 HNManager 发生的 Web 请求。

[[HNManager sharedManager] cancelAllRequests];

面向未来和更远的设计!

总的来说,如果您深入了解了这个工作的内部原理,你会注意到它非常、非常依赖于一种解析方案来获取正确的信息并使其有意义。正如每个人都知道,任何曾经构建过爬虫项目的人都知道,这**不是**一个面向未来的方案。话虽如此,我没有看到 HN 改变过它的内部结构。我认为,如果 HN 这样做,有几种方法可以使这个伟大的项目保持可维护性,其中一些是可行的,而另一些则很可能不是。

  • 说服 PG 做出一个该死的 API
  • 使用一个在线数据库,其中包含了解析的顺序以及要找的解析“标签”,这样如果 HN 发生了变化,就不会在苹果审核期间等待一周,同时他们的应用崩溃。
  • 编写一个每几分钟爬取 HN 的 API,但这需要费用来为所有可能使用它的应用提供流量,而我没有钱。

要添加的功能

我想在前进的过程中添加这些功能,但对我来说,v0.1 的目标是有一个纯 HN 特定的库。

  • 编辑用户 - 保存他们的关于/电子邮件。

使用 libHN 的应用

以下是使用 libHN 来提供美好功能的一些 iOS/Mac 应用程序列表。如果你的应用程序使用了这个库,请开一个 issue,我会在列表中添加它


许可证

libHN 使用标准 MIT 许可证。

版权 (C) 2013 年 Benjamin Gordon 所有

任何人免费获得此软件及相关文档文件(“软件”)副本的,可根据以下条件在该软件上无限制地操作,包括但不限于使用、复制、修改、合并、出版、分发、再许可和/或出售软件副本,并允许提供给软件的人这样做,前提是

上述版权声明和本许可声明应包含在软件的所有副本或实质性部分中。

本软件“按现状”提供,不提供任何类型的明示或暗示保证,包括但不限于适销性、特定用途的适用性和非侵权性。在任何情况下,作者或版权所有者均不对因本软件或本软件的使用或其他操作而产生的任何索赔、损害或其他责任负责,无论是在合同行为、侵权行为或其他行为 arising from, out of or in connection with the Software or the use or other dealing with the Software。