

博客园是国内著名的个人博客站,里面大牛如云,博客质量高。在这里将博客园作为介绍Scrapy爬取站点的练习。在这里先使用命令创建代码框架:
创建项目
scrapy startproject cnblogs
New Scrapy project 'cnblogs', using template directory 'D:\Software\Anaconda3\lib\site-packages\scrapy\templates\project', created in:
F:\Codes\cnblogs
You can start your first spider with:
cd cnblogs
scrapy genspider example example.com
创建爬虫
scrapy genspider -t crawl blog www.cnblogs.com
Created spider 'blog' using template 'crawl' in module:
cnblogs.spiders.blog
创建后,代码框架如下:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class BlogSpider(CrawlSpider):
name = 'blog'
allowed_domains = ['www.cnblogs.com']
start_urls = ['http://www.cnblogs.com/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
页面结构分析
在这里,只爬取博客园首页的博客文章列表,例如:
从中提取每个博文的标题、作者、作者链接、发布时间、摘要、评论/阅读/推荐次数等,页面与HTML的对应关系是(点击看大图):
提取函数
针对这个HTML结构,我们定义提取函数parse_page:
def parse_page(self, response):
items = []
posts = response.css(".post_item")
for post in posts:
item = {
'diggnum' : post.css(".diggnum::text").re_first(r'\d+'), #推荐数
'title' : post.css(".titlelnk::text").extract_first(), #标题
'titlelnk' : post.css(".titlelnk::attr(href)").extract_first(), #标题链接
'summary' : "".join(post.css(".post_item_summary::text").extract()), #摘要
'fack' : post.css(".pfs::attr(src)").extract_first(), #作者头像
'author' : post.css(".lightblue::text").extract_first(), #作者名称
'authorlnk' : post.css(".lightblue::attr(href)").extract_first(), #作者主页
'time' : post.css(".post_item_foot::text").re_first(r'\d+-\d+-\d+ \d+:\d+'), #发表时间
'comment' : post.css(".article_comment .gray::text").re_first(r'\d+'), #评论数
'view' : post.css(".article_comment .gray::text").re_first(r'\d+') #阅读数
}
item = self.strip(item)
items.append(item)
print(items)
在这里,为了提取数据,有几个方法需要大概了解下:
.css(...) css选择器,可以使用大部分的css表达式匹配数据
.extract() 提取匹配的所内容,它返回的是一个列表
.extract_first() 提取匹配到的第一个内容
.re_first(...) 通过正则表达式提取匹配的第一个内容
::text css选择器用于提取元素的文本(所有文本节点的内容)
::attr(href) css选择器用于提取元素的href属性
另外,除了css选择器,还支持xpath选择器。
由于匹配的文本内容可能存在\n\r的换行符,这里定义了strip函数用于处理这种情况:
def strip(self, item):
for key in item:
if(isinstance(item[key], str)):
item[key] = item[key].strip()
return item
这时直接执行爬虫是没有数据打印出来,这是因为,我们需要重新定义rules让爬虫爬取列表URL。
定义Rule
列表URL一共有两个规则,可以定义两个Rule来匹配:
rules = (
Rule(LinkExtractor(allow=r'^https://www\.cnblogs\.com/$'), callback='parse_page', follow = False),
Rule(LinkExtractor(allow=r'sitehome/p/'), callback='parse_page', follow = False),
)
由于第一页的URL只有一个斜杠(/),如果allow=r'/'的话,会匹配很多我们不需要的链接,这里用了一种比较保险的办法。
为了验证数据提取,我们先只启用第一个Rule,第二个Rule注释掉,然后进行测试:
scrapy crawl blog
正常的话,会返回类似下面的数据:
测试通过后,启用两个Rule并设置follow=True,即可以爬取所有200页的数据。
爬行延时
为了不给博客园产生太大的压力,可以启动scrapy的配置,位置在代码目录下的settings.py:
# Configure a delay for requests for the same website (default: 0)
# See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 3
这个延迟是在同一个站点的两个请求的间隔时间,如果未设置的时候,默认是0,即以最快速度爬行。
这里设置了间隔3秒,但是测试发现,爬行的时候,延迟时间并不是精确的3秒,它会在3秒范围内随机有增减。这是为了模拟人正常的鼠标点击请求。如果固定为一个精确值,爬行具有反爬策略站点时,会很容易被识别出来。
完整的代码
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
import json, os
class BlogSpider(CrawlSpider):
name = 'blog'
allowed_domains = ['www.cnblogs.com']
start_urls = ['https://www.cnblogs.com/']
rules = (
Rule(LinkExtractor(allow=r'^https://www\.cnblogs\.com/$'), callback='parse_page', follow = True),
Rule(LinkExtractor(allow=r'sitehome/p/'), callback='parse_page', follow = True),
)
def strip(self, item):
for key in item:
if(isinstance(item[key], str)):
item[key] = item[key].strip()
return item
def parse_page(self, response):
print('*' * 10, '当前URL', response.url)
items = []
posts = response.css(".post_item")
for post in posts:
item = {
'diggnum' : post.css(".diggnum::text").re_first(r'\d+'), #推荐数
'title' : post.css(".titlelnk::text").extract_first(), #标题
'titlelnk' : post.css(".titlelnk::attr(href)").extract_first(), #标题链接
'summary' : "".join(post.css(".post_item_summary::text").extract()), #摘要
'fack' : post.css(".pfs::attr(src)").extract_first(), #作者头像
'author' : post.css(".lightblue::text").extract_first(), #作者名称
'authorlnk' : post.css(".lightblue::attr(href)").extract_first(), #作者主页
'time' : post.css(".post_item_foot::text").re_first(r'\d+-\d+-\d+ \d+:\d+'), #发表时间
'comment' : post.css(".article_comment .gray::text").re_first(r'\d+'), #评论数
'view' : post.css(".article_comment .gray::text").re_first(r'\d+') #阅读数
}
item = self.strip(item)
items.append(item)
#记录数据
caches = []
if(os.path.exists('cnblogs.json')):
with(open('cnblogs.json', 'rt', encoding='UTF-8')) as f:
caches = json.load(f)
caches = caches + items
with(open('cnblogs.json', 'wt', encoding='UTF-8')) as f:
json.dump(caches, f, ensure_ascii=False)
#print(items)