LINQ To Google Image and Google Groups





5.00/5 (15投票s)
2007 年 5 月 1 日
10分钟阅读

97900

415
Google 图片/群组搜索的 LINQ 实现
应用程序
这是一个我为了更好地理解 LINQ 内部工作原理而创建的“宠物项目”。它基本上为Google 图片搜索提供了一个 LINQ 查询接口。选择图片搜索而非更流行的网页搜索的原因是图片搜索更“结构化”。例如,您可以根据图片的大小、颜色或文件格式搜索图片。
由于目前关于“LINQ”的文档不多,大部分实现是基于使用“Reflector”检查 DLINQ 程序集。毋庸置疑,解决方案文件是使用 Visual C# Express (Orcas) 的 Beta 版创建的,最终产品发布时可能会有变化。
例如,该应用程序能够执行以下 LINQ 查询
var test = from img in ImageSearch.Instance
where (img.RelatesTo("SQL")
|| img.RelatesTo("Windows"))
&& img.RelatesTo("Microsoft")
&& !img.RelatesTo("2005")
&& img.Size == ImageSize.Small
&& img.Format == ImageFormat.GIF
&& img.Domain == "www.microsoft.com"
orderby img.Rank
select img
将返回匹配搜索条件的一系列 `Image` 对象,然后可以通过 `foreach` 语句进行迭代。在这种情况下,搜索条件是:小尺寸 GIF 格式图片,必须与“Microsoft”相关,并且与“SQL”或“Windows”相关,但不与“2005”相关,且在“www.microsoft.com”域内。
以下是 `Image` 类的定义
namespace MChen.Linq.Google
{
public enum ImageFormat
{
Any, JPG, PNG, GIF
}
public enum ImageSize
{
Any, Large, Medium, Small
}
public enum ImageColor
{
Any, BlackWhite, Greyscale, Color
}
public class Image
{
public int Rank { get; internal set; }
public Uri Url { get; internal set; }
public string Domain { get; internal set; }
public string Description { get; internal set; }
public ImageFormat Format { get; internal set; }
public ImageSize Size { get; internal set; }
public ImageColor Color { get; internal set; }
public int Width { get; internal set; }
public int Height { get; internal set; }
public int FileSize { get; internal set; }
public string ThumbnailURL { get; internal set; }
public int ThumbnailWidth { get; internal set; }
public int ThumbnailHeight { get; internal set; }
}
}
但是,`Image` 类的并非每个字段都可以用作查询条件。查询的功能受到 Google 上可用搜索容量的限制。例如,您不能搜索特定 `Width` 或 `Height` 的图像,也不能按 `FileSize` 对结果进行排序。如果您尝试任何此类查询,很可能会收到“不支持”异常。
Google 没有为其图片搜索公开任何 API(或者至少我没有找到任何),所以搜索功能是通过发送 HTTP 请求,然后用“正则表达式”解析结果来实现的。由于 LINQ 是本次练习的重点,我不会深入探讨请求和从 Google 提取结果的细节。如果感兴趣,可以查看源代码。
IQueryable 链和表达式树
LINQ 的核心是表达式树和 `IQueryable` 接口的概念。`IQueryable` 是编译器非常了解的特殊接口。当编译器看到一个实现了 `IQueryable` 的类实例在 LINQ 查询的 from 子句中使用时,它会自动将查询的其余部分的每个部分转换为表达式树(如果对象只实现了 `IEnumerable`,生成的代码会大不相同,实际上会简单得多)。对于那些学习过编译器理论的人来说,表达式树本质上是一个表示源代码片段的数据结构,在这种情况下就是 LINQ 查询。编译器或解释器可以遍历表达式树以生成实际的机器指令,或者只是解释和执行表达式。作为一个快速示例,语句“`x + 3`”可以转换为一个 `BinaryExpression`,其运算符是 `Add`,两个操作数是变量“`x`”和常量值“`3`”。
表达式树被用作中间数据结构有两个原因:首先,它将源代码的转换和底层机器代码的生成分离开来。在“`x + 3`”示例中,相同的 `BinaryExpression` 可以用于生成 Intel X86、MSIL 或 JVM 指令。第二个原因是许多优化技术使用表达式树更容易实现。例如,如果编译器在相同的代码片段中发现两个结构相同的表达式树,它将创建一个临时变量来保存表达式树的结果,并在两个地方重复使用它。
在上面的 LINQ 示例查询中,生成了两个表达式树来表示查询的 `Where` 和 `Order By` 部分。也应该生成另一个表达式树来表示 `Select`,但由于我们在 `Select` 中没有做太多事情,编译器足够聪明,将其优化掉了。如果我们在那里放置一个投影运算符,就会出现一个 `Select` 表达式树。
查询随后被翻译为
//Expression tree for Where clause
Expression<...> whereClause = ...;
//Expression tree for orderBy clause
Expression<...> orderByClause = ...;
IQueryable<Image> whereResult
= ImageSearch.Instance.CreateQuery(whereClause);
IQueryable<Image> orderByResult
= whereResult.CreateQuery(orderByClause);
正如我们所看到的,编译器通过 `CreateQuery` 函数调用将两个 `IQueryable` 链接在一起。对 `ImageSearch.Instance`(一个实现 `IQueryable
IQueryable 的剖析
`IQueryable
- `IEnumerable.GetEnumerator`:`IQueryable` 继承自 `IEnumerable`。在上一节中,我们看到 `IQueryable`s 被链接在一起,链中的最后一个作为查询结果返回。客户端可能会打算使用 foreach 迭代查询结果。为了实现这一点,链中至少最后一个 `IQueryable` 对象必须实现 `GetEnumerator` 方法以返回一个有效的 `IEnumerator` 来迭代查询结果。另一方面,对于那些永远不会用作链中最后一个的 `IQueryable` 类,则不需要实现此方法。
- `Execute` 方法:当查询结果是标量(例如,`Count`、`Sum` 等)时,会调用此方法。由于我们的 Google 图片搜索不支持任何标量查询,我们只是在此方法的实现中抛出异常。
- `CreateQuery` 方法:对于返回结果列表的查询,这里就是所有神奇发生的地方。此方法遍历表达式树并生成最终可执行文件。对于 DLINQ,这将是 SQL 语句。对于我们的 Google Image LINQ,这是我们将发送请求搜索结果的实际 URL。然后它返回一个封装了这些“可执行”信息的 `IQueryable` 对象。如果这是链中的最后一个链接,当客户端遍历结果时(返回到 `GetEnumerator`),“可执行文件”将实际执行;如果链中还有更多链接,这个 `IQueryable` 将把自己的表达式树信息合并到“可执行文件”中并传递下去。
`IQueryable` 的另一个值得注意的成员是 `Expression` 属性。这个属性基本上要求 `IQueryable` 将自身封装为一个表达式,然后 LINQ 框架可以将这个表达式放入表达式树并沿着链传递。一个简单而标准的属性实现是将自身封装到一个 `ConstantExpression` 中。
public System.Linq.Expressions.Expression Expression
{
get { return Expression.Constant(this); }
}
如果您检查表达式树的打印输出,此 `ConstantExpression` 位于根节点的第一个参数中。
实现 IQueryable.CreateQuery
Google 图片搜索请求可以通过以下结构进行概括,该结构直接映射 Google 上的高级图片搜索网页:
internal class ImageQueryInfo
{
public List<string> AllWords = new List<string>();
public List<string> OrWords = new List<string>();
public List<string> NotWords = new List<string>();
public ImageSize Size { get; set; }
public ImageFormat Format { get; set; }
public ImageColor Color { get; set; }
public string Domain { get; set; }
}
`CreateQuery` 方法的任务仅仅是将传入的表达式树转换为等效的 `ImageQueryInfo` 对象。这是通过递归遍历表达式树,逐个处理节点并将相关信息填充到 `ImageQueryInfo` 对象中来实现的。
//entry point
public ImageQueryInfo BuildQuery(Expression exp)
{
ImageQueryInfo qinfo = new ImageQueryInfo();
return Visit(exp, qinfo);
}
private ImageQueryInfo Visit(Expression node, ImageQueryInfo qinfo)
{
switch (node.NodeType)
{
case ExpressionType.AndAlso:
return VisitAnd((BinaryExpression)node, qinfo);
case ExpressionType.OrElse:
return VisitOr((BinaryExpression)node, qinfo);
case ExpressionType.Equal:
return VisitEquals((BinaryExpression)node, qinfo);
case ExpressionType.Call:
return VisitMethodCall(
(MethodCallExpression)node, qinfo, false, false
);
case ExpressionType.Lambda:
return VisitLambda((LambdaExpression)node, qinfo);
case ExpressionType.Not:
return VisitNot((UnaryExpression)node, qinfo);
//...
}
}
//process And expression
private ImageQueryInfo VisitAnd(BinaryExpression node, ImageQueryInfo qinfo)
{
if (node.NodeType != ExpressionType.AndAlso)
throw new ArgumentException("Argument is not AND.", "node");
//simply visit left and right
qinfo = Visit(node.Left, qinfo);
qinfo = Visit(node.Right, qinfo);
return qinfo;
}
//...
在处理表达式树的叶节点时,我们可以非常具体地处理细节。例如,由于我们只处理一个方法调用“`RelatesTo`”,我们可以检查方法名并在无法处理时生成异常。
Type declaringType = node.Method.DeclaringType;
if (declaringType == typeof(Image))
{
if (node.Method.Name == "RelatesTo")
{
//parse the parameter
if (node.Arguments.Count != 1 ||
node.Arguments[0].NodeType != ExpressionType.Constant)
throw new ArgumentException
( "Only constant search terms are supported.");
ConstantExpression cont =
node.Arguments[0] as ConstantExpression;
string term = cont.Value.ToString();
if (forceNot) qinfo.NotWords.Add(term);
else if (forceOr) qinfo.OrWords.Add(term);
else qinfo.AllWords.Add(term);
}
else
{
throw new ArgumentException(
string.Format(
"Method {0} is not supported.", node.Method.Name));
}
}
else
{
throw new ArgumentException(
string.Format("Method {0} is not supported.", node.Method.Name));
}
return qinfo;
实现 IQueryable.GetEnumerator
一旦我们有了包含所有查询条件的 `ImageQueryInfo` 对象,我们所需要做的就是发送正确的请求,获取响应并提取图片搜索结果。此逻辑在 `IQueryable.GetEnumerator` 方法中实现。借助 `yield` 语句,实现非常简单(`RequestForPage` 方法构造请求 URL,获取响应,然后用正则表达式解析它)。
public IEnumerator<T> GetEnumerator()
{
int cnt = 1;
//implementation of enumerator
while (true)
{
IList<T> batch = RequestForPage(cnt);
foreach (var img in batch)
{
cnt++;
yield return img;
}
//stop condition
if (batch.Count == 0) break;
}
}
LambdaExpression.Compile
使 `LambdaExpression` 在其他表达式类型中脱颖而出的一个特点是它的 `Compile` 方法。顾名思义,调用 `Compile` 将表达式树的数据表示转换为实际可执行代码,表示为委托。例如,在以下代码中:
Expression<Func<int, int>> expr = a => a + 3;
Func<int, int> func = expr.Compile();
Console.WriteLine(func(5));
表达式 `(a+3)` 实际上是在运行时编译的,一个指向编译方法的委托作为 `Func
但这与我们的 LINQ 实现有什么关系呢?在下一节中,我们将看到 `LambdaExpression.Compile` 为我们提供了一种快速简便的方法来支持某些查询结构,否则我们将需要不眠之夜才能实现。
查询参数化和投影
我们的 Google 图片查询简单而实用。但它并没有让我们走得足够远。例如,它无法处理参数化查询,例如:
string param = ...;
var test = from img in Google.Images
where img.RelatesTo("microsoft")
&& img.Format == (ImageFormat)
Enum.Parse(typeof(ImageFormat), param)
&& img.Domain == "www.microsoft.com"
orderby img.Rank
select img;
请注意,查询结果的 `Format` 部分作为变量传递,并且必须使用 `Enum.Parse` 进行评估。通常这不会是一个大问题,因为它不过是一个普通的运行时方法调用。但在 LINQ 领域,编译器不会为方法调用生成 MSIL,相反,方法调用会作为表达式树的一部分进行转换,就像查询的其余部分一样。
这给我们的 LINQ 实现带来了严重的问题,因为现在我们被迫处理几乎所有 .NET 语言构造:你可能会在表达式树中看到算术运算、方法调用甚至对象创建,你必须处理所有这些才能使查询语言完整。我们几乎在编写 C# 编译器的后半部分!(前半部分,语法解析,已经由真正的 C# 编译器完成了,因为我们手上有一个表达式树)。
`LambdaExpression.Compile` 应运而生。当我们不打算在查询中处理表达式时,我们总是可以将其转换为 `LambdaExpression`,并要求 .NET 代表我们编译并执行它。当我们在 LINQ 实现中处理 `equals` 运算符时,我们总是期望运算符的一侧是对 `Image` 类的直接成员访问,另一侧是常量表达式,或者现在,是可以在查询执行之前求值为常量表达式的东西。这可以通过以下方法实现:
internal static ConstantExpression
ProduceConstantExpression<X>(Expression exp)
{
try
{
return Expression.Constant(
Expression.Lambda<Func<X>>(exp, null).Compile().Invoke());
}
catch (Exception ex)
{
return null;
}
}
类型参数 `X` 是 `LambdaExpression` 期望的返回类型。对于 `ImageFormat` 等枚举类型,它将是底层值类型 `Int32`。这非常酷:只需一个函数调用,我们现在就可以处理任何表达式,只要它能在查询执行之前在运行时求值为常量。
实现 `Projection` 运算符的思想非常相似。不同之处在于 `Projection` 运算符只能在查询执行之后,当 `Image` 对象可用时才能进行评估。
var test = from img in Google.Images
where img.RelatesTo("microsoft")
select new { img.Rank, img.Url };
匿名类型 `{img.Rank, img.Url}` 是在编译时生成的,不属于表达式树。查询的 `Select` 部分实际上被翻译成一个 `MemberInit` 表达式,它将一个 `Image` 对象作为其参数并创建匿名类型的实例。
为了支持这一点,我们可以创建一个实现 `IQueryable` 的适配器类,将其包装在我们的 `ImageSearcher` 周围,并通过 `LambdaExpression.Compile` 对结果 `Image` 对象执行 `MemberInit` 表达式。
internal sealed class Projector<T, S> :
IQueryable<T>, IOrderedQueryable<T>
{
private IQueryable<S> _chain = null;
private Func<S, T> _delegate = null;
public Projector(MethodCallExpression expr)
{
Diagnostic.DebugExpressionTree(expr);
if (expr.Method.Name == "Select" &&
expr.Arguments.Count == 2 &&
ExpressionUtil.IsLambda(expr.Arguments[1]))
{
//get last IQueryable in the chain
ConstantExpression cont = (ConstantExpression)expr.Arguments[0];
_chain = (IQueryable<S>)cont.Value;
//create delegate from LambdaExpression (wraps MemberInit inside)
LambdaExpression lambda =
ExpressionUtil.GetLambda(expr.Arguments[1]);
if (lambda.Parameters.Count == 1 &&
lambda.Parameters[0].Type == typeof(S))
{
_delegate = (Func<S, T>)lambda.Compile();
return;
}
}
//not support
throw new NotSupportedException(
string.Format("Only projection base on type {0} is supported.",
typeof(S)));
}
public IEnumerator<T> GetEnumerator()
{
foreach (S s in _chain)
{
//call the member init delegate and return the result
yield return _delegate(s);
}
}
//implement other IQueryable members as usual...
}
就是这样!现在您可以通过简单直观的 LINQ 查询在应用程序中执行 Google 图片搜索。而且它还支持投影等酷炫功能!相当不错,不是吗?
历史
- 2007 年 5 月 8 日更新
- 增加了 `LambdaExpression` 的使用以及参数化查询和投影运算符的实现。
- 源代码已重构,现在也支持 Google 群组查询。