APOD 网站抓取器,HOPE 演示





5.00/5 (10投票s)
使用高阶编程环境,抓取 APOD 网站 20 年的照片并探索 APOD。
此版本的源代码可以在这里找到: https://github.com/cliftonm/HOPE/tree/release-6-1-2014
引言
在本文中,我将演示如何编写一个简单的应用程序来处理 APOD 网站。目标是:
- 每天在特定时间抓取当前的 APOD 并显示缩略图
- 缩略图应包含元数据以
- 允许用户单击缩略图并在 APOD 网页上打开浏览器窗口
- 显示元数据,可以在单独的文本窗口中读取或打开。
- 在默认浏览器中启动关联的网页
- 用户还可以创建所有历史 APOD 网页的本地数据库
- 用户可以搜索关键词,这将显示与这些关键词匹配的图片的缩略图
- 缩略图将具有如上所述的相同行为。
本文扩展了我上一篇介绍高阶编程环境的文章。在这里,我们开发了一个更复杂的应用程序。
用户警告
我不对人们花费数小时搜索数据库和探索精美图片负责!
运行应用程序
可执行文件名是 TypeSystemExplorer.exe。如果您只下载了二进制文件(需要 .NET 4.5),请运行此程序。它将开始执行许多操作,包括创建数据库、创建 Images 文件夹以及抓取今天的 APOD。
如果您想抓取所有 APOD,首先,通过双击禁用缩略图转换器接收器。我们暂时不需要它。打开包含二进制文件的文件夹,然后将 APODEventGeneratorReceptor.dll 程序集拖放到表面上。这将创建 6000 多个载体,每个载体都有一个介于 1995 年 6 月 16 日(第一个 APOD)到今天之间的日期。然后,这些载体将由系统处理。在我的计算机上,抓取 20 年的 APOD 大约需要一到两个小时。日志记录器偶尔会发出一条消息,指示存在某个问题。完成后,请勿忘记重新启用缩略图转换器。对于那些想知道不禁用缩略图转换器会发生什么的人来说,您将在查看器中看到前 100 张图片,然后查看器将自行禁用。
要搜索,请将 SearchForReceptor.dll 程序集拖放到表面上。将出现一个窗口,您可以在其中输入搜索字符串。
您还可以拖放额外的缩略图查看器、文本显示接收器等,启用和禁用内容,等等,就像我在视频中所做的那样。
如果您只想选择一些随机图片,请将 JPG 文件拖放到表面上,它们将被显示,并且任何关于这些图片的元数据都将从数据库中自动获取。
最重要的是,玩得开心!
接收器
用纯粹的整体方式编写这个程序会相当简单。使用高阶编程环境 (HOPE),我们将任务分解为 APOD 特定的进程和通用进程。
我们将为这项活动创建六个接收器
- 键值对持久化
- 每日计时器
- 网页阅读器
- APOD 页面抓取器
- APOD URL 生成器
- 记录持久化
- 记录搜索器
我们将利用我们已经拥有的文本转语音和缩略图查看器接收器,并对可视化组件(它不是接收器)进行一些额外的增强。
请熟悉描述 HOPE 的文章中的术语。
持久化引擎
我们需要一种简单的方法来持久化应用程序数据。与其让大量独立的数据库(或伪数据库)闲置,不如让接收器作为数据库实现的接口会很有用。
使用 HOPE 架构,您可以轻松替换接收器。在这里,我们可以实现一个完全不同的存储机制,对应用程序的其余部分是透明的。
首先,让我们定义接收器使用的协议。
RequireTable 协议
我们将使用此协议,以便接收器可以告知持久化引擎某些表需要存在。
<SemanticTypeStruct DeclType="RequireTable"> <Attributes> <NativeType Name="TableName" ImplementingType="string"/> <NativeType Name="SemanticTypeName" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
我们将使用语义类型来定义表架构。例如,让我们创建用于保存计时器事件最后发生日期/时间时的语义类型协议
<SemanticTypeStruct DeclType="LastEventDateTime"> <Attributes> <NativeType Name="ID" ImplementingType="long?"/> <NativeType Name="EventName" ImplementingType="string"/> <NativeType Name="EventDateTime" ImplementingType="DateTime?"/> </Attributes> </SemanticTypeStruct>
请注意可空类型。这些有助于确定在插入记录时填充哪些字段。另外请注意,对于 SQLite,主键自动增量字段必须是 long 类型。我还没有解决其他类型转换问题。
DatabaseRecord 协议
<SemanticTypeStruct DeclType="DatabaseRecord"> <Attributes> <NativeType Name="ResponseProtocol" ImplementingType="string"/> <NativeType Name="TableName" ImplementingType="string"/> <NativeType Name="Action" ImplementingType="string"/> <!-- insert, update, delete, select --> <NativeType Name="Row" ImplementingType="ICarrier"/> <!-- the carrier with the actual row data --> <NativeType Name="Where" ImplementingType="string"/> <NativeType Name="OrderBy" ImplementingType="string"/> <NativeType Name="GroupBy" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
在这里,协议告诉接收器要操作的表名、要执行的操作,以及对于插入或更新语句,包含要记录的数据以创建或更新的载体。我们还可以指定 where、order by 和 group by 子句。这绝不是一个实体框架!请注意,对于查询,提供了响应协议。这意味着一个接收器可以发出查询,但将其定向到另一个接收器,而不是将数据返回给自己。
接收器实现
我们将使用 SQLite 作为数据库,以实现一个即插即用的简单实现(至少对用户而言是即插即用,否则用户需要指定凭据、确保数据库服务器已安装并正在运行等)。让我们先看看我是如何创建接收器模板的
public class ReceptorDefinition : IReceptorInstance { public string Name { get { return "Persistor"; } } public bool IsEdgeReceptor { get { return true; } } public bool IsHidden { get { return false; } } protected IReceptorSystem rsys; protected SQLiteConnection conn; protected Dictionary<string, Action<dynamic>> protocolActionMap; const string DatabaseFileName = "hope.db"; public ReceptorDefinition(IReceptorSystem rsys) { this.rsys = rsys; protocolActionMap = new Dictionary<string, Action<dynamic>>(); protocolActionMap["RequireTable"] = new Action<dynamic>((s) => RequireTable(s)); protocolActionMap["DatabaseRecord"] = new Action<dynamic>((s) => DatabaseRecord(s)); CreateDB(); OpenDB(); } public string[] GetReceiveProtocols() { return protocolActionMap.Keys.ToArray(); } public void Terminate() { conn.Close(); conn.Dispose(); // As per this post: // http://stackoverflow.com/questions/12532729/sqlite-keeps-the-database-locked-even-after-the-connection-is-closed // GC.Collect() is required to ensure that the file handle is released NOW (not when the GC gets a round tuit. ;) GC.Collect(); } public void ProcessCarrier(ICarrier carrier) { protocolActionMap[carrier.Protocol.DeclTypeName](carrier.Signal); }
请注意,我正在创建一个字典来映射协议到方法,这样我们就可以用一行代码将协议分派到所需的方法。
实现几个方法来创建数据库(如果它不存在)
/// <summary> /// Create the database if it doesn't exist. /// </summary> protected void CreateDBIfMissing() { string subPath = Path.GetDirectoryName(DatabaseFileName); if (!File.Exists(DatabaseFileName)) { SQLiteConnection.CreateFile(DatabaseFileName); } } protected void OpenDB() { conn = new SQLiteConnection("Data Source = " + DatabaseFileName); conn.Open(); }
…到目前为止,我们已经有了足够的代码来测试数据库是否已创建。只需将接收器拖放到表面上,它就会创建数据库,果然,数据库被创建了
HOPE 架构的另一个有趣特性是,无需运行整个应用程序或编写单元测试即可轻松创建行为。将载体拖放到表面上以测试特定行为。移除将处理该载体的接收器以检查该载体。
现在,让我们实现创建表(如果缺少)的基本要素
protected void RequireTable(dynamic signal) { if (!TableExists(signal.TableName)) { StringBuilder sb = new StringBuilder("create table " + signal.TableName + "("); // Always create a primary key as the field ID. // There is no need to put this into the semantic type definition unless it's required for queries. sb.Append("ID INTEGER PRIMARY KEY AUTOINCREMENT"); List<INativeType> types = rsys.SemanticTypeSystem.GetSemanticTypeStruct(signal.SemanticTypeName).NativeTypes; // Ignore ID field in the schema, as we specifically create it above. types.Where(t=>t.Name.ToLower() != "id").ForEach(t => { sb.Append(", "); sb.Append(t.Name); // we ignore types, as per the SQLite 3 documentation: // "Any column in an SQLite version 3 database, except an INTEGER PRIMARY KEY column, // may be used to store a value of any storage class." // http://www.sqlite.org/datatype3.html }); sb.Append(");"); Execute(sb.ToString()); } } protected bool TableExists(string tableName) { string sql = "SELECT name FROM sqlite_master WHERE type='table' AND name=" + tableName.SingleQuote() + ";"; string name = QueryScalar<string>(sql); return tableName == name; } protected T QueryScalar<T>(string query) { SQLiteCommand cmd = conn.CreateCommand(); cmd.CommandText = query; T result = (T)cmd.ExecuteScalar(); return result; }
现在我们将通过创建一个载体 XML 文件来测试这一点
<Carriers> <Carrier Protocol="RequireTable" TableName="LastEventDateTime" SemanticTypeName="LastEventDateTime"/> </Carriers>
将 Persistor 接收器拖放到表面上,将载体拖放到表面上,瞧,表已创建(使用无处不在的SQLite Database Browser)
现在我们可以将我们的模式声明为语义类型并创建表(如果它不存在),并且我们已经成功地测试了这个过程,而无需编写任何开销代码。
最后要做的是实现 CRUD 操作,我们将创建测试载体来验证行为。我们需要这个
protected Dictionary<string, object> GetColumnValueMap(ICarrier carrier) { List<INativeType> types = rsys.SemanticTypeSystem.GetSemanticTypeStruct(carrier.Protocol.DeclTypeName).NativeTypes; Dictionary<string, object> cvMap = new Dictionary<string, object>(); types.ForEach(t => cvMap[t.Name] = t.GetValue(carrier.Signal)); return cvMap; }
…以获取插入和更新操作的键值对。
插入记录
protected void Insert(dynamic signal) { Dictionary<string, object> cvMap = GetColumnValueMap(signal.Row); StringBuilder sb = new StringBuilder("insert into " + signal.TableName + "("); sb.Append(String.Join(", ", (from c in cvMap where c.Value != null select c.Key).ToArray())); sb.Append(") values ("); sb.Append(String.Join(",", (from c in cvMap where c.Value != null select "@" + c.Key).ToArray())); sb.Append(");"); SQLiteCommand cmd = conn.CreateCommand(); (from c in cvMap where c.Value != null select c).ForEach(kvp => cmd.Parameters.Add(new SQLiteParameter("@" + kvp.Key, kvp.Value))); cmd.CommandText = sb.ToString(); cmd.ExecuteNonQuery(); cmd.Dispose(); }
测试载体
<Carriers> <Carrier Protocol="LastEventDateTime" EventName="Test" EventDateTime="8/19/1962 12:15 PM"/> <Carrier Protocol="DatabaseRecord" TableName="LastEventDateTime" Action="Insert" Row="{LastEventDateTime}"/> </Carriers>
像往常一样,我们将在表面上拖放接收器,然后在上面的测试载体,然后检查数据库
更新记录
protected void Update(dynamic signal) { Dictionary<string, object> cvMap = GetColumnValueMap(signal.Row); StringBuilder sb = new StringBuilder("update " + signal.TableName + " set "); sb.Append(String.Join(",", (from c in cvMap where c.Value != null select c.Key + "= @" + c.Key).ToArray())); sb.Append(" where " + signal.Where); SQLiteCommand cmd = conn.CreateCommand(); (from c in cvMap where c.Value != null select c).ForEach(kvp => cmd.Parameters.Add(new SQLiteParameter("@" + kvp.Key, kvp.Value))); cmd.CommandText = sb.ToString(); cmd.ExecuteNonQuery(); cmd.Dispose(); }
测试载体
<Carriers> <Carrier Protocol="LastEventDateTime" EventName="I've been updated!"/> <Carrier Protocol="DatabaseRecord" TableName="LastEventDateTime" Action="Update" Row="{LastEventDateTime}" Where="ID=1"/> </Carriers>
删除记录
protected void Delete(dynamic signal) { string sql = "delete from " + signal.TableName + " where " + signal.Where; SQLiteCommand cmd = conn.CreateCommand(); cmd.CommandText = sql; cmd.ExecuteNonQuery(); cmd.Dispose(); }
测试载体
<Carriers> <Carrier Protocol="DatabaseRecord" TableName="LastEventDateTime" Action="Delete" Where="ID=1"/> </Carriers>
全部删除!
选择记录
每条选定的记录都将作为一个单独的载体发出。对于大型记录集来说,这相当低效,但对我们来说已经足够了。例如,其他实现可以将记录集合返回到一个语义类型中。我们也可以轻松地使用此机制实现分页。我们将把它留到以后,当我们真正需要处理这个问题时。当然,表连接被完全忽略了,等等。我们这里只需要基本功能!
protected void Select(dynamic signal) { StringBuilder sb = new StringBuilder("select "); List<INativeType> types = rsys.SemanticTypeSystem.GetSemanticTypeStruct(signal.ResponseProtocol).NativeTypes; sb.Append(String.Join(",", (from c in types select c.Name).ToArray())); sb.Append(" from " + signal.TableName); if (signal.Where != null) sb.Append(" where " + signal.Where); // support for group by is sort of pointless since we're not supporting any mechanism for aggregate functions. if (signal.GroupBy != null) sb.Append(" group by " + signal.GroupBy); if (signal.OrderBy != null) sb.Append(" order by " + signal.OrderBy); SQLiteCommand cmd = conn.CreateCommand(); cmd.CommandText = sb.ToString(); SQLiteDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct(signal.ResponseProtocol); dynamic outSignal = rsys.SemanticTypeSystem.Create(signal.ResponseProtocol); Type type = outSignal.GetType(); // Populate the output signal with the fields retrieved from the query, as specified by the requested response protocol types.ForEach(t => { object val = reader[t.Name]; PropertyInfo pi = type.GetProperty(t.Name); val = Converter.Convert(val, pi.PropertyType); pi.SetValue(outSignal, val); }); rsys.CreateCarrier(this, protocol, outSignal); } cmd.Dispose(); }
测试载体
<Carriers> <Carrier Protocol="DatabaseRecord" TableName="LastEventDateTime" ResponseProtocol="LastEventDateTime" Action="select"/> </Carriers>
将鼠标悬停在发出的载体上,我们可以检查属性网格中的内容
我们看到返回的数据。
当然,还有一些(也许很多!)悬而未决的问题,我们将在某个时候处理。
此时,我们拥有一个可工作的 persistor 接收器。这是一个有三条记录等待处理的示例
定时器
继续处理更简单的内容!
我们需要一个计时器接收器以特定间隔生成载体。这些载体只需指定一个协议,其中包含特定于计时器类型的消息,例如“每日”、“每刻钟”等。顺便说一句,我们可以使用“每刻钟”来创建大本钟的敲钟声,但这在这里不是目的。
由于我晚上会关闭我的计算机(并且白天可能会重新启动一次或多次),并且我怀疑其他人也会这样做,因此计时器需要记住它发出的最后一个载体,并确定自上次事件以来时间段是否已过期。如果是这样,将发出一个新的载体。这将防止系统重新触发已处理过的特定时间间隔的载体。
关于 HOPE 平台的一个观察是,它非常需要实时馈送。它非常适合处理数据流、事件以及数据不断变化的情况。
让我们从协议开始
IntervalTimerConfiguration 协议
<SemanticTypeStruct DeclType="IntervalTimerConfiguration"> <Attributes> <NativeType Name="StartDateTime" ImplementingType="DateTime"/> <NativeType Name="Interval" ImplementingType="int"/> <NativeType Name="EventName" ImplementingType="string"/> <NativeType Name="IgnoreMissedIntervals" ImplementingType="bool"/> </Attributes> </SemanticTypeStruct>
我们需要此协议来声明不同的开始时间、间隔和事件名称。间隔以秒为单位,例如,一天是 24 小时 * 60 分钟/小时 * 60 秒/分钟,即 86,400。事件名称描述了要放入 TimerEvent 信号的名称。
StartDateTime 作为下一个事件发生时间的种子。如果日期丢失,DateTime 类将使用当前日期。
TimerEvent 协议
<SemanticTypeStruct DeclType="TimerEvent"> <Attributes> <NativeType Name="EventName" ImplementingType="string"/> <NativeType Name="EventDateTime" ImplementingType="DateTime"/> </Attributes> </SemanticTypeStruct>
一个非常简单的协议,描述事件名称。
请注意,创建不同事件的载体以模拟计时器是多么容易,这使得测试接收器变得容易得多!
配置载体
现在让我们创建配置载体 (carrier-config_timer.xml)。它相当平淡,因为我们只想要每日事件
<Carriers> <Carrier Protocol="IntervalTimerConfiguration" StartDateTime="8:00 AM" Interval="86400" EventName="Daily Event" IgnoreMissedIntervals="false"/> </Carriers>
为了更令人兴奋一些,也创建一个 5 秒的事件
<Carrier Protocol="IntervalTimerConfiguration" StartDateTime="1/1/2014 12:01 AM" Interval="5" EventName="5sec" IgnoreMissedIntervals="true"/>
因此,每天早上 8 点(或您何时启动计算机),事件就会触发。如果您几天不启动计算机怎么办?那么,我们需要触发错过的时间段的事件。我们将实现这一点,以便每次错过的间隔只触发一次事件——对于 APOD 抓取器,我们希望检索所有错过的日期。现在我们明白为什么需要持久化接收器了——我们需要能够存储上次事件何时被触发的能力。
我们将使用我们之前定义的载体来返回最后的计时器事件记录
<SemanticTypeStruct DeclType="LastEventDateTime"> <Attributes> <NativeType Name="ID" ImplementingType="long?"/> <NativeType Name="EventName" ImplementingType="string"/>o9 <NativeType Name="EventDateTime" ImplementingType="DateTime?"/> </Attributes> </SemanticTypeStruct>
现在我们遇到了 HOPE 架构的第一个“现实世界问题”——完成事件。问题是这样的:当我初始化事件接收器时,我如何知道此时是否已从数据库接收到最后一个事件时间?如果这是一个新事件,数据库中没有记录,并且持久化器不会返回任何内容。这需要一个完成通知——但我们应该将其实现为载体还是由接收器系统管理?我选择使用完成通知载体,原因很简单,在分布式计算环境中,接收器系统会将载体移交给另一个系统,此时它不知道远程进程何时完成。缺点是我们必须保证载体的顺序。目前,由于载体仅由主应用程序线程创建,因此这不是问题。即使可视化器已连接,载体也是按顺序处理的——请参阅每个载体动画的 CurveIndex
属性,该属性从 0 计数到 50。此外,载体是否可以异步处理应该由接收器决定,而不是由接收器系统决定。此外,除非需要同步,例如持久化器返回的多个载体,否则这不是问题。事实上,如果我们实际上返回的是行集合而不是每条记录一个载体,就可以避免整个问题!这似乎是最好的方法,因此我为这个新行为对持久化器进行了 5 行修改。
实际实现有点艰巨,因为我们必须与持久化器的响应同步。此外,下一个事件时间的计算很复杂,因为我们要考虑到系统在停机一段时间后重新启动,并且我们可以选择为所有错过的时间点创建事件。忽略设置所有内容所必须经历的所有障碍,以下是事件触发时发生的情况(代码注释应足够解释性)
/// <summary> /// Fire an event NOW. /// </summary> protected void FireEvent(DateTime now) { LastEventTime = now; UpdateRecord(); CreateEventCarrier(); } /// <summary> /// Updates an existing record with the new "LastEventTime" /// or inserts a new record if it didn't already exist. /// </summary> protected void UpdateRecord() { ICarrier rowCarrier = CreateRow(); // If it already exists in the DB, update it if (PreExisting) { UpdateRecord(rowCarrier); } else { // Otherwise, create it PreExisting = true; InsertRecord(rowCarrier); } } /// <summary> /// Creates the carrier for the timer event. /// </summary> protected void CreateEventCarrier() { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("TimerEvent"); dynamic signal = rsys.SemanticTypeSystem.Create("TimerEvent"); signal.EventName = EventName; signal.EventDateTime = (DateTime)LastEventTime; rsys.CreateCarrier(receptor, protocol, signal); } /// <summary> /// Creates the carrier (as an internal carrier, not exposed to the system) for containing /// our record information. Assuming only an update, this sets only the EventDateTime field. /// </summary> protected ICarrier CreateRow() { // Create the type for the updated data. ISemanticTypeStruct rowProtocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("LastEventDateTime"); dynamic rowSignal = rsys.SemanticTypeSystem.Create("LastEventDateTime"); rowSignal.EventDateTime = LastEventTime; ICarrier rowCarrier = rsys.CreateInternalCarrier(rowProtocol, rowSignal); return rowCarrier; } /// <summary> /// Creates a carrier instructing the persistor to update the LastEventDateTime field for our event name. /// </summary> protected void UpdateRecord(ICarrier rowCarrier) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("DatabaseRecord"); dynamic signal = rsys.SemanticTypeSystem.Create("DatabaseRecord"); signal.TableName = "LastEventDateTime"; signal.Row = rowCarrier; signal.Action = "update"; signal.Where = "EventName = " + EventName.SingleQuote(); rsys.CreateCarrier(receptor, protocol, signal); } /// <summary> /// Creates a carrier instructing the persistor to create a new entry. Note that we also /// add the EventName to the field set. /// </summary> protected void InsertRecord(ICarrier rowCarrier) { rowCarrier.Signal.EventName = EventName; ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("DatabaseRecord"); dynamic signal = rsys.SemanticTypeSystem.Create("DatabaseRecord"); signal.TableName = "LastEventDateTime"; signal.Row = rowCarrier; signal.Action = "insert"; rsys.CreateCarrier(receptor, protocol, signal); }
网页抓取器接收器
一般来说,我们需要能够返回给定网页的 HTML。我们将使用两个协议。这个协议定义了请求,我们在其中提供要抓取的页面的 URL 以及用于将 HTML 放入其中的响应协议。
<SemanticTypeStruct DeclType="ScrapeWebpage"> <Attributes> <NativeType Name="URL" ImplementingType="string"/> <NativeType Name="ResponseProtocol" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
对于我们的目的,我们希望由 APOD 网页抓取器接收器(见下文)使用的协议来处理响应,以便解析 HTML。
<SemanticTypeStruct DeclType="APODWebpage"> <Attributes> <NativeType Name="URL" ImplementingType="string"/> <NativeType Name="HTML" ImplementingType="string"/> <NativeType Name="Errors" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
我们还返回请求的 URL 以及读取页面时可能发生的任何异常。接收器实现很简单。请注意,处理是异步执行的
public async void ProcessCarrier(ICarrier carrier) { try { string html = await Task.Run(() => { // http://stackoverflow.com/questions/599275/how-can-i-download-html-source-in-c-sharp using (WebClient client = new WebClient()) { // For future reference, if there are parameters, like: // www.somesite.it/?p=1500 // use: // client.QueryString.Add("p", "1500"); //add parameters return client.DownloadString(carrier.Signal.URL); } }); Emit(html, carrier.Signal.ResponseProtocol); } catch (Exception ex) { EmitError(ex.Message, carrier.Signal.ResponseProtocol); } } protected void Emit(string html, string protocolName) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct(protocolName); dynamic signal = rsys.SemanticTypeSystem.Create(protocolName); signal.HTML = html; rsys.CreateCarrier(this, protocol, signal); } protected void EmitError(string error, string protocolName) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct(protocolName); dynamic signal = rsys.SemanticTypeSystem.Create(protocolName); signal.Errors = error; rsys.CreateCarrier(this, protocol, signal); }
现在我们有了一个简单可重用的接收器,用于获取网页的 HTML。
APOD 网页抓取器接收器
此接收器接收用于抓取页面的载体协议,同时还接收来自网页抓取器的 HTML。
protected Dictionary<string, Action<dynamic>> protocolActionMap; public ReceptorDefinition(IReceptorSystem rsys) { this.rsys = rsys; protocolActionMap = new Dictionary<string, Action<dynamic>>(); protocolActionMap["TimerEvent"] = new Action<dynamic>((s) => TimerEvent(s)); protocolActionMap["APODWebpage"] = new Action<dynamic>((s) => ProcessPage(s)); } public string[] GetReceiveProtocols() { return protocolActionMap.Keys.ToArray(); }
当此接收器收到每日事件以处理今天的 APOD 时,它会格式化 URL 并发送请求以抓取网页。
protected void TimerEvent(dynamic signal) { if (signal.EventName == "ScrapeAPOD") { DateTime eventDate = signal.EventDateTime; // Create a URL in this format: // http://apod.nasa.gov/apod/ap140528.html string url = "http://apod.nasa.gov/apod/ap" + eventDate.ToString("yyMMdd") + ".html"; EmitUrl(url); } } protected void EmitUrl(string url) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("ScrapeWebpage"); dynamic signal = rsys.SemanticTypeSystem.Create("ScrapeWebpage"); signal.URL = url; signal.ResponseProtocol = "APODWebpage"; rsys.CreateCarrier(this, protocol, signal); }
现在,抓取网页内容往往是经验之谈而非科学,尤其是因为内容布局可能会改变,设计这些页面的用户并不考虑他们的页面如何被自动化使用,所以没有 div,当然也没有有用的 id 属性等等。我不会让您厌烦实现细节,我已尽力使其尽可能健壮,以处理 19 年的每日 APOD(近 7000 页)。
但是,为了成功抓取页面,我们为其他接收器打包了图像文件名,并向数据库中记录了一些信息。使用 ImageFilename 协议发出图像文件,该协议是我在上篇文章中创建的,并可以触发缩略图创建器、查看器和编写器接收器中实现的进程。
protected void EmitImageFile(string fn) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("ImageFilename"); dynamic fsignal = rsys.SemanticTypeSystem.Create("ImageFilename"); fsignal.Filename = fn; // TODO: The null here is really the "System" receptor. rsys.CreateCarrier(this, protocol, fsignal); }
为了在数据库中记录条目,我们需要一个定义模式的语义类型
<SemanticTypeStruct DeclType="APOD"> <Attributes> <NativeType Name="ID" ImplementingType="long?"/> <NativeType Name="URL" ImplementingType="string"/> <NativeType Name="ImageFile" ImplementingType="string"/> <NativeType Name="Title" ImplementingType="string"/> <NativeType Name="Keywords" ImplementingType="string"/> <NativeType Name="Explanation" ImplementingType="string"/> <NativeType Name="Error" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
请注意,旧 APOD 页面缺少关键词。此外,我们还会记录任何抓取错误,以便我们可以访问页面并查看 HTML 的情况并纠正算法。
我们需要确保此表存在,因此接收器会发出一个 RequireTable 载体协议。
public void Initialize() { RequireAPODTable(); } protected void RequireAPODTable() { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("RequireTable"); dynamic signal = rsys.SemanticTypeSystem.Create("RequireTable"); signal.TableName = "APOD"; signal.Schema = "APOD"; rsys.CreateCarrier(this, protocol, signal); }
现在,记录数据很简单
protected void LogImage(string url, string fn, string keywords, string title, string explanation, List<string> errors) { dynamic record = rsys.SemanticTypeSystem.Create("APOD"); record.URL = url; record.ImageFile = fn; record.Keywords = keywords; record.Explanation = explanation; record.Title = title; record.Errors = String.Join(", ", errors.ToArray()); ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("DatabaseRecord"); dynamic signal = rsys.SemanticTypeSystem.Create("DatabaseRecord"); signal.TableName = "APOD"; signal.Action = "insert"; signal.Row = record; rsys.CreateCarrier(this, protocol, signal); }
这是数据库中 8 个条目的示例
当然,观看 HOPE 实时处理这些页面几乎和观看美丽的 APOD 照片一样有趣!
处理错误
我们还将错误发送到 Logger 接收器,该接收器为任何“调试消息”创建一个弹出窗口。
// Use the debug message receptor to display error counts. if (errors.Count > 0) { ++totalErrors; ISemanticTypeStruct dbgMsgProtocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("DebugMessage"); dynamic dbgMsgSignal = rsys.SemanticTypeSystem.Create("DebugMessage"); dbgMsgSignal.Message = totalErrors.ToString() + ": " + record.Errors; rsys.CreateCarrier(this, dbgMsgProtocol, dbgMsgSignal); }
这样,我们可以直观地判断页面抓取过程中是否突然出现了一连串错误。
抓取所有图像
为了实际抓取所有页面,我们将创建一个接收器,该接收器只做一件事,那就是模拟具有日期的计时器事件,从 1995 年 6 月 16 日的第一个 APOD 开始,直到今天。载体看起来像这样
<Carrier Protocol="TimerEvent" EventName="ScrapeAPOD" EventDateTime="5/19/2014"/>
这是一个“即插即用”的接收器——一旦将其拖放到表面上,它就开始发出载体。疯狂。
public void Initialize() { DateTime start = DateTime.Parse("6/16/1995"); DateTime stop = DateTime.Parse("7/16/1995"); // DateTime.Now; for (DateTime date = start; date <= stop; date = date.AddDays(1)) { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("TimerEvent"); dynamic signal = rsys.SemanticTypeSystem.Create("TimerEvent"); signal.EventName = "ScrapeAPOD"; signal.EventDateTime = date; rsys.CreateCarrier(this, protocol, signal); } rsys.Remove(this); }
另请注意,当此接收器生成载体完成后,它会自行删除。
将一个月日期发送到 APOD 接收器
一个月图像显示在缩略图查看器接收器中(鼠标滚轮像轮播一样旋转图像)
锦上添花
在缩略图查看器中查看 300 张或更多图像会开始拖慢系统。另外,一旦所有这些图像都已进入系统,最好能够用简单的文本搜索来过滤它们。对于查看器轮播中的主图像,最好还能执行以下操作:
- 查看全尺寸图像
- 读取(或朗读)“解释”
- 转到实际网页
图像元数据
让我们先处理图像元数据。这个过程的关键是使其尽可能通用。一种方法是将元数据与图像本身关联起来。这存在重大缺点。我们无法保证元数据始终存在。如果用户将缩略图拖放到查看器中会怎样?我们可以与图像关联的唯一有用的元数据是文件名——当我们加载图像时,我们可以将其放入图像的 Tag 属性中。我们当然可以根据图像查询数据库以获取任何元数据信息(URL、标题、完整图像文件名和解释)。缩略图查看器可以从能够提供它的某个接收器请求元数据包
<SemanticTypeStruct DeclType="GetImageMetadata"> <Attributes> <NativeType Name="ImageFile" ImplementingType="string"/> <NativeType Name="ResponseProtocol" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
以及响应
<SemanticTypeStruct DeclType="HaveImageMetadata"> <Attributes> <NativeType Name="ImageFile" ImplementingType="string"/> <NativeType Name="Metadata" ImplementingType="dynamic"/> </Attributes> </SemanticTypeStruct>
可视化器可以为轮播中“聚焦”的图像生成此请求。
protected void GetImageMetadata(IReceptor r) { CarouselState cstate = carousels[r]; int idx = cstate.Offset % cstate.Images.Count; if (idx < 0) { idx += cstate.Images.Count; } Image img = cstate.Images[idx]; ISemanticTypeStruct protocol = Program.Receptors.SemanticTypeSystem.GetSemanticTypeStruct("GetImageMetadata"); dynamic signal = Program.Receptors.SemanticTypeSystem.Create("GetImageMetadata"); signal.ImageFile = img.Tag.ToString(); signal.ResponseProtocol = "HaveImageMetadata"; Program.Receptors.CreateCarrier(null, protocol, signal); }
APOD 接收器对此协议感兴趣,并将向 Persistor 接收器发送请求,因为它毕竟是在数据库中创建了这些图像。我们还可以重用定义我们想要返回内容的协议,即 APOD schema-protocol。
在 APOD 接收器中
protected void GetImageMetadata(dynamic signal) { string imageFile = signal.ImageFile; // Sort of kludgy, we're stripping off the "-thumbnail" portion of the filename if the user // happens to have dropped a thumbnail file. Rather dependent upon the fact that the thumbnail // writer writes image files with string added to the filename! imageFile = imageFile.Surrounding("-thumbnail"); ISemanticTypeStruct dbprotocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("DatabaseRecord"); dynamic dbsignal = rsys.SemanticTypeSystem.Create("DatabaseRecord"); dbsignal.TableName = "APOD"; dbsignal.Action = "select"; dbsignal.ResponseProtocol = "APOD"; // Wildcard prefix to ignore path information. dbsignal.Where = "ImageFile LIKE '%" + imageFile + "'"; rsys.CreateCarrier(this, dbprotocol, dbsignal); }
在这里,调试视图,是一个响应示例
或者,当我们把一张图片拖放到表面上时,我们可以检查载体日志(由载体导出接收器创建)。
<Carriers> <Carrier Protocol="ImageFilename" Filename="E:\HOPE\TypeSystemExplorer\bin\Debug\Images\02mantar_feresten.jpg" /> <Carrier Protocol="ThumbnailImage" Filename="E:\HOPE\TypeSystemExplorer\bin\Debug\Images\02mantar_feresten-thumbnail.jpg" Image="System.Drawing.Bitmap" /> <Carrier Protocol="GetImageMetadata" ImageFile="E:\HOPE\TypeSystemExplorer\bin\Debug\Images\02mantar_feresten.jpg" ResponseProtocol="HaveImageMetadata" /> <Carrier Protocol="DatabaseRecord" ResponseProtocol="APOD" TableName="APOD" Action="select" Where="ImageFile LIKE '%02mantar_feresten.jpg'" Tag="HaveImageMetadata" /> <Carrier Protocol="APODRecordset"> <Recordset> <APOD ID="3003" URL="http://apod.nasa.gov/apod/ap031206.html" ImageFile="Images\02mantar_feresten.jpg" Keywords="sundial, observatory, india" Title="Jaipur Observatory Sundial" Explanation="..." </Recordset> </Carrier> </Carriers>
现在我们“简单地”将此响应返回给请求者,包含我们想要包含的字段值(有点笨拙但目前足够了)。
protected void ProcessAPODRecordset(dynamic signal) { // Allows for custom protocols. ISemanticTypeStruct respProtocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("HaveImageMetadata"); dynamic respSignal = rsys.SemanticTypeSystem.Create("HaveImageMetadata"); List<dynamic> records = signal.Recordset; // TODO: What if more than one image filename matches? if (records.Count > 0) { dynamic firstMatch = records[0]; respSignal.ImageFile = firstMatch.ImageFile; ICarrier responseCarrier = CreateAPODRecordCarrier(); responseCarrier.Signal.URL = firstMatch.URL; responseCarrier.Signal.Keywords = firstMatch.Keywords; responseCarrier.Signal.Title = firstMatch.Title; responseCarrier.Signal.Explanation = firstMatch.Explanation; respSignal.Metadata = responseCarrier; // Off it goes! rsys.CreateCarrier(this, respProtocol, respSignal); } // else, APOD knows nothing about this image file, so there's no response. }
后退一步,前进两步
但是,还有一个重要部分缺失,那就是向前端描述可以对这些信息做什么。可视化器如何表明,例如,可以打开网页,或者标题或解释可以被读取或朗读?答案是正确利用语义类型系统。我们将 APOD 语义类型重写如下(引入“SemanticElement”元素)
<SemanticTypeStruct DeclType="APOD"> <Attributes> <SementicElement Name="PrimaryKey"/> <SemanticElement Name="URL"/> <SemanticElement Name="ImageFilename"/> <SemanticElement Name="Keywords"/> <SemanticElement Name="Title"/> <SemanticElement Name="Explanation"/> <NativeType Name="Errors" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
如果我们现在查看载体,我们可以看到语义类型是如何成为元素而不是值的。这是请求和响应
<Carrier Protocol="DatabaseRecord" ResponseProtocol="APOD" TableName="APOD" Action="select" Where="ImageFilename LIKE '%02mantar_feresten.jpg'" /> <Carrier Protocol="APODRecordset"> <Recordset> <APOD Errors="" /> <URL Value="http://apod.nasa.gov/apod/ap031206.html" /> <ImageFilename Filename="Images\02mantar_feresten.jpg" /> <Keywords> <Text Value="sundial, observatory, india" /> </Keywords> <Title> <Text Value="Jaipur Observatory Sundial" /> </Title> <Explanation> <Text Value="<a href="http://www.bomhard.de/_englisch/index.htm">Walk through</a> these doors and up the stairs to begin your journey along a line from Jaipur, India toward the <a href="ap980912.html">North Celestial Pole</a>. Such cosmic alignments abound in <a href="http://www.ferestenphoto.com/mantar_subdirect.html">marvelous Indian observatories</a> where the architecture itself allows astronomical measurements. The structures were <a href="http://www.atco-fr.com/cadrans/jaipur/jaip_uk.php3">built in Jaipur</a> and other cities in the eighteenth century by the Maharaja <a href="http://www.atributetohinduism.com/ articles_hinduism/46.htm">Jai Singh II (1686-1743)</a>. Rising about 90 feet high, this stairway actually forms a shadow caster or <a href="http://www.cosmicgnomon.com/sdindex.htm">gnomon</a>, part of what is still perhaps the largest <a href="http://www.sundials.org/">sundial on planet Earth</a>. Testaments to Jai Singh II's passion for astronomy, the design and large scale of his observatories' structures still provide impressively accurate measurements of <a href="http://www.nsta.org/awsday">shadows and sightings</a> of celestial angles." /> </Explanation> </Recordset> </Carrier>
这如何解决如何处理元数据信息的问题?嗯,现在,如果我们知道,例如,用户单击 URL,我们应该将哪个协议和信号打包到载体中——协议是语义类型“URL”。如果我们有一个接收器对具有此协议的载体感兴趣,那么当可视化器打包该元数据项时,它就会响应,正如我们接下来将看到的。
向前发展:处理元数据
我们基本上想向用户展示与关联图像可用的元数据,因此我们首先收集元数据并确定元数据是否可操作(它将是一个语义元素而不是本地类型)。
public void ProcessImageMetadata(dynamic signal) { ICarrier metadata = signal.Metadata; string protocol = metadata.Protocol.DeclTypeName; string path = signal.ImageFilename.Filename; string fn = Path.GetFileName(path); var carousel = carousels.FirstOrDefault(kvp => kvp.Value.ActiveImageFilename == fn); // The user could have removed the viewer by the time we get a response. if (carousel.Value != null) { InitializeMetadata(protocol, carousel.Value.MetadataPackets, metadata.Signal); } } // This is complex piece of code. /// <summary> /// Gets the metadata tags reflectively, so that we have a general purpose function for display image metadata. /// </summary> protected void InitializeMetadata(string protocol, List<MetadataPacket> packets, dynamic signal) { packets.Clear(); // Get all the native and semantic types so we can get the values of these types from the signal. List<IGetSetSemanticType> types = Program.SemanticTypeSystem.GetSemanticTypeStruct(protocol).AllTypes; // Get the type of the signal for reflection. Type t = signal.GetType(); // For each property in the signal where the value of the property isn't null (this check may not be necessary)... t.GetProperties().Where(p => p.GetValue(signal) != null).ForEach(p => { // We get the value, which is either a NativeType or SemanticElement object obj = p.GetValue(signal); string itemProtocol = null; // If it's a SemanticElement, then we have a protocol that we can use for actionable metadata. // We would package up this protocol into a carrier with the metadata signal in order to let // other receptors process the protocol. if (obj is IRuntimeSemanticType) { itemProtocol = p.Name; } // Here we the IGetSetSemanticType instance (giving us access to Name, GetValue and SetValue operations) for the type. IGetSetSemanticType protocolType = types.Single(ptype => ptype.Name == itemProtocol); // Create a metadata packet. MetadataPacket metadataPacket = new MetadataPacket() { ProtocolName = itemProtocol, Name = p.Name }; // Get the object value. This does some fancy some in the semantic type system, // depending on whether we're dealing with a native type (simple) or a semantic element (complicated). object val = protocolType.GetValue(Program.SemanticTypeSystem, signal); // If the type value isn't null, then we have some metadata we can display for the image. val.IfNotNull(v => { metadataPacket.Value = v.ToString(); packets.Add(metadataPacket); }); }); }
现在我们可以想出一个显示元数据的方案,例如,在图像下方。但是我们将限制内容宽度。
kvp.Value.MetadataPackets.ForEach(meta => { Rectangle region = new Rectangle(location.X, y, location.Width, 15); string data = meta.Name + ": " + meta.Value; e.Graphics.DrawString(data, font, whiteBrush, region); y += 15; });
其中 kvp.Value 是“聚焦”图像的轮播。结果现在是元数据出现在查看器轮播中聚焦图像的下方。
现在,有趣的部分是,当我们单击这些元数据字段之一时,我们可以创建语义元素的载体,这些元素可以执行某些操作,当然是基于“监听”该协议的接收器想要做什么。这在可视化器中处理,并且由于与语义类型系统的交互而有点复杂。
protected bool TestImageMetadataDoubleClick(Point p) { ISemanticTypeSystem sts = Program.SemanticTypeSystem; foreach(var kvp in carousels) { Rectangle imgArea = kvp.Value.ActiveImageLocation; int imgidx = kvp.Value.ActiveImageIndex; int idx = -1; foreach(var meta in kvp.Value.Images[imgidx].MetadataPackets) { ++idx; Rectangle metaRect = new Rectangle(imgArea.Left, imgArea.Bottom + 10 + (MetadataHeight * idx), imgArea.Width, MetadataHeight); if (metaRect.Contains(p)) { // This is the metadata the user clicked on. // Now check if it's semantic data. In all cases, this should be true, right? if (!String.IsNullOrEmpty(meta.ProtocolName)) { // The implementing type is a semantic type requiring a drill into? if (sts.GetSemanticTypeStruct(meta.Name).SemanticElements.Exists(st => st.Name == meta.PropertyName)) { // Yes it is. Emit a carrier with with protocol and signal. string implementingPropertyName = sts.GetSemanticTypeStruct(meta.ProtocolName).SemanticElements.Single(e => e.Name == meta.PropertyName).GetImplementingName(sts); ISemanticTypeStruct protocol = Program.SemanticTypeSystem.GetSemanticTypeStruct(meta.PropertyName); dynamic signal = Program.SemanticTypeSystem.Create(meta.PropertyName); protocol.AllTypes.Single(e => e.Name == implementingPropertyName).SetValue(Program.SemanticTypeSystem, signal, meta.Value); Program.Receptors.CreateCarrier(null, protocol, signal); // Ugh, I hate doing this, but it's a lot easier to just exit all these nests. return true; } else if (sts.GetSemanticTypeStruct(meta.Name).NativeTypes.Exists(st => st.Name == meta.PropertyName)) { // No, it's just a native type. ISemanticTypeStruct protocol = Program.SemanticTypeSystem.GetSemanticTypeStruct(meta.ProtocolName); dynamic signal = Program.SemanticTypeSystem.Create(meta.ProtocolName); sts.GetSemanticTypeStruct(meta.ProtocolName).NativeTypes.Single(st => st.Name == meta.PropertyName).SetValue(Program.SemanticTypeSystem, signal, meta.Value); Program.Receptors.CreateCarrier(null, protocol, signal); // Ugh, I hate doing this, but it's a lot easier to just exit all these nests. return true; } // else: we don't have anythin we can do with this. } } } } return false; }
URL 元数据在 Web 浏览器中打开页面
例如,当我们单击 URL 时,我们可以打开一个 Web 浏览器。为什么会这样?因为存在一个语义类型“URL”。
<SemanticTypeStruct DeclType="URL"> <Attributes> <NativeType Name="Value" ImplementingType="string"/> </Attributes> </SemanticTypeStruct>
这定义了协议,URL 接收器会关注这一点。
public string[] GetReceiveProtocols() { return new string[] { "URL" }; } public void ProcessCarrier(ICarrier carrier) { string url = carrier.Signal.Value; try { Process.Start(url); } catch { // Eat exceptions. } }
同样,如果标题、解释和关键词是实现“Text”语义类型的语义类型,我们已经为该类型建立了一个协议,并且可以由适当的接收器接收。
如果您一路读完本文,那么这个系统的灵活性可能会变得显而易见!轻量级接收器响应可以根据需要添加和移除的协议,以根据个人特定目的配置系统。
搜索数据库
这是一个重要的部分,通过一个接收器实现,该接收器创建一个小窗口用于输入搜索字符串,并在用户按下 Enter 键时发出一个带有“SearchFor”协议的载体,APOD 接收器理解该协议。如果与数据存储相关联的其他接收器收到此信息,它们也将执行搜索。
想象一下输入一个搜索字符串,能够查询各种数据源(尤其是 Google 无法索引的数据源),而不是必须启动每个访问数据库的应用程序并使用它来查询结果。
一个简单的 SearchFor 接收器提供了输入搜索字符串的功能,当用户按下 Enter 键时,将创建一个带有 SearchFor 协议的载体和一个带有 TextBox 文本的信号。
protected void CreateForm() { Form form = new Form(); form.Text = "Search For:"; form.Location = new Point(100, 100); form.Size = new Size(500, 60); form.TopMost = true; tb = new TextBox(); tb.KeyPress += OnKeyPress; form.Controls.Add(tb); tb.Dock = DockStyle.Fill; form.Show(); form.FormClosed += WhenFormClosed; } protected void OnKeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == '\r') { ISemanticTypeStruct protocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("SearchFor"); dynamic signal = rsys.SemanticTypeSystem.Create("SearchFor"); signal.SearchString = tb.Text; rsys.CreateCarrier(this, protocol, signal); } }
APOD 接收器对该协议感兴趣,并在收到载体时查询数据库。任何返回的记录都将转换为“ImageFilename”载体协议信号。
/// <summary> /// Search the APOD database for matches. /// </summary> protected void SearchFor(dynamic signal) { string searchFor = signal.SearchString; ISemanticTypeStruct dbprotocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("DatabaseRecord"); dynamic dbsignal = rsys.SemanticTypeSystem.Create("DatabaseRecord"); dbsignal.TableName = "APOD"; dbsignal.Action = "select"; dbsignal.ResponseProtocol = "APODSearchResults"; // will respond actuall with "APODRecordset" // TODO: Use parameters dbsignal.Where = "Keywords LIKE '%" + searchFor + "' or Title LIKE '%" + searchFor + "' or Explanation LIKE '%" + searchFor + "'"; rsys.CreateCarrier(this, dbprotocol, dbsignal); } /// <summary> /// Create carriers for the images that meet the returned search criteria. /// </summary> protected void ProcessSearchResults(dynamic signal) { List<dynamic> records = signal.Recordset; foreach (dynamic d in records) { ISemanticTypeStruct outprotocol = rsys.SemanticTypeSystem.GetSemanticTypeStruct("ImageFilename"); dynamic outsignal = rsys.SemanticTypeSystem.Create("ImageFilename"); outsignal.Filename = d.ImageFilename.Filename; rsys.CreateCarrier(this, outprotocol, outsignal); } }
无需执行其他操作,因为我们可以利用缩略图创建器和缩略图查看器接收器来显示匹配的图像,获取元数据等。顺便说一句,我们也可以简单地将大量图像拖放到表面上,系统将尝试查找它们的元数据。您显然可以有多个元数据源。
现在,如果我们做一些花哨的事情,例如,我们可以比较两个星系,M31 和 M33。这是通过使用两个不同的缩略图查看器并禁用第一个(双击它以切换启用/禁用)来完成的,以便将第二个搜索定向到第二个缩略图。这是搜索 M31 的结果(注意左侧禁用的查看器)。
现在,我们禁用右侧的缩略图查看器,启用左侧的,然后搜索 M33。
结论
到目前为止,我创建的接收器是:
APODEventGeneratorReceptor | 这将生成 20 年的“抓取此页面”事件。将此接收器拖放到表面上以启动抓取过程并构建数据库和图像库。 |
APODScraperReceptor | 负责从 APOD 网站实际抓取 HTML。 |
CarrierExporterReceptor | 创建接收器接收的所有载体的日志文件(仅接收到的载体,如果没有接收器,则看不到日志条目)。 |
HelloWorldReceptor | 我的第一个测试接收器。 |
ImageViewerReceptor | 将图像显示为单独的窗口,按比例缩放到窗口尺寸。 |
ImageWriterReceptor | 写入缩略图图像,在文件名后附加“-thumbnail”。 |
LoggingReceptor | 我编写的第二个测试接收器,为调试消息信号生成弹出窗口。 |
PersistenceReceptor | 实现 SQLite 数据库持久化。 |
SearchForReceptor | 显示一个小窗口用于输入搜索文本,并在用户按下 Enter 键时发出带有该文本的载体。 |
TextDisplayReceptor | 显示文本协议的文本。 |
TextToSpeechReceptor | 朗读文本转语音协议和文本协议。 |
ThumbnailCreatorReceptor | 将图像转换为缩略图并在载体上发出图像。 |
ThumbnailViewerReceptor | 响应缩略图图像协议并在轮播中显示图像。 |
TimerReceptor | 在指定时间间隔发生时触发事件(在 XML 中声明)。 |
UrlReceptor | 使用指定的 URL 启动默认浏览器。 |
WeatherInfoReceptor | 整理天气和邮政编码信息并发出文本转语音载体。 |
WeatherServiceReceptor | 接收邮政编码协议并查询 NOAA 以获取该地点的今日天气。 |
ZipCodeReceptor | 接收邮政编码协议并查询 Web 服务以获取该地点的城镇/城市和州。 |
我们实现了两个特定于生成 APOD 抓取事件和实际解析 APOD 网页的接收器。我们利用现有接收器来创建和显示缩略图。我们添加了几个通用接收器。在此过程中,我们还添加了许多底层行为,尤其是在可视化器中,例如轮播(还有些需要打磨!)。
这是一个我没有实现的接收器——将文本翻译成另一种语言。这里有一个Ravi Bhavnani 的 Code Project 文章——将其放入一个接收文本协议的接收器中,您就可以即时翻译任何文本信号。
总而言之,我们也证明了 HOPE 可以作为一个环境来做一些真正有用的事情。网页抓取器展示了系统的健壮性,处理了数千个页面,并利用了 .NET 的异步 Task 库。