码迷,mamicode.com
首页 > 其他好文 > 详细

Elasticsearch / Kibana Queries - 深度教程

时间:2017-08-23 10:33:25      阅读:261      评论:0      收藏:0      [点我收藏+]

标签:等于   相同   ecif   适用于   standard   帮助   size   ide   简单的   

Elasticsearch/Kibana Queries - In Depth Tutorial

  本教程是关于如何在Kibana顶部的搜索栏中编写查询或在Elasticsearch中使用查询字符串查询的深入讲解。所使用的查询语言是Lucene查询语言,因为Lucene在Elasticsearch内部用于索引数据。

  有很多教程已经解释了Lucene查询语言,那么为什么要写另一个?这些教程大部分仅涵盖了Lucene查询语言,但不考虑Elasticsearch 。了解您的数据在Elasticsearch中的索引如何高效地影响您通过查询能搜索的内容和方式。

  所以本教程的主题不仅仅是解释查询语言,而且还解释了为什么可能找到或者找不到您存储在Elasticsearch中的文档。它应该可以帮助您解决您在Kibana中的查询找不到您要查找的文档的一些情况,您是否想知道为什么?

  如果您只是想要一些很简单的概述关于您可以在Kibana / Elasticsearch中输入什么样的查询,并且不会对细节造成麻烦,或者没有发现任何未找到的文档的问题,即使您期望它们,其他教程中的一个可能是更好的开始选择。

Indexing of documents

  首先了解Elasticsearch如何索引数据至关重要。因此,我们将以下两个文档放入我们想象的Elasticsearch 实例中:

{
  "title": "The Hitchhiker‘s Guide to the Galaxy",
  "author": "Douglas Adams"
}
{
  "title": "The Deeper Meaning of Liff",
  "author": "Douglas Adams"
}

  如果我们没有在该索引的Elasticsearch 映射中更改任何内容,Elasticsearch将在插入第一个文档时自动检测字符串作为两个字段的类型。

  分析仪做什么?分析仪有几个附加到它的编译器和/或过滤器。编译器将获得应该被索引的字段的值(例如“The Hitchhiker‘s Guide to the Galaxy”),并且可以将该值分割成用户应该能够搜索的多个块(稍后更多)。分析器的过滤器可以转换或过滤编译器产生的令牌。

  所有产生的令牌将被存储在所谓的倒排索引中。该索引将包含分析器生成的所有令牌以及包含它们的文档的链接。因此,如果用户使用搜索词显示Elasticsearch,那么只需要在倒排索引中查找它,它将立即看到需要返回哪些文档。

  由于我们没有为我们的Elasticsearch索引指定任何映射,因此默认情况下,使用标准分析器分析字符串类型的字段。

  该分析仪将首先将字段值分成字(将使用空格和标点符号作为边界),然后使用过滤器将所有令牌转换为小写。

  在插入上述两个文件之后,标题栏的倒排索引如下所示,1参考第一个文档(“The Hitchhiker‘s Guide to the Galaxy”),2参考“The Deeper Meaning of Liff”:  

TERMDOCUMENTS
the 1, 2
hitchhiker‘s 1
guide 1
deeper 2
liff 2
of 2
to 1
galaxy 1
meaning 2

  还将为author字段创建同样的倒排索引。这将包含两个条目:一个用于“douglas”,一个用于“adams”,两者都链接到这两个文档。

  如果用户搜索“guide”,则Elasticsearch可以快速查找要返回的文档。此外,Elasticsearch / Kibana中的Terms-Aggregation只是查看该倒排索引并返回具有最多/最少(取决于用户指定的顺序)匹配的文档。

Mappings in Elasticsearch

  如果你插入到elasticsearch的数据不是真正的文本,例如一个URL或者类似的,那么默认的分析器没有什么意义。特别是如果您要使用Kibana可视化您的数据,你不希望一个包含“http”条目的顶层访问URL图形,并且每个斜杠上的路径分开。您只想在每个真实域中输入一个条目。您想要的是Elasticsearch不会分析文档中的值。

  因此,您需要手动定义索引的映射。这个教程没有涵盖,但请看官方文档。如果在映射中指定index:not_analyzed,title字段的倒排索引将如下所示:

TERMDOCUMENTS
The Hitchhiker‘s Guide to the Galaxy 1
The Deeper Meaning of Liff 2

  而author字段的倒排索引现在如下所示:  

