Shixiang Wang

>上士闻道
勤而行之

R-网页爬虫:初识

王诗翔 · 2018-09-16

分类: r  
标签: r   爬虫  

数据处理的数据集并非立即可得,有时我们需要自己收集�数据,对很多研究领域,网页内容是一个重要的数据源。

查阅网页内容

下面是一个简单的网页,几乎所有的网页都可以查看源代码(一般是右键——点击查看源代码)。

<!DOCTYPE html>
<html>
<head>
    <title>Simple page</title>
</head>
<body>
    <h1>Heading 1</h1>
    <p>This is a paragraph.</p>
</body>
</html>

网页源码其实是HTML(Hyper Text Markup Language)文件。HTML是互联网使用最广泛的语言,它描述了网页的布局排版和内容,浏览器则根据Web标准将代码渲染到网页上。

尽管上面的网页代码很简单,但实际上认真点就会发现HTML是一些标签的嵌套结构,这些标签包括:<html><title><body><h1><p>,现代浏览器根据HTML的第一行决定使用哪种标准进行网页渲染。上面例子使用的是HTML 5。

网页的这些标签并不是随意命名的,也不能任意包含其他标签,每个标签对浏览器都有特殊含义,且只允许包含一部分特定标签或不允许包含任何标签。

标签<html>是所有HTML的根元素。HTML通常也包括<head><body>。其中<head>通常包含<title>,展示在标题栏,包含浏览标签和网页元数据。而<body>则在网页内容和排版方面起主要作用。

<body>标签中,标签可以更自由地嵌套,最简单的一个网页可以只包含一级标题(<h1>)和一个段落(<p>)。

关于表格:

一些其他标签:

HTML有一个属性,称为style,用于定义这些元素的样式外观。

HTML使用CSS可以避免冗长的样式定义。

每个CSS元素都包含一个CSS选择器用来匹配HTML元素和样式以便渲染应用。CSS选择器不仅用于应用样式,也常用于提取网页内容,以便我们感兴趣的HTML元素可以被正确匹配,这正是网络爬虫的底层技术。

对于网络爬虫,使用下面例子展示最常见的CSS选择器

语法 匹配
* All elements
h1,h2,h3 <h1>,<h2>,<h3>
#table <* id="table">
.product-list <* class="product-list">
div#container <div id="container">
div a <div><a>and<div><p><a>
div >a <div><a>but not<div><p><a>
div >a.new <div><a class="new">
ul > li:first-child First <li> in <ul>
ul > li:last-child Last <li> in <ul>
ul > li:nth-child(3) 3rd <li> in <ul>
p + * Next element of <p>
img[title] <img> with title attribute
table[border=l] <table border="l">

使用CSS选择器从网页中提取数据

R里面爬虫最简单易用的扩展包是rvest,安装:

install.packages("rvest")

下面我们读取一个简单表格HTML数据并提取表格:

library(rvest)
#> Loading required package: xml2
single_table_page = read_html("../../../static/datasets/single-table.html")
single_table_page
#> {html_document}
#> <html>
#> [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
#> [2] <body>\n  <p>The following is a table</p>\n  <table id="table1" border="1 ...

single_table_page是HTML解析文档,是HTML节点的嵌套数据结构。

使用rvest函数从网页上爬取信息的典型过程是这样的。首先,定位需要从中提取数据的HTML节点。然后使用CSS选择器或者XPath表达式筛选HTML节点,从而选择需要的节点,剔除不需要的节点。最后,对已解析的网页使用合适的选择器,用html_nodes()提取节点子集,用html_attrs()提取属性,用html_text()提取文本。

rvest包提供了一些简单函数可直接用于提取数据并返回一个数据框,例如提取网页中的<table>元素,直接使用html_table()

html_table(single_table_page)
#> [[1]]
#>    Name Age
#> 1 Jenny  18
#> 2 James  19

为提取第一个元素,我们先选择第一个节点,然后再提取表格:

html_table(html_node(single_table_page, "table"))
#>    Name Age
#> 1 Jenny  18
#> 2 James  19

连续的操作可以使用管道:

single_table_page %>% 
    html_node("table") %>% 
    html_table()
#>    Name Age
#> 1 Jenny  18
#> 2 James  19

现在我们读取一个产品信息网页,然后用html_nodes()匹配<span class="name">节点:

products_page = read_html("../../../static/datasets/products.html")
products_page %>% 
    html_nodes(".product-list li .name")
#> {xml_nodeset (3)}
#> [1] <span class="name">Product-A</span>
#> [2] <span class="name">Product-B</span>
#> [3] <span class="name">Product-C</span>

这里我们选择的是product-list类的<li>标签下name类的节点,因此使用.product-list li .name,如果对符号不熟悉,需要详细记忆CSS表。

之后提取内容:

products_page %>% 
    html_nodes(".product-list li .name") %>% 
    html_text()
#> [1] "Product-A" "Product-B" "Product-C"

类似的可以提取产品价格:

products_page %>% 
    html_nodes(".product-list li .price") %>% 
    html_text()
#> [1] "$199.95" "$129.95" "$99.95"

使用XPath选择器

一般来说,CSS选择器足够满足绝大多数HTML节点匹配的需要。但需要根据某些特殊条件选择节点时,需要更强大的技术。

请看下面新的产品信息网页的源代码:

