要学习自然语言处理,必须要有的就是语料库(corpus),除了公开的语料库以外,如果要对特定的内容进行分析,就需要自己准备数据。不出意外的话,要想获得大量的文本预料数据,就只能使用网络爬虫进行爬取。因此,这几天开始学习网络爬虫,看网上很多说法,要学习各种各样的网络协议,对我个人而言,主要是为了快速掌握一门知识,因此就直接从最快的东东下手了——Scrapy——一个异常成熟的网络爬虫框架。说它是框架,是因为里面已经准备好了各种各样的工具,如认证、双向爬取等等,在框架中的特定位置填充好代码后,只需要在终端中运行一条条命令的,就可以进行爬取了。下面我们来看看Scrapy时怎样爬取网络数据的。
需要说明的是,本文的代码都是在Python3下完成的。
1.XPath
众所周知,在HTML中所有的数据都是以树的形式保存数据的,要想抓取特定位置的数据,就需要使用XPath。XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。
如果想要测试XPath,可以使用Chrome浏览器的开发者工具(Developers Tools)-Console来直接测试XPath语句的正确性。具体使用方式如下:
$x('<XPath表达式>')
关于XPath的语法,其实也非常简单,我们一般用到的主要有三种,如下表所示:
表达式 | 描述 |
---|---|
/ | 从根节点选取特定类型的元素 |
// | 从任意位置选取特定类型的元素 |
@ | 所要选取的节点属性 |
看了这张表,可能还未必完全理解,需要举几个例子,以example这个网站为例,其HTML代码如下:
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 50px;
background-color: #fff;
border-radius: 1em;
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
body {
background-color: #fff;
}
div {
width: auto;
margin: 0 auto;
border-radius: 0;
padding: 1em;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is established to be used for illustrative examples in documents. You may use this
domain in examples without prior coordination or asking for permission.</p>
<p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
如果需要选取<div>中的<h1>元素,就可以使用如下代码:
代码1:
/html/body/div/h1
代码2:
//div/h1
上面两段代码都能实现刚才提到的功能,其中第一段代码使用了从根开始的定位方法,而第二段代码则使用了双斜杠,意味着在整个文档中查找所有div,然后在其子节点中寻找h1节点。使用上述代码后,得到的都是:
<h1>Example Domain</h1>
如果想要直接获得h1
标签内的内容,则需要使用
//div/h1/text()
如果有多个h1标签,而我们又只想获得某一个,则可以使用
//div/h1[index]
其中index为第i个元素的索引,需要注意的是,索引是从1开始计算
如果想要获得某个标签的特定属性,例如我们想要获得链接的href
属性,则可以
//a/@href
当然,XPath的代码远不止上面这么简单,我们再来看几个例子,以下例子均来自于《精通Python爬虫框架Scrapy》
#获得id为'firstHeading'的h1标签中所包含的文字信息
//h1[@id="firstHeading"]/text()
# 获取id为toc的div标签内的无序列表(ul)中的所有链接的URL
//div[@id="toc"]/ul//a/@href
# 获取class属性为ltr以及skin-vector的任意元素内,所有的h1中的文本
//*[contains(@class, "ltr") and contains(@class, "skin-vector")]//h1//text()
2. Scrapy
2.1 Scrapy Shell
Scrapy Shell 是Scrapy框架提供的一个能够爬取单一页面的工具,能够有效帮助我们调试程序遇到的各类问题。在此,我们先给出几个小例子,以百度百科为例吧:
scrapy shell -s USER_AGENT="Mozilla/5.0" https://baike.baidu.com/item/%E5%BB%B6%E7%A6%A7%E6%94%BB%E7%95%A5/20481391#hotspotmining
执行上述代码后:
可以看到,已经成功获得了网页信息,如果要测试XPath,就可以使用如下代码:
response.xpath('//dd[@class="lemmaWgt-lemmaTitle-title"]/h1/text()').extract()
用上述代码可以成功获得网页的标题信息
['延禧攻略']
此外,还可以使用css函数,直接选择对应的元素
response.css('.lemmaWgt-lemmaTitle-title').xpath('//h1/text()').extract()
如果要退出Scrapy Shell,则需要使用Ctrl+D
但是对某些网页执行
scrapy shell
的时候(例如万方),会无法进入shell的交互模式,不知道为什么,目前我怀疑和网页的加载方式有关。
2.2 Scrapy框架
从这里开始,我们正式进入Scrapy的框架学习。
2.2.1 建立项目
要想生成一个Scrapy项目,只需要使用如下代码:
scrapy startproject <your project name>
使用Tree命令后,可以看到生成之后的目录结构:
其中item.py中定义了我们将要收集的信息,例如我们可能需要采集"北京大学"中的标题信息、位置信息以及描述信息(请原谅,我又换了一个例子,其他的网站都有反爬机制),那么这些就可以通过在
items.py
中定义一个类来描述:
from scrapy.item import Item, Field
class PkuItem(Item):
title = Field()
date = Field()
2.2.2 真正的爬虫
此时,整个框架中还没有爬虫,只是定义了我们的数据内容,要想编写爬虫,同样需要使用我们的框架,在my_scrapy_project
的根目录下运行如下命令:
scrapy genspider [options] <name> <domain>
其中的options包括
例如,我们如果要生成一个名为pku(北京大学)的网络爬虫,就可以使用如下代码:
scrapy genspider pku pku.edu.cn
执行成功后,会看到爬虫的目录发生了变化——在spiders文件夹下多了一个dzdp.py
文件。该文件就是我们需要编写爬虫的地方
其代码模板会生成如下:
import scrapy
class PkuSpider(scrapy.Spider):
name = "pku"
allowed_domains = ["pku.edu.cn"]
start_urls = ['http://news.pku.edu.cn']
def parse(self, response):
pass
其中allowed_domains
表示允许爬取的域名范围,start_urls
中存储的是我们要爬取的网页地址,我们先将其进行简单的修改,只爬取一个网页http://news.pku.edu.cn/xwzh/2018-08/28/content_304033.htm
。
parse
函数是我们需要编写的部分,可以很清楚的看到parse
函数中的参数response,这是我们在shell中已经见到过的,我们先将其修改为最简单的一种写法:
import scrapy
from my_scrapy_project.items import PkuItem
class PkuSpider(scrapy.Spider):
name = "pku"
allowed_domains = ["pku.edu.cn"]
start_urls = ['http://news.pku.edu.cn/xwzh/2018-08/26/content_304015.htm']
def parse(self, response):
item = PkuItem()
item['title'] = response.css('.con18').xpath('text()').extract()
item['date'] = response.css('.be12').xpath('text()').re('\d{4}-\d{2}-\d{2}')
return item
编写好之后就可以运行爬虫了,运行爬虫可以直接在shell中运行:
scrapy crawl pku
执行之后的结果如下图所示,可以从中看出我们已经在网站中获得了想要的信息。
此外,还可以使用ItemLoader来简化上述代码:
# -*- coding: utf-8 -*-
import scrapy
from my_scrapy_project.items import PkuItem
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose
class PkuSpider(scrapy.Spider):
name = "pku"
allowed_domains = ["www.pku.edu.cn/"]
start_urls = ['http://news.pku.edu.cn/xwzh/2018-08/26/content_304015.htm']
def parse(self, response):
l = ItemLoader(item = PkuItem(), response = response)
l.add_xpath('title', '//td[@class="con18"]/text()', MapCompose(str.strip))
l.add_xpath('date', '//*[@class="be12"]/text()', MapCompose(str.strip), re='\d{4}-\d{2}-\d{2}')
return l.load_item()
当然,我们还可以将爬取到的数据直接存储为csv、json等格式:
scrapy crawl pku -o items.json
scrapy crawl pku -o items.csv
scrapy crawl pku -o items.jl
scrapy crawl pku -o items.xml
2.2.3 爬取多个URL
在之前的例子中,我们只是爬取了http://news.pku.edu.cn/xwzh/2018-08/26/content_304015.htm
一个网址,如果我们想爬取多个网址的话,就需要使用新的方法。这次我们要爬取的对象是北大新闻网,对于这样一个网站,网络爬虫主要有两个爬取方向:
- 横向爬取 所谓横向就是从一个列表页面跳转到另一个列表页面
- 纵向爬取 所谓纵向爬取是指从一个列表页面提取出每个详情页面
要实现横向爬取,首先需要获取下一页
的xPath
,通过观察,我们将北大新闻网站上的下一页的xpath设置为//a[@class="be12"]/@href
;而纵向爬取时,每个新闻的xpath可以使用//table[@id="nav2_7Tabcontent_10"]//a/@href
在确定了需要爬取的对象之后,我们就可以名正言顺的开始编写爬虫的代码了。首先我们将之前编写的parse
函数重新命名为parse_item
,之后在重新编写parse
函数,parse函数的代码如下:
# -*- coding: utf-8 -*-
import scrapy
from my_scrapy_project.items import PkuItem
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose
from scrapy.http import Request
import urllib
class PkuSpider(scrapy.Spider):
name = "pku"
allowed_domains = ["pku.edu.cn"]
start_urls = ['http://news.pku.edu.cn/xwzh/xwzh.htm']
# 解析函数
def parse(self, response):
next_selector = response.xpath('//a[@class="be12"]/@href').extract()
for url in next_selector:
yield Request(response.urljoin(url))
item_selector = response.xpath(
'//table[@id="nav2_7Tabcontent_10"]//a/@href').extract()
for url in item_selector:
yield Request(response.urljoin(url), callback=self.parse_item)
# 原parse函数
def parse_item(self, response):
l = ItemLoader(item=PkuItem(), response=response)
l.add_xpath(
'title', '//td[@class="con18"]/text()', MapCompose(str.strip))
l.add_xpath('date', '//*[@class="be12"]/text()',
MapCompose(str.strip), re='\d{4}-\d{2}-\d{2}')
return l.load_item()
2.2.4 爬取多个URL的另一种实现方式
同样是爬取多个URL,除了使用上面的方法以外,还可以更改爬虫的模板。默认情况下,使用genspider
命令时,会使用默认的模板basic
来生成爬虫,如果要想查看可用的模板,可以用如下命令
scrapy genspider --list
能够看到,scrapy提供了四种模板
Available templates:
basic
crawl
csvfeed
xmlfeed
这次,我们来使用crawl来实现上面的双向爬取的功能。首先,以模板crawl
来生成爬虫:
scrapy genspider -t crawl easy_pku pku.edu.cn
此时,在spiders文件夹下,会生成一个easy_pku.py
的文件:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class EasyPkuSpider(CrawlSpider):
name = 'easy_pku'
allowed_domains = ['pku.edu.cn']
start_urls = ['http://pku.edu.cn/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
i = {}
#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
#i['name'] = response.xpath('//div[@id="name"]').extract()
#i['description'] = response.xpath('//div[@id="description"]').extract()
return i
其中绝大部分与之前的pku爬虫相同,最重要的不同在于亮点:
- 继承自不同的类,
EasyPkuSpider
继承自CrawlSpider
- 多了一个rules
可以预见的是,要实现双向爬取,就需要在rules上面做文章了。我们先将start_urls设置为起始页面http://news.pku.edu.cn/xwzh/xwzh.htm
,然后将前面编写的解析每个详情页面的函数,放到parse_item
函数中,最后修改rules
:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy.loader.processors import MapCompose
from my_scrapy_project.items import PkuItem
from scrapy.loader import ItemLoader
class EasyPkuSpider(CrawlSpider):
name = 'easy_pku'
allowed_domains = ['pku.edu.cn']
start_urls = ['http://news.pku.edu.cn/xwzh/xwzh.htm']
rules = (
Rule(LinkExtractor(restrict_xpaths='//*[contains(@class, "be12")]')),
Rule(LinkExtractor(restrict_xpaths='//table[@id="nav2_7Tabcontent_10"]//*'), callback='parse_item'),
)
def parse_item(self, response):
l = ItemLoader(item=PkuItem(), response=response)
l.add_xpath(
'title', '//td[@class="con18"]/text()', MapCompose(str.strip))
l.add_xpath('date', '//*[@class="be12"]/text()',
MapCompose(str.strip), re='\d{4}-\d{2}-\d{2}')
return l.load_item()