R-网页爬虫:初识
王诗翔 · 2018-09-16
数据处理的数据集并非立即可得,有时我们需要自己收集�数据,对很多研究领域,网页内容是一个重要的数据源。
查阅网页内容
下面是一个简单的网页,几乎所有的网页都可以查看源代码(一般是右键——点击查看源代码)。
<!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>
)。
关于表格:
<table>
按行构建<tr>
是表格的行<th>
是表头单元格<td>
是表格的单元格
一些其他标签:
<div>
章节<ul>
无序列表<li>
列表项目<span>
应用样式的章节
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>
节点。
下面是一些用法:
- 选择所有
<p>
节点
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来实现了:
- 选择所有包含
<p>
子节点的<div>
节点:
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 ...
- 选择所有的
<span class = "info-value">Good</span>
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。