码迷,mamicode.com
首页 > 其他好文 > 详细

爬虫框架之Scrapy

时间:2019-06-04 22:52:57      阅读:231      评论:0      收藏:0      [点我收藏+]

标签:method   direct   快速   split   use   通过   contain   service   函数返回   

爬虫框架之Scrapy
一、介绍

二、安装

三、命令行工具

四、项目结构以及爬虫应用简介

五、Spiders

六、Selectors

七、Items

八、Item Pipelin

九、 Dowloader Middeware

十、Sider Middlewear

十一、自定义扩展

十二、setitings.py

十三、获取亚马逊商品信息

一、介绍

Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。整体架构大致如下

在Scrapy的数据流是由执行引擎控制,具体流程如下:

1、spiders产生request请求,将请求交给引擎
2、引擎(EGINE)吧刚刚处理好的请求交给了调度器,以一个队列或者堆栈的形式吧这些请求保存起来,调度一个出来再传给引擎
3、调度器(SCHEDULER)返回给引擎一个要爬取的url
4、引擎把调度好的请求发送给download,通过中间件发送(这个中间件至少有 两个方法,一个请求的,一个返回的),
5、一旦完成下载就返回一个response,通过下载器中间件,返回给引擎,引擎把response 对象传给下载器中间件,最后到达引擎
6、引擎从下载器中收到response对象,从下载器中间件传给了spiders(spiders里面做两件事,1、产生request请求,2、为request请求绑定一个回调函数),spiders只负责解析爬取的任务。不做存储,
7、解析完成之后返回一个解析之后的结果items对象及(跟进的)新的Request给引擎
就被ITEM PIPELUMES处理了
8、引擎将(Spider返回的)爬取到的Item给Item Pipeline,存入数据库,持久化,如果数据不对,可重新封装成一个request请求,传给调度器
9、(从第二步)重复直到调度器中没有更多地request,引擎关闭该网站
scrapy框架分为七大部分核心的组件

1、引擎(EGINE)

引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。有关详细信息,请参见上面的数据流部分。

2、调度器(SCHEDULER)

用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址

3、下载器(DOWLOADER)

用于下载网页内容, 并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的

4、爬虫(SPIDERS)

SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求

5、项目管道(ITEM PIPLINES)

在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作

6、下载器中间件(Downloader Middlewares)

下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 下载器中间件(Downloader Middleware) 。

7、爬虫中间件(Spider Middlewares)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 Spider中间件(Middleware) 。

二、安装

复制代码

Windows平台

1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
3、pip3 install lxml
4、pip3 install pyopenssl
5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
8、pip3 install scrapy

Linux平台

1、pip3 install scrapy

复制代码

三、命令行工具

复制代码

1 查看帮助

scrapy -h
scrapy <command> -h

2 有两种命令:其中Project-only必须切到项目文件夹下才能执行,而Global的命令则不需要

Global commands:
    startproject #创建项目
    genspider    #创建爬虫程序
    settings     #如果是在项目目录下,则得到的是该项目的配置
    runspider    #运行一个独立的python文件,不必创建项目
    shell        #scrapy shell url地址  在交互式调试,如选择器规则正确与否
    fetch        #独立于程单纯地爬取一个页面,可以拿到请求头
    view         #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求
    version      #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本
Project-only commands:
    crawl        #运行爬虫,必须创建项目才行,确保配置文件中ROBOTSTXT_OBEY = False
    check        #检测项目中有无语法错误
    list         #列出项目中所包含的爬虫名
    edit         #编辑器,一般不用
    parse        #scrapy parse url地址 --callback 回调函数  #以此可以验证我们的回调函数是否正确
    bench        #scrapy bentch压力测试

3 官网链接

https://docs.scrapy.org/en/latest/topics/commands.html

复制代码

复制代码
1 全局命令:所有文件夹都使用的命令,可以不依赖与项目文件也可以执行
2 项目的文件夹下执行的命令
3 1、scrapy startproject Myproject #创建项目
4 cd Myproject
5 2、scrapy genspider baidu www.baidu.com #创建爬虫程序,baidu是爬虫名,定位爬虫的名字
6 #写完域名以后默认会有一个url,
7 3、scrapy settings --get BOT_NAME #获取配置文件
8 #全局:4、scrapy runspider budui.py
9 5、scrapy runspider AMAZON\spiders\amazon.py #执行爬虫程序
10 在项目下:scrapy crawl amazon #指定爬虫名,定位爬虫程序来运行程序
11 #robots.txt 反爬协议:在目标站点建一个文件,里面规定了哪些能爬,哪些不能爬
12 # 有的国家觉得是合法的,有的是不合法的,这就产生了反爬协议
13 # 默认是ROBOTSTXT_OBEY = True
14 # 修改为ROBOTSTXT_OBEY = False #默认不遵循反扒协议
15 6、scrapy shell https://www.baidu.com #直接超目标站点发请求
16 response
17 response.status
18 response.body
19 view(response)
20 7、scrapy view https://www.taobao.com #如果页面显示内容不全,不全的内容则是ajax请求实现的,以此快速定位问题
21 8、scrapy version #查看版本
22 9、scrapy version -v #查看scrapy依赖库锁依赖的版本
23 10、scrapy fetch --nolog http://www.logou.com #获取响应的内容
24 11、scrapy fetch --nolog --headers http://www.logou.com #获取响应的请求头
25 (venv3_spider) E:\twisted\scrapy框架\AMAZON>scrapy fetch --nolog --headers http://www.logou.com
26 > Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
27 > Accept-Language: en
28 > User-Agent: Scrapy/1.5.0 (+https://scrapy.org)
29 > Accept-Encoding: gzip,deflate
30 >
31 < Content-Type: text/html; charset=UTF-8
32 < Date: Tue, 23 Jan 2018 15:51:32 GMT
33 < Server: Apache
34 >代表请求
35 <代表返回
36 10、scrapy shell http://www.logou.com #直接朝目标站点发请求
37 11、scrapy check #检测爬虫有没有错误
38 12、scrapy list #所有的爬虫名
39 13、scrapy parse http://quotes.toscrape.com/ --callback parse #验证回调函函数是否成功执行
40 14、scrapy bench #压力测试
复制代码
四、项目结构以及爬虫应用简介

复制代码
project_name/
scrapy.cfg
project_name/
init.py
items.py
pipelines.py
settings.py
spiders/
init.py
爬虫1.py
爬虫2.py
爬虫3.py
复制代码

注意:一般创建爬虫文件时,以网站域名命名

默认只能在cmd中执行爬虫,如果想在pycharm中执行需要做:

复制代码

在项目目录下新建:entrypoint.py

from scrapy.cmdline import execute

execute([‘scrapy‘, ‘crawl‘, ‘amazon‘,‘--nolog‘]) #不要日志打印

execute([‘scrapy‘, ‘crawl‘, ‘amazon‘])

我们可能需要在命令行为爬虫程序传递参数,就用下面这样的命令

acrapy crawl amzaon -a keyword=iphone8

execute([‘scrapy‘, ‘crawl‘, ‘amazon1‘,‘-a‘,‘keyword=iphone8‘,‘--nolog‘]) #不要日志打印

execute([‘scrapy‘, ‘crawl‘, ‘amazon1‘])

复制代码

1 import sys,os
2 sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding=‘gb18030‘)
五、Spiders