<!DOCTYPE html>
<html>
<head>
  <title>New Products</title>
  <style>
    p {
      margin: 2px;
    }
    .product-list {
      width: 80%;
    }
    .product-list ul {
      padding-left: 8px;
    }
    .product-list ul li {
      padding-left: 2px;
      list-style: none;
    }
    .product-list > ul > li {
      margin-top: 16px;
      list-style: none;
    }
    .product-list .name {
      font-weight: bold;
      font-size: 1.25em;
    }
    .product-list .price {
      color: green;
    }
    .product-list .info {
      background-color: #efefef;
      border-radius: 4px;
    }
    .info-key {
      font-weight: bold;
    }
    .info-value {
      
    }
    .unit {
      padding-left: 4px;
      color: #818181;
    }
    .bordered {
      border: 1px gray dashed;
      border-radius: 4px;
    }
  </style>
</head>
<body>
  <h1>New Products</h1>
  <p>The following is a list of products</p>
  <div id="list" class="product-list">
    <ul>
      <li>
        <span class="name">Product-A</span>
        <span class="price">$199.95</span>
        <div class="info bordered">
          <p>Description for Product-A</p>
          <ul>
            <li><span class="info-key">Quality</span> <span class="info-value">Good</span></li>
            <li><span class="info-key">Duration</span> <span class="info-value">5</span><span class="unit">years</span></li>
          </ul>
        </div>
      </li>
      <li class="selected">
        <span class="name">Product-B</span>
        <span class="price">$129.95</span>
        <div class="info">
          <p>Description for Product-B</p>
          <ul>
            <li><span class="info-key">Quality</span> <span class="info-value">Medium</span></li>
            <li><span class="info-key">Duration</span> <span class="info-value">2</span><span class="unit">years</span></li>
          </ul>
        </div>
      </li>
      <li>
        <span class="name">Product-C</span>
        <span class="price">$99.95</span>
        <div class="info">
          <p>Description for Product-C</p>
          <ul>
            <li><span class="info-key">Quality</span> <span class="info-value">Good</span></li>
            <li><span class="info-key">Duration</span> <span class="info-value">4</span><span class="unit">years</span></li>
          </ul>
        </div>
      </li>
    </ul>
  </div>
  <p>All products are available for sale!</p>
</body>
</html>

我们先读入网页:

page = read_html("../../../static/datasets/new-products.html")

在继续之前,我们需要了解下XML,编写良好且组织规范的HTML文档可以被看做XML文档的一个特例,与HTML不同,XML允许任意的标签和属性。下面是一个简单示例:

<?xml version = "1.0"?>
<root>
    <product id = "1">
        <name>Product-A</name>
        <price>$199.95</price>
    </product>
    <product id = "2">
        <name>Product-B</name>
        <price>$129.95</price>
    </product>
</root>

XPath专门用于提取XML文档中的数据,html_nodes()支持XPath表达式,可以通过参数xpath=实现。

CSS选择器会匹配所有子层级的节点,而XPath表达式中,标签///匹配不同的节点,即//标签引用所有子层级的节点,而/标签只引用第1个子层级的<tag>节点。

下面是一些用法

page %>% html_nodes(xpath = "//p")
#> {xml_nodeset (5)}
#> [1] <p>The following is a list of products</p>
#> [2] <p>Description for Product-A</p>
#> [3] <p>Description for Product-B</p>
#> [4] <p>Description for Product-C</p>
#> [5] <p>All products are available for sale!</p>

选择所有具有class属性的<li>节点:

page %>% html_nodes(xpath = "//li[@class]")
#> {xml_nodeset (1)}
#> [1] <li class="selected">\n        <span class="name">Product-B</span>\n      ...

选择<div id = "list"><ul>节点所有<li>子节点:

page %>% html_nodes(xpath = "//div[@id = 'list']/ul/li")
#> {xml_nodeset (3)}
#> [1] <li>\n        <span class="name">Product-A</span>\n        <span class="p ...
#> [2] <li class="selected">\n        <span class="name">Product-B</span>\n      ...
#> [3] <li>\n        <span class="name">Product-C</span>\n        <span class="p ...

选择所有嵌套于<div id = "list"><li>标签下的<span class = "name">子节点:

page %>% html_nodes(xpath = "//div[@id = 'list']//li/span[@class='name']")
#> {xml_nodeset (3)}
#> [1] <span class="name">Product-A</span>
#> [2] <span class="name">Product-B</span>
#> [3] <span class="name">Product-C</span>

选择嵌套于<li class = "selected">中<span class = "name">子节点。

page %>% 
    html_nodes(xpath = "//li[@class='selected']/span[@class = 'name']")
#> {xml_nodeset (1)}
#> [1] <span class="name">Product-B</span>

下面例子就不能用等效CSS来实现了:

page %>% 
    html_nodes(xpath = "//div[p]")
#> {xml_nodeset (3)}
#> [1] <div class="info bordered">\n          <p>Description for Product-A</p>\n ...
#> [2] <div class="info">\n          <p>Description for Product-B</p>\n          ...
#> [3] <div class="info">\n          <p>Description for Product-C</p>\n          ...
page %>% 
    html_nodes(xpath = "//span[@class ='info-value' and text() = 'Good']")
#> {xml_nodeset (2)}
#> [1] <span class="info-value">Good</span>
#> [2] <span class="info-value">Good</span>

XPath非常灵活,在匹配节点方面是强大的工具。

更多匹配内容可以阅读 W3School,本文更多相关数据放在 https://github.com/ShixiangWang/shixiangwang/tree/master/R/learningR_data