TERMDOCUMENTS
Douglas Adams 1, 2

  如你所见,Elasticsearch不会拆分值,也不会将它们转换为小写。

  您的值观是否被分析(即倒排索引中的哪些术语)将对您可以搜索的内容以及如何搜索有巨大的影响,我们将在以下部分中看到。当我们谈论“analyzed data”时,这意味着你有分析的字符串字段中的数据。当我们谈论“non-analyzed data”时,这意味着你有一个映射,这两个字段都没有被分析。

  提示:从Elasticsearch5,不再有字符串字段类型了。分析字符串现在将是类型文本,未分析的字符串来自版本5之后的类型关键字。这背后的基本逻辑没有改变。因此,本教程将继续讨论分析和未分析的字符串。请参阅changelogs

Simple queries on fields

  由于我们现在解释了Elasticsearch如何索引数据,我们可以继续实际的主题:搜索。在“Discover”选项卡,visualization和/或dashboards的顶部,Kibana中始终可以使用以下查询。此外,这些查询可以在直接与Elasticsearch连接时在查询字符串查询中使用。

  我们从简单的查询author:douglas开始。如果在分析的数据集上输入此查询,Elasticsearch将返回两个文档。为什么?它将在author字段的倒排索引中查找术语“douglas”。它连接到这两个文档,所以Elasticsearch将返回这两个文档作为结果。

  如果您在非分析数据集上使用相同的搜索,您将无法获得结果。为什么?Elasticsearch在倒排索引中再次查找“douglas”,没有仅仅作为“douglas”条目的术语(仅适用于“Douglas Adams),因此不会返回任何结果。

  如果您尝试在分析的数据中搜索author:Douglas(大写第一个字母),那么您仍然会收到这两个文档。为什么?因为Elasticsearch识别author字段已被分析,并尝试将相同的分析器应用于搜索查询“Douglas”,这意味着它也将被转换为小写,然后才能在反向索引中查找。这就是为什么它仍然找到文件。对未分析数据的相同查询仍然不会产生结果,因为没有“Douglas”条目(仅适用于“Douglas Adams”)。

  注意:冒号后没有空格。搜索author: douglas与搜索author:douglas 不一样,很有可能不会带来任何有意义的结果。

Search for phrases

  假如你想搜索更多单词, 你可以把这些单词放在引号内。我们来搜索整个名字,使用author:"douglas adams"。

  如果您跳过引号(即 author:douglas adams),您将搜索完全不同的内容,我们将进一步介绍的几个部分。

  如果您搜索author:"douglas adams"对未分析的数据,您将获得 - 戏剧性的暂停 - 没有结果(如您所预期的)。为什么?它会在倒排的索引中查找“douglas adams”的条目,但“Douglas Adams”只有一个 - 搜索区分大小写。您可能已经猜到了,但是寻找author:"Douglas Adams"将在未分析的数据中返回两个文档,因为这正是存储在反向索引中的“key”。

  如果您在分析的数据上搜索author:“douglas adams”,它将返回两个文档。为什么?Elasticsearch再次认识到,author字段被分析,并尝试将相同的分析器应用于您的查询,即在这种情况下将单词分解并将其转换为小写。之后,它发现两个文件有“douglas”和“adams”条目,所以它将返回两者。搜索author:"Douglas Adams"将返回相同的,因为Elasticsearch在实际搜索之前将小写过滤器应用于您的查询(如上所述)。

