scrapy

基本操作

创建scrapy项目

scrapy startproject mySpider

scrapy目录结构

scrapy.cfg :项目的配置文件

mySpider/ :项目的Python模块,将会从这里引用代码
mySpider/items.py :项目的字段文件(在爬虫文件中保存数据使用)
mySpider/pipelines.py :项目的管道文件
mySpider/settings.py :项目的设置文件
mySpider/spiders/ :存储爬虫文件

创建spider

# itcast是爬虫名, "itcast.cn"是url范围
scrapy genspider itcast "itcast.cn"

自定义items字段类(类似字典)

  • items字段类中所定义的字段, 将在spiders目录下的爬虫代码中使用
  • 爬虫代码中使用items中字段类的对象保存提取的数据, yield返回给engine
  • 如果直接使用return item, 数据不会经过pipelines
import scrapy

class TencentJobItem(scrapy.Item):
    # define the fields for your item here like:
    # 定义提取数据时需要放入字典的key
    title = scrapy.Field()
    company = scrapy.Field()
    address = scrapy.Field()
    salary = scrapy.Field()
    date = scrapy.Field()
    ...
    ...
    # 在提取数据时, item的key对应 字段类中定义的字段
    def parse(self, response):
    div_list = response.xpath("//div[@class='dw_table']/div")[3:-5]
    for div in div_list:
        item = TencentJobItem() # 使用字段类实例化一个用于保存数据的对象(类似字典)
        item['title'] = div.xpath("./p/span/a/@title").extract_first()
        item['company'] = div.xpath("./span[1]/a/text()").extract_first()
        item['address'] = div.xpath("./span[2]/text()").extract_first()
        item['salary'] = div.xpath("./span[3]/text()").extract_first()
        item['date'] = div.xpath("./span[4]/text()").extract_first()
        print(self.num, item)
        yield item  # 传递数据给engine, 由engine传递给piplines
        ...
        ...
        # 在pipeline中使用mongodb时
        # 在写入item之前, 需要转换为dict类型

# 在pipelines中:
from pymongo import MongoClient

client = MongoClient()
collection = client['tieba']['job']

class TencentJobPipeline(object):
    def process_item(self, item, spider):
        # 由于只能讲dict类型写入mongo中, 所以需要类型转换
        # 将TencentJobItem字段类对象转换为dict
        collection.insert(dict(item))
        print('写入成功')
        return item

使用log

  • 在setting.py中加入log配置
# 日志信息等级
# 'WARNING'代表比此等级低的log都不会输出
# LOG_LEVEL = 'WARNING'
# LOG_LEVEL = 'ERROR'
# LOG_LEVEL = 'DEBUG'
LOG_LEVEL = 'INFO'
# log输出的文件
LOG_FILE = 'log.txt'

# cookies的信息
COOKIES_DEBUG = True
  • 在需要打印log的py文件中使用logging模块
import logging

# 创建一个日志对象, 传入__name__, 输出时将打印出本模块(文件)的名字
logger = logging.getLogger(__name__)

logger.warning('warning')
logger.info('info')
logger.error('error')
logger.debug('debug')

  • 如果在普通项目中想打印log, 可以自定义logger
import logging

logging.basicConfig('xxx')
logger = logging.getLogger()
logger.warning('xxxx')

yield 传递数据给 scrapy engine

  • yield支持的数据类型:
    • None
    • dict
    • Request
    • BaseItem

User-Agent的设置

  • 在setting.py文件中配置
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36'

构造Request

  • Request用于将新的请求封装, 发送给scrapy engine
    • url 请求的地址
    • callback 响应的处理方法(提取数据)
    • meta 传递item字典到其他的处理函数中使用
    • dont_filter 默认False, 请求的地址不重复
    • method 请求方式
    • headers 请求头
    • body 请求体
    • cookies 携带cookie
class Request(object_ref):

    def __init__(self, url, callback=None, method='GET', headers=None, body=None,
                 cookies=None, meta=None, encoding='utf-8', priority=0,
                 dont_filter=False, errback=None, flags=None):

实现翻页请求

    def parse(self, response):
        div_list = response.xpath("//div[@class='dw_table']/div")[3:-5]
        for div in div_list:
            item = dict()
            item['title'] = div.xpath("./p/span/a/@title").extract_first()
            item['company'] = div.xpath("./span[1]/a/text()").extract_first()
            item['address'] = div.xpath("./span[2]/text()").extract_first()
            item['salary'] = div.xpath("./span[3]/text()").extract_first()
            item['date'] = div.xpath("./span[4]/text()").extract_first()
            print(self.num, item)
            yield item

        # 获取下一页的地址
        next_url = response.xpath("//a[text()='下一页']/@href").extract_first()
        print('next_url: ', next_url)

        # 当下一页地址不是None时发送请求
        if next_url != None :
            # 构造新的请求发送给引擎
            yield scrapy.Request( 
                next_url,   # 下一页的地址
                callback=self.parse # 处理下一页地址的方法
            )

