【爬虫实战】利用scrapy框架爬取豆瓣图书信息

​本文作者:陈 鼎 中南财经政法大学统计与数学学院

文字编辑:任 哲

技术总编:张馨月

一、前言

  scrapy是基于twisted的异步处理框架,与传统的requests爬虫程序执行流程不同,scrapy使用多线程,将发送请求,提取数据,保存数据等操作分别交给Scheduler(调度器),Downloader(下载器),Spider(爬虫),Pipeline(管道)等爬虫“组件”来完成。多线程的运行框架使得爬虫的效率大大提升,让爬虫程序变得更快,更强。基于以上特点,本文将以爬取豆瓣图书信息为例,简要阐述基于scrapy框架下的爬虫实现流程。

二、爬虫流程以及代码实现

(一)分析需要爬取的网页结构

  在编写一个爬虫项目之前,我们需要对所需爬取的网页有一个清晰的认识。爬虫的本质是在响应中的字符串提取所需信息,即只有我们提取到的响应中存在我们所需要的数据时,我们才能进行爬虫。我们访问豆瓣读书(https://book.douban.com/tag/?view=type),发现豆瓣图书标签中存在许多大分类(文学,文化...),大分类中存在许多小分类(小说,外国文学...)。点开每个小分类标签,会呈现出不同类型的书的列表清单,且不止一页。我们要做的就是提取豆瓣所有类型书籍下的所有书籍的简要信息,包括图书作者,书名,图书价格,豆瓣评分,书籍评论人数等。网页的页面如下图所示:

image

图1.豆瓣的图书标签页

image

图2.豆瓣每个小标签下的url页面

(二)创建scrapy项目

  创建scrapy项目十分简单,首先打开命令提示符,通过cd命令路径,将工作路径定位到我们需要创建项目的路径下,然后创建一个scrapy项目,用到的程序如下:

scrapy startproject douban_books #创建一个名字为douban_books的爬虫项目
cd douban_books #定位到项目文件夹内
scrapy genspider book book.douban.com #创建爬虫所需的脚本文件book.py;book.douban.com设置允许爬取的网页范围(allow_domains)                                                                     
image

图3.scrapy项目内容显示

(三)设置USER_AGENT,LOG_LEVEL

  接下里我们切换到settings.py文件中,对爬虫项目进行变量配置与赋值。首先,利用url地址请求头中的USER_AGENT对发送请求进行伪装,可更加顺利地发送请求并获取到服务器的响应。用LOG_LEVEL设置的日志级别,让打印出来的结果更加干净,整洁。

USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36' #设置useragent
LOG_LEVEL = 'WARNING' #设置日志级别,即输出结果只会显示warning以及warning以上的日志

(四)编写爬虫程序

  打开spiders文件夹下的book.py文件,我们将在此文件中编写实现提取数据的核心代码。

  在此程序中,start_url为我们首先要发送的url地址,该url地址不受allowed_domains约束;parse函数用来执行提取start_url页面数据的主要逻辑,需要注意,该函数名不可以随意更改。如下:

import scrapy
import re
from copy import deepcopy

class BookSpider(scrapy.Spider):
    name = 'book'#爬虫名
    allowed_domains = ['book.douban.com'] #允许爬取的url地址范围
    start_urls = ['https://book.douban.com/tag/?view=type'] #首先发送请求的url地址

    def parse(self, response): #实现提取数据等主要逻辑
        pass

  程序的基本框架搭建好后,就可以开始编写获取网页信息的程序。

1. 提取豆瓣图书的大标签,小标签

  在开发者工具的elements界面,可以通过对网页的观察定位到所需信息的xpath并进行相应提取。此外,大家也可使用爬虫利器xpath helper进行定位。

  通过网页标签分析可以看出,大标题(小说,外国文学...)对应了六块大标签,每块大标签下存放了以行捆绑的中标签,中标签下才是我们所需要提取的小标签(文学,文化...)。因此想要提取到所有数据,我们需要对响应进行三次遍历操作。

image

图4.该6个div标签下存放有标签数据

image

图5.每个小便签按照每一行进行分组

image

图6.每个td小标签下有我们所需要的标签数据和详情页地址

def parse(self, response):
    item = {}
    div_list = response.xpath(".//div[@class='article']/div[2]/div")  # 进行分组
    for div in div_list:
        item["big_title"] = div.xpath("./a/@name").extract_first()  # 提取大标签
        tr_list = div.xpath(".//table[@class='tagCol']")  # 进行分组
        for tr in tr_list:
            td_list = tr.xpath(".//td")
            for td in td_list:
                item["small_title"] = td.xpath("./a/text()").extract_first()
                item["cate_list_url"] = td.xpath("./a/@href").extract_first()
image

图7.我们所获取到的大标签、小标签以及对应的url地址

2.发送每个小标签地址的请求,获取每个小标签url地址的响应

  从图7可以看出,我们抓取的url地址不完整,因此需要对其进行补充,再分别发送请求。

if item["cate_list_url"] is not None:
    item["cate_list_url"] = 'https://book.douban.com' + item["cate_list_url"]
    yield scrapy.Request(
        item["cate_list_url"],
        callback=self.parse_list,
        meta={"item": deepcopy(item)}
        )
3. 提取书籍的数据

  在第2节中我们获取了每个小标签url地址的响应,接下来只需要新定义一个函数parse_list来处理响应,就可以抓取到所需数据。而这些数据中往往带有换行符、制表符以及空格等我们所不需要的字符,因此可以使用正则表达式进行处理,使最终提取的数据更为美观。

def parse_list(self, response):
    item = response.meta["item"]
    li_list = response.xpath(".//ul[@class='subject-list']/li")  # 分组
    for li in li_list:
        item["book_name"] = li.xpath(".//div[@class='info']/h2/a/@title").extract_first()
        item["book_name"] = re.sub(r"[(\n)(\t)( )]", "", item["book_name"]) #删除书名中的空格与换行符等
        item["book_score"] = li.xpath(".//div[@class='star clearfix']/span[@class='rating_nums']/text()").extract_first()
        book_detail_str = li.xpath(".//div[@class='info']//div[@class='pub']/text()").extract_first()
        book_detail_str = re.sub(r"[(\n)( )]", "", book_detail_str) #提取书籍简要信息,并对简要信息进行切片处理,提取切片中的内容
        book_detail_list = list(book_detail_str.split("/"))
        item["book_price"] = book_detail_list[-1] if len(book_detail_list) > 0 else None
        item["book_author"] = book_detail_list[0] if len(book_detail_list) > 0 else None
        item["book_comment_nums"] = li.xpath(".//div[@class='star clearfix']/span[@class='pl']/text()").extract_first()
        item["book_comment_nums"] = re.sub(r"[(\n)( )]", "", item["book_comment_nums"])
        print(item)
image

图8.数据简要预览

4 实现翻页请求

  通过对网页结构进行分析,小编发现每一个小便签下的图书信息不止一页,因此需要设置翻页请求,定位到“下一页”按钮标签后,同样发现抓取的url地址不全,需要将地址补全。小编将发出翻页请求所对应的响应(即callback)也放入parse_list函数。

image

图9.每一页数据爬取完毕需对下一页进行请求

next_page = response.xpath(".//span[@class='next']/a/@href").extract_first()#提取url地址
if next_page is not None:#判断,如果还有下一页,就继续发送请求
    next_page  = 'https://book.douban.com' + next_page
    yield scrapy.Request(
        next_page,
        callback=self.parse_list,#把发出的请求交给parse_list函数进行处理
        meta = {"item":deepcopy(item)}
        )
    yield item
5. 开启管道,保存数据

  管道用于将爬取的数据保存到本地文件或数据库中。管道需要事先在settings.py文件中开启,将# Configure item pipelines下的注释行解除,便可在pipeline.py中实现保存功能,本文以创建一个.txt文档对数据进行保存为例。

import json
class DoubanBooksPipeline:
    def process_item(self, item, spider):
        with open("douban_book_list.txt","a",encoding="utf-8") as f:
            f.write(json.dumps(item,ensure_ascii=False))
6. 运行项目,爬取数据

  完成上述设置后,就可运行项目抓取数据啦~运行项目仅需打开命令提示符,输入如下程序即可执行:

scrapy crawl book

三、注意事项

  1.deepcopy是用于不同函数在多线程传输之中进行备份操作的行为。由于数据公用一个item字典,在多线程操作中可能出现重复值的现象,利用deepcopy可以有效地解决item中出现重复值的问题。
  2.豆瓣具备一定反爬虫,scrapy可能用同一个headers运行数秒就会被服务器监测为爬虫行为,需要利用一些反反爬虫的操作。例如:在settings.py中设置一个USER_AGENT和IP池,并在发送请求时随机选择一个USER_AGENT和IP,便可以进行有效地伪装,达到更完善的爬虫效果。

  (ps:需要完整的程序可以关注微信公众号:Stata and Python数据分析,并在后台回复“豆瓣图书”来获取~)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342