65.9K
CodeProject 正在变化。 阅读更多。
Home

Web Scraping Library (完全 .NET)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (24投票s)

2015 年 7 月 16 日

GPL3

6分钟阅读

viewsIcon

84732

downloadIcon

6574

这只是另一个完全用 .NET 编写的网页抓取器,但最终没有使用 mshtml!

引言

搜索和收集发布在网站上的数据 一直是一项漫长而乏味的 manual 任务。通过这个项目,我试图为您提供一个工具,可以帮助自动化其中一些任务,并将结果有序地保存。
它仅仅是另一个用 Microsoft .NET Framework (C# 和 VB.NET) 编写的网页抓取器,但最终没有使用 Microsoft mshtml 解析器!
我经常使用这个轻量级版本,因为它易于定制并包含在新项目中。

它由以下组件构成

  • 一个 parser 对象 gkParser,它使用 Jamierte 版本的 HtmlParserSharp (https://github.com/jamietre/HtmlParserSharp) 并提供导航功能
  • 一个 ScrapeBot 对象 gkScrapeBot,它提供搜索、提取和清理数据的功能
  • 一些辅助类,用于加速数据库操作的开发

架构

搜索和提取方法要求将 HTML 语言转换为 XML,即使它格式不正确。这样做,就可以更简单地定位网页中的数据。然后,基本架构侧重于进行此转换并在 XML 结果文档上执行查询。

Parser 类包含导航和解析的所有功能。在此版本中,解析仅限于 HTML 和 JSON。
当导航函数返回成功响应时,您将获得网页的 XML DOM 表示。

此时,另一个对象 ScrapeBot 可以使用 XPath 语法执行查询、提取和清理所需数据。

gkScrapeBot 是您将在项目中使用的主要对象。它已经为您使用了 Parser。它提供了一些导航函数的包装器、一些有用的查询函数以及其他一些提取和清理数据的功能。

让我们来看看这些对象。

Parser 类 (gkParser)

此组件是用 VB.NET 编写的,并使用 Jamietre 版本的 Validator.nu 解析器的端口 (http://about.validator.nu/htmlparser/)。

为什么不使用 Microsoft Parser? 嗯,好的。我想花点时间写写这个痛苦的选择。 ;-)

此项目的第一个版本是使用 mshtml 制作的。这就是我决定更改的原因
第一:我的意图是无窗口使用此组件。网上有很多关于使用 mshtml 的文档。没有一个是微软官方的。然而,关于用户遇到的麻烦却有很多… 微软提供的少数有用文档可以追溯到 1999 年 (inet sdk 的 walkall 示例)!它有效,但我很快就发现了它的局限性。
第二:然后我开始基于 walkall 示例编写 .NET 代码。克服了 COM 互操作的困难后,我发现 mshtml 只能进行 GET 请求。那么 POST 呢?……微软在某些地方写道,可以通过实现接口、编写一些回调函数来定制请求过程……不行,不起作用!
第三:我需要控制下载链接的文档、JavaScript、图像、CSS… 哦,是的,微软对此有所提及。它说您可以完全控制这一点……不!
我使用 wireshark 观察我的进程下载了什么,这个功能不起作用。我看到它只在被重量级的 MS WebBrowser 组件托管时才起作用。
然后:我明白了,微软不喜欢开发者使用它的解析器。

组件

导航函数是通过使用 WebRequestWebResponse 类实现的,HTML 解析器使用 HtmlParserSharp.SimpleHtmlParser 对象实现。Navigate 方法是用于同时进行 GETPOST 请求的唯一 public 函数。它有 4 个重载以允许不同的行为。

Public Sub Navigate(ByVal url As String)
Public Sub Navigate(ByVal url As String, ByVal dontParse As Boolean)
Public Sub Navigate(ByVal url As String, ByVal postData As String)
Public Sub Navigate(ByVal url As String, ByVal postData As String, ByVal dontParse As Boolean)

创建一个完全实现所有导航功能的类并不容易,我创建了一个实现了基本 cookie 管理的类,并且没有完全实现 https 协议。

所有方法都是同步的,它们返回时,XML DOM 文档已准备就绪。
在 Web 请求获得成功响应后,该类会检查内容类型并实例化正确的解析器。
Jamietre 的解析器会返回非常规范的 XML。对于我们的目的来说太规范了。此外,一些网页非常大且复杂,拥有更小的 XML 会很有用。为此,我实现了一个有趣的算法,可以过滤标签和属性:您可以指示解析器仅考虑所需的标签和属性,并排除不需要的标签和属性。
以下两个属性控制此行为

Public Property ExcludeInstructions() As String
Public Property IncludeInstructions() As String

'default values example
p_tag2ExcInstruction = "SCRIPT|META|LINK|STYLE"
p_tag2IncInstruction = "A:href|IMG:src,alt|INPUT:type,value,name"

通过此功能,您可以自定义结果 XML,使其更易于理解和教会机器人。

Scraper

另一个主要类是 gkScrapeBot。这是您需要使用的类。
它使用 gkParser 来导航、获取 XML 进行分析以及从中提取数据。
它实现了辅助函数以满足这些要求

'
'Navigation functions:
'
'Makes a simple GET request and return the XML image of the entire html page
Public Sub Navigate(ByVal url As String)
'Makes a GET request, look for a subel element id and 
' return only the html contained in the subel element
Public Sub Navigate(ByVal url As String, ByVal subel As String)
'As above, and wait given millisecond
Public Sub Navigate(ByVal url As String, ByVal subel As String, ByVal wait As Integer)

'Makes a POST request and return the XML image of the entire html page
Public Sub Post(ByVal url As String, ByVal postData As String)
'Makes a POST request, look for a subel element id and 
' return only the html contained in the subel element
Public Sub Post(ByVal url As String, ByVal postData As String, ByVal subel As String)
'As above, and wait given millisecond
Public Sub Post(ByVal url As String, ByVal postData As String, _
                ByVal subel As String, ByVal wait As Integer)
'
' XPATH Search functions
'
Public Function GetNode_byXpath(ByVal xpath As String, _
   Optional ByRef relNode As XmlNode = Nothing, _
   Optional ByVal Attrib As String = "") As XmlNode
Public Function GetNodes_byXpath(ByVal xpath As String, _
   Optional ByRef relNode As XmlNode = Nothing, _
   Optional ByVal Attrib As String = "") As XmlNodeList
Public Function GetText_byXpath(ByVal xpath As String, _
   Optional ByRef relNode As XmlNode = Nothing, _
   Optional ByVal Attrib As String = "") As String
Public Function GetValue_byXpath(ByVal xpath As String, _
   Optional ByRef relNode As XmlNode = Nothing, _
   Optional ByVal Attrib As String = "") As String
Public Function GetHtml_byXpath(ByVal xpath As String, _
   Optional ByRef relNode As XmlNode = Nothing) As String

看看下面的例子,看看它是如何工作的。

如何使用:包含测试项目

警告。 网页抓取通常被网站政策禁止。
在抓取之前,您需要确保目标网站的政策允许这样做。

我假设您知道网站如何工作(URL、请求方法和参数等)。我使用浏览器提供的开发者工具来发现所有发送到服务器的参数和请求,以及导航 HTML 树。

让我们看看它是如何工作的
下载包中包含的测试项目向您展示了如何从在线商店获取产品详细信息。 https://testscrape.gekoproject.com
我选择这个例子是因为它使用了抓取器的关键功能:cookie 管理和用于登录阶段的 POST 请求,以及用于获取和存储提取数据的节点探索和数据库功能

访客用户无法看到产品。只有注册用户才能查看产品和价格。
登录过程基于 cookie。因此,首先,我们需要简单地导航到网站以获取 cookie。

'Navigate to homepage and get cookies. 
url = "https://testscrape.gekoproject.com/index.php/author-login"
bot.Navigate(url)

在登录页面,表单中有两个 string,需要将它们 POST 回服务器才能成功发送登录请求。

'Then look for two parameters useful to login
token1 = bot.GetText_byXpath("//DIV[@class='login']//INPUT[@type='hidden'][1]", , "value")
token2 = bot.GetText_byXpath("//DIV[@class='login']//INPUT[@type='hidden'][2]", , "name")

'Now login with username e password
url = "https://testscrape.gekoproject.com/index.php/author-login?task=user.login"
data = "username=" & USER & "&password=" & PASS & "&return=" & token1 & "&" & token2 & "=1"
bot.Post(url, data)

如果一切顺利,您将被重定向到用户页面,然后您可以检查并获取“注册日期”信息

mytext = bot.GetText_byXpath("//DT[contains(.,'Registered Date')]/following-sibling::DD[1]")
Console.WriteLine("User {0}, Registered Date: {1}", USER, mytext.Trim)

一旦登录,您就可以导航到产品列表页面并开始数据抓取。

在示例中,只抓取了第一页的数据,但您可以对分页器中的每一页重复此任务。

以下是检索产品及其属性列表的代码

Dim url As String
Dim name As String
Dim desc As String
Dim price_str As String
Dim price As Double
Dim img_path As String

'Navigate to front-end store 
url = "https://testscrape.gekoproject.com/index.php/front-end-store"
bot.Navigate(url)

'find all product "div"
Dim ns As XmlNodeList = bot.GetNodes_byXpath_
("//DIV[@class='row']//DIV[contains(@class, 'product ')]")
If ns.Count > 0 Then

  'Write to a XML file
  Dim writer As XmlWriter = Nothing

  'Create an XmlWriterSettings object with the correct options.
  Dim settings As XmlWriterSettings = New XmlWriterSettings()
      settings.Indent = True
      settings.IndentChars = (ControlChars.Tab)
      settings.OmitXmlDeclaration = True

  writer = XmlWriter.Create("data.xml", settings)
  writer.WriteStartElement("products")

  '********************
  ' Main scraping loop
  '********************
  For Each n As XmlNode In ns

    'Find and collect data using relative xpath syntax
    name = bot.GetText_byXpath(".//DIV[@class='vm-product-descr-container-1']/H2", n)
    desc = bot.GetText_byXpath(".//DIV[@class='vm-product-descr-container-1']/P", n)
    desc = gkScrapeBot.FriendLeft(desc, 50)
    img_path = bot.GetText_byXpath(".//DIV[@class='vm-product-media-container']//IMG", n, "src")
    price_str = bot.GetText_byXpath(".//DIV[contains(@class,'PricesalesPrice')]", n)
    If price_str <> "" Then
      price = gkScrapeBot.GetNumberPart(price_str, ",")
    End If

    '
    'write xml product element
    '
    writer.WriteStartElement("product")
    writer.WriteElementString("name", name)
    writer.WriteElementString("description", desc)
    writer.WriteElementString("price", price)
    writer.WriteElementString("image", img_path)
    writer.WriteEndElement()

    '
    'Insert data into DB
    '
    db.CommantType = DBCommandTypes.INSERT
    db.Table = "Articles"
    db.Fields("Name") = name
    db.Fields("Description") = desc
    db.Fields("Price") = price
    Dim ra As Integer = db.Execute()
    If ra = 1 Then
      Console.WriteLine("Inserted new article: {0}", name)
    End If

  Next

  writer.WriteEndElement()
  writer.Flush()
  writer.Close()

End If

结论

我希望这个项目能帮助您从网络收集数据。
我知道发现一个网站的工作原理并不容易,尤其是当它大量使用 JavaScript 来进行异步请求时。
那么这个项目就不能解决所有网站的问题;如果您需要比这个项目更多的东西,您可以通过在下方留下评论来联系我,并确保您已获得抓取授权。 ;-)

祝您抓取愉快!

更新

  • 28-06-2019
  • 16-07-2015
    • 修复了演示站点上的权限错误,该错误导致在运行测试项目时发生运行时异常
© . All rights reserved.