06 scrapy框架

06 scrapy框架

Scrapy是纯Python开发的一个高效,结构化的网页抓取框架;

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。 Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试 Scrapy使用了Twisted 异步网络库来处理网络通讯。

一、简单使用

1、新建项目

语法格式:scrapy startproject <project_name> [project_dir]
该命令会在project_dir文件加下创建一个名为project_name的Scrapy新项目。如果project_dir没有指定,project_dir与project_name相同。
进入你想要存放项目的目录下运行一下命令:
scrapy startproject tz_spider
该命令会在当前目录创建包含一下文件的名为tz_spider的目录。

scrapy startproject mingyan

2、编写爬虫

创建一个TzcSpider的类,它必须继承scrapy.Spider类,需要定义一下三个属性:
name: spider的名字,必须且唯一
start_urls: 初始的url列表
parse(self, response) 方法:每个初始url完成之后被调用。这个函数要完成一下两个功能:
解析响应,封装成item对象并返回这个对象
提取新的需要下载的url,创建新的request,并返回它
我们也可以通过命令创建爬虫
语法格式:scrapy genspider [-t template] <name> <domain>
运行命令:scrapy genspider tzc www.shiguangkey.com
会在spiders文件下生成tzc.py文件,文件内容如下:

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

class TzcSpider(scrapy.Spider):
    name = 'tzc'
    # allowed_domains = ['www.']
    start_urls = ['http://www.shiguangkey.com/']

    def parse(self, response):
        pass

3、文件分析

1)scrapy.cfg: 项目的配置文件,现在可以先忽略。

2)tanzhou_spider.py: 该项目的python模块

3)items.py: 项目中的item文件。

Item 是保存爬取到的数据的容器;其使用方法和python字典类似, 并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。

4)pipelines.py: 项目中的pipelines文件。

Scrapy提供了pipeline模块来执行保存数据的操作。在创建的 Scrapy 项目中自动创建了一个 pipeline.py 文件,同时创建了一个默认的 Pipeline 类。比如我们要把item提取的数据保存到mysql数据库 。

5)settings.py: 项目的设置文件。

settings.py是Scrapy中比较重要的配置文件,里面可以设置的内容非常之多。比如我们在前面提到的在pipelines.py中编写了把数据保存到mysql数据的class。

6)middlewares.py:中间件。

4、提取数据

css选择器、xpath选择器、正则

1)css选择器

# 提取a标签
response.css("a")
# 提取a标签的href属性的值
response.css("a::attr(href)")
# 提取a标签的文档内容
response.css("a::text")

CSS 高级用法:
CSS选择器用于选择你想要的元素的样式的模式。"CSS"列表示在CSS版本的属性定义(CSS1,CSS2,或对CSS3)。

选择器 示例  示例说明    CSS
.class  .intro  选择所有class="intro"的元素    1
#id #firstname  选择所有id="firstname"的元素   1
*   *   选择所有元素  2
element p   选择所有<p>元素   1
element,element div,p   选择所有<div>元素和<p>元素   1
element element div p   选择<div>元素内的所有<p>元素  1
element>element div>p   选择所有父级是 <div> 元素的 <p> 元素    2
element+element div+p   选择所有紧接着<div>元素之后的<p>元素  2
[attribute] [target]    选择所有带有target属性元素    2
[attribute=value]   [target=-blank] 选择所有使用target="-blank"的元素    2
[attribute~=value]  [title~=flower] 选择标题属性包含单词"flower"的所有元素 2
[attribute|=language]   [lang|=en]  选择一个lang属性的起始值="EN"的所有元素    2
:link   a:link  选择所有未访问链接   1
:visited    a:visited   选择所有访问过的链接  1
:active a:active    选择活动链接  1
:hover  a:hover 选择鼠标在链接上面时  1
:focus  input:focus 选择具有焦点的输入元素 2
:first-letter   p:first-letter  选择每一个<P>元素的第一个字母    1
:first-line p:first-line    选择每一个<P>元素的第一行  1
:first-child    p:first-child   指定只有当<p>元素是其父级的第一个子级的样式。    2
:before p:before    在每个<p>元素之前插入内容  2
:after  p:after 在每个<p>元素之后插入内容  2
:lang(language) p:lang(it)  选择一个lang属性的起始值="it"的所有<p>元素 2
element1~element2   p~ul    选择p元素之后的每一个ul元素 3
[attribute^=value]  a[src^="https"] 选择每一个src属性的值以"https"开头的元素   3
[attribute$=value]  a[src$=".pdf"]  选择每一个src属性的值以".pdf"结尾的元素    3
[attribute*=value]  a[src*="runoob"]    选择每一个src属性的值包含子字符串"runoob"的元素   3
:first-of-type  p:first-of-type 选择每个p元素是其父级的第一个p元素  3
:last-of-type   p:last-of-type  选择每个p元素是其父级的最后一个p元素 3
:only-of-type   p:only-of-type  选择每个p元素是其父级的唯一p元素   3
:only-child p:only-child    选择每个p元素是其父级的唯一子元素   3
:nth-child(n)   p:nth-child(2)  选择每个p元素是其父级的第二个子元素  3
:nth-last-child(n)  p:nth-last-child(2) 选择每个p元素的是其父级的第二个子元素,从最后一个子项计数   3
:nth-of-type(n) p:nth-of-type(2)    选择每个p元素是其父级的第二个p元素  3
:nth-last-of-type(n)    p:nth-last-of-type(2)   选择每个p元素的是其父级的第二个p元素,从最后一个子项计数   3
:last-child p:last-child    选择每个p元素是其父级的最后一个子级。 3
:root   :root   选择文档的根元素    3
:empty  p:empty 选择每个没有任何子级的p元素(包括文本节点)  3
:target #news:target    选择当前活动的#news元素(包含该锚名称的点击的URL)   3
:enabled    input:enabled   选择每一个已启用的输入元素   3
:disabled   input:disabled  选择每一个禁用的输入元素    3
:checked    input:checked   选择每个选中的输入元素 3
:not(selector)  :not(p) 选择每个并非p元素的元素    3
::selection ::selection 匹配元素中被用户选中或处于高亮状态的部分    3
:out-of-range   :out-of-range   匹配值在指定区间之外的input元素  3
:in-range   :in-range   匹配值在指定区间之内的input元素  3
:read-write :read-write 用于匹配可读及可写的元素    3
:read-only  :read-only  用于匹配设置 "readonly"(只读) 属性的元素 3
:optional   :optional   用于匹配可选的输入元素 3
:required   :required   用于匹配设置了 "required" 属性的元素    3
:valid  :valid  用于匹配输入值为合法的元素   3
:invalid    :invalid    用于匹配输入值为非法的元素   3

