最近在学习Python这门语言,禀着实践是最好的学习方法的原则,上来就迫不及待的学习了Scrapy框架,并结合网上的例子进行了实验,不得不说感觉到了Python和Scrapy的强大之处,同时也感觉算是站在Python的门外,窥得厅堂里面的东西,还是有些小激动的。那么本帖算是对往日工作的一个总结,也希望自己能够把总结这个习惯坚持下去,一方面帮助自己,一方面帮助他人。
在此还是很感谢网上许许多多大神们的帖子和经验,这里先列出学习过程中参考的链接,希望可以帮助更多的人。
Scrapy tutorial:https://doc.scrapy.org/en/latest/intro/tutorial.html 很有用的官方教程,建议初学者看一下
知乎参考程序:https://www.zhihu.com/question/27621722/answer/269085034 个人感觉还是很有意思的,程序理解起来也不难
Python3语句变为Python2:http://blog.csdn.net/whatday/article/details/54710403 这个博客讲了urllib库在Python2和Python3中的区别,想用Python2的话就把里面对应的语句改一下就可以了(亲测好用)。
首先奉上整个框架的框架图:
可以看到,Scrapy主要包含一下几个方面的组件:
Scrapy Engine(引擎):主要用来处理整个系统的数据流,触发事务。
Scheduler(调度器):主要接收引擎发过来的请求,途中的数据流表示爬虫(spider)将请求给引擎,引擎再给调度器。这个调度器可以看成是一个带有去除重复URL的队列,由他决定下一时刻要去抓取哪个网页,同时去除重复的网址。
Downloader(下载器):由图可以看到,Downloader用于接收Scheduler的请求,之后就开始对请求的网页进行下载,下载后的网页将储存在Response中给spider。
Spider(爬虫):这部分就是我们需要重点对待的部分了,也是程序的主题部分。当spider收到回复之后,就可以使用Scrapy的Selector进行感兴趣内容的抓取,同时还可以生成下次要抓取的页面通过请求的方式给引擎。
ItemPipeline(项目管道):Item是整个抓取过程的实体,也就是我们所需要的资源,个人认为一般就是我们需要的链接,例如图片,视频等的URL;而Pipeline接到这样的实体之后,就可以对实体中的内容进行处理,例如下载图片和音频等工作,这部分也是我们需要编程的部分。
Middlewares(中间件):各种中间件,从图中来看应该是用于数据的流向,具体的功能还没有研究到。
再来大致讲一下整个框架的运行流程:
1. 爬虫将URL以请求(Request)的形式发送给引擎,引擎给予调度器。
2. 引擎从调度器中取出将要抓取的URL(更确切的说是请求),发送给下载器,下载器对该URL进行下载。
3. 下载器将下载的内容以回复(Response)的方式给引擎,引擎转送给爬虫。
4. 爬虫对回复进行解析,将解析出的实体(Item)通过引擎给予管道(Pipeline),将解析出的URL(下一次要抓取的网页)通过引擎给调度器。
5. 管道接收到实体之后对其中的资源进行下载。
作为一个来自二次元的程序员,这个网站着实让人眼前一亮,毕竟之前只知道B站:D
废话不多说,直接说如何进行该网站的抓取。
首先用浏览器打开要抓取的网页acg.fi,随后我们查看他的源代码,发现这个网站的源代码真心的乱啊,一点儿都不归正:|不过没有关系,我们可以用检查的模式进行代码的浏览,很快就能找到我们想要解析的地方如下图所示
可以看到我们主要要解析的标签为
如果这个地方没有了问题,那么解析网页基本上就没啥大问题了,可以看到后面的操作基本上都是这么找标签,然后提取。爬虫部分的代码也就可以贴上来了:D
#!/usr/bin/env python
# coding:utf-8
from acg.items import AcgItem;
from acg import settings;
import os;
import scrapy;
import urllib.request;
import urllib.parse;
class acgspiders(scrapy.Spider):
''' 爬取一个网站 '''
name = 'images';
start_urls = [
"http://www.acg.fi",
];
count = 0;
page = 1;
''' 三级爬取 '''
''' 该部分主要获取要取得的内容 '''
def parse3(self, response):
# 判断已经获取了多少张图片了
# 如果获取了够3000张图片了,就返回
if self.count >= 3000:
return None;
# 先获取item
item = response.meta["item"];
# 抓取图片的url
image_url = response.xpath("//article[@class='article-content']//img/@src").extract();
# 打印一共有多少个图片url
print("一共找到图片%d张" % len(image_url));
# 进行图片的抓取
for url in image_url:
url = urllib.request.quote(url,safe='/:?=.!');
if "jpg" in url:
self.count += 1;
item["img_url"] = url;
item["img_name"] = "img"+str(self.count);
yield item;
if "png" in url:
self.count += 1;
item["img_url"] = url;
item["img_name"] = "img"+str(self.count);
yield item;
''' 二级爬取 '''
''' 该爬取主要是将各栏目中的页面的url给爬取出来 '''
def parse2(self, response):
# 先获取item
item = response.meta["item"];
# 爬取该栏目下的分页情况
pages_url = response.xpath("//div[@class='fenye']//a/@href").extract();
# 这个num主要是为了产生各下级页面的url
num = 1;
# 先判断该栏目是否只有一个页面
if len(pages_url)==0:
# 如果是的话,把该栏目该页的url作为iterator的内容给parse3
page = response.xpath("//h1[@class='article-title']//a/@href").extract_first();
yield scrapy.Request(page, meta={"item":item}, callback=self.parse3);
else:
# 如果该栏目下有多页的话,则把各个页面的url取出来
# 这里一定要进行切片操作,因为每次进来的都是第一页,而下面总会有一个
# 按钮名曰“下一页”,这里就是要舍弃这个下一页
for page in pages_url[:len(pages_url)-1]:
# 对该页面进行请求
yield scrapy.Request(page, meta={"item":item}, callback=self.parse3);
# 下级页面自加
num+=1;
# 打印当前自己爬取了多少界面
print(">>> [parse2] -> 该栏目一共有%d个分页" % num);
''' 一级爬取 '''
''' 该爬取主要找到当前界面下有多少个栏目(二级界面) '''
def parse1(self, response):
# 首先获取item的值
item = response.meta["item"];
# 爬取该网站下的二级界面
pages_url = response.xpath("//div[@class='card-item']//h3//a/@href").extract();
# 打印出来找到了多少个二级页面
print(">>> [parse1] -> %s栏目的第%d页一共有%d个卡片" % (item["dir_name"],int(item["page_num"]),len(pages_url)));
# 生成generator
for url in pages_url:
yield scrapy.Request(url,meta={"item":item}, callback=self.parse2);
# 爬到网页数自加,由于网页数量可能比较多,因此不希望无限制的抓取下去
if item["page_num"] < 20:
item["page_num"]=item["page_num"]+1;
print(">>> [parse1] -> item's page_num is : %d" % item["page_num"]);
# 找寻下个一级页面
next_url = item["lm_url"]+"/page/"+str(item["page_num"]);
# 这里只生成一个generator
yield scrapy.Request(next_url,meta={"item":item},callback=self.parse1);
''' 初级爬取 '''
''' 该函数主要找到该网站下的各个分栏,之后给parse1 '''
def parse(self, response):
# 抓取分栏
# 要拿到名称和链接
lm_name = response.xpath("//li[contains(@id,'menu-item')]//a/text()").extract();
lm_url = response.xpath("//li[contains(@id,'menu-item')]//a/@href").extract();
# 去除首页和后面的分栏
for idx in range(1,7):
# 建立文件夹
# 先确定要存储的位置
path = settings.SAVE_PATH+lm_name[idx];
# 判断是否有该位置的文件夹,没有则重新建立
if not os.path.exists(path):
os.mkdir(path);
# 打印栏目的名称和url
print(">>> [parse] -> 栏目名称 : %s url : %s" % (lm_name[idx],lm_url[idx]));
# 产生一个item
item = AcgItem();
item["dir_name"] = str(lm_name[idx]);
item["lm_url"] = str(lm_url[idx]);
item["page_num"] = 1;
# 这里预留图像的url和img_name的值
# 生成generator
yield scrapy.Request(lm_url[idx], meta={"item":item}, callback=self.parse1);
这里程序也比较简单,也带有我个人认为比较清楚的注释,我的建议是最好亲手撸一遍,过程中不懂的就baidu, bing, google,一般都能找到满意的回答。
这里也着重说一下具有好感的Item,开始在接触这个框架的时候就在想如何在请求与请求之间传递参数,例如本例中的下载路径文件夹,这显然是要在不停的解析下传递下去的,看到Item我就放心了,Scrapy确实为我们想好了策略,而且这个Item还是字典的形式,各种取值赋值都很简单,这里主要还是提醒说在传递的时候,主要通过scrapy.Request中的meta变量进行传递,而且程序中也可以清晰的看到,传递的参数可以是一个,也可以是很多个的!只要把键值区分开就行。最后,一旦觉得实体可以给Pipeline了,直接yield一下该Item就行,太方便了。
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import scrapy
class AcgItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 文件夹的名称
dir_name = scrapy.Field(serializer=str);
lm_url = scrapy.Field(serializer=str);
page_num = scrapy.Field(serializer=int);
img_url = scrapy.Field(serializer=str);
img_name = scrapy.Field(serializer=str);
这里Scrapy的Field有一个很好的属性就是serializer,后面的配置可以限制这个变量的类型。
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from acg.items import AcgItem;
from acg import settings;
from http.client import IncompleteRead;
import urllib.error;
import urllib.request;
import urllib.parse;
import numpy as np;
import cv2;
import os;
class AcgPipeline(object):
''' 初始化函数 '''
def __init__(self):
self.img_seen = set();
''' 执行函数 '''
def process_item(self, item, spider):
# 判断是否见过这个image_name
if item["img_name"] in self.img_seen:
# 如果是的话打印出信息
print(">>> [pipeline] -> old url is used!");
# 并返回
return item;
else:
# 把图片给resize一下,防止过大
maxSize = 512;
# 获取url
url = item["img_url"];
# 模仿浏览器进行访问
req = urllib.request.Request(url=url);
req.add_header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1");
# req.add_header("GET",url);
# req.add_header("Host", "img.gov.com.de");
req.add_header("Referer", url);
# 数据的获取
try:
# 获取该数据
res = urllib.request.urlopen(req, timeout=100).read();
# 转化为numpy类型的数据
image = np.asarray(bytearray(res), dtype="uint8");
# 转化为opencv的数据
image = cv2.imdecode(image, cv2.IMREAD_COLOR);
# reszie
imgResize = cv2.resize(image, dsize=(maxSize, maxSize), interpolation=cv2.INTER_CUBIC);
# 存储
# 先确定要存储的位置
path = settings.SAVE_PATH+item["dir_name"];
# 判断是否有该位置的文件夹,没有则重新建立
if not os.path.exists(settings.SAVE_PATH):
os.makedirs(path);
# 生成图片保存的路径
img_path = path + "/" + item["img_name"] + ".jpg";
# 保存图片
cv2.imwrite(img_path, imgResize);
# 打印保存信息
print(">>> [pipeline] -> image saves in %s" % img_path);
except urllib.error.HTTPError as e:
print(">>> [pipeline] -> HTTPError occur, it's code is %s" % str(e.code));
except (IncompleteRead) as e:
print(">>> [pipeline] -> IncompleteRead occur, it's code is %s" % str(e.code));
except urllib.error.URLError as e:
print(">>> [pipeline] -> URLError occur, it's code is %s" % str(e.code));
# 别忘了返回item
return item;
这部分代码也没有特别多的地方需要讲,硬说的话主要是在urllib的urlopen时尽量加上timeout的设置。
PS:如果要用Pipeline进行资源下载,一定要在setting里面把关于Pipeline的注解删掉并加上自己的Pipeline名称,例如本程序中是下面代码所示的样子,然后最后的数字是0~1000之间就行,表示优先级,具体的可以看注释的网址:D
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'acg.pipelines.AcgPipeline': 300,
}
可以看到最后是完成了我们的愿想:D
1. Python真心强大,这几天还在继续看一些基础的教程,发现很多很好用的语句和内置函数(太TM强大了,让在C和C++摸爬滚打的我十分佩服),还是很值得学习一下
2. Scrapy真心强大,分布式的抓取,速度很快,但是看网上的建议说速度太快也不好,应该加个延时,但是我在跑程序的时候并没有遇到锁IP的情况,因此就没有加延时
3. 整个程序里面有很多不太规范的地方,比如每句后面都加了分号,而且字符串大多数都用的是双影号,主要是为了和C++编程习惯一致,毕竟以后还要靠C++吃饭:x
4. 最后是整个程序的代码,如果想作为参考的可以到GitHub上下载,链接:https://github.com/wuRDmemory/acg_spider
以上的所有都是个人的理解,如果有错误,还请各位大神能够指点纠正,在此不胜感谢!