Wildcard Queries

  您也可以在搜索查询中使用通配符。有两个通配符可用:? (问号)将是一个字符的占位符。 *(星号)是任意数量字符(包括0)的占位符。

  注意:您不能在短语内使用通配符。如果您搜索author:“Do?glas Adams”,问号不会用作通配符,但必须是索引值的一部分(这不是我们的情况)。更多的关注:由于Elasticsearch将分析器应用于您的查询,如果将它们放在词的开头/结尾,它可能看起来像通配符在短语内正在工作---例如author:“Douglas Adams*”仍然会在分析的数据上返回两个文档,但并不是因为通配符按预期工作,只是因为在分析查询时分析仪剥离了该星号。该查询找不到“Douglas Adamsxxx”的值。

  在展示了什么不起作用之后(短语中的通配符),我们来看看它们是如何工作的。假设我们想搜索以“doug”开头名字的作者的所有图书。如果我们搜索author:doug*在分析的数据,我们将得到这两个文件。相反,搜索author:doug不会返回任何东西,因为“doug”的倒排索引中没有条目。当输入该查询时,Elasticsearch将查找倒排索引并搜索与“doug *”匹配的条目(星号是任意数量的字符)。倒排索引中有一个条目(即“douglas”),链接到这两个文档,因此这两个文件都将被返回。

  现在我们来看一个可能令人困惑的部分查询语言。如果我们对未分析的数据使用相同的搜索项,我们将不会得到结果。到目前为止,这不应该是一个惊喜,因为在倒排索引中只有“Douglas Adams”(大写字母)的条目,意味着搜索“doug *”将不会产生任何结果。所以让我们来巧妙地搜索author:Doug*。从我们到现在所知道的,现在应该在倒排的索引中找到“Douglas Adams”条目。但是如果你搜索它,它不会返回任何结果。

  那里发生了什么?一旦您在查询中使用通配符,Elasticsearch将自动小写您的查询。无论您正在搜索的字段是否被分析。意味着搜索author:Doug *将被转换为author:doug *,因此在未分析的倒排索引中将找不到“Douglas Adams”。如果您直接与Elasticsearch通信,则编写JSON查询,则可以在query_string对象中将lowercase_expanded_terms设置为false以禁用该行为。如果您在Kibana中进行搜索,并希望在搜索大写值时使用通配符(在未分析的字段中),则必须编写JSON查询,我将在本教程末尾进行说明。

  还不太困惑?然后让我们直接跳到下一节。

Writing queries without a field

Old behavior

  以下行为是旧的Elasticsearch行为,但为了完整性在这里描述。如果您在5.1之前使用Elasticsearch版本或者在5.1之前仍然有数据索引,则可能会影响您。

  如果你只是写一个像Douglas一样查询,那么Elasticsearch就不知道你想看哪个倒排索引。如果在直接查询Elasticsearch时使用JSON,您可以使用query_string对象中的default_field选项指定应该查找的字段。如果您不指定它(或在Kibana中输入该查询),则它将默认为_all字段。这是一个Elasticsearch为您创建的特殊字段,它具有自己的倒排索引。所以搜索Douglas就像搜索_all:Douglas一样。

  那么_all字段的倒排索引是什么?默认情况下,插入文档时,Elasticsearch会将所有字段的值连接成一个大字符串 - 无论原始字段是不是字符串类型,还是根本没有分析。它将构建一个大的值,由标准分析器进行分析,并将其归结为自己的倒排索引。

  我们来看一个例子。假设我们将以下文档放到Elasticsearch中:

{
  "some_field": "foo bar",
  "unanalyzed_field": "Douglas Adams",
  "numeric_field": 42
}
{
  "another_field": "foo adams"
}

  之后,_all字段的倒排索引将如下所示:  

 

TERMDOCUMENTS
douglas 1
foo 1, 2
bar 1
42 1
adams 1, 2

  因此,_all字段允许您在默认情况下甚至搜索未分析字段中的单个字词。而unanalyzed_field的倒排索引(在上面的示例文档中)将仅包含条目“Douglas Adams”,因为该字段设置为未分析(在我们想象的Elasticsearch映射中)。

  回到我们以前的数据(我们关于Douglas Adams的两本书籍文件):如果我们设置了author字段no_analyzed,它的反向索引将只有一个条目:“Douglas Adams”。_all字段的反向索引将同时包含:“douglas”条目和“adams”条目,因为文档中每个字段的值都将以“元字段”进行分析和索引。

  这意味着 - 查看我们未分析的数据 - 搜索Douglas (或同等的_all:Douglas)将返回两个文档。搜索author:Douglas不会返回任何结果(即使作者字段最初包含该值)。为了搜索未分析的作者字段,您需要在其反向索引(即“Douglas Adams”)中指定精确匹配,以便在_all字段中搜索分析的值(如“douglas”)。

  如果您想知道,为什么搜索_all:Douglas (大写)仍然找到该文档,即使_all反向索引有“douglas”(小写)索引:Elasticsearch将使用与前面提到的相同的自动检测。它检测到_all字段是一个分析的字段,因此它将对搜索值(“Douglas”)使用相同的分析器,其中将值转换为小写。

