1.环境
python3.8或python3.7
pycharm2021.2
MongoDB
Scrapy
2.信息提取
2.1 创建Scrapy项目
在cmd模式下创建Scrapy项目
# 进入要存放该项目的文件夹下
cd E:\Scrapy Project
# 创建Scrapy项目
scrapy startproject douban
# 进入该项目下,“douban”这个文件夹有一个一级文件夹和二级文件夹,我们只用进入一级文件夹
cd douban
# 创建Spider
scrapy genspider douban_book douban.com
2.2 在Pycharm中打开该项目
打开后,文件树结构为:
注意scrapy.cfg配置文件一定在“douban”一级文件夹下,不然在后面运行该爬虫时,会报错。
2.3 项目目标
1.获取按评分排行的书籍排行榜前任意页的书籍基本信息
2.获取每本书籍的读者评论信息和读者主页链接
3.获取读者的地址
4.写入MongoDB数据库
2.4 初始化数据
初始化rank_url、book_comment_url、headers、allowed_domain等信息,其中name是不能修改的。
刚打开douban_book.py,是如下图所示的界面。
然后修改上述信息。如下图所示。
其中allowed_domain一般情况下不用修改,我这里是测试的时候,发现访问书籍链接地址的时候,其域名是“book.douban.com”,所以添加了进去,其实还有另一种方法,就是在Request函数(很关键的函数)中把域名过滤给关闭掉。
start_urls是在你没有指定访问链接的时候,默认的访问链接,如果有就不会访问该链接。
这里我们有访问的链接,所以我们把start_urls删除了,写上了自己要访问的rank_url,也就是书籍排行榜的访问链接地址,这里有四个,是不同分类的书籍排行榜,分别是文化分类下的“历史”,“哲学”等几个标签,当然也可以继续增加其他访问链接地址。
但是这个链接地址需要进行预处理,也就是如下图勾画的地方
我们先找到按评分排行的书籍排行榜第二页(因为第一页一般看不出规律)。如下图
然后再到第三页,可以看到start=后面的数字在不断变化,其他都没有变化。
上述写成start={start_rank}是因为字符串的format()方法可以填充该值。
其中book_comments_url也是用相似的方法找到规律后进行的预处理。
2.5 访问书籍排行榜
def start_requests(self):
for url in self.rank_url:
for page in range(0, 30):
start_rank = page * 20
yield Request(url.format(start_rank=start_rank), headers=self.headers,
callback=self.parse_to_books)
其中Request方法表示使用headers去访问url,返回的信息用callback中的方法进行处理。
2.6 对书籍排行榜的信息进行处理——xpath方法
def parse_to_books(self, response):
# 从html信息中可以看到每本书籍的基本信息都在单独的subject-item,所以先找到所有的subject-item
results = response.xpath('//li[@class="subject-item"]')
# 然后遍历每个subject-item,这里用的results保存的所有subject-item
for result in results:
book_href = result.xpath('./div[@class="info"]/h2/a/@href')
book_name = result.xpath('normalize-space(./div[@class="info"]/h2/a/@title)')
book_pub = result.xpath('normalize-space(./div[@class="info"]/div[@class="pub"]/text())')
book_score = result.xpath('./div[@class="info"]/div/span[@class="rating_nums"]/text()')
book_population = result.xpath('normalize-space(./div[@class="info"]/div/span[@class="pl"]/text())')
# 用创建BookItem()对象,用其保存书籍的基本信息
item_book = BookItem()
item_book['book_name'] = book_name.extract()
item_book['book_href'] = book_href.extract()
print(book_href.extract())
# 这里获取book_id,用于访问书籍的评论列表
book_id = str(item_book['book_href']).split('/')[-2]
print(book_id)
item_book['book_pub'] = book_pub.extract()
item_book['book_score'] = book_score.extract()
item_book['book_population'] = book_population.extract()
# 然后遍历该书籍的评论列表前10页,在不登录的情况下,只能访问前10页内容。
for page in range(0, 10):
# 访问书籍评论列表链接,对返回的页面信息用parse_to_reader处理,meta表示传给parse_to_reader会用到的一些数据,是一个字典,可以根据需要继续添加一些数据。
yield Request(self.book_comments_url.format(book_id=book_id, start_comments=page * 20),
headers=self.headers,
callback=self.parse_to_reader, meta={'book_href': book_href.extract()})
yield item_book
2.6 和2.8节会用到BookItem()和ReaderItem_culture(),这几个数据结构在2.9中展示。
2.7 对书籍评论列表页面信息进行处理
这里获取了读者的主页链接、读者的评论,还有传入的meta,也就是书籍的链接,并将上述信息传入到parse_to_reader_address。
def parse_to_reader(self, response):
results = response.xpath('//li[@class="comment-item"]')
for result in results:
reader_href = result.xpath('./div[@class="avatar"]/a/@href').extract()
reader_comment = result.xpath('./div[@class="comment"]/p[@class="comment-content"]/span['
'@class="short"]/text()').extract()
book_href = response.meta['book_href']
yield Request(str(reader_href).strip("['").strip("']"), headers=self.headers,
callback=self.parse_to_reader_address,
meta={'book_href': book_href, 'reader_href': reader_href,
'reader_comment': reader_comment})
其基本原理和2.6节一样。这里就不一一赘述了。
2.8 对读者主页信息进行处理
这里获取了读者的地址信息,其基本原理和上述一样,并将信息传入ReaderItem_culture()对象。
def parse_to_reader_address(self, response):
print("获取用户地址")
book_href = str(response.meta['book_href']).strip("['").strip("']")
reader_href = str(response.meta['reader_href']).strip("['").strip("']")
reader_comment = str(response.meta['reader_comment'])
reader_address_raw = response.xpath(
'normalize-space(//div[@class="basic-info"]/div[@class="user-info"]/a/text())').extract()
reader_address = str(reader_address_raw).strip("['").strip("']")
if reader_address.strip() == '':
print("地址为空")
return
item_reader = ReaderItem_culture()
item_reader['book_href'] = book_href
item_reader['reader_href'] = reader_href
item_reader['reader_comment'] = reader_comment
item_reader['reader_address'] = reader_address
yield item_reader
2.9 以上用到的Item对象
Item对象在Items.py中申明
import scrapy
class BookItem(scrapy.Item):
collection = 'douban_books_culture'
book_name = scrapy.Field()
book_href = scrapy.Field()
book_pub = scrapy.Field()
book_score = scrapy.Field()
book_population = scrapy.Field()
class ReaderItem_culture(scrapy.Item):
collection = 'douban_readers_culture'
book_href = scrapy.Field()
reader_href = scrapy.Field()
reader_comment = scrapy.Field()
reader_address = scrapy.Field()
其中collection表示的是MongoDB的集合名(类似于MySQL中的表名)。
2.10 小结
以上做了这几件事:
1.对豆瓣的书籍基本信息做了提取,并传入到了BookItem()对象。
2.对读者评论信息、主页链接、地址进行了提取,并传入到了ReaderItem_culture对象。
接下来需要将上述提取的信息存入到MongoDB中。
3.存入数据
存入数据的代码在pipeline.py中书写。
import pymongo
import logging
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
# 这一步是从settings配置文件里面获取MONGO_URI、MONGO_DB两个自己设置的常量
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DB')
)
# 这一步是连接MongoDB数据库
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def process_item(self, item, spider):
result = self.db[item.collection].find(item)
logging.debug("*"*50)
if result.count() == 0:
logging.debug("### 无重复、插入数据 ###")
self.db[item.collection].insert(dict(item))
else:
logging.debug("### 有重复、没有插入 ###")
logging.debug("*"*50)
return item
def close_spider(self, spider):
self.client.close()
4.反爬虫
豆瓣的反爬虫机制很简单,就是看你的访问频率,因此我们仅需要做一个随机延迟访问就可以绕过他的反爬虫机制。
随机延迟在middleware.py中书写。
import logging
import random
import time
class RandomDelayMiddleware(object):
def __init__(self, delay):
self.delay = delay
@classmethod
def from_crawler(cls, crawler):
delay = crawler.spider.settings.get("RANDOM_DELAY", 10)
if not isinstance(delay, int):
raise ValueError("RANDOM_DELAY need a int")
return cls(delay)
def process_request(self, request, spider):
delay = random.randint(1, self.delay)
logging.debug("### random delay: %s s ###" % delay)
time.sleep(delay)
5.其他设置
需要修改settings.py文件
还有添加数据库的用户名和密码以及随机延迟上限。
最后修改pipeline.py中的类的优先级以及middleware.py中类的优先级。数字越大,优先级越高。由于我们上述两个文件都分别只有一个类,所以下面配置的意思是先执行RandomDelayMiddleware,再执行MongoPipeline。
6.一些帮助理解的东西
6.1 Scrapy的编码流程
-
先写douban_book.py。这里是进行网页页面信息处理的,包括访问、信息提取等。
-
再写items.py,如果douban_book.py中需要用到item,就在这里写。
-
接着在middlewares.py中写访问延迟。
-
然后再pipeline.py中写对item的存储。
-
最后在settings.py中配置,包括关闭ROBOTSTXT_OBEY,一些上述文件用到的常量以及优先级设置。
6.2 Scrapy工作流程
这部分仅是个人理解,各取所需。
其实Scrapy项目的执行过程不仅限于我们的项目之下的文件,还有在Scrapy包中的文件。Scrapy项目由以下几个部分组成:Engine(引擎)、Item(项目)、Scheduler(调度器)、Downloader(下载器)、Spiders(蜘蛛)、Item Pipeline(项目管道)、Downloader Middlewares(下载器中间件)、Spider Middleware(蜘蛛中间件)。
其中
- Item就是items.py中的东西
- Spiders就是douban_book.py中的东西
- Downloader Middlewares一部分在Scrapy包中,一部分就是middlewares.py中的东西
- Item Pipeline就是pipelines.py中的东西。
其工作方式简单点理解就是如下:
- Engine从Spider,也就是douban_book.py开始执行,
- 然后Engine从Spider中获取了URL,然后将这些URL放入到了Scheduler调度器,通过Request方法调度。应该类似于操作系统里面的调度。
- 接着由调度器传URL给Engine,Engine让Downloader Middlewares处理后发送给Downloader进行下载,也就是页面信息等的下载,Downloader会生成该页面信息的Response,然后将Response通过Downloader Middlewares处理后返回给Spider。
- 然后Spider处理Response,提取Item,或者新的Request给Engine。
- 最后Engine将返回的Item给Item Pipeline,新的Request给Scheduler。
- 重复2-6。
Engine就相当于中介,负责Scheduler、Spider、Downloader、Item Pipeline的联系,然后Engine和Downloader联系过程中需要用到Downloader Middlewares进行处理。