25. 爬虫项目的代码实现
25.1 数据库的准备:
启动MySQL和Redis数据库
在MySQL数据库中创建数据库:
doubandb
,并进入数据库中创建books
数据表
CREATE TABLE `books` (
`id` bigint(20) unsigned NOT NULL COMMENT 'ID号',
`title` varchar(255) DEFAULT NULL COMMENT '书名',
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`press` varchar(255) DEFAULT NULL COMMENT '出版社',
`original` varchar(255) DEFAULT NULL COMMENT '原作名',
`translator` varchar(128) DEFAULT NULL COMMENT '译者',
`imprint` varchar(128) DEFAULT NULL COMMENT '出版年',
`pages` int(10) unsigned DEFAULT NULL COMMENT '页数',
`price` double(6,2) unsigned DEFAULT NULL COMMENT '定价',
`binding` varchar(32) DEFAULT NULL COMMENT '装帧',
`series` varchar(128) DEFAULT NULL COMMENT '丛书',
`isbn` varchar(128) DEFAULT NULL COMMENT 'ISBN',
`score` varchar(128) DEFAULT NULL COMMENT '评分',
`number` int(10) unsigned DEFAULT NULL COMMENT '评论人数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
25.2 模块1的实现:
实现豆瓣图书信息所有标签信息的爬取,并图书的标签信息写入到Redis数据库中,此模块可使用rquests简单实现。
创建一个独立的python文件:load_tag_url.py 代码如下
#使用requests加pyquery爬取所有豆瓣图书标签信息,并将信息储存于redis中
import requests
from pyquery import PyQuery as pq
import redis
def main():
#使用requests爬取所有豆瓣图书标签信息
url = "https://book.douban.com/tag/?view=type&icn=index-sorttags-all"
res = requests.get(url)
print("status:%d" % res.status_code)
html = res.content.decode('utf-8')
# 使用Pyquery解析HTML文档
#print(html)
doc = pq(html)
#获取网页中所有豆瓣图书标签链接信息
items = doc("table.tagCol tr td a")
# 指定Redis数据库信息
link = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
#遍历封装数据并返回
for a in items.items():
#拼装tag的url地址信息
tag = a.attr.href
#将信息以tag:start_urls写入到Redis中
link.lpush("book:tag_urls",tag)
print("共计写入tag:%d个"%(len(items)))
#主程序入口
if __name__ == '__main__':
main()
- 运行:
python load_tag_url.py
25.3 模块2的实现:
此模块负责从Redis中获取每个图书标签,并分页式的爬取每本图书的url信息,并将信息写入到redis中。
① 首先在命令行编写下面命令,创建项目master(主)和爬虫文件
scrapy startproject master
cd master
scrapy genspider book book.douban.com
- ② 编辑
master/item.py
文件
import scrapy
class MasterItem(scrapy.Item):
# define the fields for your item here like:
url = scrapy.Field()
#pass
- ③ 编辑
master/settings.py
文件
...
ROBOTSTXT_OBEY = False
...
#下载器在下载同一个网站下一个页面前需要等待的时间。该选项可以用来限制爬取速度, 减轻服务器压力。同时也支持小数:
DOWNLOAD_DELAY = 2
...
# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0',
}
...
ITEM_PIPELINES = {
'master.pipelines.MasterPipeline': 300,
}
...
# 指定使用scrapy-redis的去重
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# 指定使用scrapy-redis的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 在redis中保持scrapy-redis用到的各个队列,从而允许暂停和暂停后恢复,也就是不清理redis queues
SCHEDULER_PERSIST = True
# 指定排序爬取地址时使用的队列,
# 默认的 按优先级排序(Scrapy默认),由sorted set实现的一种非FIFO、LIFO方式。
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue'
# REDIS_URL = 'redis://localhost:6379' # 一般情况可以省去
REDIS_HOST = 'localhost' # 也可以根据情况改成 localhost
REDIS_PORT = 6379
- ④ 编辑
master/spiders/book.py
文件
# -*- coding: utf-8 -*-
import scrapy
from master.items import MasterItem
from scrapy import Request
from urllib.parse import quote
import redis,re,time,random
class BookSpider(scrapy.Spider):
name = 'master_book'
allowed_domains = ['book.douban.com']
base_url = 'https://book.douban.com'
def start_requests(self):
''' 从redis中获取,并爬取标签对应的网页信息 '''
r = redis.Redis(host=self.settings.get("REDIS_HOST"), port=self.settings.get("REDIS_PORT"), decode_responses=True)
while r.llen('book:tag_urls'):
tag = r.lpop('book:tag_urls')
url = self.base_url + quote(tag)
yield Request(url=url, callback=self.parse,dont_filter=True)
def parse(self, response):
''' 解析每页的图书详情的url地址信息 '''
print(response.url)
lists = response.css('#subject_list ul li.subject-item a.nbg::attr(href)').extract()
if lists:
for i in lists:
item = MasterItem()
item['url'] = i
yield item
#获取下一页的url地址
next_url = response.css("span.next a::attr(href)").extract_first()
#判断若不是最后一页
if next_url:
url = response.urljoin(next_url)
#构造下一页招聘列表信息的爬取
yield scrapy.Request(url=url,callback=self.parse)
- ⑤ 编辑
master/pipelines.py
文件
import redis,re
class MasterPipeline(object):
def __init__(self,host,port):
#连接redis数据库
self.r = redis.Redis(host=host, port=port, decode_responses=True)
@classmethod
def from_crawler(cls,crawler):
'''注入实例化对象(传入参数)'''
return cls(
host = crawler.settings.get("REDIS_HOST"),
port = crawler.settings.get("REDIS_PORT"),
)
def process_item(self, item, spider):
#使用正则判断url地址是否有效,并写入redis。
bookid = re.findall("book.douban.com/subject/([0-9]+)/",item['url'])
if bookid:
if self.r.sadd('books:id',bookid[0]):
self.r.lpush('bookspider:start_urls', item['url'])
else:
self.r.lpush('bookspider:no_urls', item['url'])
- ⑥ 测试运行:
scarpy crawl master_book
25.4 模块3的实现:
负责从Redis中获取每个图书的url地址,并爬取对应的图书详情,将每本图书详情信息写回到redis数据库中。
① 首先在命令行编写下面命令,创建项目salve(从)和爬虫文件
scrapy startproject salve
cd salve
scrapy genspider book book.douban.com
- ② 编辑
salve/item.py
文件
import scrapy
class BookItem(scrapy.Item):
# define the fields for your item here like:
id = scrapy.Field() #ID号
title = scrapy.Field() #书名
author = scrapy.Field() #作者
press = scrapy.Field() #出版社
original = scrapy.Field() #原作名
translator = scrapy.Field()#译者
imprint = scrapy.Field() #出版年
pages = scrapy.Field() #页数
price = scrapy.Field() #定价
binding = scrapy.Field() #装帧
series = scrapy.Field() #丛书
isbn = scrapy.Field() #ISBN
score = scrapy.Field() #评分
number = scrapy.Field() #评论人数
#pass
- ③ 编辑
salve/settings.py
文件
BOT_NAME = 'slave'
SPIDER_MODULES = ['slave.spiders']
NEWSPIDER_MODULE = 'slave.spiders'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
...
# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0',
}
...
ITEM_PIPELINES = {
#'slave.pipelines.SlavePipeline': 300,
'scrapy_redis.pipelines.RedisPipeline': 400,
}
...
# 指定使用scrapy-redis的去重
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# 指定使用scrapy-redis的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 在redis中保持scrapy-redis用到的各个队列,从而允许暂停和暂停后恢复,也就是不清理redis queues
SCHEDULER_PERSIST = True
# 指定排序爬取地址时使用的队列,
# 默认的 按优先级排序(Scrapy默认),由sorted set实现的一种非FIFO、LIFO方式。
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue'
# REDIS_URL = 'redis://localhost:6379' # 一般情况可以省去
REDIS_HOST = 'localhost' # 也可以根据情况改成 localhost
REDIS_PORT = 6379
- ④ 编辑
salve/spiders/book.py
文件
# -*- coding: utf-8 -*-
import scrapy,re
from slave.items import BookItem
from scrapy_redis.spiders import RedisSpider
class BookSpider(RedisSpider):
name = 'slave_book'
#allowed_domains = ['book.douban.com']
#start_urls = ['http://book.douban.com/']
redis_key = "bookspider:start_urls"
def __init__(self, *args, **kwargs):
# Dynamically define the allowed domains list.
domain = kwargs.pop('domain', '')
self.allowed_domains = filter(None, domain.split(','))
super(BookSpider, self).__init__(*args, **kwargs)
def parse(self, response):
print("======================",response.status)
item = BookItem()
vo = response.css("#wrapper")
item['id'] = vo.re_first('id="collect_form_([0-9]+)"') #ID号
item['title'] = vo.css("h1 span::text").extract_first() #书名
#使用正则获取里面的info里面的图书信息
info = vo.css("#info").extract_first()
#print(info)
authors = re.search('<span.*?作者.*?</span>(.*?)<br>',info,re.S).group(1)
item['author'] = "、".join(re.findall('<a.*?>(.*?)</a>',authors,re.S)) #作者
item['press'] = " ".join(re.findall('<span.*?出版社:</span>\s*(.*?)<br>',info)) #出版社
item['original'] = " ".join(re.findall('<span.*?原作名:</span>\s*(.*?)<br>',info)) #原作名
yz = re.search('<span.*?译者.*?</span>(.*?)<br>',info,re.S)
if yz:
item['translator'] = "、".join(re.findall('<a.*?>(.*?)</a>',yz.group(1),re.S)) #译者
else:
item['translator'] = ""
item['imprint'] = re.search('<span.*?出版年:</span>\s*([0-9\-]+)<br>',info).group(1) #出版年
item['pages'] = re.search('<span.*?页数:</span>\s*([0-9]+)<br>',info).group(1) #页数
item['price'] = re.search('<span.*?定价:</span>.*?([0-9\.]+)元?<br>',info).group(1) #定价
item['binding'] = " ".join(re.findall('<span.*?装帧:</span>\s*(.*?)<br>',info,re.S)) #装帧
item['series'] = " ".join(re.findall('<span.*?丛书:</span>.*?<a .*?>(.*?)</a><br>',info,re.S)) #丛书
item['isbn'] = re.search('<span.*?ISBN:</span>\s*([0-9]+)<br>',info).group(1) #ISBN
item['score'] = vo.css("strong.rating_num::text").extract_first().strip() #评分
item['number'] = vo.css("a.rating_people span::text").extract_first() #评论人数
#print(item)
yield item
- ⑤ 编辑
salve/pipelines.py
文件
class SlavePipeline(object):
def process_item(self, item, spider):
return item
- ⑥ 测试运行
# 在spider目录下和book.py在一起。
scrapy runspider book.py
25.5 模块4的实现:
负责从Redis中获取每本图书的详情信息,并将信息依次写入到MySQL数据中,作为最终的爬取信息。
在当前目录下创建一个:item_save.py的独立爬虫文件
#将Redis中的Item信息遍历写入到数据库中
import json
import redis
import pymysql
def main():
# 指定Redis数据库信息
rediscli = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
# 指定MySQL数据库信息
db = pymysql.connect(host="localhost",user="root",password="",db="doubandb",charset="utf8")
#使用cursor()方法创建一个游标对象cursor
cursor = db.cursor()
while True:
# FIFO模式为 blpop,LIFO模式为 brpop,获取键值
source, data = rediscli.blpop(["book:items"])
print(source)
try:
item = json.loads(data)
#组装sql语句
dd = dict(item)
keys = ','.join(dd.keys())
values=','.join(['%s']*len(dd))
sql = "insert into books(%s) values(%s)"%(keys,values)
#指定参数,并执行sql添加
cursor.execute(sql,tuple(dd.values()))
#事务提交
db.commit()
print("写入信息成功:",dd['id'])
except Exception as err:
#事务回滚
db.rollback()
print("SQL执行错误,原因:",err)
#主程序入口
if __name__ == '__main__':
main()
- 使用python命令测试即可
25.6 反爬处理:
- 降低爬取频率
- 浏览器伪装
- IP代理服务的使用