New behavior

  从Elasticsearch 5.1开始,_all字段被all_fields搜索模式替换。如果您在更现代的Elasticsearch版本中搜索没有字段的字符串(例如上述示例中的Douglas),搜索将不会针对特定的_all反向索引,而是针对所有反向索引。这样可以在每个(可搜索)字段中搜索该值,但是将使用该字段的实际分析器。

  您可以在GitHub的pull请求中找到更多关于_all字段何时可以使用all_fields搜索的详细信息。

AND & OR Operators

  到目前为止,我们只给出了一个条件。但是当然你也可以使用AND和OR来指定多于一个条件。你必须把这些操作符写成大写。如果你把它们写成小写,就不会被检测到。(实际上你可能会发现结果很奇怪,因为你只是将它们写入小写,因为它只会抛出另一个_all:and个别_all:or查询。)

  我们来搜索author:douglas AND author:adams。AND周围的每个查询部分的工作原理如前几节所述。因此,无论数据是否被分析,对于每个部分都是重要的,但对于AND / OR操作者本身并不重要。这就是为什么我们从现在开始仅仅查看分析数据。

  搜索author:douglas AND author:adams将返回这两个文件,因为反向索引中有一个“douglas”条目和一个“adams"条目,并且都指向相同的文件,所以返回。

  搜索author:douglas OR author:terry将导致相同的两个文档,因为它们都匹配查询的第一部分(它只需要匹配其中一个部分)。author:douglas AND author:terry不会返回任何文件,因为没有任何文件完整填满查询的两个部分。

  如果你只是键入author:douglas author:terry Elasticsearch再次需要知道你是否意味着OR或AND在这种情况下。如果您向Elasticsearch编写JSON查询,则可以使用query_string对象中的default_operator选项指定应该插入哪个操作符。默认情况下,如果您不指定(或从Kibana进行搜索),则为OR。意味着author:douglas author:terry相当于author:douglas OR author:terry。

  在本教程的开头,我提到如果要搜索短语,设置引号是多么的重要。我说author:"Douglas Adams"搜索与author:Douglas Adams是完全不同的东西。我们现在知道一切,以了解后期查询搜索的内容。假设您没有更改default_operator和default_field,这个查询将等同于author:Douglas OR  _all:Adams将最有可能导致与author:“Douglas Adams”不同的文档。

  如果您在查询中使用多于两个部分,则可以在其周围放置括号以更改分组。默认情况下,Elasticsearch将在查看OR运算符之前首先查看是否所有AND运算符匹配。

  除了使用关键字AND和OR,您还可以使用&&或||分别。

  关于这些操作符的官方文档非常详细。

Plus Operator

  除了使用AND和OR之外,还有一个加号运算符(+)。如果把它放在查询部分的前面,这个查询部分必须匹配。所有其他查询部分(前面没有加号)是可选的。例如  +author:adams title:guide将匹配在author字段中包含adams的所有文档,并且可选title字段中包含guide。

  通常使用加号运算符比使用AND和OR查询更容易理解。Elastic同时也建议在可能的情况下使用+号(减号操作符在下一章 讲解)而不是使用AND 和 OR。

  请注意,加号和实际查询部分之间不允许有空格。

Exclusion (NOT operator)

  如果要排除匹配特定条件的文档,可以在查询的该部分前面加上减号( - ),感叹号(!)或NOT字。如果要搜索包含“douglas”,但不包含“adams”的文档,查询author:douglas -author:adams。

  小心:不要在减号或感叹号和实际查询之间有空格。

Regular expressions

  Elasticsearch还通过将搜索字符串包装在斜杠中来支持正则表达式搜索,例如 author:/[Dd]ouglas.*/。像其他查询一样,将在反向索引中搜索正则表达式,即正则表达式必须与反向索引中的条目匹配,而不是实际的字段值。

  例如,如果我们搜索author:/[Dd]ouglas.*[Aa]dams/在未分析的数据中,它将产生两个文档,因为在倒排索引中有一个条目“Douglas Adams”。

  如果在分析数据上使用相同的查询,则不会得到任何结果,因为它与任何反向索引条目不匹配。只有“douglas”和“adams”的条目,但没有一个符合上述正则表达式。

  支持的正则表达式语法是Lucene特有的,您可以查找文档以查看正则表达式运算符的支持。

  注意:执行正则表达式搜索可能相当昂贵,因为Elasticsearch可能需要将每个反向索引条目与正则表达式进行比较,这可能需要一些时间。如果您不用正则表达式并且使用其他查询类型之一,则应该这样做。

