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代理服务的使用

results matching ""

    No results matching ""