DeBug信息

  • Filtered offsite request to ‘xxx.com
    • 请求的域名被allowed_domains = [‘xxx’]过滤掉了, 不在规定的域名范围内

setting.py文件说明

# -*- coding: utf-8 -*-

# Scrapy settings for yang_guang_spider project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     https://doc.scrapy.org/en/latest/topics/settings.html
#     https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
#     https://doc.scrapy.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'yang_guang_spider'

SPIDER_MODULES = ['yang_guang_spider.spiders']
NEWSPIDER_MODULE = 'yang_guang_spider.spiders'

LOG_LEVEL = 'WARNING'

# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36'

# Obey robots.txt rules
ROBOTSTXT_OBEY = True

# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 并发请求数量
#CONCURRENT_REQUESTS = 32

# 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
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16

# 是否带有cookie信息, 默认带有
# Disable cookies (enabled by default)
#COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = 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',
#}

# Enable or disable spider middlewares
# See https://doc.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'yang_guang_spider.middlewares.YangGuangSpiderSpiderMiddleware': 543,
#}

# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
#    'yang_guang_spider.middlewares.YangGuangSpiderDownloaderMiddleware': 543,
#}

# Enable or disable extensions
# See https://doc.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   'yang_guang_spider.pipelines.YangGuangSpiderPipeline': 300,
}


# 自动限速
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

访问settings.py文件中的内容

  • 在spiders下的爬虫代码中
self.settings.get("MONGO_HOST")
  • 在pipelines中
spider.settings.get("MONGO_HOST")

数据清洗

  • 字符串数组去掉不需要的字符
content = [re.sub(r"\xa0|\s", "", i) for i in content]  # 替换字符串中的 '\xa0' 和 空白字符(\n\t等)为 ''
content = [i for i in content if len(i) > 0]    # 删除空字符串

使用日志

配置log

LOG_FILE = "TencentSpider.log" # 日志文件
LOG_LEVEL = "INFO" # 日志等级
# LOG_ENABLED   默认: True,启用logging
# LOG_ENCODING  默认: 'utf-8',logging使用的编码
# LOG_FILE      默认: None,在当前目录里创建logging输出文件的文件名
# LOG_LEVEL     默认: 'DEBUG',log的最低级别
# LOG_STDOUT    默认: False 如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
#               例如,执行 print "hello" ,其将会在Scrapy log中显示。

日志等级

Scrapy提供5层logging级别:
CRITICAL - 严重错误(critical)
ERROR - 一般错误(regular errors)
WARNING - 警告信息(warning messages)
INFO - 一般信息(informational messages)
DEBUG - 调试信息(debugging messages) 最低等级

spider编写

1. scrapy.Spider

  • 创建爬虫:
    scrapy genspider tencent “tencent.com
from mySpider.items import TencentItem
import scrapy
import re

class TencentSpider(scrapy.Spider):
    name = "tencent"
    allowed_domains = ["hr.tencent.com"]
    start_urls = [
        "http://hr.tencent.com/position.php?&start=0#a"
    ]
    # 解析start_urls
    def parse(self, response):
    items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
    for item in items:
        temp = dict(
            name=item.xpath("./td[1]/a/text()").extract()[0],
            detailLink="http://hr.tencent.com/"+item.xpath("./td[1]/a/@href").extract()[0],
            positionInfo=item.xpath('./td[2]/text()').extract()[0] if len(item.xpath('./td[2]/text()').extract())>0 else None,
            peopleNumber=item.xpath('./td[3]/text()').extract()[0],
            workLocation=item.xpath('./td[4]/text()').extract()[0],
            publishTime=item.xpath('./td[5]/text()').extract()[0]
        )
        yield temp

    now_page = int(re.search(r"\d+", response.url).group(0))
    print("*" * 100)
    if now_page < 216:
        url = re.sub(r"\d+", str(now_page + 10), response.url)
        print("this is next page url:", url)
        print("*" * 100)
        yield scrapy.Request(url, callback=self.parse)

2. crawlSpider

  • 说明:
CrawlSpider使用rules来决定爬虫的爬取规则,并将匹配后的url请求提交给引擎。
所以在正常情况下,CrawlSpider不需要单独手动返回请求了。
在rules中包含一个或多个Rule对象,每个Rule对爬取网站的动作定义了某种特定操作,比如提取当前相应内容里的特定链接,是否对提取的链接跟进爬取,对提交的请求设置回调函数等。
如果多个rule匹配了相同的链接,则根据规则在本集合中被定义的顺序,第一个会被使用。
  • 创建spider时添加参数: -t crawl
    scrapy genspider –t crawl csdn csdn.cn

  • Rule类