Range Queries

  如果要在数字字段上搜索,您当然可以使用一个简单的查询,如number:42(假设您的文档中有一个名为数字的数字字段)来查找此字段为42的所有文档。使用数字时,您通常需要在特定范围内搜索,而不仅仅是为了固定值。因此,Elasticsearch为您提供范围查询:

  • number:[32 TO 42] -- 将找到所有文档,其number在32到42之间(32和42仍然包括在结果中)
  • number:[32 TO 42} -- 将找到所有number在32到42之间的文档(包括32,42被排除在结果之外)
  • number:[23 TO *] -- 将找到所有number大于或等于23的文档

  如您所见,方括号总是包括实际的数字,而花括号将从搜索中排除指定的数字。您当然也可以使用星号和花括号,用于范围的下边界。

  如果你的范围有一个开放的结尾(星号),那么写一个比更大或者更少的速记查询语法:

  • number:>42
  • number:<42
  • number:>= 42
  • number:<=42

Ranged queries on string fields

  您还可以对字符串字段使用范围查询。字符串按字母顺序排列,大写字母在小写字母之前,即它们的ASCII顺序。所以一个正确的字符串顺序是:

A < D < Douglas < Douglas Adams < a < d < douglas

  搜索author:>=n对分析的数据将返回所有文件,其名称或姓氏以n开头。再次,该比较是针对该字段的反向索引,这就是为什么在分析数据时,名称的一部分足以匹配此查询。

  我们使用上面的大或等于运算符。在搜索author:>n时(只有大于)你可能会认为,这只会显示以o或更高开头的名字,但事实并非如此。它将显示大于“n”的所有名称,这是以“n”开头的每个名称,除了唯一的字符串“n”本身。

  警告:在字符串字段上使用范围查询时存在一个陷阱。如果您不将“通配符查询”部分中介绍的lowercase_expanded_terms选项更改为false,那么Elasticsearch默认将查询转换为小写的查询,意思是搜索author:>D等效于搜索author:>d。如果您的数据未分析,并且在倒排索引中实际上有一个“Douglas Adams”条目,您不会指望author:<C找到它,因为您只想搜索所有低于“C”的authors 。由于这将被转换为author:<c它将找到您的文档,因为所有大写字母总是小于任何小写字母,意思是“D”<“c”,“Douglas”<“c”也是如此。如果您不希望该行为,则在使用JSON与Elasticsearch进行通信时,您需要在query_string对象中将lowercase_expanded_terms设置为false。

More Query Types

  还有一些查询类型,详细的说明可以在官方文档中找到。您现在应该具有关于反向索引如何工作的所有知识,以便从官方文档中了解这些运算符。我将简要介绍一下缺少的操作:

Fuzziness

  如果要搜索与特定值类似的术语,但不一定需要相同,可以使用模糊(?)运算符:

  doglas?将搜索所有类似“doglas”的事物。默认情况下,它必须在 Damerau-Levenshtein distance为2(您需要编辑/插入/删除以将查询更改为实际索引项的字符数量)。

  您可以用操作符后面的数字指定允许的距离:doglas?1

Proximity

  模糊运算符类似于近似运算符。如果您搜索author:“adams douglas”短语 Elasticsearch希望单一术语在原始文档中按照该顺序显示,并且找不到任何文档。

  指定邻近像author:“adams douglas”?2允许这些单词以另一个顺序或最多(在这种情况下)在实际文档中分开2个字。

Search for (non)existing fields

  有两个特殊的“字段”来检查一个文档是否包含一个字段,或者没有。

  如果要搜索所有文档,那么没有“author”字段或没有值(即为空),您可以使用查询_missing_:author。

  果要检查所搜索文档中是否存在特定字段,并且具有非空值,则可以使用_exists_:author。

  由于Elasticsearch对数据索引的方式,您无法看到文档在插入到Elasticsearch之后是否没有字段,或者该值是否为null时的任何差异。所以在查询中没有可能分离这两种情况。