2)xpath选择器

提取a标签
response.xpath('//a')

提取class="inp"的a标签的href内容

response.xpath('//a[@class="inp"]/@href')

提取class="inp"的a标签的文本内容

response.xpath('//a[@class="inp"]/text()')

路径表达式 结果
/bookstore/book[1] 选取属于 bookstore 子元素的第一个 book 元素。
/bookstore/book[last()] 选取属于 bookstore 子元素的最后一个 book 元素。
/bookstore/book[last()-1] 选取属于 bookstore 子元素的倒数第二个 book 元素。
/bookstore/book[position()<3] 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。
//title[@lang] 选取所有拥有名为 lang 的属性的 title 元素。
//title[@lang='eng'] 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。
/bookstore/book[price>35.00] 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。
/bookstore/book[price>35.00]/title 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。
路径表达式 结果
bookstore 选取 bookstore 元素的所有子节点。
/bookstore
选取根元素 bookstore。

注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!

bookstore/book 选取属于 bookstore 的子元素的所有 book 元素。
//book 选取所有 book 子元素,而不管它们在文档中的位置。
bookstore//book 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。
//@lang 选取名为 lang 的所有属性。
XPath 通配符可用来选取未知的 HTML元素。

通配符 描述

  • 匹配任何元素节点。
    • 匹配任何属性节点。
      de() 匹配任何类型的节点。
      在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:

      表达式 结果
      ookstore/* 选取 bookstore 元素的所有子元素。

      • 选取文档中的所有元素。
        itle[@*] 选取所有带有属性的 title 元素。

3)正则

response.css("a").re('href="(.*?)"')

4)extract()、extract_first()

# 将符合要求的数据提取到列表中
response.css("a").re('href="(.*?)"').extract()
# 将符合要求的数据的第一条提取出来
response.css("a").re('href="(.*?)"').extract()

5、运行爬虫

首先进入项目根目录,然后运行命令:scrapy crawl tzc就可以启动爬虫了。 这个命令会启动我们刚才创建的那个名为tzc的爬虫,你会在屏幕上得到类似左图的输出。

scrapy crawl tzc
# 无日志运行
scrapy crawl tzc --nolog
# 查看本工程中有哪些spider
scrapy list

6、追踪链接

#爬取多页数据,返回一个scrapy.Request对象
import scrapy

class TzcSpider(scrapy.Spider):
    name = 'tzc'
    # allowed_domains = ['www.']
    start_urls = ['https://ke.qq.com/course/list?mt=1001/']

    def parse(self, response):
        url = response.url
        next_url = response.xpath('//a[@class="page-next-btn icon-font i-v-right"]/@href').extract_first()
        # 返回一个request请求
        return scrapy.Request(next_url,callback=self.parse)
# -*- coding: utf-8 -*-
import scrapy


class TanzhouSpiderSpider(scrapy.Spider):
    name = 'tanzhou_spider'
    # allowed_domains = ['www']
    start_urls = ['https://www.qiushibaike.com/hot']

    def parse(self, response):
        next_url = response.xpath('//ul[@class="pagination"]/li/a/span[text()="\n下一页\n"]').xpath('../@href').extract_first()
        detial_urls = response.xpath('//div[@id="content-left"]/div/a/@href').extract()
        if detial_urls:
            detial_urls = list(set(detial_urls))
            for detial_url in detial_urls:
                detial_url = 'https://www.qiushibaike.com' + detial_url
                print(detial_url)
                yield scrapy.Request(detial_url, callback=self.detial_parse)
        if next_url:
            next_url = 'https://www.qiushibaike.com'+ next_url
            print(next_url)
            return scrapy.Request(next_url, callback=self.parse)


    def detial_parse(self,response):
        print(response.url)

7、定义管道

parse函数在解析出我们需要的信息之后,可以将这些信息打包成一个字典对象或scray.Item对象(一般都是item对象,下面我们再讲),然后返回。这个对象会被发送到item管道,该管道会通过顺序执行几个组件处理它。每个item管道组件是一个实现简单方法的Python类。他们收到一个item并对其执行操作,同时决定该item是否应该继续通过管道或者被丢弃并且不再处理。

item管道的典型用途是:

清理HTML数据
验证已删除的数据(检查项目是否包含某些字段)
检查重复项(并删除它们)
将已爬取的item进行数据持久化

items.py

import scrapy
class MyspiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    url = scrapy.Field()
    next_url = scrapy.Field()
    pass

pipelines.py

# 编写管道类
import json

class MyspiderPipeline(object):
    def open_spider(self,spider):
        self.f = open("course.txt","w",encoding="utf-8")
        # pass

    def process_item(self, item, spider):
        self.f.write(json.dumps(dict(item),ensure_ascii=False))
        self.f.write("\n")
        return item

    def close_spider(self,spider):
        self.f.close()

# pass

激活管道组件 settings.py,类后面的数字代表优先等级

ITEM_PIPELINES = {
   'MySpider.pipelines.MyspiderPipeline': 300,
}
# 君子协议
# Obey robots.txt rules
ROBOTSTXT_OBEY = False

8、数据写入文件、数据库

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被关闭时,这个方法被调用

写入文件

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"
        self.file.write(content)
        return item

    def close_spider(self, spider):
        self.file.close()

写入mysql

# 将item写入数据库
# 小数据可以使用同步写入
import pymysql
class MysqlPipeine(object):
    def __init__(self):
        self.db_config = {
                            'user': 'aliyun',
                            'password': '123456',
                            'db': 'class37',
                            'charset': 'utf8',
                        }
        # 建立连接
        self.conn = pymysql.connect(**self.db_config)
        # 创建游标
        self.cursor = self.conn.cursor()
        
    # 处理item的函数
    def process_item(self,item,spider):
        # 准备sql语句
        sql = 'insert into student value(item)'
        self.cursor.execute(sql)
        self.conn.commit()
    
    # 关闭数据库连接
    def close_spider(self,spider):
        self.cursor.close()
        self.conn.close()

二、运行流程

上图显示了Scrapy框架的体系结构及其组件,以及系统内部发生的数据流(由红色的箭头显示。)
Scrapy中的数据流由执行引擎控制,流程如下:

1、运行流程

首先从爬虫获取初始的请求
将请求放入调度模块,然后获取下一个需要爬取的请求
调度模块返回下一个需要爬取的请求给引擎
引擎将请求发送给下载器,依次穿过所有的下载中间件
一旦页面下载完成,下载器会返回一个响应包含了页面数据,然后再依次穿过所有的下载中间件。
引擎从下载器接收到响应,然后发送给爬虫进行解析,依次穿过所有的爬虫中间件
爬虫处理接收到的响应,然后解析出item和生成新的请求,并发送给引擎
引擎将已经处理好的item发送给管道组件,将生成好的新的请求发送给调度模块,并请求下一个请求
该过程重复,直到调度程序不再有请求为止。

2、组件介绍

Scrapy引擎
引擎负责控制系统所有组件之间的数据流,并在发生某些操作时触发事件。

调度程序
调度程序接收来自引擎的请求,将它们排入队列,以便稍后引擎请求它们。

下载器
下载程序负责获取web页面并将它们提供给引擎,引擎再将它们提供给spider。

爬虫
爬虫是由用户编写的自定义的类,用于解析响应,从中提取数据,或其他要抓取的请求。

item管道

管道负责在数据被爬虫提取后进行后续处理。典型的任务包括清理,验证和持久性(如将数据存储在数据库中)框架。它使用非阻塞(也成为异步)代码实现并发。

下载中间件
下载中间件是位于引擎和下载器之间的特定的钩子,它们处理从引擎传递到下载器的请求,以及下载器传递到引擎的响应。
如果你要执行以下操作之一,请使用Downloader中间件:
在请求发送到下载程序之前处理请求(即在scrapy将请求发送到网站之前)
在响应发送给爬虫之前
直接发送新的请求,而不是将收到的响应传递给蜘蛛
将响应传递给爬行器而不获取web页面;
默默的放弃一些请求

爬虫中间件
爬虫中间件是位于引擎和爬虫之间的特定的钩子,能够处理传入的响应和传递出去的item和请求。
如果你需要以下操作请使用爬虫中间件:
处理爬虫回调之后的 请求或item
处理start_requests
处理爬虫异常
根据响应内容调用errback而不是回调请求
事件驱动的网络

scrapy 是用Twisted编写的,Twisted是一个流行的事件驱动的Python网络框架。它使用非阻塞(也成为异步)代码实现并发。

3、编写流程

scrapy爬虫编写流程

1、创建爬虫项目

scrapy startproject mingyan

2、创建spider文件

scrapy genspider tzc_spider www.shiguangkey.com

3、编写items文件

知道需要爬取的数据

4、解析数据

提取Request或者items

5、保存数据

pipelines.py中保存数据
settings.py 激活管道

三、媒体管道

1、媒体管道特性

媒体管道都实现了以下特性:
避免重新下载最近下载的媒体
指定存储位置(文件系统目录,Amazon S3 bucket,谷歌云存储bucket)
图像管道具有一些额外的图像处理功能:
将所有下载的图片转换为通用格式(JPG)和模式(RGB)
生成缩略图
检查图像的宽度/高度,进行最小尺寸过滤

2、工作流程

它的工作流是这样的:

1.在爬虫中,您可以返回一个item,并将所需的url放入file_urls字段。
2.item从爬虫返回并进入item管道。
3.当item到达文件管道时,file_urls字段中的url将使用标准的Scrapy调度器和下载程序(这意味着将重用调度器和下载程序中间件)计划下载, 但是具有更高的优先级,在其他页面被爬取之前处理它们。在文件下载完成(或由于某种原因失败)之前,该项在特定管道阶段保持“锁定”状态。
4.下载文件后,将使用另一个字段(files)填充results。这个字段将包含一个包含有关下载文件信息的dicts列表,例如下载的路径、原始的剪贴url(从file_urls字段中获得)和文件校验和。文件字段列表中的文件将保持原来file_urls字段的顺序。如果某些文件下载失败,将记录一个错误,文件将不会出现在files字段中。

3、API

# 在媒体管道中,我们可以重写的方法:
# get_media_requests(item, info) 根据item中的file_urls/image_urls生成请求

def get_media_requests(self, item, info):
    for file_url in item['file_urls']:
        yield scrapy.Request(file_url)

# item_completed(requests, item, info)  当item里的 所有媒体文件请求完成调用

from scrapy.exceptions import DropItem
def item_completed(self, results, item, info):
    file_paths = [x['path'] for ok, x in results if ok]
    if not file_paths:
          raise DropItem("Item contains no files")
    item['file_paths'] = file_paths
    return item

# file_path 可以变保存文件的路径
def file_path(self, request, response=None, info=None):
    # 提取url前面名称作为图片名。
    image_guid = request.url.split('/')[-1]
    # 接收上面meta传递过来的图片名称
    name = request.meta['name']
    # 分文件夹存储的关键:{0}对应着name;{1}对应着image_guid
    filename = u'{0}/{1}'.format(name, image_guid)
    return filename

4、媒体管道的设置

ITEM_PIPELINES = {'scrapy.pipelines.images.ImagesPipeline': 1}  启用
FILES_STORE = '/path/to/valid/dir'      文件管道存放位置
IMAGES_STORE = '/path/to/valid/dir'     图片管道存放位置
FILES_URLS_FIELD = 'field_name_for_your_files_urls'    自定义文件url字段
FILES_RESULT_FIELD = 'field_name_for_your_processed_files'   自定义结果字段
IMAGES_URLS_FIELD = 'field_name_for_your_images_urls'  自定义图片url字段
IMAGES_RESULT_FIELD = 'field_name_for_your_processed_images'  结果字段
FILES_EXPIRES = 90    文件过期时间   默认90天
IMAGES_EXPIRES = 90    图片过期时间   默认90天
IMAGES_THUMBS = {'small': (50, 50), 'big':(270, 270)}  缩略图尺寸
IMAGES_MIN_HEIGHT = 110   过滤最小高度
IMAGES_MIN_WIDTH = 110   过滤最小宽度
MEDIA_ALLOW_REDIRECTS = True    是否重定向

5、自定义图片管道

pipelines.py

from scrapy.pipelines.images import ImagesPipeline
from scrapy import Request

# 继承 ImagesPipeline
class DownloadimagePipeline(ImagesPipeline):
    
    # 需要注释掉process_item,否则不能执行get_media_requests()
    # def process_item(self, item, spider):
    #     print(item,'=================')
    #     return item

    def get_media_requests(self, item, info):
        # 循环每一张图片地址下载,若传过来的不是集合则无需循环直接yield
        for image_url in item['img_urls']:
            # 将 img_names值通过meta传到后面
            yield Request(image_url, meta={'name': item['img_names']})

    def file_path(self, request, response=None, info=None):
        # 提取url前面名称作为图片名。
        image_guid = request.url.split('/')[-1]
        # 接收上面meta传递过来的图片名称
        name = request.meta['name']
        # 分文件夹存储的关键:{0}对应着name;{1}对应着image_guid
        filename = u'{0}/{1}'.format(name, image_guid)
        return filename

settings.py

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

import os
#配置保存本地的地址
project_dir=os.path.abspath(os.path.dirname(__file__))  #获取当前爬虫项目的绝对路径
IMAGES_STORE=os.path.join(project_dir,'images')  #组装新的图片路径
# IMAGES_STORE = 'D:\ImageSpider'

6、自定义文件管道

pipelines.py

import scrapy
from scrapy.pipelines.files import FilesPipeline

class Mp3Pipeline(FilesPipeline):
    '''自定义管道'''

    def get_media_requests(self, item, info):
        # 循环每一首歌地址下载,若传过来的不是集合则无需循环直接yield
        for music,name in zip(item['music_urls'],item['music_names']):
            # 将 img_names值通过meta传到后面
            yield scrapy.Request(music, meta={'title': item['music_title'],'name':name})

    def file_path(self, request, response=None, info=None):
        title = request.meta['title']
        name = request.meta['name']
        filename = u'{0}/{1}.mp3'.format(title, name)
        return filename

settings.py

# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   # 'LuoWang.pipelines.LuowangPipeline': 300,
    'LuoWang.pipelines.Mp3Pipeline': 300,
}
import os
#配置保存本地的地址
project_dir=os.path.abspath(os.path.dirname(__file__))  #获取当前爬虫项目的绝对路径
FILES_STORE=os.path.join(project_dir,'落网音乐')  #组装新的文件路径
# FILES_STORE = 'D:/luowang/'

四、Scrapy shell

1、启动 Scrapy shell

scrapy shell [option][url|file]

命令行输入:

scrapy shell http://httpbin.org/get

2、设置 shell

Scrapy 的shell是基于运行环境中的python 解释器shell
本质上就是通过命令调用shell,并在启动的时候预定义需要使用的对象
scrapy允许通过在项目配置文件”scrapy.cfg”中进行配置来指定解释器shell

# 例如:
[settings]
shell = ipython

[settings]
default = login_spider.settings
shell = ipython

[deploy]
#url = http://localhost:6800/
project = login_spider

3、shell的使用

Scrapy shell 本质上就是个普通的python shell
只不过提供了一些需要使用的对象,快捷方法便于我们调试。

快捷方法:

shelp() # 打印可用的对象和方法
fetch(url[,redirect=True])  # 爬取新的 URL 并更新相关对象
fetch(request)  # 通过 request 爬取,并更新相关对象
view(response)  # 使用本地浏览器打开爬取的页面
scrapy 对象:
crawler # Crawler 对象
spider  # 爬取使用的 spider
request # 请求
response    # 响应
settings    # 设置

实例

$ scrapy shell 'http://scrapy.org' --nolog

[s] Available Scrapy objects:
[s]  scrapy    scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]  crawler    <scrapy.crawler.Crawler object at 0x7f4e2fb915f8>
[s]  item      {}
[s]  request    <GET http://scrapy.org>
[s]  response  <200 https://scrapy.org/>
[s]  settings  <scrapy.settings.Settings object at 0x7f4e2e0179e8>
[s]  spider    <DefaultSpider 'default' at 0x7f4e2dbf98d0>
[s] Useful shortcuts:
[s]  fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s]  fetch(req)                  Fetch a scrapy.Request and update local objects 
[s]  shelp()          Shell help (print this help)
[s]  view(response)    View response in a browser

In [1]: response.xpath('//title/text()').extract_first()
Out[1]: 'Scrapy | A Fast and Powerful Scraping and Web Crawling Framework'

In [2]: fetch("http://reddit.com")

In [3]: response.xpath('//title/text()').extract()
Out[3]: ['reddit: the front page of the internet']

In [4]: request = request.replace(method="POST")

In [5]: fetch(request)

In [6]: response.status
Out[6]: 404

4、在 spider 内调用 shell

使用 scrapy.shell.inspect_response 函数:

import scrapy


class MySpider(scrapy.Spider):
    name = "myspider"
    start_urls = [
        "http://example.com",
        "http://example.org",
        "http://example.net",
    ]

    def parse(self, response):
        # We want to inspect one specific response.
        if ".org" in response.url:
            from scrapy.shell import inspect_response
            inspect_response(response, self)

        # Rest of parsing code.

启动爬虫,将会在执行到inspect_response时进入 shell,当处使用完使用Ctrl-D退出 shell,爬虫会恢复运行。

五、scrapy.Spider

1、运行流程

首先生成初始请求以爬取第一个URL,并指定要使用从这些请求下载的响应调用的回调函数。

在回调函数中,解析响应(网页)并返回,Item对象, Request对象或这些对象的可迭代的dicts。

最后,从蜘蛛返回的项目通常会持久保存到数据库(在某些项目管道中)或使用Feed导出写入文件。

在回调函数中,通常使用选择器解析页面内容 (但您也可以使用BeautifulSoup,lxml或您喜欢的任何机制)并使用解析的数据生成item。

2、详解

1)name

spider的名称

一个字符串,用于定义此蜘蛛的名称。蜘蛛名称是Scrapy如何定位(并实例化)蜘蛛,因此它必须是唯一的。这是最重要的蜘蛛属性,它是必需的

2)start_urls

起始urls

蜘蛛将开始爬取的URL列表。因此,下载的第一页将是此处列出的页面。后续Request将从起始URL中包含的数据连续生成。

3)customer_settings

customer_settings 自定义设置

运行此蜘蛛时将覆盖项目范围的设置。必须将其定义为类属性,因为在实例化之前更新了设置。

4)logger 日志

使用Spider创建的Python日志器。您可以使用它来发送日志消息。

5)from_crawler

创建spider的类方法

这是Scrapy用于创建spider的类方法。一般不用覆盖。

6)start_requests

开始请求

此方法必须返回一个iterable,其中包含第一个要爬网的请求。它只会被调用一次

7) parse(response)

默认回调函数

这是Scrapy在其请求未指定回调时处理下载的响应时使用的默认回调

8) close

关闭spider

spider关闭时调用

六、CrawlSpider

1、创建CrawlSpider

scrapy genspider -t crawl hr.tencent hr.tencent.com

url 就是你想要爬取的网址
注意:分析本地文件是一定要带上路径,scrapy shell默认当作url

scrapy genspider -t crawl mycrawlspider hr.tencent.com
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class MycrawlspiderSpider(CrawlSpider):
    name = 'mycrawlspider'
    allowed_domains = ['www']
    start_urls = ['http://www/']

    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        item = {}
        #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
        #item['name'] = response.xpath('//div[@id="name"]').get()
        #item['description'] = response.xpath('//div[@id="description"]').get()
        return item

2、Rule

Rule用来定义CrawlSpider的爬取规则

参数:
link_extractor  Link Extractor对象,它定义如何从每个已爬网页面中提取链接。
callback  回调函数
cb_kwargs  是一个包含要传递给回调函数的关键字参数的dict
follow 它指定是否应该从使用此规则提取的每个响应中跟踪链接。
process_links  用于过滤连接的回调函数
process_request  用于过滤请求的额回调函数

3、LinkExtractor

LinkExractor也是scrapy框架定义的一个类
它唯一的目的是从web页面中提取最终将被跟踪的额连接。

我们也可定义我们自己的链接提取器,只需要提供一个名为

extract_links的方法,它接收Response对象
并返回scrapy.link.Link对象列表。

七、CrawlSpider url去重

八、Scrapy url去重

九、Request

1、Scrapy.http.Request

Scrapy.http.Request类是scrapy框架中request的基类。它的参数如下:
url(字符串) - 此请求的URL
callback(callable)- 回调函数
method(string) - 此请求的HTTP方法。默认为'GET'。
meta(dict) - Request.meta属性的初始值。
body(str 或unicode) - 请求体。如果没有传参,默认为空字符串。
headers(dict) - 此请求的请求头。
cookies - 请求cookie。
encoding(字符串) - 此请求的编码(默认为'utf-8')此编码将用于对URL进行百分比编码并将body转换为str(如果给定unicode)。
priority(int) - 此请求的优先级(默认为0)。
dont_filter(boolean) - 表示调度程序不应过滤此请求。
errback(callable) - 在处理请求时引发任何异常时将调用的函数。
flags(list) - 发送给请求的标志,可用于日志记录或类似目的。

2、属性和方法

url 包含此请求的URL的字符串。该属性是只读的。更改请求使用的URL replace()。
method  表示请求中的HTTP方法的字符串。
headers 类似字典的对象,包含请求头。
body 包含请求正文的str。该属性是只读的。更改请求使用的URL replace()。
meta 包含此请求的任意元数据的字典。
copy() 返回一个新的请求,改请求是此请求的副本。
replace([ URL,method,headers,body,cookies,meta,encoding,dont_filter,callback,errback] ) 返回一个更新对的request

3、利用request.meta传递参数

4、FormRequest

get请求和post请求是最常见的请求。scrapy框架内置了一个FormRequest类
它扩展了基类Request,具有处理HTML表单的功能。
它使用lxml.html表单,来预先填充表单字段,其中包含来自Response对象的表单数据。

5、from_response()

参数:
response(Responseobject) - 包含HTML表单的响应
formname(string) - 如果给定,将使用name属性设置为此值的表单。
formid(string) - 如果给定,将使用id属性设置为此值的表单。
formxpath(string) - 如果给定,将使用与xpath匹配的第一个表单。
formcss(string) - 如果给定,将使用与css选择器匹配的第一个表单。
formnumber(整数) - 当响应包含多个表单时要使用的表单数。
formdata(dict) - 要在表单数据中覆盖的字段。。
clickdata(dict) - 用于查找单击控件的属性。
dont_click(boolean) - 如果为True,将提交表单数据而不单击任何元素。

十、Response

参数:
url(字符串) - 此响应的URL
status(整数) - 响应的HTTP状态。默认为200。
headers(dict) - 此响应的响应头。dict值可以是字符串(对于单值标头)或列表(对于多值标头)。
body(字节) - 响应主体。要将解码后的文本作为str(Python 2中的unicode)访问,您可以使用response.text来自编码感知的 Response子类,例如TextResponse。
flags(列表) - 是包含Response.flags属性初始值的列表 。如果给定,列表将被浅层复制。
request(Requestobject) - Response.request属性的初始值。这表示Request生成此响应的内容。
属性和方法:
url
status
headers
body
request
meta
flags
copy()
replace([ url,status,headers,body,request,flags,cls ] )
urljoin(url )
follow(url)

十一、日志配置和使用

Scrapy logger 在每个Spider实例中提供了一个可以访问和使用的实例,使用方法,见下图
当然可以通过python的logging来记录。
比如:logging.warning('This is a warning!')

但是为了后期维护方面,我们可以创建不同的记录器来封装消息。

并且使用组件或函数的名称进行命名,见下图案例:
这些设置可用于配置日志记录:

LOG_FILE        日志输出文件,如果为None,就打印在控制台
LOG_ENABLED 是否启用日志,默认True
LOG_ENCODING    日期编码,默认utf-8
LOG_LEVEL   日志等级,默认debug
LOG_FORMAT  日志格式
LOG_DATEFORMAT  日志日期格式
LOG_STDOUT  日志标准输出,默认False,如果True所有标准输出都将写入日志中
LOG_SHORT_NAMES   短日志名,默认为False,如果True将不输出组件名

项目中一般设置:
LOG_FILE = 'logfile_name'
LOG_LEVEL = 'INFO'

十二、模拟登陆

分析

捕捉获取登陆时需要上传的form,认为构造formdata,使用FormRequest请求

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


class MyLoginSpiderSpider(scrapy.Spider):
    name = 'my_login_spider'
    # allowed_domains = ['www']
    start_urls = ['http://example.webscraping.com/places/default/user/login']

    def parse(self, response):
        data = {
            'email':'570454176@qq.com',
            'password':'12345678'
        }
        data['_next'] = response.xpath('//input[@name="_next"]/@value').extract_first()
        data['_formkey'] = response.xpath('//input[@name="_formkey"]/@value').extract_first()
        data['_formname'] = response.xpath('//input[@name="_formname"]/@value').extract_first()
        # print(data)
        # return scrapy.Request(
                                url='http://example.webscraping.com/places/default/user/login',
                                method='POST',
                                meta=data,
                                callback=self.detail_parse)
        yield scrapy.FormRequest(
                                url='http://example.webscraping.com/places/default/user/login',
                                formdata=data,
                                callback=self.detail_parse)
        # pass
        
    def detail_parse(self,response):
        # print(response.body)
        login_msg = response.xpath('//div[@class="flash"]/text()').extract_first()
        if login_msg == 'Logged in':
            print('模拟登陆成功')
        else:
            print('模拟登陆失败')
        # pass

十三、下载中间件

下载中间件是一个用来hooks进Scrapy的request/response处理过程的框架。
它是一个轻量级的底层系统,用来全局修改scrapy的request和response。

scrapy框架中的下载中间件,是实现了特殊方法的类。

scrapy系统自带的中间件被放在DOWNLOADER_MIDDLEWARES_BASE设置中

用户自定义的中间件需要在DOWNLOADER_MIDDLEWARES中进行设置
改设置是一个dict,键是中间件类路径,期值是中间件的顺序,是一个正整数0-1000.越小越靠近引擎。

1、API

process_request(request,spider) 处理请求,对于通过中间件的每个请求调用此方法 请求前

返回None---scrapy 直接返回这个Request
返回Response---scrapy 直接返回这个Response
返回Request ---scrapy  重新调度返回的Request请求
返回异常  ---scrapy 调用 process_exception

process_response(request, response, spider) 处理响应,对于通过中间件的每个响应,调用此方法 请求后

process_exception(request, exception, spider) 处理请求时发生了异常调用

from_crawler(cls,crawler )

2、内置下载中间件

常用内置中间件:
CookieMiddleware        支持cookie,通过设置COOKIES_ENABLED 来开启和关闭

HttpProxyMiddleware HTTP代理,通过设置request.meta['proxy']的值来设置

UserAgentMiddleware 与用户代理中间件。

3、下载中间件的使用

用户代理池

# 定义代理池
USER_AGENT =[
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3641.400 QQBrowser/10.4.3284.400"
]
import random
class UserAgentMiddleware(object):
        # 用户代理中间件
    def process_request(self,request,spider):
        '''
        对request添加cookies信息
        :param request:
        :param spider:
        :return:
        '''
        # 在USER_AGENT中随机一个代理
        request.headers['User-Agent'] = random.choice(USER_AGENT)
        print(request.headers,'=================')

    def process_response(self,request,response,spider):
        if response.status ==403:
            print('请求失败...')
            return request
        return response

十四、scrapy 常用设置

1、设置优先级

命令行选项(优先级最高)
设置per-spider
项目设置模块
各命令默认设置
默认全局设置(低优先级)

如下:

# 项目名
BOT_NAME = 'Baidu'
# 项目路径
SPIDER_MODULES = ['Baidu.spiders']
NEWSPIDER_MODULE = 'Baidu.spiders'

# 用户代理
# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'Baidu (+http://www.yourdomain.com)'

# 君子协议
# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 最大并发值
# Configure maximum concurrent requests performed by Scrapy (default: 16)
#CONCURRENT_REQUESTS = 32

# item处理最大并发数,默认100
CONCURRENT_ITEMS            
# 下载最大并发数
CONCURRENT_REQUESTS     
# 单个域名最大并发数
CONCURRENT_REQUESTS_PER_DOMAIN  
# 单个ip最大并发数
CONCURRENT_REQUESTS_PER_IP  

# 请求延时
# 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

# 禁用COOKIES 
# Disable cookies (enabled by default)
#COOKIES_ENABLED = False

# HEADERS请求头
# 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 downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    'Baidu.middlewares.BaiduDownloaderMiddleware': 543,
        'Baidu.middlewares.UserAgentMiddleware': 543,
}

# Pipeline 管道
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
#ITEM_PIPELINES = {
#    'Baidu.pipelines.BaiduPipeline': 300,
#}

十五、Scrapy对接Selenium

chrome的webdriver: http://chromedriver.storage.googleapis.com/index.html

Firefox Firefox驱动下载地址为:https://github.com/mozilla/geckodriver/releases/

IE浏览器驱动下载地址为:http://selenium-release.storage.googleapis.com/index.html (不推荐,没人用)

根据操作系统,以及浏览器版本,下载相应的驱动

并且将下载的webdriver的路径设置到环境变量中

1、Selenium的使用

# 导入模块
from selenium import webdriver
import time

# 模拟调用Chrome浏览器
drvier = webdriver.Chrome()
# 访问网站
drvier.get('https://www.baidu.com')

# 延时5s
time.sleep(5)

# 获取js渲染之后的数据
print(drvier.page_source)

2、使用中间件

from selenium import webdriver
import time
import scrapy

class UserAgentMiddleware(object):
    def process_request(self, request, spider):
        # 打开Chrome浏览器
        self.driver = webdriver.Chrome()
        # 打开相应的网站
        self.driver.get(request.url)
        # 延时
        time.sleep(5)
        # 获取js渲染后的内容
        html = self.driver.page_source
        # 关闭driver
        self.driver.quit()
        # 返回Response对象
        return scrapy.http.HtmlResponse(url=request.url,body=html,encoding='utf-8',request=request)

十六、Scrapy项目部署

1、scrapyd

Scrapyd是一个运行Scrapy spider的开源应用程序。它提供了一个带有HTTP API的服务器,能够运行和监控Scrapy蜘蛛。要将spider部署到Scrapyd,可以使用由Scrapyd客户端包提供的Scrapyd -deploy工具。
Scrapyd安装
Scrapyd依赖于以下库,但安装过程负责安装缺少的库:

Python2.6以上
Twisted8.0以上
Scrapy0.17以上
如何安装Scrapy取决于您正在使用的平台。通用的方法是从PyPI安装它:
    pip  install  scrapyd
安装之后 通过scrapyd命令启动即可:scrapyd
scrapyd带有一个最小的Web界面,启动后,通过访问http://localhost:6800。如左图:

2、部署流程

命令:scrapyd 启动一个web服务
访问:127.0.0.1:6800 http://192.168.32.129:6800/

在本地不能打开127.0.0.1:6800网页,需要配置端口转发:6800 修改配置文件 0.0.0.0

进入到目录:cd /usr/local/lib/python3.6/dist-packages/scrapyd
进入文件:sudo vim default_scrapyd.conf
修改 bind_address=127.0.0.1 修改为0.0.0.0

第一步:启动scrapyd
第二步:修改scrapy.cfg文件
[settings]
default = DownloadImage.settings

​ [deploy]
url = http://127.0.0.1:6800/ # scrapyd 服务器地址
project = DownloadImage
第三步:上传 scrapyd-deploy <target> -p <projectname>
第四步:启动 curl http://localhost:6800/schedule.json -d project=<projectname> -d spider=<spidername>
第五步:停止爬虫 curl http://localhost:6800/cancel.json -d project=tutorial -d job=4fc26e4209da11e9b344000c292b839

3、API

上传

scrapyd-deploy <target> -p <projectname>
例子:scrapyd-deploy -p DownloadImage

启动

curl http://localhost:6800/schedule.json -d project=<projectname> -d spider=<spidername>

停止

curl http://localhost:6800/cancel.json -d project=<projectname> -d <spidername> job=jobid

删除项目

curl http://localhost:6800/delproject.json -d project=<projectname>

列出项目

curl http://localhost:6800/listproject.json

列出爬虫

curl http://localhost:6800/listspider.json?project=myproject

官方文档

https://scrapyd.readthedocs.io/en/latest/api.html

scrapyd设置

Scrapyd在以下位置搜索配置文件,并按顺序解析它们,最新的配置文件具有更高的优先级:

/etc/scrapyd/scrapyd.conf (Unix)
c:\scrapyd\scrapyd.conf (Windows)
/etc/scrapyd/conf.d/* (in alphabetical order, Unix)
scrapyd.conf
~/.scrapyd.conf (users home directory)

scrapyd部署

使用官方提供的工具scrapyd 分为服务端和客户端

在测试服务器上开启scrapyd服务

在客户端 通过scrapy-client 上传本地的代码到服务端

4、window下部署

python absolute_dir/scrapyd-deploy -p <projectname>

十七、scrapy-redis

scrapy-redis是scrapy框架基于redis数据库的组件,用于scrapy项目的分布式开发和部署。

特征:分布式爬取
您可以启动多个spider工程,相互之间共享单个redis的requests队列。最适合广泛的多个域名网站的内容爬取。

分布式数据处理
爬取到的scrapy的item数据可以推入到redis队列中,这意味着你可以根据需求启动尽可能多的处理程序来共享item的队列,进行item数据持久化处理

Scrapy即插即用组件
Scheduler调度器 + Duplication复制 过滤器,Item Pipeline,基本spider

1、request请求共享

首先Slaver端从Master端拿任务(Request、url)进行数据抓取,Slaver抓取数据的同时,产生新任务的Request便提交给 Master 处理;

Master端只有一个Redis数据库,负责将未处理的Request去重和任务分配,将处理后的Request加入待爬队列,并且存储爬取的数据。

Scrapy-Redis默认使用的就是这种策略,我们实现起来很简单,因为任务调度等工作Scrapy-Redis都已经帮我们做好了,我们只需要继承RedisSpider、指定redis_key就行了。

缺点是,Scrapy-Redis调度的任务是Request对象,里面信息量比较大(不仅包含url,还有callback函数、headers等信息),可能导致的结果就是会降低爬虫速度、而且会占用Redis大量的存储空间,所以如果要保证效率,那么就需要一定硬件水平。

2、安装

通过pip 安装: pip install scrapy-redis

官方文档:https://scrapy-redis.readthedocs.io/en/stable/

源码位置:https://github.com/rmax/scrapy-redis

博客:https://www.cnblogs.com/kylinlin/p/5198233.html

3、配置文件

1(必须). 使用了scrapy_redis的去重组件,在redis数据库里做去重

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

2(必须). 使用了scrapy_redis的调度器,在redis里分配请求

SCHEDULER = "scrapy_redis.scheduler.Scheduler"

3(可选). 在redis中保持scrapy-redis用到的各个队列,从而True允许暂停和暂停后恢复,也就是不清理redis queues

SCHEDULER_PERSIST = True

4(必须). 通过配置RedisPipeline将item写入key为 spider.name : items 的redis的list中,供后面的分布式处理item
这个已经由 scrapy-redis 实现,不需要我们写代码,直接使用即可

ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 100
}

5(必须). 指定redis数据库的连接参数

REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379

4、redis键名介绍

1、 “项目名:items”
list 类型,保存爬虫获取到的数据item
内容是 json 字符串

2、 “项目名:dupefilter”
set类型,用于爬虫访问的URL去重
内容是 40个字符的 url 的hash字符串

3、 “项目名: start_urls”
List 类型,用于获取spider启动时爬取的第一个url

4、 “项目名:requests”
zset类型,用于scheduler调度处理 requests
内容是 request 对象的序列化 字符串

十八、分布式爬虫

scrapy-redis 改写爬虫

1、dlimg_spider.py

修改爬虫文件 dlimg_spider.py

from scrapy_redis.spiders import RedisSpider

    class DlimgSpiderSpider(RedisSpider):
        name = 'dlimg_spider'
        # allowed_domains = ['sss']
        # start_urls = ['http://lab.scrapyd.cn/archives/55.html', 'http://lab.scrapyd.cn/archives/57.html']
        redis_key = 'DownloadImage:start_url'

2、settings.py

修改爬虫文件 settings.py

# scrapy_redis 去重
DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupefilter'

# scrapy_redis的调度器
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'

# 保持队列 允许暂停和恢复
SCHEDULER_PERSIST = True

# redis的配置
REDIS_HOST = '127.0.0.1'

REDIS_PORT = 6379

ITEM_PIPELINES = {
    # redis的Pipeline,尽量不要把数据存到redis,会让服务器卡
   'scrapy_redis.pipelines.RedisPipeline':400,
   # 'DownloadImage.pipelines.DownloadimagePipeline': 300,
}

3、运行爬虫

在不同的机器上运行此爬虫

4、键入start_url

通过redis数据库输入start_url

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

推荐阅读更多精彩内容