1、介绍

1、Spiders是由一系列类(定义了一个网址或一组网址将被爬取)组成,具体包括如何执行爬取任务并且如何从页面中提取结构化的数据。

2、换句话说,Spiders是你为了一个特定的网址或一组网址自定义爬取和解析页面行为的地方

2、Spiders会循环做如下事情

复制代码

1、生成初始的Requests来爬取第一个URLS,并且标识一个回调函数

第一个请求定义在start_requests()方法内默认从start_urls列表中获得url地址来生成Request请求,默认的回调函数是parse方法。回调函数在下载完成返回response时自动触发

2、在回调函数中,解析response并且返回值

返回值可以4种:
包含解析数据的字典
Item对象
新的Request对象(新的Requests也需要指定一个回调函数)
或者是可迭代对象(包含Items或Request)

3、在回调函数中解析页面内容

通常使用Scrapy自带的Selectors,但很明显你也可以使用Beutifulsoup,lxml或其他你爱用啥用啥。

4、最后,针对返回的Items对象将会被持久化到数据库

通过Item Pipeline组件存到数据库:https://docs.scrapy.org/en/latest/topics/item-pipeline.html#topics-item-pipeline)
或者导出到不同的文件(通过Feed exports:https://docs.scrapy.org/en/latest/topics/feed-exports.html#topics-feed-exports)
复制代码
3、Spiders总共提供了五种类:

1、scrapy.spiders.Spider #scrapy.Spider等同于scrapy.spiders.Spider

2、scrapy.spiders.CrawlSpider

3、scrapy.spiders.XMLFeedSpider

4、scrapy.spiders.CSVFeedSpider

5、scrapy.spiders.SitemapSpider

4、导入使用

复制代码
import scrapy
class AmazonSpider(scrapy.Spider):

name = 'amazon'  # 必须唯一
allowed_domains = ['www.amazon.cn']  # 允许域
start_urls = ['http://www.amazon.cn/']  # 如果你没有指定发送的请求地址,会默认使用只一个

def parse(self,response):
      pass

复制代码

5、class scrapy.spiders.Spider

这是最简单的spider类,任何其他的spider类都需要继承它(包含你自己定义的)。

该类不提供任何特殊的功能,它仅提供了一个默认的start_requests方法默认从start_urls中读取url地址发送requests请求,并且默认parse作为回调函数

复制代码
import scrapy
class AmazonSpider(scrapy.Spider):
def init(self,keyword=None,*args,**kwargs): #在entrypoint文件里面传进来的keyword,在这里接收了
super(AmazonSpider,self).__init__(*args,**kwargs)
self.keyword = keyword

name = 'amazon'  # 必须唯一
allowed_domains = ['www.amazon.cn']  # 允许域
start_urls = ['http://www.amazon.cn/']  # 如果你没有指定发送的请求地址,会默认使用只一个

custom_settings = {  # 自定制配置文件,自己设置了用自己的,没有就找父类的
    "BOT_NAME": 'HAIYAN_AMAZON',
    'REQUSET_HEADERS': {},
}

def start_requests(self):
    url = 'https://www.amazon.cn/s/ref=nb_sb_noss_1/461-4093573-7508641?'
    url+=urlencode({"field-keywords":self.keyword})
    print(url)
    yield  scrapy.Request(
        url,
        callback = self.parse_index,  #指定回调函数
        dont_filter = True,  #不去重,这个也可以自己定制
        # dont_filter = False,  #去重,这个也可以自己定制
        # meta={'a':1}  #meta代理的时候会用
    )
    #如果要想测试自定义的dont_filter,可多返回结果重复的即可

复制代码
在settings中

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; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
}

