昨晚在群里看到一朋友问用scrapy框架抓取妹子图网站,抓到的总是一个页面,清明期间理顺了数据存储关系就写下练手,在用scrapy之前我用单独的py脚本测试了一下,没大问题就往框架上套用,有许多细节还是需要关心的,稍后会总结一下稍不留神就犯的错,下一篇文章将对比框架和单独py脚本的实现逻辑。
一、大致画一个爬取流程图
![](https://img.haomeiwen.com/i3888998/821e11ceb9d83ffb.png)
进入http://www.meizitu.com/a/list_1_1.html
这个页面会有封面图,封面图的url类似http://www.meizitu.com/a/5517.html。
跳转到封面图页面后再抓这个页面的图片。
类似于入口url一样的url有92个,也就是到list_1_92.html,假如第一页有30个封面图,每个封面图有10张图,最后抓取到的详情页的图片应该是92乘30乘10=27600张图片(*替换乘号居然不显示,封面图不计算在内),因此我们会在spider中定义两个函数,一个parse函数 从入口url解析出封面url,对封面url进行回调parse_page函数获取详情页的图片数据并接收parse函数获取到的items.py里定义的需要获取的数据,同时parse函数里包含一个除了入口url外的url列表,对其循环回调自身parse函数实现所有url的遍历和跨页面的数据获取,因此我存储的数据结构也是按照这个逻辑,去代码实现
数据存储结构:可以看到我是按网站进入的层级层层存储,字段顺序是一对多的关系,从左到右代表入口页的url可以有多个封面图,而封面图又可以有多张图片
![](https://img.haomeiwen.com/i3888998/d902e5d1035055eb.png)
二、具体代码实现(跟随想存储的数据结构写逻辑)
1、items.py
from scrapy import Item,Field
class MeizituItem(Item):
list_url=Field()
fengmian_url=Field()
image_url=Field()
image_name=Field()
2、MeizituSpider.py
#coding:utf-8
import sys
sys.path.append('..')
from scrapy.http import Request
from scrapy.selector import Selector
from scrapy.spiders import CrawlSpider
from items import MeizituItem
class MeiziSpider(CrawlSpider):
name = 'meizituespider'
start_urls = ['http://www.meizitu.com/a/list_1_1.html']
allowed_domains = ["meizitu.com"]
def parse(self,response):
#空列表用来添加下面对封面url列表循环时候当前请求的url和当前页面解析出的每个封面url
items=[]
print response.url
selector=Selector(response)
infos = selector.xpath('//li[@class="wp-item"]//h3[@class="tit"]')
for info in infos:
#item实例化 数据类型字典 在循环里实例化 最后将字典添加进items列表
item=MeizituItem()
fengmian_url=info.xpath('a/@href')[0].extract()
print fengmian_url
item['list_url']=response.url
item['fengmian_url'] = fengmian_url
items.append(item)
#此时所有的封面url和请求的url数据都是一个字典中的value,字典作为一个元素添加进列表
#items列表此时不再空,有多少封面url就有多少个item字典元素
#若此时入口url的值如果为a,封面url为(b1,b2,b3,b4),那么items数据为
#tems=[
# {'list_url':'a','fengmian_url':‘b1’},
# {'list_url':'a','fengmian_url':'b2'},
#{'list_url':'a','fengmian_url':'b3'},
#{'list_url':'a','fengmian_url':'b4'}
#
#这样写代码,是和我设定的数据存储目标一致的,见上面数据储存结构
#如果不存这层数据 items空列表是没有必要建立的,在上面循环里可以直接回调parse_page函数了。
for item in items:
#对列表解析 列表的第一个item现在是item={'list_url':'a','fengmian_url':‘b1’}
#此时就可以回调parse_page函数,利用meta参数将第一层的入口url传递给下一层了,meta是一个字典
yield Request(url=item['fengmian_url'],meta={'item_1': item}, callback=self.parse_page)
#入口url为http://www.meizitu.com/a/list_1_1.html,这样的页面有92个因此我们用一个循环,遍历生成新的url回调parse这个自身函数解析,就依次进入第2到92个url的每个封面url,获取每个封面url的所有图片了
for i in range(2,93):
req_url='http://www.meizitu.com/a/list_1_%s.html' % i
yield Request(url=req_url,callback=self.parse)
def parse_page(self,response):
#进入封面url的页面 首先接收传递出来的数据 items为字典
#items={'list_url':'a','fengmian_url':‘b1’} 此时response.url为b1
items = response.meta['item_1']
selector=Selector(response)
infos = selector.xpath('//div[@class="postContent"]//p/img')
print len(infos)
#对b1封面url页面的所有图片进行循环遍历
for info in infos:
#实例化一个MeizituItem()对象 这个才是最后提交到piplines处理的
item=MeizituItem()
image_url = info.xpath('@src')[0].extract()
image_name = info.xpath('@alt')[0].extract()
#添加图片数据入item
item['image_url'] = image_url
item['image_name'] = image_name
#这里因为for 循环获取的是每个图片的信息 根据我们最终的数据储存结构必须包含 list_url :a 封面url:b1
#因此下面代码是将接收的值复制给item的这两个key :'list_url'和 'fengmian_url' 。至此 items,py里定义的4个Field字段都在一个item里
#一个item是一个图片的信息字典 也对应着数据表中的一行记录
item['list_url']=items['list_url']
item['fengmian_url']=items['fengmian_url']
print item
yield item
3、piplines.py
import MySQLdb
def dbHandle():
conn = MySQLdb.connect(
host = "localhost",
user = "root",
passwd = "密码",
charset = "utf8",
db='local_db',
port=3306)
return conn
class MeizituPipeline(object):
def process_item(self, item, spider):
dbObject = dbHandle()
cursor = dbObject.cursor()
sql = "insert into meizitu values (%s,%s,%s,%s)"
try:
cursor.execute(sql, (item['list_url'], item['fengmian_url'], item['image_url'], item['image_name']))
cursor.connection.commit()
except BaseException as e:
print("存储错误", e, "<<<<<<原因在这里")
dbObject.rollback()
return item
4、settings.py
BOT_NAME = 'meizitu'
SPIDER_MODULES = ['meizitu.spiders']
NEWSPIDER_MODULE = 'meizitu.spiders'
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.54 Safari/536.5'
ROBOTSTXT_OBEY = False
ITEM_PIPELINES = {
'meizitu.pipelines.MeizituPipeline': 300,
}
DOWNLOAD_DELAY =1
5、main.py
#coding:utf-8
from scrapy import cmdline
cmdline.execute("scrapy crawl meizituespider".split())
执行main.py即可输出数据存入数据库
三、对比下单独py脚本实现的逻辑 下篇文章做下对比
忙的很框架和单独py脚本下载图片保存本地的部分,单独py脚本也没写存库
#coding:utf-8
import requests
from lxml import etree
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
'Connection': 'keep-alive'
}
def parse(url):
html=requests.get(url,headers=headers).content
selector=etree.HTML(html)
infos=selector.xpath('//li[@class="wp-item"]//h3[@class="tit"]')
print len(infos)
for info in infos:
fengmian_url=info.xpath('a/@href')[0]
fengmian_names = info.xpath('a/text()')
if len(fengmian_names )>0:
fengmian_name=fengmian_names[0]
else:
fengmian_name=info.xpath('a/b/text()')[0]
print fengmian_url,fengmian_name
#解析出fengmian_url后调用parse_page函数
#类似 框架中yield Request(url=item['fengmian_url'],meta={'item_1': item}, callback=self.parse_page)
parse_page(fengmian_url)
urls = ['http://www.meizitu.com/a/list_1_{}.html'.format(i) for i in range(2, 93)]
for url in urls:
# 类似 框架中 yield Request(url=req_url,callback=self.parse)
parse(url)
def parse_page(url):
html=requests.get(url).content
selector=etree.HTML(html)
infos=selector.xpath('//div[@class="postContent"]//p//img')
print len(infos)
for info in infos:
image_url=info.xpath('@src')[0]
image_name=info.xpath('@alt')[0]
print image_name,image_url
if __name__ == '__main__':
#下面注释的部分和parse函数里的一样 放在parse函数里是为了和scrapy保持一致 对比 理解
# urls=['http://www.meizitu.com/a/list_1_{}.html'.format(i) for i in range(1,91)]
# for url in urls:
# parse(url)
#类似start_urls=['http://www.meizitu.com/a/list_1_1.html']
url='http://www.meizitu.com/a/list_1_1.html'
parse(url)
四、总结
1、细心。
①items.py Field字段在爬虫主程序里item的key值一定完全一致,昨晚调试一个字段多了个空格导致失败,并且不容易发现
②name为爬虫名和项目名称不能重复,start_urls,allowed_domains这两个变量命名不能变,都是列表类型, allowed_domains包含了spider允许爬取的域名(domain)列表(list),这样写allowed_domains = ["meizitu.com"] ,不要写一个具体的url页面如allowed_domains = ['http://www.meizitu.com/a/list_1_1.html']
③字典类型 取值应该是 字典[key] 中括号
2、对网页源码进行解析。
找你最拿手的,xpath,bs4,正则等可以灵活运用,xpath语法在单独脚本和scrapy中不同,框架需要加extract()才可以取出数据,很多人会犯这个错,如果以后用框架推荐xpath
3、学习方法
初学爬虫不建议上来用框架,先写单独的py脚本,封装函数,函数调用,字典
for循环、操作数据库操作比用框架要实用的多,对想要的数据结构或者结果有良好的思维是第一步,不然一切都是徒劳,切忌求快
网友评论