学习实例
一. 新建项目(scrapy startproject)
-
在开始爬取之前,必须创建一个新的Scrapy项目。进入scrapy所在的项目目录中,运行下列命令:
scrapy startproject mySpider
其中, mySpider 为项目名称 。
-
在当前目录下继续输入命令,表示将在mySpider/spiders目录下创建一个名为itcast的爬虫,并指定爬取域的范围 (即所爬的网站域名):
scrapy genspider itcast "itcast.cn"
二、运行该项目
-
scrapy crawl itcast
其中,itcast,是 下面代码中name属性的值 ,所以在不同的爬虫必须定义不同的名字。
-
可以用 -o 输出指定格式的文件
json格式
scrapy crawl itcast -o teachers.json
csv逗号表达式,可用excel打开
scrapy crawl itcast -o teachers.csv
xml格式
scrapy crawl itcast -o teachers.xml
三、以新片场网站说明(代码有删减)
- 制作爬虫 (spiders/xxspider.py):制作爬虫开始爬取网页
class DiscoverySpider(scrapy.Spider):
name = 'discovery'
# 页面跳转后出现的不同的域名都要写下来,不然无法爬取
allowed_domains = ['xinpianchang.com', 'openapi-vtom.vmovier.com']
start_urls = ['http://www.xinpianchang.com/channel/index/sort-like?from=tabArticle']
page_count = 0
def parse(self, response):
pass
要建立一个Spider, 你必须用scrapy.Spider类创建一个子类,并确定了三个强制的属性 和 一个方法。
name = "" :这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。
allow_domains = [] 是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。
start_urls = () :爬取的URL元祖/列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。
parse(self, response) :解析的方法,每个初始URL完成下载后将被调用,所以一开始只要start_urls有值,该函数会被自动执行。调用的时候传入从每一个URL传回的Response对象来作为唯一参数,主要作用如下:
负责解析返回的网页数据(response.body),提取结构化数据(生成item)
生成需要下一页的URL请求
为什么要带参数response?
该参数是一个对象。
因为通常,Request对象 在 爬虫程序中生成并传递到系统,直到它们到达下载程序,后者执行请求并返回一个 Response对象,该对象 返回到发出请求的爬虫程序。
修改parse函数
import json
import random
import re
import string
import scrapy
from scrapy import Request
from xinpianchang.items import PostItem
def strip(s):
if s:
return s.strip()
return ''
def convert_int(s):
if isinstance(s, str):
return int(s.replace(',', ''))
return 0
cookies = dict(
Authorization='893373EA5DB6D55E55DB6D460A5DB6DB0615DB6D8562238E7CD9')
# 随机生成26个由小写字母和数字组成的序列
# 一般服务器会通过这串序列来判断在一定时间内访问了多少次来判断是否为爬虫
# 为什么?因为服务器内部的某个数据表通过一串26个由小写字母和数字组成的序列来记录存取信息
# 为什么是PHPSESSID而不是其他数据?easy,只要他符合26个由小写字母和数字组成的序列。
def gen_sessionid():
return ''.join(random.choice(string.ascii_lowercase + string.digits, k=26))
def parse(self, response):
self.page_count += 1
# 超过一百多页时需要更换PHPSESSID才能继续爬信息
if self.page_count >= 100:
# 由于PHPSESSID在cookies上,所以更新cookies
cookies.update(PHPSESSID=gen_sessionid())
self.page_count = 0
# 根据xpath找到作品的区域
post_list = response.xpath('//ul[@class="video-list"]/li')
url = "http://www.xinpianchang.com/a%s?from=ArtcleList"
for post in post_list:
# 作品的id
pid = post.xpath('./@data-articleid').get()
# 请求网页并执行parse_post函数
request = response.follow(url % pid, self.parse_post)
# 请求中带过去的信息
request.meta['pid'] = pid
# 由于该图片是懒加载,是动态加载的,所以我们不能通过找到它的位置(是错的)
# 所以用response.text.find('579dhf7sgja.jeog')先搜索到路劲的大概位置 38081
# 再用response.text[38081-100,38081+100]找到附近的html代码 再写出具体位置
request.meta['thumbnail'] = post.xpath('./a/img/@_src').get()
# 每发出一个请求都要yield一下 或者需要打印的时候
yield request
pages = response.xpath('//div[@class="page"]/a/@href').extract()
for page in pages:
yield response.follow(page, self.parse, cookies=cookies)
- 提取信息
get() == extract_first()
返回的是一个string,是extract()结果中第一个值。
getall() == extract()
返回的所有数据,存在一个list里 。
- response.follow与Request的区别:
功能都是请求网页。但response.follow可使用相对路径。
这是第一点不同,Request需要你提供完整的路径(绝对路径)才可以进行请求。
- yield
- 因为使用的yield,而不是return。parse函数将会被当做一个生成器使用。scrapy会逐一获取parse方法中生成的结果,并判断该结果是一个什么样的类型;
- 如果是request则加入爬取队列,如果是item类型则使用pipeline处理,其他类型则返回错误信息 ;
- 在yield后面代码也要注意,因为,它会先执行yield后面代码,执行完后,再到request队列中取request,才会调用回调函数。
注:
yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) (生成斐波那契数列)不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。
也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
def parse_post(self, response):
# meta是随着Request产生时传递的,下一个函数得到的response对象中就会有meta,即response.meta
pid = response.meta['pid']
# 像创建字典一样 实际上是赋值"pid"=“pid"
# PostItem()是一个类,在items.py文件中
post = PostItem(pid=pid)
post['thumbnail'] = response.meta['thumbnail']
post['title'] = response.xpath('//div[@class="title-wrap"]/h3/text()').get()
# yield post
vid, = re.findall('vid: \"(\w+)\"\,', response.text)
video_url = 'https://openapi-vtom.vmovier.com/v3/video/%s?expand=resource&usage=xpc_web'
cates = response.xpath('//span[contains(@class, "cate")]//text()').extract()
post['category'] = ''.join([cate.strip() for cate in cates])
post['created_at'] = response.xpath('//span[contains(@class, "update-time")]/i/text()').get()
post['play_counts'] = convert_int(response.xpath('//i[contains(@class, "play-counts")]/@data-curplaycounts').get())
post['like_counts'] = convert_int(response.xpath('//span[contains(@class, "like-counts")]/@data-counts').get())
post['description'] = strip(response.xpath('//p[contains(@class, "desc")]/text()').get())
request = Request(video_url % vid, callback=self.parse_video)
request.meta['post'] = post
yield request
comment_url = 'https://app.xinpianchang.com/comments?resource_id=%s&type=article&page=1&per_page=24'
request = Request(comment_url % pid, callback=self.parse_comment)
request.meta['pid'] = pid
yield request
creator_list = response.xpath('//div[@class="user-team"]//ul[@class="creator-list"]/li')
composer_url = 'https://www.xinpianchang.com/u%s?from=articleList'
for creator in creator_list:
cid = creator.xpath('./a/@data-userid').get()
request = response.follow(composer_url % cid, self.parse_composer)
request.meta['cid'] = cid
# 因为在爬取该页面时cookies出现了叠加上次传递的cookies,导致cookies越来越多
# request.meta 中的dont_merge_cookies设为TRUE,可以避免与现有cookie合并
request.meta['dont_merge_cookies'] = True
yield request
# 建立中间表,方便数据库建立并查找对应关系
/*cr = CopyrightItem()
cr['pcid'] = '%s_%s' % (pid, cid)
cr['pid'] = pid # 作品编号
cr['cid'] = cid # 作者编号
cr['roles'] = creator.xpath('./div[@class="creator-info"]/span/text()').get()
yield cr*/
def parse_video(self, response):
post = response.meta['post']
# 一般不显示出具体信息的有可能是动态加载的,需要自己去找找js网页,
# 搜索过滤关键词,比如评论,搜索comment。 返回的有可能是json格式内容
# 将html文本转化为json格式
result = json.loads(response.text)
data = result['data']
if 'resource' in data:
post['video'] = data['resource']['default']['url']
else:
d = data['third']['data']
post['video'] = d.get('iframe_url', d.get('swf', ''))
post['preview'] = result['data']['video']['cover']
post['duration'] = result['data']['video']['duration']
post['video_format'] = '高清'
yield post
- **明确目标 (编写items.py):明确你想要抓取的目标 **
- Item 定义结构化数据字段,用来保存爬取到的数据,有点像 Python 中的 dict,但是提供了一些额外的保护减少错误。
- 可以通过创建一个 scrapy.Item 类, 并且定义类型为 scrapy.Field 的类属性来定义一个 Item(可以理解成类似于 ORM 的映射关系)。
import scrapy
from scrapy import Field
class PostItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 方便创建数据表名
table_name = 'posts'
# 创建名为pid的字段,指明了每个字段的元数据(metadata)
pid = Field()
title = Field()
thumbnail = Field()
preview = Field()
video = Field()
video_format = Field()
duration = Field()
category = Field()
created_at = Field()
play_counts = Field()
like_counts = Field()
description = Field()
-
存储内容 (pipelines.py):设计管道存储爬取内容
-
根据需要自定义 Pipeline 类名,然后在 settings.py 文件中进行配置
# 指定用来处理数据的 Pipeline 类,后面的数字代表执行顺序,取值范围是 0-1000 range. # 数值小的 Pipeline 类优先执行 ITEM_PIPELINES = { 'myspider.pipelines.MysqlPipeline': 2, }
-
将item内容存到mysql数据库中
import pymysql class MysqlPipeline(object): # spider 打开时(处理数据前)回调该方法,通常用于在处理数据之前完成某些初始化工作,如连接数据库,与close_spider(self,spider) 结合使用 def open_spider(self, spider): self.conn = pymysql.connect( host='127.0.0.1', port=3306, db='xpc_gp01', user='root', password='123123', charset='utf8mb4', # 支持表情 ) self.cur = self.conn.cursor() # spider 关闭时(处理数据后)回调该方法,通常用于在卡碍事处理数据之后完成某些清理工作,如关闭数据库 def close_spider(self, spider): self.cur.close() self.conn.close() # 在主程序中把需要加到数据库的Item yield出去,yield出去之后, # 这个Item就会在Spider中被收集,之后进入到pipelines里面去。 # 所以这个函数必须有item和spider这个参数 def process_item(self, item, spider): # 单个 * 如:*parameter是用来接受任意多个参数并将其放在一个元组中。 # 两个 ** 如:**parameter用于接收类似于关键参数一样赋值的形式的多个实参放入字典中 # zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。 keys, values = zip(*item.items()) # 相当于 # keys = item.keys() # values = [item[k] for k in keys] # str.format() 方法通过字符串中的花括号 {} 来识别替换字段 replacement field,从而完成字符串的格式化。 # ON DUPLICATE KEY UPDATE 表示当insert已经存在的记录时,执行Update sql = "insert into {} ({}) values ({}) ON DUPLICATE KEY UPDATE {}".format ( item.table_name, ','.join(keys), ','.join(['%s']*len(values)), ','.join(['`{}`=%s'.format(k) for k in keys]) ) self.cur.execute(sql, values * 2) self.conn.commit() return item return item
-