复制代码
1 #1、name = ‘amazon‘
2 定义爬虫名,scrapy会根据该值定位爬虫程序
3 所以它必须要有且必须唯一(In Python 2 this must be ASCII only.)
4
5 #2、allowed_domains = [‘www.amazon.cn‘]
6 定义允许爬取的域名,如果OffsiteMiddleware启动(默认就启动),
7 那么不属于该列表的域名及其子域名都不允许爬取
8 如果爬取的网址为:https://www.example.com/1.html,那就添加‘example.com‘到列表.
9
10 #3、start_urls = [‘http://www.amazon.cn/‘]
11 如果没有指定url,就从该列表中读取url来生成第一个请求
12
13 #4、custom_settings
14 值为一个字典,定义一些配置信息,在运行爬虫程序时,这些配置会覆盖项目级别的配置
15 所以custom_settings必须被定义成一个类属性,由于settings会在类实例化前被加载
16
17 #5、settings
18 通过self.settings[‘配置项的名字‘]可以访问settings.py中的配置,如果自己定义了custom_settings还是以自己的为准
19
20 #6、logger
21 日志名默认为spider的名字
22 self.logger.debug(‘=============>%s‘ %self.settings[‘BOT_NAME‘])
23
24 #5、crawler:了解
25 该属性必须被定义到类方法from_crawler中
26
27 #6、from_crawler(crawler, *args, **kwargs):了解
28 You probably won’t need to override this directly because the default implementation acts as a proxy to the init() method, calling it with the given arguments args and named arguments kwargs.
29
30 #7、start_requests()
31 该方法用来发起第一个Requests请求,且必须返回一个可迭代的对象。它在爬虫程序打开时就被Scrapy调用,Scrapy只调用它一次。
32 默认从start_urls里取出每个url来生成Request(url, dont_filter=True)
33
34 #针对参数dont_filter,请看自定义去重规则
35
36 如果你想要改变起始爬取的Requests,你就需要覆盖这个方法,例如你想要起始发送一个POST请求,如下
37 class MySpider(scrapy.Spider):
38 name = ‘myspider‘
39
40 def start_requests(self):
41 return [scrapy.FormRequest("http://www.example.com/login",
42 formdata={‘user‘: ‘john‘, ‘pass‘: ‘secret‘},
43 callback=self.logged_in)]
44
45 def logged_in(self, response):
46 # here you would extract links to follow and return Requests for
47 # each of them, with another callback
48 pass
49
50 #8、parse(response)
51 这是默认的回调函数,所有的回调函数必须返回an iterable of Request and/or dicts or Item objects.
52
53 #9、log(message[, level, component]):了解
54 Wrapper that sends a log message through the Spider’s logger, kept for backwards compatibility. For more information see Logging from Spiders.
55
56 #10、closed(reason)
57 爬虫程序结束时自动触发
复制代码

复制代码
1 去重规则应该多个爬虫共享的,但凡一个爬虫爬取了,其他都不要爬了,实现方式如下
2
3 #方法一:
4 1、新增类属性
5 visited=set() #类属性
6
7 2、回调函数parse方法内:
8 def parse(self, response):
9 if response.url in self.visited:
10 return None
11 .......
12
13 self.visited.add(response.url)
14
15 #方法一改进:针对url可能过长,所以我们存放url的hash值
16 def parse(self, response):
17 url=md5(response.request.url)
18 if url in self.visited:
19 return None
20 .......
21
22 self.visited.add(url)
23
24 #方法二:Scrapy自带去重功能
25 配置文件:
26 DUPEFILTER_CLASS = ‘scrapy.dupefilter.RFPDupeFilter‘ #默认的去重规则帮我们去重,去重规则在内存中
27 DUPEFILTER_DEBUG = False
28 JOBDIR = "保存范文记录的日志路径,如:/root/" # 最终路径为 /root/requests.seen,去重规则放文件中
29
30 scrapy自带去重规则默认为RFPDupeFilter,只需要我们指定
31 Request(...,dont_filter=False) ,如果dont_filter=True则告诉Scrapy这个URL不参与去重。
32
33 #方法三:
34 我们也可以仿照RFPDupeFilter自定义去重规则,
35
36 from scrapy.dupefilter import RFPDupeFilter,看源码,仿照BaseDupeFilter
37
38 #步骤一:在项目目录下自定义去重文件cumstomdupefilter.py
39 ‘‘‘
40 if hasattr("MyDupeFilter",from_settings):
41 func = getattr("MyDupeFilter",from_settings)
42 obj = func()
43 else:
44 return MyDupeFilter()
45 ‘‘‘
46 class MyDupeFilter(object):
47 def init(self):
48 self.visited = set()
49
50 @classmethod
51 def from_settings(cls, settings):
52 ‘‘‘读取配置文件‘‘‘
53 return cls()
54
55 def request_seen(self, request):
56 ‘‘‘请求看过没有,这个才是去重规则该调用的方法‘‘‘
57 if request.url in self.visited:
58 return True
59 self.visited.add(request.url)
60
61 def open(self): # can return deferred
62 ‘‘‘打开的时候执行‘‘‘
63 pass
64
65 def close(self, reason): # can return a deferred
66 pass
67
68 def log(self, request, spider): # log that a request has been filtered
69 ‘‘‘日志记录‘‘‘
70 pass
71
72 #步骤二:配置文件settings.py
73 # DUPEFILTER_CLASS = ‘scrapy.dupefilter.RFPDupeFilter‘ #默认会去找这个类实现去重
74 #自定义去重规则
75 DUPEFILTER_CLASS = ‘AMAZON.cumstomdupefilter.MyDupeFilter‘
76
77 # 源码分析:
78 from scrapy.core.scheduler import Scheduler
79 见Scheduler下的enqueue_request方法:self.df.request_seen(request)
复制代码

