LINQ to World Wide Web (www) - LINQ 的另一种风味






4.90/5 (22投票s)
LINQ to World Wide Web 是一个查询 REST 类网站的有用工具。
引言
搜索“数据,维基”的结果是:
“数据是指通常通过经验、观察或实验收集到的信息或事实,或计算机系统中的过程,或前提。数据可以包括数字、文字或图像,特别是作为一组变量的测量值或观察值。数据通常被视为抽象的最低层次,信息和知识从中派生。”
“数据为王”,一位著名作者这样开始解释 LINQ。正如 MSDN 所建议的——用简单的术语解释 LINQ——“LINQ 是一个通用的查询工具(或功能),用于查询数据。不仅是关系数据或 XML,而是所有信息数据源。在过去的几年里,我们已经有了几种 LINQ 的风味:LINQ to SQL、LINQ to XML、LINQ to CSV 文件、LINQ to Text 文件等。所有这些都针对特定的数据介质(或形式)。LINQ 的目标是提供一个通用简单的接口来查询数据。
如今,数据通过各种不同的渠道以多种不同的格式呈现,例如数据库、XML、原始文本、文本文件、二进制文件、通过 TCP、UDP、HTTP、FTP 等的 RSS feed。'LINQ to www' 是用于查询符合要求的(REST 类)网站数据的 LINQ。举个例子:假设您有一个喜欢的汽车列表网站,它在其网站上显示了成千上万页的汽车列表。使用 LINQ2www 工具,您可以查询该网站以获取所需信息。例如,您可以使用 LINQ2www 查询价格大于 15,000 美元但小于 30,000 美元的汽车。这意味着,您无需手动浏览数百个连续的网页来提取符合您兴趣(15,000 美元至 30,000 美元)的汽车列表。您所要做的就是编写一个适当的 LINQ 查询来提取这些信息。同样的原理可以应用于金融数据页面、公司账户数据、网络电话簿等等。
您想如何阅读?
最好按顺序阅读本文。但是,并非每个人都处于相同的情况。因此,这里有一个简短的指南,可以帮助您快速找到所需内容。
- 如果您是一位好奇的读者,想看看这是如何完成的——欢迎您,请跳过本节,继续阅读下一节。
- 如果您是一位可以通过阅读标题来理解文章内容的极客,并且今天您只需要知道:如何使用 LINQ2www 程序集——请跳转到“代码用法”部分。您可能还会对“有趣的点”部分感兴趣。
- 如果您是一位管理人员,想看到一个可行的示例,并且您只需要演示该库的用法——请跳转到“Web 爬虫应用程序”部分:一个基于 WPF 的 Web 爬虫,将数据建模为图形化的 3D 信息。下载演示,然后按“开始”按钮。
- 如果您不属于以上三个兴趣类别中的任何一个,请在本文章底部通过链接给我留言。嗯,别忘了详细说明您的兴趣类别。
Web 爬虫应用程序
先决条件:要运行演示,您需要在机器上安装 .NET framework 3.5。如果尚未安装,可以从此处免费下载:http://www.microsoft.com/downloads/details.aspx?FamilyId=333325FD-AE52-4E35-B531-508D977D32A6&displaylang=en
Web 爬虫是一个示例 WPF 应用程序,它使用 LINQ2www 程序集。该应用程序逐页爬取 CodeProject 的“Who's who”页面。要查看此应用程序的演示,请从本文的第一行下载演示项目 - 运行应用程序。按“开始”按钮。给它 2 到 3 分钟的时间,您将看到数据从 CodeProject 服务器流式传输到您计算机的 3D 条形图。您可以在 WPF 应用程序底部的状态栏中观察到连续的更新。
什么是 REST 及其与此库的关系?
“表示性状态转移”是像万维网这样的软件系统的架构风格。所有功劳归功于 Roy Fielding 的博士论文(参考文献 1)。LINQ2www 是一个能够查询基于 REST(符合规范)的网站的工具。例如,考虑 CodeProject 的“Who's who”的第一页:https://codeproject.org.cn/script/Membership/Profiles.aspx?ml_ob=MemberId&mgtid=-1&mgm=False&pgnum=1。
现在要获取第二页,我们只需要删除页码 1 并替换为页码 2。即,将 pgnum=1 替换为 pgnum=2 以转到下一页,依此类推……这是一个 RESTful Web 接口。
使用上述网页链接,我们可以从第 1 页到第 1000 页浏览多达 1000 名成员。现在,考虑您的要求只是获取这 1000 个网页中的黄金会员列表。手动浏览(遍历)所有 1000 个页面既乏味又容易出错。编写一个小程序来自动执行此操作是一个好主意。将这样一个程序泛化以解决类似问题可以被认为是下一步。如何泛化到这种程度:您所要做的就是编写“两行代码”即可从 1000 个网页中获取所有黄金会员?这就是 LINQ 的强大功能。我们正在专门化 LINQ 以实现我们的“两行代码”目标,这称为 LINQ2www - LINQ 2 World Wide Web。还有一个重要的方面需要提及,我们不检查或需要 100% 符合 REST 的网站。LINQ2www 可以与 REST 类网页链接协同工作。LINQ2www 工作的基础要求是所有页面都需要通过 href 链接相互连接。即使它在其他方面不符合 REST,LINQ2www 也能正常工作。
LINQ 的工作原理 - 基本理解
理解 LINQ 的工作原理对于将其专门化以满足我们的需求至关重要。让我们从一个非常简单的例子开始。考虑我们有一个输送液体的管道。管道中流淌着绿色、红色和蓝色混合的液体。有人不断地通过这个管道输送这些混合液体。您这边唯一知道的是,如果您打开水龙头(水嘴),就会有混合液体流出。假设您只需要绿色液体。您准备了一个只能过滤绿色液体的过滤器。现在,当我们把这个过滤器安装在水龙头(水嘴)上时,只有绿色液体会通过管道流出。这个过滤器就是 LINQ,混合液体就是原始数据。LINQ 帮助您从原始数据流中提取信息。根据液体类型,您需要不同的过滤器。同样,根据数据种类,您需要不同的 LINQ。这就是我们有 LINQ 2 SQL、LINQ to XML 等的原因。
LINQ 的工作原理 - 一点 LINQ 代码
让我们做一个小的代码示例。假设我们有一个姓名列表。我们只需要以“S”开头的姓名。让我们尝试编写一行 LINQ 代码来获得我们想要的东西。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TestLinq4
{
class Program
{
static void Main(string[] args)
{
List<string> listOfNames = new List<string>()
{
"Nina", "Kyle", "Steven",
"Joe", "Neal", "Sanjay", "Elijah",
"Steen", "Stech", "Donn",
"Thomas", "Peter", "Steinberg"
};
var Result = from Name in listOfNames where Name.StartsWith("S") select Name;
foreach (string NameReturned in Result)
{
System.Console.WriteLine(NameReturned);
}
}
}
}
在这里,我们所做的只是编写一个简单的 LINQ 语句来获得查询结果。当我们使用 foreach
枚举 Result
时,查询实际上会执行。数据会根据我们的 where
条件进行过滤。然后,如果它通过了 where
条件,就会被选中并返回到 foreach
中的 NameReturned
变量。
LINQ 的工作原理 - 'where' 方法在哪里?
让我们仔细看看。第一次看到上面的代码可能会出现几个问题。一个重要的问题可能是:where
方法在哪里?select
方法在哪里?以及到底发生了什么。Where
是 IEnumerable
类中的一个方法。这是一个特殊的称为“扩展方法”的方法。顾名思义,您可以通过编写静态方法来扩展一个类(甚至是一个 sealed
类)。我们很快就会看到如何编写一个。Where
是一个重要的方法,因为它是我们的过滤器。
首先,让我们看看什么是扩展方法以及如何编写一个。然后,我们将重写(专门化)上面示例中的默认 where
方法,为其添加一个附加功能。
考虑一个众所周知的 .NET 类 - String
。我们知道 String
是一个 sealed
类。这意味着我们不能专门化(继承)String
类。但是,我们可以通过添加一个扩展方法来扩展 String
类。这个扩展方法会显示得好像它是 String
类的一个公开方法。我们现在来编写一个。
public static class StringExtension
{
public static bool CompareCaseInsensitive(this string strSource, string strTarget)
{
if (string.Compare(strSource, strTarget, true) == 0)
{
return true;
}
return false;
}
}
请注意,扩展方法定义在静态类 StringExtension
中。另请注意,静态扩展方法的第一个参数以 this
关键字开头,即它正在扩展 String
类。下图显示了当我们定义了上述扩展方法时的 intellisense。
在学习了扩展方法之后,就不难发现 where
是 IEnumerable
类的扩展方法。现在,让我们看看我们的 LINQ 语句是如何转换的,这可以增进我们的理解。
var Result = from Name in listOfNames where Name.StartsWith("S") select Name;
被转换为
var Result = listOfNames.Where(delegate(string item)
{ return item.StartsWith("S");} ).Select(delegate(string item){ return item; } );
LINQ2www - 挑战
在获得足够的背景知识后,让我们看看编写 LINQ to www(World Wide Web)的挑战。
- 数据不容易获取进行过滤。考虑浏览数千个网页。该库需要先获取网页,然后使用过滤条件(LINQ 语句中的
where
条件)来过滤网页数据。这可能会长时间阻塞查询执行。这是不可接受的。 - 假设(1)中的所有内容都进展顺利,用户正在观察过滤信息的连续流。现在用户认为她已经获得了足够的信息,并希望停止更新。这种情况可能经常发生,不仅因为从网站连续更新耗时,而且因为已获得的信息足以做出决定。
- 我们没有标准语言来查询此类数据。考虑数据库;我们有 SQL 类似的语言来查询它们。但对于网站的 HTML 数据呢?我们没有标准的查询语言。但是,如果它是一个 REST 类页面,我们可以预期它具有某种标准格式。
- 我们应该提供一种接口,以便可以使用一个 HTTP 连接同时执行多个查询。这提高了客户端的性能并节省了服务器资源。
使用代码
让我们看看如何使用代码。这将帮助我们理解一些挑战是如何解决的。
让我们以 CodeProject 的 Who's who 网页链接为例。假设我们的目标是获取 CodeProject 中不同类型的会员状态。执行此查询的代码如下:
Linq2www linq2wwwUrl = new Linq2www("https://codeproject.org.cn/script/" + "Membership/Profiles.aspx?mgtid=1&%3bmgm=True" + "&ml_ob=MemberId&mgm=False&pgnum=1", "https://codeproject.org.cn/script/Membership/Profiles.aspx?" + "mgtid=1&%3bmgm=True&ml_ob=MemberId&mgm=False&pgnum="); int CancelId = from webItem in linq2wwwUrl where webItem.GetMatchDyn( @"class=""Member(?<name>.*?)"">(\k<name>)", this.CallThisMethod) select webItem;
第一行构造函数接受两个参数。第一个参数是起始网页地址链接。下一个是可选参数,它告诉我们在获取第一个页面时,查找下一个页面链接,该链接看起来像这个参数(即第二个参数)。
下一行是一个与常规 LINQ 语句略有不同的 LINQ 语句。正如您可能猜到的,我们正在使用正则表达式从该网站提取所需信息。另一个不同之处在于,我们传入了一个回调方法,以便在更新时调用。这意味着,当像 Web 爬虫/爬虫一样遍历网页时,如果找到任何匹配此正则表达式的内容,它就会调用提供的回调方法(CallThisMethod
)。此方法将持续调用,直到访问完所有页面及其链接。但是,用户可以随时使用返回的整数值 CancelId
取消此查询。执行此操作的代码如下:
linq2wwwUrl.CancelUpdate(cancel1);
因此,我们现在知道上一节中提到的挑战(1)、(2)和(3)是如何解决的。我们使用回调方法来更新查询调用者。这将使用户能够根据需要取消更新。我们从这个 LINQ 查询中只需要过滤后的信息,这些信息是异步接收的。由于我们没有标准的语言来查询 HTML 数据,因此我们使用正则表达式,这是一种强大的工具,可以查询任何原始的、类文本的数据。
LINQ2www - 重写 'where' 方法
为什么这个 LINQ 是另一种风味?LINQ2www 是另一种风味,因为我们在这种 LINQ 中做了两件不寻常的事情,这将在本节中解释。
where
方法是 LINQ2www 中唯一重写的方法。where
方法的目的在这里有点不寻常。where
方法仅设置条件和回调。不寻常之处在于:它不枚举数据。这是因为,如您所知,在调用 where
方法时,完整的数据不可用。但是,它会在类中设置必要的信息,以便回调将开始获取过滤后的信息。where
方法的实现如下:
public static class LinqExtnsnOvrride
{
public static int Where<linq2www>(
this IEnumerable<linq2www> enumLinq2www, Func<linq2www,> predicate)
{
enumLinq2www.GetEnumerator().Reset();
Linq2www Item = enumLinq2www.First();
return predicate(Item); // To set the Condition and Callback
}
}
where
方法所做的下一个不寻常之处是,它返回一个整数。这是在回调方法中停止接收更新时需要传递的 ID。第一行和第二行将枚举器重置到集合中的第一个项。下一行调用回调,从而设置正则表达式来过滤并设置回调函数。
LINQ2www - 整合
让我们将我们所讲的一切整合起来,解释 LINQ2www
类。让我们遵循自顶向下的方法。
Linq2www linq2wwwUrl = new Linq2www(WebLink, webLinkTemplate);
当调用 LINQ2www
构造函数时,我们创建一个后台线程。该线程的工作是获取网页链接的内容。我们将其存储在一个 multimap 中(参考文献 3)。Multimap 是一种复杂的类似字典的集合类。它可以存储键值对。首先,我们将网页链接及其内容存储在 multimap 中。接下来,我们解析(遍历)网页链接的内容以查找连接的下一页。如果提供了可选的第二个参数,我们将使用它作为模板来查找下一页。否则,我们将使用网页链接来创建一个连接模板。一旦找到下一页,我们再次执行相同的操作——将链接和内容存储在 multimap 中。然后,我们解析其内容以查找下一个链接。我们一直这样做,直到没有更多*新*链接可浏览为止。
下面是我们上面示例中解释的第二行。这一行实际上为我们从整个数据中获取了有用的过滤信息。正如您将注意到的,我们向 GetMatchDyn
传递了两个参数。第一个参数是正则表达式 - 过滤器。第二个参数是回调。这个回调会连续接收过滤后的信息。
int CancelId = from webItem in linq2wwwUrl where
webItem.GetMatchDyn(MyRegularExpression, this.CallThisMethod) select webItem;
让我们看看我们是如何做到的。在上一节中,我们看到了 where
方法的重写。当我们执行上面的行时,就会调用我们重写的 where
方法。正如我们在上面一节中看到的,where
方法调用 GetMatchDyn
。这意味着 where
方法调用委托,委托又调用 GetMatchDyn
。GetMatchDyn
创建一个线程。该线程读取存储在 multimap 中的数据。该线程逐项移动(枚举)multimap 中的项,以读取每个网页链接的数据。它使用调用者传递给 GetMathDyn
的正则表达式来过滤数据。一旦正则表达式匹配,它就会调用用户传递的回调方法。请记住,这是 GetMatchDyn
方法的第二个参数。
上图解释了我们本节中的描述。
最后但并非最不重要的是,我们应该提供一个取消 LINQ 调用 的方法。正如我们之前所见,由于这是从 HTTP 进行的连续更新,因此可能非常耗时。用户应该能够随时取消更新。这可以通过使用我们进行的 LINQ 调用 的返回值(整数)轻松完成。下面的行执行此操作:
linq2wwwUrl.CancelUpdate(CancelId);
这个简单的方法定义如下:
public bool CancelUpdate(int ThreadId)
{
bool retVal = false;
Regex regDet = threadDetails.GetFirstItem(ThreadId);
if (regDet != null)
{
retVal = threadDetails.Remove(ThreadId);
}
lock (this) Monitor.PulseAll(this);
return retVal;
}
我们所做的就是:我们删除在调用 GetMatchDyn
方法时先前存储的正则表达式。然后,我们触发 GetMatchDyn
方法创建的线程。当该线程尝试读取正则表达式(它所关联的)时,它将获得一个 null 值。这是因为,我们在触发线程之前刚刚删除了它。由 GetMatchDyn
方法创建的线程将优雅地关闭。因此,回调将不再接收任何更新。
关注点
- 这个项目可以很容易地扩展到搜索任何类型的网页,而不仅仅是 REST 类 www。
- 如果有人有创造力,能够提出一种比正则表达式更容易查询 HTML 数据 的语言,那么这个项目就可以扩展到支持该语言,方法是实现一个
IQueryable
接口。 - LINQ2www 是一种 Web 机器人。所以我们需要遵守一定的标准(参考文献 5)。
- 不幸的是,我们使用正则表达式作为过滤数据的工具。并非每个人都熟悉使用正则表达式。如果您是正则表达式新手,参考文献 6 是一个很好的入门读物。阅读之后,您可以考虑阅读参考文献 7 或 8。我认为参考文献 7 是一个有用的快速参考。
- 示例应用程序源代码中使用的 WPF 3D 条形图也可在 CodeProject 上找到 - 参考文献 4。
- multimap 源代码可在参考文献 3 中找到。
重要提示!
我想听听大家的反馈。请留下详细的留言,无论您的投票如何。谢谢!
参考文献
- http://en.wikipedia.org/wiki/Representational_State_Transfer
- http://msdn.microsoft.com/en-us/library/bb308959.aspx
- https://codeproject.org.cn/KB/cs/MultiMap_P_2.aspx
- https://codeproject.org.cn/KB/WPF/WPF_3D_Bar_chart_control.aspx
- http://en.wikipedia.org/wiki/Representational_State_Transfer
- http://www.radsoftware.com.au/articles/regexlearnsyntax.aspx
- https://boost.ac.cn/doc/libs/1_38_0/libs/regex/doc/html/index.html
- http://msdn.microsoft.com/en-us/library/2k3te2cs(VS.80).aspx
历史
- 2009 年 4 月 26 日 - 第一个版本。