class scrapy.spiders.Rule(
        link_extractor,         # 该对象定义了url提取规则
        callback = None,        # 该函数用于解析符合规则的url, 它接收response对象
        cb_kwargs = None,
        follow = None,          # 是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进.
                                # 如果callback为None,follow 默认设置为True ,否则默认为False。
        process_links = None,   # 指定该spider中哪个的函数将会被调用, 从link_extractor中获取到链接列表时将会调用该函数。
                                # 该方法主要用来过滤url。
        process_request = None  # 指定该spider中哪个的函数将会被调用, 该规则提取到每个request时都会调用该函数。 
                                # 用来过滤request
)
  • LinkExtractor类
class scrapy.linkextractors.LinkExtractor(
    allow = (),             # 满足括号中“正则表达式”的URL会被提取,如果为空,则全部匹配。
    deny = (),              # 满足括号中“正则表达式”的URL一定不提取(优先级高于allow)。
    allow_domains = (),     # 会被提取的链接的domains。
    deny_domains = (),      # 一定不会被提取链接的domains。
    deny_extensions = None,
    restrict_xpaths = (),   # 使用xpath表达式,和allow共同作用过滤链接。
    tags = ('a','area'),
    attrs = ('href'),
    canonicalize = True,
    unique = True,
    process_value = None
)
  • scrapy shell 测试提取url
