不积跬步,无以至千里;不积小流,无以成江海。

Dean's blog

  • Join Us on Facebook!
  • Follow Us on Twitter!
  • LinkedIn
  • Subcribe to Our RSS Feed

使用 Scrapy 爬取博客园列表

Scrapy博客园

博客园是国内著名的个人博客站,里面大牛如云,博客质量高。在这里将博客园作为介绍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)

 
不允许评论
粤ICP备17049187号-1