复制代码
1 #例一:
2 import scrapy
3
4 class MySpider(scrapy.Spider):
5 name = ‘example.com‘
6 allowed_domains = [‘example.com‘]
7 start_urls = [
8 ‘http://www.example.com/1.html‘,
9 ‘http://www.example.com/2.html‘,
10 ‘http://www.example.com/3.html‘,
11 ]
12
13 def parse(self, response):
14 self.logger.info(‘A response from %s just arrived!‘, response.url)
15
16
17 #例二:一个回调函数返回多个Requests和Items
18 import scrapy
19
20 class MySpider(scrapy.Spider):
21 name = ‘example.com‘
22 allowed_domains = [‘example.com‘]
23 start_urls = [
24 ‘http://www.example.com/1.html‘,
25 ‘http://www.example.com/2.html‘,
26 ‘http://www.example.com/3.html‘,
27 ]
28
29 def parse(self, response):
30 for h3 in response.xpath(‘//h3‘).extract():
31 yield {"title": h3}
32
33 for url in response.xpath(‘//a/@href‘).extract():
34 yield scrapy.Request(url, callback=self.parse)
35
36
37 #例三:在start_requests()内直接指定起始爬取的urls,start_urls就没有用了,
38
39 import scrapy
40 from myproject.items import MyItem
41
42 class MySpider(scrapy.Spider):
43 name = ‘example.com‘
44 allowed_domains = [‘example.com‘]
45
46 def start_requests(self):
47 yield scrapy.Request(‘http://www.example.com/1.html‘, self.parse)
48 yield scrapy.Request(‘http://www.example.com/2.html‘, self.parse)
49 yield scrapy.Request(‘http://www.example.com/3.html‘, self.parse)
50
51 def parse(self, response):
52 for h3 in response.xpath(‘//h3‘).extract():
53 yield MyItem(title=h3)
54
55 for url in response.xpath(‘//a/@href‘).extract():
56 yield scrapy.Request(url, callback=self.parse)
57
58
59 #例四:
60 # -- coding: utf-8 --
61 from urllib.parse import urlencode
62 # from scrapy.dupefilter import RFPDupeFilter
63 # from AMAZON.items import AmazonItem
64 from AMAZON.items import AmazonItem
65 ‘‘‘
66
67 spiders会循环做下面几件事
68 1、生成初始请求来爬取第一个urls,并且绑定一个回调函数
69 2、在回调函数中,解析response并且返回值
70 3、在回调函数中,解析页面内容(可通过Scrapy自带的Seletors或者BeautifuSoup等)
71 4、最后、针对返回的Items对象(就是你从返回结果中筛选出来自己想要的数据)将会被持久化到数据库
72 Spiders总共提供了五种类:
73 #1、scrapy.spiders.Spider #scrapy.Spider等同于scrapy.spiders.Spider
74 #2、scrapy.spiders.CrawlSpider
75 #3、scrapy.spiders.XMLFeedSpider
76 #4、scrapy.spiders.CSVFeedSpider
77 #5、scrapy.spiders.SitemapSpider
78 ‘‘‘
79 import scrapy
80 class AmazonSpider(scrapy.Spider):
81 def init(self,keyword=None,*args,**kwargs): #在entrypoint文件里面传进来的keyword,在这里接收了
82 super(AmazonSpider,self).__init__(*args,**kwargs)
83 self.keyword = keyword
84
85 name = ‘amazon‘ # 必须唯一
86 allowed_domains = [‘www.amazon.cn‘] # 允许域
87 start_urls = [‘http://www.amazon.cn/‘] # 如果你没有指定发送的请求地址,会默认使用只一个
88
89 custom_settings = { # 自定制配置文件,自己设置了用自己的,没有就找父类的
90 "BOT_NAME": ‘HAIYAN_AMAZON‘,
91 ‘REQUSET_HEADERS‘: {},
92 }
93
94 def start_requests(self):
95 url = ‘https://www.amazon.cn/s/ref=nb_sb_noss_1/461-4093573-7508641?‘
96 url+=urlencode({"field-keywords":self.keyword})
97 print(url)
98 yield scrapy.Request(
99 url,
100 callback = self.parse_index, #指定回调函数
101 dont_filter = True, #不去重,这个也可以自己定制
102 # dont_filter = False, #去重,这个也可以自己定制
103 # meta={‘a‘:1} #meta代理的时候会用
104 )
105 #如果要想测试自定义的dont_filter,可多返回结果重复的即可
106
107
108
109 def parse_index(self, response):
110 ‘‘‘获取详情页和下一页的链接‘‘‘
111 detail_urls = response.xpath(‘//[contains(@id,"result_")]/div/div[3]/div[1]/a/@href‘).extract()
112 print(detail_urls)
113 # print("%s 解析 %s",(response.url,(len(response.body))))
114 for detail_url in detail_urls:
115 yield scrapy.Request(
116 url=detail_url,
117 callback=self.parse_detail #记得每次返回response的时候记得绑定一个回调函数
118 )
119 next_url = response.urljoin(response.xpath(response.xpath(‘//
[@id="pagnNextLink"]/@href‘).extract_first()))
120 # 因为下一页的url是不完整的,用urljoin就可以吧路径前缀拿到并且拼接
121 # print(next_url)
122 yield scrapy.Request(
123 url=next_url,
124 callback=self.parse_index #因为下一页也属于是索引页,让去解析索引页
125 )
126 def parse_detail(self,response):
127 ‘‘‘详情页解析‘‘‘
128 name = response.xpath(‘//[@id="productTitle"]/text()‘).extract_first().strip()#获取name
129 price = response.xpath(‘//
[@id="price"]//[@class="a-size-medium a-color-price"]/text()‘).extract_first()#获取价格
130 delivery_method=‘‘.join(response.xpath(‘//
[@id="ddmMerchantMessage"]//text()‘).extract()) #获取配送方式
131 print(name)
132 print(price)
133 print(delivery_method)
134 #上面是筛选出自己想要的项
135 #必须返回一个Item对象,那么这个item对象,是从item.py中来,和django中的model类似,
136 # 但是这里的item对象也可当做是一个字典,和字典的操作一样
137 item = AmazonItem()# 实例化
138 item["name"] = name
139 item["price"] = price
140 item["delivery_method"] = delivery_method
141 return item
142
143 def close(spider, reason):
144 print("结束啦")
复制代码

复制代码
1 我们可能需要在命令行为爬虫程序传递参数,比如传递初始的url,像这样
2 #命令行执行
3 scrapy crawl myspider -a category=electronics
4
5 #在__init__方法中可以接收外部传进来的参数
6 import scrapy
7
8 class MySpider(scrapy.Spider):
9 name = ‘myspider‘
10
11 def init(self, category=None, *args, **kwargs):
12 super(MySpider, self).__init__(*args, **kwargs)
13 self.start_urls = [‘http://www.example.com/categories/%s‘ % category]
14 #...
15
16
17 #注意接收的参数全都是字符串,如果想要结构化的数据,你需要用类似json.loads的方法
复制代码
6、其他通用Spiders:https://docs.scrapy.org/en/latest/topics/spiders.html#generic-spiders

六、Selectors

复制代码

1 //与/

2 text

3、extract与extract_first:从selector对象中解出内容

4、属性:xpath的属性加前缀@

4、嵌套查找

5、设置默认值

4、按照属性查找

5、按照属性模糊查找

6、正则表达式

7、xpath相对路径

8、带变量的xpath

复制代码

复制代码
1 response.selector.css()
2 response.selector.xpath()
3 可简写为
4 response.css()
5 response.xpath()
6
7 #1 //与/
8 response.xpath(‘//body/a/‘)#
9 response.css(‘div a::text‘)
10
11 >>> response.xpath(‘//body/a‘) #开头的//代表从整篇文档中寻找,body之后的/代表body的儿子
12 []
13 >>> response.xpath(‘//body//a‘) #开头的//代表从整篇文档中寻找,body之后的//代表body的子子孙孙
14 [Name: My image 1 <‘>, Name: My image 2 <‘>, Name: My image 3 <‘>, Name: My image 4 <‘>, Name: My image 5 <‘>]
16
17 #2 text
18 >>> response.xpath(‘//body//a/text()‘)
19 >>> response.css(‘body a::text‘)
20
21 #3、extract与extract_first:从selector对象中解出内容
22 >>> response.xpath(‘//div/a/text()‘).extract()
23 [‘Name: My image 1 ‘, ‘Name: My image 2 ‘, ‘Name: My image 3 ‘, ‘Name: My image 4 ‘, ‘Name: My image 5 ‘]
24 >>> response.css(‘div a::text‘).extract()
25 [‘Name: My image 1 ‘, ‘Name: My image 2 ‘, ‘Name: My image 3 ‘, ‘Name: My image 4 ‘, ‘Name: My image 5 ‘]
26
27 >>> response.xpath(‘//div/a/text()‘).extract_first()
28 ‘Name: My image 1 ‘
29 >>> response.css(‘div a::text‘).extract_first()
30 ‘Name: My image 1 ‘
31
32 #4、属性:xpath的属性加前缀@
33 >>> response.xpath(‘//div/a/@href‘).extract_first()
34 ‘image1.html‘
35 >>> response.css(‘div a::attr(href)‘).extract_first()
36 ‘image1.html‘
37
38 #4、嵌套查找
39 >>> response.xpath(‘//div‘).css(‘a‘).xpath(‘@href‘).extract_first()
40 ‘image1.html‘
41
42 #5、设置默认值
43 >>> response.xpath(‘//div[@id="xxx"]‘).extract_first(default="not found")
44 ‘not found‘
45
46 #4、按照属性查找
47 response.xpath(‘//div[@id="images"]/a[@href="image3.html"]/text()‘).extract()
48 response.css(‘#images a[@href="image3.html"]/text()‘).extract()
49
50 #5、按照属性模糊查找
51 response.xpath(‘//a[contains(@href,"image")]/@href‘).extract()
52 response.css(‘a[href*="image"]::attr(href)‘).extract()
53
54 response.xpath(‘//a[contains(@href,"image")]/img/@src‘).extract()
55 response.css(‘a[href*="imag"] img::attr(src)‘).extract()
56
57 response.xpath(‘//[@href="image1.html"]‘)
58 response.css(‘
[href="image1.html"]‘)
59
60 #6、正则表达式
61 response.xpath(‘//a/text()‘).re(r‘Name: (.)‘)
62 response.xpath(‘//a/text()‘).re_first(r‘Name: (.
)‘)
63
64 #7、xpath相对路径
65 >>> res=response.xpath(‘//a[contains(@href,"3")]‘)[0]
66 >>> res.xpath(‘img‘)
67 [‘>]
68 >>> res.xpath(‘./img‘)
69 [‘>]
70 >>> res.xpath(‘.//img‘)
71 [‘>]
72 >>> res.xpath(‘//img‘) #这就是从头开始扫描
73 [‘>, ‘>, ‘>, <Selector xpa
74 th=‘//img‘ data=‘技术图片‘>, ‘>]
75
76 #8、带变量的xpath
77 >>> response.xpath(‘//div[@id=$xxx]/a/text()‘,xxx=‘images‘).extract_first()
78 ‘Name: My image 1 ‘
79 >>> response.xpath(‘//div[count(a)=$yyy]/@id‘,yyy=5).extract_first() #求有5个a标签的div的id
80 ‘images‘
复制代码
https://docs.scrapy.org/en/latest/topics/selectors.html

七、Items

https://docs.scrapy.org/en/latest/topics/items.html

八、Item Pipelin

复制代码
1 #一:可以写多个Pipeline类
2 #1、如果优先级高的Pipeline的process_item返回一个值或者None,会自动传给下一个pipline的process_item,
3 #2、如果只想让第一个Pipeline执行,那得让第一个pipline的process_item抛出异常raise DropItem()
4
5 #3、可以用spider.name == ‘爬虫名‘ 来控制哪些爬虫用哪些pipeline
6
7 二:示范
8 from scrapy.exceptions import DropItem
9
10 class CustomPipeline(object):
11 def init(self,v):
12 self.value = v
13
14 @classmethod
15 def from_crawler(cls, crawler):
16 """
17 Scrapy会先通过getattr判断我们是否自定义了from_crawler,有则调它来完
18 成实例化
19 """
20 val = crawler.settings.getint(‘MMMM‘)
21 return cls(val)
22
23 def open_spider(self,spider):
24 """
25 爬虫刚启动时执行一次
26 """
27 print(‘000000‘)
28
29 def close_spider(self,spider):
30 """
31 爬虫关闭时执行一次
32 """
33 print(‘111111‘)
34
35
36 def process_item(self, item, spider):
37 # 操作并进行持久化
38
39 # return表示会被后续的pipeline继续处理
40 return item
41
42 # 表示将item丢弃,不会被后续pipeline处理
43 # raise DropItem()
复制代码
示范
https://docs.scrapy.org/en/latest/topics/item-pipeline.html

九、 Dowloader Middeware

复制代码
下载中间件的用途
1、在process——request内,自定义下载,不用scrapy的下载
2、对请求进行二次加工,比如
设置请求头
设置cookie
添加代理
scrapy自带的代理组件:
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
from urllib.request import getproxies
复制代码

复制代码
1 class DownMiddleware1(object):
2 def process_request(self, request, spider):
3 """
4 请求需要被下载时,经过所有下载器中间件的process_request调用
5 :param request:
6 :param spider:
7 :return:
8 None,继续后续中间件去下载;
9 Response对象,停止process_request的执行,开始执行process_response
10 Request对象,停止中间件的执行,将Request重新调度器
11 raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
12 """
13 pass
14
15
16
17 def process_response(self, request, response, spider):
18 """
19 spider处理完成,返回时调用
20 :param response:
21 :param result:
22 :param spider:
23 :return:
24 Response 对象:转交给其他中间件process_response
25 Request 对象:停止中间件,request会被重新调度下载
26 raise IgnoreRequest 异常:调用Request.errback
27 """
28 print(‘response1‘)
29 return response
30
31 def process_exception(self, request, exception, spider):
32 """
33 当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
34 :param response:
35 :param exception:
36 :param spider:
37 :return:
38 None:继续交给后续中间件处理异常;
39 Response对象:停止后续process_exception方法
40 Request对象:停止中间件,request将会被重新调用下载
41 """
42 return None
复制代码

复制代码
1 from scrapy import signals
2 from scrapy.http import Response
3 from scrapy.exceptions import IgnoreRequest
4 from AMAZON.proxy_handle import get_proxy,delete_proxy
5 class AmazonSpiderMiddleware(object):
6 # Not all methods need to be defined. If a method is not defined,
7 # scrapy acts as if the spider middleware does not modify the
8 # passed objects.
9
10 @classmethod
11 def from_crawler(cls, crawler):
12 # This method is used by Scrapy to create your spiders.
13 s = cls()
14 crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
15 return s
16
17 def process_spider_input(self, response, spider):
18 # Called for each response that goes through the spider
19 # middleware and into the spider.
20
21 # Should return None or raise an exception.
22 return None
23
24 def process_spider_output(self, response, result, spider):
25 # Called with the results returned from the Spider, after
26 # it has processed the response.
27
28 # Must return an iterable of Request, dict or Item objects.
29 for i in result:
30 yield i
31
32 def process_spider_exception(self, response, exception, spider):
33 # Called when a spider or process_spider_input() method
34 # (from other spider middleware) raises an exception.
35
36 # Should return either None or an iterable of Response, dict
37 # or Item objects.
38 pass
39
40 def process_start_requests(self, start_requests, spider):
41 # Called with the start requests of the spider, and works
42 # similarly to the process_spider_output() method, except
43 # that it doesn’t have a response associated.
44
45 # Must return only requests (not items).
46 for r in start_requests:
47 yield r
48
49 def spider_opened(self, spider):
50 spider.logger.info(‘Spider opened: %s‘ % spider.name)
51
52
53 class AmazonDownloaderMiddleware(object):
54 # Not all methods need to be defined. If a method is not defined,
55 # scrapy acts as if the downloader middleware does not modify the
56 # passed objects.
57
58 @classmethod
59 def from_crawler(cls, crawler):
60 # This method is used by Scrapy to create your spiders.
61 s = cls()
62 crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
63 return s
64
65 def process_request(self, request, spider):
66 # Called for each request that goes through the downloader
67 # middleware.
68
69 # Must either:
70 # - return None: continue processing this request
71 # - or return a Response object
72 # - or return a Request object
73 # - or raise IgnoreRequest: process_exception() methods of
74 # installed downloader middleware will be called
75 return None
76
77 def process_response(self, request, response, spider):
78 # Called with the response returned from the downloader.
79
80 # Must either;
81 # - return a Response object
82 # - return a Request object
83 # - or raise IgnoreRequest
84 return response
85
86 def process_exception(self, request, exception, spider):
87 # Called when a download handler or a process_request()
88 # (from other downloader middleware) raises an exception.
89
90 # Must either:
91 # - return None: continue processing this exception
92 # - return a Response object: stops process_exception() chain
93 # - return a Request object: stops process_exception() chain
94 pass
95
96 def spider_opened(self, spider):
97 spider.logger.info(‘Spider opened: %s‘ % spider.name)
98
99 class DownMiddleware1(object):
100 def process_request(self, request, spider):
101 """
102 请求需要被下载时,经过所有下载器中间件的process_request调用
103 :param request:
104 :param spider:
105 :return:
106 None,继续后续中间件去下载;
107 Response对象,停止process_request的执行,开始执行process_response,会先打印response2,后打印response1
108 Request对象,停止中间件的执行,将Request重新调度器(无论在那个阶段,只要返回request,就丢给引擎了。)
109 raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
110 """
111 # spider.name
112 print(‘下载中间件1‘)
113 # request.meta[‘proxy‘]=‘http://user:pwd@ip:port‘
114 request.meta[‘download_timeout‘]=10 #设置超时时间
115 request.meta[‘proxy‘]=‘http://‘+get_proxy() #request.meta加代理,注意这里的key必须叫proxy
116 print(request.meta)
117 # return Response(‘http://www.xxx.com‘)
118 # print(request.dont_filter)
119 # return request
120 # raise IgnoreRequest
121 # raise TimeoutError
122
123 def process_response(self, request, response, spider):
124 """
125 spider处理完成,返回时调用
126 :param response:
127 :param result:
128 :param spider:
129 :return:
130 Response 对象:转交给其他中间件process_response
131 Request 对象:停止中间件,request会被重新调度下载
132 raise IgnoreRequest 异常:调用Request.errback
133 """
134 print(‘response1‘)
135 return response
136
137 def process_exception(self, request, exception, spider):
138 """
139 当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
140 :param response:
141 :param exception:
142 :param spider:
143 :return:
144 None:继续交给后续中间件处理异常;
145 Response对象:停止后续process_exception方法
146 Request对象:停止中间件,request将会被重新调用下载
147 """
148 print(‘异常1‘)
149 # return None
150
151 # 删旧代理 delelte request.meta[‘proxy‘]
152 old_proxy=request.meta[‘proxy‘].split("//")[-1]
153 delete_proxy(old_proxy) #删除就代理,
154
155 request.meta[‘proxy‘]=‘http://‘+get_proxy() #换新代理
156 return request
复制代码
十、Sider Middlewear

复制代码
1 class SpiderMiddleware(object):
2
3 def process_spider_input(self,response, spider):
4 """
5 下载完成,执行,然后交给parse处理
6 :param response:
7 :param spider:
8 :return:
9 """
10 pass
11
12 def process_spider_output(self,response, result, spider):
13 """
14 spider处理完成,返回时调用
15 :param response:
16 :param result:
17 :param spider:
18 :return: 必须返回包含 Request 或 Item 对象的可迭代对象(iterable)
19 """
20 return result
21
22 def process_spider_exception(self,response, exception, spider):
23 """
24 异常调用
25 :param response:
26 :param exception:
27 :param spider:
28 :return: None,继续交给后续中间件处理异常;含 Response 或 Item 的可迭代对象(iterable),交给调度器或pipeline
29 """
30 return None
31
32
33 def process_start_requests(self,start_requests, spider):
34 """
35 爬虫启动时调用
36 :param start_requests:
37 :param spider:
38 :return: 包含 Request 对象的可迭代对象
39 """
40 return start_requests
复制代码
十一、自定义扩展

自定义扩展(与django的信号类似)
1、django的信号是django是预留的扩展,信号一旦被触发,相应的功能就会执行
2、scrapy自定义扩展的好处是可以在任意我们想要的位置添加功能,而其他组件中提供的功能只能在规定的位置执行

复制代码
1 #1、在与settings同级目录下新建一个文件,文件名可以为extentions.py,内容如下
2 from scrapy import signals
3
4
5 class MyExtension(object):
6 def init(self, value):
7 self.value = value
8
9 @classmethod
10 def from_crawler(cls, crawler):
11 val = crawler.settings.getint(‘MMMM‘)
12 obj = cls(val)
13
14 crawler.signals.connect(obj.spider_opened, signal=signals.spider_opened)
15 crawler.signals.connect(obj.spider_closed, signal=signals.spider_closed)
16
17 return obj
18
19 def spider_opened(self, spider):
20 print(‘=============>open‘)
21
22 def spider_closed(self, spider):
23 print(‘=============>close‘)
24
25 #2、配置生效
26 EXTENSIONS = {
27 "Amazon.extentions.MyExtension":200
28 }
复制代码
十二、setitings.py

复制代码
1 #==>第一部分:基本配置<===
2 #1、项目名称,默认的USER_AGENT由它来构成,也作为日志记录的日志名
3 BOT_NAME = ‘Amazon‘
4
5 #2、爬虫应用路径
6 SPIDER_MODULES = [‘Amazon.spiders‘]
7 NEWSPIDER_MODULE = ‘Amazon.spiders‘
8
9 #3、客户端User-Agent请求头
10 #USER_AGENT = ‘Amazon (+http://www.yourdomain.com)‘
11
12 #4、是否遵循爬虫协议
13 # Obey robots.txt rules
14 ROBOTSTXT_OBEY = False
15
16 #5、是否支持cookie,cookiejar进行操作cookie,默认开启
17 #COOKIES_ENABLED = False
18
19 #6、Telnet用于查看当前爬虫的信息,操作爬虫等...使用telnet ip port ,然后通过命令操作
20 #TELNETCONSOLE_ENABLED = False
21 #TELNETCONSOLE_HOST = ‘127.0.0.1‘
22 #TELNETCONSOLE_PORT = [6023,]
23
24 #7、Scrapy发送HTTP请求默认使用的请求头
25 #DEFAULT_REQUEST_HEADERS = {
26 # ‘Accept‘: ‘text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8‘,
27 # ‘Accept-Language‘: ‘en‘,
28 #}
29
30
31
32 #===>第二部分:并发与延迟<===
33 #1、下载器总共最大处理的并发请求数,默认值16
34 #CONCURRENT_REQUESTS = 32
35
36 #2、每个域名能够被执行的最大并发请求数目,默认值8
37 #CONCURRENT_REQUESTS_PER_DOMAIN = 16
38
39 #3、能够被单个IP处理的并发请求数,默认值0,代表无限制,需要注意两点
40 #I、如果不为零,那CONCURRENT_REQUESTS_PER_DOMAIN将被忽略,即并发数的限制是按照每个IP来计算,而不是每个域名
41 #II、该设置也影响DOWNLOAD_DELAY,如果该值不为零,那么DOWNLOAD_DELAY下载延迟是限制每个IP而不是每个域
42 #CONCURRENT_REQUESTS_PER_IP = 16
43
44 #4、如果没有开启智能限速,这个值就代表一个规定死的值,代表对同一网址延迟请求的秒数
45 #DOWNLOAD_DELAY = 3
46
47
48
49 #===>第三部分:智能限速/自动节流:AutoThrottle extension<===
50 #一:介绍
51 from scrapy.contrib.throttle import AutoThrottle #http://scrapy.readthedocs.io/en/latest/topics/autothrottle.html#topics-autothrottle
52 设置目标:
53 1、比使用默认的下载延迟对站点更好
54 2、自动调整scrapy到最佳的爬取速度,所以用户无需自己调整下载延迟到最佳状态。用户只需要定义允许最大并发的请求,剩下的事情由该扩展组件自动完成
55
56
57 #二:如何实现?
58 在Scrapy中,下载延迟是通过计算建立TCP连接到接收到HTTP包头(header)之间的时间来测量的。
59 注意,由于Scrapy可能在忙着处理spider的回调函数或者无法下载,因此在合作的多任务环境下准确测量这些延迟是十分苦难的。 不过,这些延迟仍然是对Scrapy(甚至是服务器)繁忙程度的合理测量,而这扩展就是以此为前提进行编写的。
60
61
62 #三:限速算法
63 自动限速算法基于以下规则调整下载延迟
64 #1、spiders开始时的下载延迟是基于AUTOTHROTTLE_START_DELAY的值
65 #2、当收到一个response,对目标站点的下载延迟=收到响应的延迟时间/AUTOTHROTTLE_TARGET_CONCURRENCY
66 #3、下一次请求的下载延迟就被设置成:对目标站点下载延迟时间和过去的下载延迟时间的平均值
67 #4、没有达到200个response则不允许降低延迟
68 #5、下载延迟不能变的比DOWNLOAD_DELAY更低或者比AUTOTHROTTLE_MAX_DELAY更高
69
70 #四:配置使用
71 #开启True,默认False
72 AUTOTHROTTLE_ENABLED = True
73 #起始的延迟
74 AUTOTHROTTLE_START_DELAY = 5
75 #最小延迟
76 DOWNLOAD_DELAY = 3
77 #最大延迟
78 AUTOTHROTTLE_MAX_DELAY = 10
79 #每秒并发请求数的平均值,不能高于 CONCURRENT_REQUESTS_PER_DOMAIN或CONCURRENT_REQUESTS_PER_IP,调高了则吞吐量增大强奸目标站点,调低了则对目标站点更加”礼貌“
80 #每个特定的时间点,scrapy并发请求的数目都可能高于或低于该值,这是爬虫视图达到的建议值而不是硬限制
81 AUTOTHROTTLE_TARGET_CONCURRENCY = 16.0
82 #调试
83 AUTOTHROTTLE_DEBUG = True
84 CONCURRENT_REQUESTS_PER_DOMAIN = 16
85 CONCURRENT_REQUESTS_PER_IP = 16
86
87
88
89 #===>第四部分:爬取深度与爬取方式<===
90 #1、爬虫允许的最大深度,可以通过meta查看当前深度;0表示无深度
91 # DEPTH_LIMIT = 3
92
93 #2、爬取时,0表示深度优先Lifo(默认);1表示广度优先FiFo
94
95 # 后进先出,深度优先
96 # DEPTH_PRIORITY = 0
97 # SCHEDULER_DISK_QUEUE = ‘scrapy.squeue.PickleLifoDiskQueue‘
98 # SCHEDULER_MEMORY_QUEUE = ‘scrapy.squeue.LifoMemoryQueue‘
99 # 先进先出,广度优先
100
101 # DEPTH_PRIORITY = 1
102 # SCHEDULER_DISK_QUEUE = ‘scrapy.squeue.PickleFifoDiskQueue‘
103 # SCHEDULER_MEMORY_QUEUE = ‘scrapy.squeue.FifoMemoryQueue‘
104
105
106 #3、调度器队列
107 # SCHEDULER = ‘scrapy.core.scheduler.Scheduler‘
108 # from scrapy.core.scheduler import Scheduler
109
110 #4、访问URL去重
111 # DUPEFILTER_CLASS = ‘step8_king.duplication.RepeatUrl‘
112
113
114
115 #===>第五部分:中间件、Pipelines、扩展<===
116 #1、Enable or disable spider middlewares
117 # See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
118 #SPIDER_MIDDLEWARES = {
119 # ‘Amazon.middlewares.AmazonSpiderMiddleware‘: 543,
120 #}
121
122 #2、Enable or disable downloader middlewares
123 # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
124 DOWNLOADER_MIDDLEWARES = {
125 # ‘Amazon.middlewares.DownMiddleware1‘: 543,
126 }
127
128 #3、Enable or disable extensions
129 # See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
130 #EXTENSIONS = {
131 # ‘scrapy.extensions.telnet.TelnetConsole‘: None,
132 #}
133
134 #4、Configure item pipelines
135 # See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
136 ITEM_PIPELINES = {
137 # ‘Amazon.pipelines.CustomPipeline‘: 200,
138 }
139
140
141
142 #===>第六部分:缓存<===
143 """
144 1. 启用缓存
145 目的用于将已经发送的请求或相应缓存下来,以便以后使用
146
147 from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
148 from scrapy.extensions.httpcache import DummyPolicy
149 from scrapy.extensions.httpcache import FilesystemCacheStorage
150 """
151 # 是否启用缓存策略
152 # HTTPCACHE_ENABLED = True
153
154 # 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
155 # HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
156 # 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
157 # HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
158
159 # 缓存超时时间
160 # HTTPCACHE_EXPIRATION_SECS = 0
161
162 # 缓存保存路径
163 # HTTPCACHE_DIR = ‘httpcache‘
164
165 # 缓存忽略的Http状态码
166 # HTTPCACHE_IGNORE_HTTP_CODES = []
167
168 # 缓存存储的插件
169 # HTTPCACHE_STORAGE = ‘scrapy.extensions.httpcache.FilesystemCacheStorage‘

爬虫框架之Scrapy

标签:method   direct   快速   split   use   通过   contain   service   函数返回   

原文地址:https://www.cnblogs.com/yx12138/p/10976378.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!