scrapy shell "http://hr.tencent.com/position.php?&start=0#a"
from scrapy.linkextractors import LinkExtractor
page_lx = LinkExtractor(allow=('position\.php\?&start=\d+')) # 提取规则, 注意.和?需要转义
page_lx.extract_links(response) # 从response对象中提取所有url

  • 注意:
    由于CrawlSpider使用parse方法来实现其逻辑,如果覆盖了 parse方法,crawl spider将会运行失败。

  • 案例 1:

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class TecentSpider(CrawlSpider):
    name = 'tecent'
    allowed_domains = ['hr.tencent.com']
    start_urls = ['http://hr.tencent.com/position.php?&start=0']
    page_lx = LinkExtractor(allow=r'start=\d+') # 提取规则
    #position.php?&start=10#a
    rules = (
        Rule(page_lx, callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        items = response.xpath('//*[contains(@class,"odd") or contains(@class,"even")]')
        for item in items:
            temp = dict(
                position=item.xpath("./td[1]/a/text()").extract()[0],
                detailLink="http://hr.tencent.com/" + item.xpath("./td[1]/a/@href").extract()[0],
                type=item.xpath('./td[2]/text()').extract()[0] if len(
                    item.xpath('./td[2]/text()').extract()) > 0 else None,
                need_num=item.xpath('./td[3]/text()').extract()[0],
                location=item.xpath('./td[4]/text()').extract()[0],
                publish_time=item.xpath('./td[5]/text()').extract()[0]
            )
            print(temp)
            yield temp

    # parse() 方法不需要重写     
    # def parse(self, response):                                              
    #     pass
  • 案例 2:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

import re

class CfSpider(CrawlSpider):
    name = 'cf'
    allowed_domains = ['segmentfault.com']
    start_urls = ['https://segmentfault.com/']

    # 提取url地址的规则
    rules = (
        # url提取器, 根据规则提取url发送请求
        # 发送后的请求交给callback的方法进行处理
        # follow代表在当前获取的响应中是否继续使用该规则提取url,提取翻页地址需要设置为true
        # Rule(LinkExtractor(allow=r'/chinese/home/docView/xzcf_[a-zA-Z0-9]+\.html'), callback='parse_item'),
        Rule(LinkExtractor(allow=r'/a/\d+'), callback='parse_item'),
    )

    def parse_item(self, response):
        item = {}
        item['title'] = response.xpath("//h1[@id='articleTitle']/a/text()").extract_first()
        item['author'] = response.xpath("//div[@class='article__authorleft']/a/img/@alt").extract_first()
        print(item)

Request与Response

Request

  • 常用参数
url:        就是需要请求,并进行下一步处理的url
callback:   指定该请求返回的Response,由那个函数来处理。
method:     请求一般不需要指定,默认GET方法,可设置为"GET", "POST", "PUT"等,且保证字符串大写
headers:    请求时,包含的头文件。一般不需要。内容一般如下:
            # 自己写过爬虫的肯定知道
            Host: media.readthedocs.org
            User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
            Accept: text/css,*/*;q=0.1
            Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
            Accept-Encoding: gzip, deflate
            Referer: http://scrapy-chs.readthedocs.org/zh_CN/0.24/
            Cookie: _ga=GA1.2.1612165614.1415584110;
            Connection: keep-alive
            If-Modified-Since: Mon, 25 Aug 2014 21:59:35 GMT
            Cache-Control: max-age=0
meta:       比较常用,在不同的请求之间传递数据使用的.dict类型
            response会携带meta指定的字典.
            例如:
            request_with_cookies = Request(
                url="http://www.example.com",
                cookies={'currency': 'USD', 'country': 'UY'},
                meta={'item': (title, content_url)} # name, content_url可以传递给parse_detail方法使用
                callback='parse_detail'
            )
            def parse_detail(self, response):
                title, content_url = response.meta['item']
                pass      
encoding:   使用默认的 'utf-8' 就行。
dont_filter: 表明该请求不由调度器过滤。这是当你想使用多次执行相同的请求,忽略重复的过滤器。默认为False。
errback:    指定错误处理函数

Response对象

  • 常用参数
status:             响应码
_set_body(body):   响应体
_set_url(url):     响应url
self.request = request

登录处理

携带cookies访问登录之后的页面

  • 某些信息在登录之后才能访问数据

  • 首先使用账户名和密码登录网站, 复制登录网站的cookies

  • 默认情况下, 首地址的请求时engine自动发送的, 现在需要携带cookies

  • 重写start_requests方法, 让首地址的请求携带cookies信息

# -*- coding: utf-8 -*-
import scrapy
import re

class GithubSpider(scrapy.Spider):
    name = 'github'
    allowed_domains = ['github.com']
    start_urls = ['https://github.com/ken723'] # 首地址
    # 从登录后页面的响应中复制的cookies信息
    cookies = "has_recent_activity=1; _device_id=965692cc867f21a27eb1f797bdaa9ea6; _octo=GH1.1.864247960.1558680030; _ga=GA1.2.2073575103.1558680038; tz=Asia%2FShanghai; user_session=4pAlPpAijwGX_pLUMFVeCLVR5oI0R_ewSv6kWoTfmKIIS_MZ; __Host-user_session_same_site=4pAlPpAijwGX_pLUMFVeCLVR5oI0R_ewSv6kWoTfmKIIS_MZ; logged_in=yes; dotcom_user=ken723; _gat=1; _gh_sess=LzY5Ni9KMy9qWngrSXFwNjNMQWI4bU9qbWdzWEFmSGRRNWhZQkw0MkhaVk4wL0JNV1ovQlR2alZHMmN4VjZmLzBHd09yT1Bjc2d2TGtYRE5hcDdsY2EyWnh0YVhvaHppWi9sYXJjZmd3ZDZHUW9IYlNzZE9GWk4xWjJyaGdmRmRPSFdZRUtBV01rVUpva045dUlFcEhCOE5pVnFVY1pjVU5LUk9UOVMvcVVuaXdLZDNUTjAwNkd0QkhnZEs3RmVtenlnL2NhanRSM1Fqa2NSZlVTQkdaTGVWTW83ZjlnZzRCVzN5d2xNU2JKL3lCNjFKOU4veElBN1gxd3lyOXJXb0NrRkxYWWVaWHAxM2lLajFQVm5OOEQrenFQSUlranUzY0doQXh1L2RVR01mbXFqWWJuTFNPcCtIV1RHWXZtMlJTSW5seXg5aC8xOWRyeThxVFJXVXd3PT0tLXRncitlbVpiMm8rSWJrV2hKUVl1OWc9PQ%3D%3D--d54930605abd55998836f9649bf1608217fa7d7a"
    # 将cookies字符串转换为dict
    cookies = {i.split("=")[0]:i.split("=")[1] for i in cookies.split("; ")}
    # print(cookies)

    # 首地址的请求默认调用该方法
    # 重写该方法, 携带自定义cookies
    def start_requests(self):
        yield scrapy.Request(
            url=self.start_urls[0], # 首地址
            callback=self.parse,    # 处理响应的方法
            cookies=self.cookies    # 自定义的cookies
        )

    # 处理响应
    def parse(self, response):
        # print('parse_func')
        # 如果访问成功, 可以匹配到登录后的邮箱名
        print(re.findall("wqhken723@sina.com",response.body.decode()
))

数据解析 Selector选择器

xpath() 常用

  • 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表

css()

  • 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表,语法同 BeautifulSoup4

re()

  • 根据传入的正则表达式对数据进行提取,返回字符串list列表

extract() 常用

  • 序列化该节点为字符串并返回list, 用于提取节点中的数据组成的list
  • 同getall()

extract_first() 常用

  • 序列化节点为字符串并返回, 用于提取节点中的数据
  • 同get()

xpath表达式案例:

/html/head/title # 选择<HTML>文档中 <head> 标签内的 <title> 元素
/html/head/title/text() # 选择上面提到的 <title> 元素的文字
//td # 选择所有的 <td> 元素
//div[@class="mine"] # 选择所有具有 class="mine" 属性的 div 元素

scrapy shell

# 返回 xpath选择器对象列表
response.xpath('//title')
# [<Selector xpath='//title' data='<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title'>]

# 使用 extract()方法返回字符串列表
response.xpath('//title').extract()
# ['<title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>']

# 打印列表第一个元素,没有则返回None
response.xpath('//title').extract_first()
# <title>职位搜索 | 社会招聘 | Tencent 腾讯招聘</title>

# response常用属性
response.url:              # 当前响应的url地址
response.request.url:      # 当前响应对应的请求的url地址
response.headers:          # 响应头
response.body:             # 响应体,也就是html代码,默认是byte类型
response.requests.headers: # 当前响应的请求头


下载中间件的使用

1. 使用user-agent池

2. 使用proxy池

  • settings.py文件, 设置user-agent列表, 设置proxy列表
  • 开启下载中间件
DOWNLOADER_MIDDLEWARES = {
   # 'login.middlewares.LoginDownloaderMiddleware': 543,
   # 这里对应着middlewares.py文件中定义的类
   'login.middlewares.RandomUserAgentMiddleware': 543,
}

USER_AGENTS_LIST = [
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
]

PROXY_IP_LIST = [
    '118.89.150.177:1080',
    '119.36.161.174:80',
    '52.83.253.74:3128',
    '39.137.168.230:80',
    '103.205.26.78:56341',
    '106.2.238.2:3128',
    '182.111.129.37:53281',
    '193.112.113.26:1080'
]

  • 编写middlewares.py文件, 自定义下载中间件类
from scrapy import signals
import random

# 自定义下载中间件
class RandomUserAgentMiddleware():

    # 每一个request通过下载中间件时被调用
    def process_request(self, request, spider):
        print('request_func')

        # 调用settings.py中的列表
        proxy_ip_list = spider.settings.get('PROXY_IP_LIST')
        proxy = random.choice(proxy_ip_list)
        # 设置代理ip
        request.meta['proxy'] = 'http://' + proxy   
       
        # 调用settings.py中的列表
        user_agent_list = spider.settings.get("USER_AGENTS_LIST")
        # 随机一个列表内的user-agent
        user_agent = random.choice(user_agent_list)
        # 设置请求头的user-agent
        request.headers['User-Agent'] = user_agent

    # 当下载器完成请求, 传递response给engine时被调用
    def process_response(self, request, response, spider):
        # 获取请求头中的user-agent        
        user_agent = request.headers['User-Agent']
        proxy = request.meta['proxy']
        print(user_agent)

        return response

爬虫中间件的使用

process_spider_output()

  • yield scrapy.Request()或者yield item之后调用
# 该函数在数据传递到pipeline之前调用, 可以对parse解析之后的request或者item进行处理.
# result参数记录了Request和item对象.
def process_spider_output(response, result, spider):
    for item in result:
        if isinstance(item, scrapy.Item):
            print('传递给pipeline的item对象', item)
        yield item # 传递给pipeline

    for request in result:
        request.meta['request_start_time'] = time.time()
        yield request # 传递给调度器

process_spider_exception()

  • 爬虫代码出现异常时调用

process_spider_input()

  • 爬虫中解析函数parse调用之前调用

process_start_requests()

  • start_requests()运行时调用
import datetime
class ExceptionCheckSpider(object):
    
    def process_spider_exception(self, response, exception, spider):
        print('爬虫异常类型: ', type(exception))
        # 记录出错的请求, 传递给pipeline
        # page = response.meta['page'] # 获取设置在Request()里面携带的meta
        # now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        # error_item = ErrorItem() # 使用items.py中定义的错误类ErrorItem
        # error_item['page'] = page
        # error_item['error_time'] = now_time
        # yield error_item
        # 如果直接返回了Request或者item对象, 将绕过爬虫, 传递给pipeline
        # pipeline可以保存到数据库
        # yield scrapy.Request()
        # yield item
        return None

POST请求

使用scrapy.FormRequest() 发送post请求登录

  • 构造post请求进行登录
  • 需要寻找登录请求的地址和构造post需要的数据
# -*- coding: utf-8 -*-
import scrapy
import re

class PostLoginSpider(scrapy.Spider):
    name = 'post_login'
    allowed_domains = ['github.com']
    start_urls = ['https://github.com/login'] # 登录页面

    def parse(self, response):
        # 由于post请求需要的某些数据在https://github.com/login响应中.
        # 首先发送login地址, 提取需要的数据.
        # 提取post请求需要的数据
        commit = response.xpath("//input[@name='commit']/@value").extract_first()
        authenticity_token = response.xpath("//input[@name='authenticity_token']/@value").extract_first()
        utf8 = response.xpath("//input[@name='utf8']/@value").extract_first()
        password = 'root'
        login = 'root'

        # 发送post请求需要的数据
        data = dict(
            commit=commit,
            utf8=utf8,
            authenticity_token=authenticity_token,
            login=login,
            password=password
        )

        # post登录请求的地址
        post_url = "https://github.com/session"
        # 发送post请求
        yield scrapy.FormRequest(
            url=post_url,
            formdata=data,
            callback=self.parse_user_page # 处理登录之后页面的响应
        )

    def parse_user_page(self, response):
        # 验证登录之后的响应中是否有用户名
        user_info = re.findall("root", response.body.decode())
        print(user_info)

使用scrapy.FormRequest().from_response()自动登录

  • 在form中定义了action的地址可以使用自动登录
  • 特别的, 当一个页面中有两个或以上的form时, 可以根据from_resonse()的参数定位某一个form. id, name, xpath都可以进行定位form
# from_response方法的源码
def from_response(
    cls, 
    response, 
    formname=None, formid=None, formnumber=0, formdata=None,
    clickdata=None, 
    dont_click=False, 
    formxpath=None, 
    formcss=None, **kwargs):
# -*- coding: utf-8 -*-
import scrapy
import re

class PostLogin2Spider(scrapy.Spider):
    name = 'post_login2'
    allowed_domains = ['github.com']
    start_urls = ['https://github.com/login']

    def parse(self, response):
        # post需要的数据
        data = dict(
            # login和password是form中的input的name属性
            login='root', 
            password='root'
        )
        # from_response可以自动搜索form表单的提交地址进行登录请求
        # 只需要提供用户名和密码
        yield scrapy.FormRequest.from_response(
            response,
            formdata=data,
            callback=self.parse_user_page
        )

    def parse_user_page(self, response):
        html = response.body.decode()
        print(html)
        user_name = re.findall("root", html)
        print(user_name)

baidu贴吧 进入详情页获取图片地址

requests.utils.unquote(item[‘href’])地址解码

urllib.parse.urljoin(response.url, )自动拼接完整url

# -*- coding: utf-8 -*-
import scrapy
import urllib
import re
import requests

class TbSpider(scrapy.Spider):
    name = 'tb'
    allowed_domains = ['tieba.baidu.com']
    start_urls = ['http://tieba.baidu.com/mo/q---E48FA39FEC36AE1C5C941D0C8BE63613%3AFG%3D1--1-3-0--2--wapp_1558705534165_899/m?kw=python&lp=5011&lm=&pn=0']

    def parse(self, response):
        # 分组
        div_list = response.xpath("//div[@class='i']")
        for div in div_list:
            item = dict()
            item['title'] = div.xpath("./a/text()").extract_first()
            item['title'] = re.sub(r"\xa0", "", item['title'])
            item['url'] = div.xpath("./a/@href").extract_first()
            item['img_url'] = list()
            # print(item)
            if item['url'] is not None:
                # 这里使用urllib.parse.urljoin()方法根据response.url自动拼接url
                item['url'] = urllib.parse.urljoin(response.url, item['url'])

            yield scrapy.Request(
                url=item['url'],
                callback=self.parse_detail,
                meta={'item': item}
            )

        #翻页
        next_url = response.xpath("//a[text()='下一页']/@href").extract_first()
        if next_url is not None:
            next_url = urllib.parse.urljoin(response.url, next_url)
            yield scrapy.Request(
                url=next_url,
                callback=self.parse
            )

    def parse_detail(self, response):
        item = response.meta['item']
        item['img_url'] = response.xpath("//div[@class='d']/div[1]/a/@href").extract()
        # 对地址进行解码, 然后切割获取需要的部分
        item['img_url'] = [requests.utils.unquote(i).split("src=")[-1] for i in item['img_url']]
        print(item)

数据存储 与 pipelines

不使用pipeline 直接导出json,jsonl,csv,xml

# json格式,默认为Unicode编码
scrapy crawl itcast -o teachers.json
# json lines格式,默认为Unicode编码
scrapy crawl itcast -o teachers.jsonl
# csv 逗号表达式,可用Excel打开
scrapy crawl itcast -o teachers.csv
# xml格式
scrapy crawl itcast -o teachers.xml

使用item pipeline

  • pipiline组件是一个独立的Python类,其中process_item()方法必须实现
  • 使用之前需要在settings里面配置:
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'mySpider.pipelines.SomethingPipeline': 300,
}
  • pipelines文件:
import something

class SomethingPipeline(object):
    def __init__(self):    
        # 可选实现,做参数初始化等
        # doing something

    def process_item(self, item, spider):
        # item (Item 对象) – 被爬取的item
        # spider (Spider 对象) – 爬取该item的spider
        # 这个方法必须实现,每个item pipeline组件都需要调用该方法,
        # 这个方法必须返回一个 Item 对象,不返回item将不会被之后的pipeline组件所处理。
        return item

    def open_spider(self, spider):
        # spider (Spider 对象) – 被开启的spider
        # 可选实现,当spider被开启时,这个方法被调用。

    def close_spider(self, spider):
        # spider (Spider 对象) – 被关闭的spider
        # 可选实现,当spider被关闭时,这个方法被调用

普通写入json文件

import json
class ItcastJsonPipeline(object):
    def __init__(self):
        self.file = open('teacher.json', 'wb')
    
    def process_item(self, item, spider):
        content = json.dumps(dict(item), ensure_ascii=False) + "\n" # item需要转换为dict
        self.file.write(content)
        return item
    
    def close_spider(self, spider):
        self.file.close()

异步写入json文件

JsonItemExporter
  • 保存json数据的时候,可以使用这两个类,让操作变得得更简单。
  • JsonItemExporter:这个是每次把数据添加到内存中。最后统一写入到磁盘中。
  • 好处是,存储的数据是一个满足json规则的数据。
  • 坏处是, 如果数据量比较大,那么比较耗内存。示例代码如下:
from scrapy.exporters import JsonItemExporter

class QsbkPipeline(object):
    def __init__(self):
        self.fp = open("duanzi.json",'wb')
        self.exporter = JsonItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')
        self.exporter.start_exporting() # 设置导出器开始导出

    def open_spider(self,spider):
        print('爬虫开始了...')

    def process_item(self, item, spider):
        self.exporter.export_item(item) # 导出器导出数据
        return item

    def close_spider(self,spider):
        self.exporter.finish_exporting() # 导出器关闭
        self.fp.close() # 文件资源释放
        print('爬虫结束了...')
JsonLinesItemExporter
  • JsonLinesItemExporter:这个是每次调用export_item的时候就把这个item存储到硬盘中。
  • 坏处是, 每一个字典是一行,整个文件不是一个满足json格式的文件。
  • 好处是, 每次处理数据的时候就直接存储到了硬盘中,这样不会耗内存,数据也比较安全。示例代码如下:
from scrapy.exporters import JsonLinesItemExporter
class QsbkPipeline(object):
    def __init__(self):
        self.fp = open("duanzi.json",'wb')
        self.exporter = JsonLinesItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')

    def open_spider(self,spider):
        print('爬虫开始了...')

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

    def close_spider(self,spider):
        self.fp.close()
        print('爬虫结束了...')
from scrapy.exporters import JsonLinesItemExporter
from soufang.items import NewHouseItem
from soufang.items import EsfHouseItem

# 异步写入json文件
class SoufangPipeline(object):
    def __init__(self):
        self.newhouse_fp = open('newhouse.json', 'wb')
        self.esfhouse_fp = open('esfhouse.json', 'wb')
        # 默认情况下, 写入的是asc2编码, 需要关闭
        self.newhouse_exporter = JsonLinesItemExporter(self.newhouse_fp, ensure_ascii=False)
        self.esfhouse_exporter = JsonLinesItemExporter(self.esfhouse_fp, ensure_ascii=False)

    def process_item(self, item, spider):
        # 根据不同的item类型, 写入不同的文件
        if isinstance(item, NewHouseItem):
            self.newhouse_exporter.export_item(item) # 写入

        if isinstance(item, EsfHouseItem):
            self.esfhouse_exporter.export_item(item)

        return item

    def close_spider(self, spider):
        self.newhouse_fp.close() # 释放资源
        self.esfhouse_fp.close()

3. 异步写入mysql

  • 在pipelines中处理存储操作
  • twisted提供了创建异步连接池的方式操作数据库
# -*- coding: utf-8 -*-

import pymysql
from twisted.enterprise import adbapi # 使用twisted中的异步连接池
from pymysql import cursors

# 普通写入mysql
class JianshuAllPipeline(object):
    def __init__(self):
        self.conn = pymysql.connect(host='192.168.145.130', port=3306, database='jianshu', user='ken', password='ken723', charset='utf8')
        self.cursor = self.conn.cursor()
        self.sql = "insert into article values(0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"

    def process_item(self, item, spider):
        print('开始写入'+'='*200)
        self.cursor.execute(self.sql, (item['title'],
                                       item['content'],
                                       item['author'],
                                       item['avatar'],
                                       item['publish_time'],
                                       item['article_id'],
                                       item['origin_url'],
                                       item['read_count'],
                                       item['like_count'],
                                       item['word_count'],
                                       item['subjects'],
                                       item['comment_count']))
        self.conn.commit()
        print('写入成功'+'='*200)
        return item

# 异步写入mysql
class JianshuAllTwistedPipeline(object):
    def __init__(self):
        dbparams = {
            'host': '192.168.145.130',
            'port': 3306,
            'database': 'jianshu',
            'user': 'ken',
            'password': 'ken723',
            'charset': 'utf8',
            'cursorclass': cursors.DictCursor
        }
        # 使用twidted创建异步连接池
        self.dbpool = adbapi.ConnectionPool('pymysql', **dbparams)
        # 定义插入语句
        self.sql = "insert into article values(0, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);"

    def process_item(self, item, spider):
        print('开始写入' + '*'*150)
        # 使用异步连接池执行insert_item方法
        defer = self.dbpool.runInteraction(self.insert_item, item)
        # 如果执行insert_item出现异常, 执行handle_error
        defer.addErrback(self.handle_error, item, spider)
        print('写入成功' + '*' * 150)
        return item

    # 插入数据库
    def insert_item(self, cursor, item):
        # 执行插入语句
        cursor.execute(self.sql, (item['title'],
                                  item['content'],
                                  item['author'],
                                  item['avatar'],
                                  item['publish_time'],
                                  item['article_id'],
                                  item['origin_url'],
                                  item['read_count'],
                                  item['like_count'],
                                  item['word_count'],
                                  item['subjects'],
                                  item['comment_count']))
    # 错误处理
    def handle_error(self, error, item, spider):
        print('*'*20 + 'error' + '*'*20)
        print(error)
        print('*'*20 + 'error' + '*'*20)

4. 写入mongodb

# 在pipelines中:
from pymongo import MongoClient

client = MongoClient()
collection = client['tieba']['job']

class TencentJobPipeline(object):
    def process_item(self, item, spider):
        # 由于只能讲dict类型写入mongo中, 所以需要类型转换
        # 将TencentJobItem字段类对象转换为dict
        collection.insert(dict(item))
        print('写入成功')
        return item

scrapy使用selenium

  • 在spider拿到响应信息之前, 在下载中间件中截获url, 使用selenium发送请求
  • 使用selenium获取到的完整网页源码, 封装成response对象返回给spider
  • spider部分的解析不变, 只是每次拿到的是经过selenium获取的动态加载后的完整网页源码.
# 下载中间件
# -*- coding: utf-8 -*-
from scrapy import signals
from selenium import webdriver
import time
from scrapy.http.response.html import HtmlResponse

# 截获request对象的url, 使用selenium发送请求获得网页源码,
# 将地址和源码封装成Response对象, 直接返回给spider
class SeleniumDownloaderMiddleware(object):

    def __init__(self):
        self.driver = webdriver.Chrome(r'C:\Software\chromedriver.exe')

    def process_request(self, request, spider):
        self.driver.get(request.url) # 截获request对象的url,使用selenium发送请求
        time.sleep(1)
        # 进行额外的点击操作, 获得所有页面的动态内容
        try:
            while True:
                show_more = self.driver.find_element_by_class_name("show-more") # 查看更多按钮
                show_more.click()
                time.sleep(0.5)
                if not show_more:
                    break
        except:
            pass
        source = self.driver.page_source # 获取所有动态内容加载后的的网页源码
        # 封装Response对象
        response = HtmlResponse(url=self.driver.current_url, body=source, request=request, encoding='utf-8')
        return response # 返回对象给爬虫部分

settings.py常用设置

BOT_NAME
# 默认: 'scrapybot'
# 当您使用 startproject 命令创建项目时其也被自动赋值。

CONCURRENT_ITEMS
# 默认: 100
# Item Processor(即 Item Pipeline) 同时处理(每个response的)item的最大值。

CONCURRENT_REQUESTS
# 默认: 16
# Scrapy downloader 并发请求(concurrent requests)的最大值。

DEFAULT_REQUEST_HEADERS
# 默认: 如下
# {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
# }
# Scrapy HTTP Request使用的默认header。

DEPTH_LIMIT
# 默认: 0
# 爬取网站最大允许的深度(depth)值。如果为0,则没有限制。

DOWNLOAD_DELAY
# 默认: 0
# 下载器在下载同一个网站下一个页面前需要等待的时间。该选项可以用来限制爬取速度, 减轻服务器压力。同时也支持小数:
# DOWNLOAD_DELAY = 0.25 # 250 ms of delay
# 默认情况下,Scrapy在两个请求间不等待一个固定的值, 而是使用0.5到1.5之间的一个随机值 * DOWNLOAD_DELAY 的结果作为等待间隔。

DOWNLOAD_TIMEOUT
# 默认: 180
# 下载器超时时间(单位: 秒)。

ITEM_PIPELINES
# 默认: {}
# 项目中启用的pipeline及其顺序的字典。该字典默认为空,值(value)任意,不过值(value)习惯设置在0-1000范围内,值越小优先级越高。
# ITEM_PIPELINES = {
# 'mySpider.pipelines.SomethingPipeline': 300,
# 'mySpider.pipelines.ItcastJsonPipeline': 800,
# }

LOG_ENABLED
# 默认: True
# 是否启用logging。

LOG_ENCODING
# 默认: 'utf-8'
# logging使用的编码。

LOG_LEVEL
# 默认: 'DEBUG'
# log的最低级别。可选的级别有: CRITICAL、 ERROR、WARNING、INFO、DEBUG 。

USER_AGENT
# 默认: "Scrapy/VERSION (+http://scrapy.org)"
# 爬取的默认User-Agent,除非被覆盖。

PROXIES: 
# 代理设置
# PROXIES = [
#   {'ip_port': '111.11.228.75:80', 'password': ''},
#   {'ip_port': '120.198.243.22:80', 'password': ''},
#   {'ip_port': '111.8.60.9:8123', 'password': ''},
#   {'ip_port': '101.71.27.120:80', 'password': ''},
#   {'ip_port': '122.96.59.104:80', 'password': ''},
#   {'ip_port': '122.224.249.122:8088', 'password':''},
# ]

COOKIES_ENABLED = False
# 禁用Cookies
# 默认是注释的, 也就是开启了cookies
  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值