Boosting

  Elasticsearch排序它找到的结果,首先返回最匹配的文档。您可以使用boost操作符(^)来更改单个查询部分的重要性。

  例如。当搜索author:douglas OR title:guide^5在排序重要性方面,第二部分是第一部分查询的五倍(默认增强值为1)。

  该操作符不影响Elasticsearch发现的文档,只响应这些结果排列的顺序。(如果您限制搜索结果的数量,当然这个顺序可以确定哪些文档实际上返回给用户。)

  从Kibana搜索时,通常会在整个教程中将实际的查询字符串输入到顶部栏中。如果查询字符串不足以满足您的需要,您也可以在该栏中编写JSON。

  您可以编写JSON对象,当您将Elasticsearch 与该框进行通信时,您将附加到“query”键,例如:

{ "range": { "numeric": { "gte": 10 } } }

  这相当于在该框中写入numeric:> = 10。如果您需要访问仅在JSON查询中可用的选项,而不是查询字符串中的选项,这通常才有意义。

  再一次警告:如果您将query_string的JSON写入该字段(例如,因为要访问lowercase_expanded_terms),则Kibana将为查询存储正确的JSON,但会再次向你仅显示(在按下enter键之后)您Json的"query"部分。这可能是超级混乱,当然如果您现在输入文本并再次按Enter键,它也会失去您通过JSON设置的选项,所以这应该真的要小心使用。

More Special cases

  本节应该介绍一些更特殊的情况,您可能会认为:“我阅读了整个教程,我了解了一切,但是我的查询仍然找不到我希望找到的数据。”

Elasticseach doesn‘t find terms in long fields

  这是从我的经验 - 一个很常见的问题,并不容易找到,如果你不知道你在查找什么。

  Elasticsearch有一个ignore_above设置,您可以在每个字段的映射中设置。这是一个数字值,当插入文档时,这将导致Elasticsearch不指定比指定的ignore_above值更长的值。该值仍将被存储,所以当查看文档时,您将看到该值,但是您不能搜索它。

  你如何检查一个字段上设置的值?您需要通过调用<your-elasticsearch-domain> / <your-index-name> / _mapping来检索Elasticsearch的映射。在返回的JSON中,会显示您正在查找的字段的映射,可能如下所示:

"fieldName": {
  "type": "string",
  "ignore_above": 15
}

  在这种情况下,超过15个字符的值不会被索引,您无法搜索它们。

  示例:假设上述映射,我们将两个文档插入到Elasticsearch中:

{ "fieldName": "short string" }
{ "fieldName": "a string longer as ignore_above" }

  如果现在列出所有文档(在Kibana或Elasticsearch本身),您将看到两个文档都在那里,两个字段的值都是您插入的字符串。但是如果您现在搜索fieldName:longer ,您将不会得到任何结果(而fieldName:short将返回第一个文档)。Elasticsearch发现值“a string longer as ignore_above”超过15个字符,因此它只将其存储在文档中,但不对其进行索引,所以您无法搜索任何内容,因为该字段的倒排索引中不会有该值的任何内容。

Searching needs a specific field it doesn‘t work without

  如果您可以搜索例如author:foo,但不是为了foo,这最有可能是您的default_field的“problem”。Elasticsearch在foo前面添加了默认字段。该字段可以配置为与_all不同的东西。

  可能的是,index.query.default_field设置被设置为不同的事情,Elasticsearch不使用可能导致问题的_all字段。

  另一种可能性是,_all字段的行为不像您期望它的行为,因为它以其他方式配置。您可以从_all字段中排除特定字段(例如,在上述示例中,fieldName可能已从_all字段中的索引中排除),或者_all字段映射中的分析/索引选项已更改。

What‘s next?

  我希望这个在Kibana / Elasticsearch中的查询语言的深入概述可以帮助您更好地了解查询,希望您现在可以理解查询是否(或不)匹配数据中的文档。

   如果您觉得我忘记了任何重要的部分或边缘案件,或者您有任何其他问题,请随时在下面发表评论。

 

原文地址:https://www.timroes.de/2016/05/29/elasticsearch-kibana-queries-in-depth-tutorial/

Elasticsearch / Kibana Queries - 深度教程

标签:等于   相同   ecif   适用于   standard   帮助   size   ide   简单的   

原文地址:http://www.cnblogs.com/benjiming/p/7337628.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!