一, 环境
- Windows 8.1
- python3.6.4
- scrapy1.5.1
明白 Python 的基本语法系列
二, 知识点
xpath
文字内容爬取并存本地文件
翻页爬取
图片爬取并存本地
简单的反爬虫
数据存数据库(MySQL)
日志
网站地址: https://movie.douban.com/top250
三, 项目构建及文件说明
1, 项目创建
scrapy startproject Douban
2, 项目初始化
- cd Douban
- scrapy genspider douban "douban.com"
3, 各文件说明
四, xpath 解析说明
以 Chrome 插件 (XPath Helper) 为例.
//ol[@class='grid_view']/li/div[@class='item']
即可解析出所有的电影信息模块, 然后循环遍历进行处理即可.
PS:"//" 即代表从任意路径下开始寻找
五, 字段设置
即 item.py 文件.
- # 电影名字
- film_name = scrapy.Field()
- # 导演和主演名字
- director_performer_name = scrapy.Field()
- # 主演名字
- # performer_name = scrapy.Field()
- # 电影上映年份
- film_year = scrapy.Field()
- # 电影国家
- film_country = scrapy.Field()
- # 电影类型
- film_type = scrapy.Field()
- # 电影评分
- film_rating = scrapy.Field()
- # 电影评论人数
- film_reviews_num = scrapy.Field()
- # 电影经典语句
- film_quato = scrapy.Field()
- # 电影图片
- film_img_url = scrapy.Field()
六, 爬虫编写
即 douban.py 文件.
以下代码, 注释很详细, 细节暂不赘述, 简单提一下 yield 的用法:
yield 是个很重要的语法, 有着 return 的部分功能, 但完全不同于 return.
return 会返回信息并且终止当前的方法, 而 yield 虽然也会返回一个信息给调用者, 但是调用者使用完了之后程序还会回到此处继续执行.
比如用在此爬虫的 for 循环中的妙处是: 此处生成 item 之后返回给调度器进行相关的处理, 然后程序再回到这里继续运行, 即继续下一个循环, 然后再生成一个新的 item 提供给调度器, 如此往复, 直到循环结束.
- # 爬虫名称(必须唯一)
- name = 'douban'
- # 非此域名下的链接均不进行爬取
- allowed_domains = ['douban.com']
- base_url = 'https://movie.douban.com/top250'
- off_set = '?start=0&filter='
- # 起始的爬取地址
- start_urls = [base_url + off_set]
- # 每次的爬取都会默认走这个 parse 方法
- def parse(self, response):
- # xpath 解析出每个电影的信息模块
- films = response.xpath("//ol[@class='grid_view']/li/div[@class='item']")
- # 遍历每个电影模块
- for film in films:
- # 创建电影信息存储的 item 对象
- item = DoubanItem()
- # 标题
- titles = film.xpath("./div[@class='info']/div[@class='hd']/a/span/text()").extract()
- film_name = ''
- # 拼接电影名
- for title in titles:
- film_name += title.strip()
- # 电影信息
- infos = film.xpath("./div[@class='info']/div[@class='bd']/p/text()").extract()
- director_performer_name = ""
- for temp in infos[0]:
- director_performer_name += temp.strip()
- year_country = infos[1]
- film_year = year_country.split("/")[0].strip()
- film_country = year_country.split("/")[1].strip()
- film_type = year_country.split("/")[2].strip()
- # 电影评分
- film_rating = film.xpath("./div[@class='info']/div[@class='bd']/div[@class='star']/span[@class='rating_num']/text()").extract()[0].strip()
- # 电影参与评论人数
- film_reviews_num = film.xpath("./div[@class='info']/div[@class='bd']/div[@class='star']/span[last()]/text()").extract()[0].strip()[:-3]
- # 电影经典语句
- film_quato = "" film_quato_temp = film.xpath("./div[@class='info']/div[@class='bd']/p[@class='quote']/span/text()").extract()
- if film_quato_temp:
- film_quato = film_quato_temp[0].strip()
- # 电影图片链接
- film_img_url = film.xpath("./div[@class='pic']/a/img/@src").extract()[0].strip()
- # item 字段赋值
- item['film_name'] = film_name
- item['director_performer_name'] = director_performer_name
- # item['director_name'] = director_name
- # item['performer_name'] = performer_name
- item['film_year'] = film_year
- item['film_country'] = film_country
- item['film_type'] = film_type
- item['film_rating'] = film_rating
- item['film_reviews_num'] = film_reviews_num
- item['film_quato'] = film_quato
- item['film_img_url'] = film_img_url
- # 返回 item 进行解析, 解析完了之后再回到这里继续运行
- yield item
- # 翻页
- # 解析出下一页
- next_url = response.xpath("//div[@class='paginator']/span[@class='next']/a/@href").extract()
- if next_url:
- # 如果下一页存在的话再进行请求, 并传递回调函数 parse()
- yield scrapy.Request(self.base_url + next_url[0], self.parse)
七,"管道" 说明
即 pipelines.py 文件.
说明: 正如其名 "管道", 它是用来处理 item 的, 所以, 我们可以写多个 "管道" 文件来处理 item, 但是要注意:
1 每个 "管道" 处理完之后记得 return item, 否则后续管道无法再进行处理, 毕竟拿不到了嘛;
2 管道是有执行顺序的, 所以需要我们进行定义其顺序(settings.py 文件), 数字小, 先执行:
- # Configure item pipelines
- # See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
- ITEM_PIPELINES = {
- 'Douban.pipelines.DoubanMoviePipeline': 300,
- 'Douban.pipelines.DoubanImgPipeline': 301,
- 'Douban.pipelines.DoubanDBPipeline': 400
- }
1, 文本内容存本地文件
即配置里面的:'Douban.pipelines.DoubanMoviePipeline': 300,
也是比较简单, 看下文代码的注释即可, 但是要注意编码.
- class DoubanMoviePipeline(object):
- """
- 处理电影信息
- """
- def __init__(self):
- # 初始化: 文件打开
- self.f = codecs.open("doubanData.json", mode="w", encoding="utf-8")
- def process_item(self, item, spider):
- # 内容, 结尾增加了换行
- content = JSON.dumps(dict(item), ensure_ascii=False) + ",\n"
- # 内容写入文件
- self.f.write(content)
- # 一定要记得 return, 否则之后的 pipeline 拿不到 item, 也就没法继续处理了
- return item
- def close_spider(self, spider):
- # 爬虫关闭时进行: 文件关闭
- self.f.close()
2, 图片内容保存本地
我们写在同一个 "管道" 文件里面.
注意继承类: ImagesPipeline, 源码见: D:\IT\Python\Python36\Lib\site-packages\scrapy\pipelines\images.py
注意在 settings.py 中设置图片的下载路径: IMAGES_STORE = "D:\IT\Python\workspace\SpiderDemo\Douban\images\"
代码同样比较简单, 见下面的注释即可, 注意, 此处进行了文件重命名操作, 并有打异常日志, 日志后面会讲到.
- class DoubanImgPipeline(ImagesPipeline):
- """
- 处理图片信息
- """
- def get_media_requests(self, item, info):
- """
- 图片下载
- """ film_img_url = item['film_img_url']
- yield scrapy.Request(film_img_url)
- def item_completed(self, results, item, info):
- """
- 图片重命名
- """
- # 获取文件下载的路径
- path = [x['path'] for ok, x in results if ok]
- # 原始的完整路径
- film_img_disk_url1 = settings.IMAGES_STORE + path[0]
- # 准备存放的新的完整路径
- film_img_disk_url = settings.IMAGES_STORE + 'full\\' + item['film_name'].split("/")[0].strip() + ".jpg"
- try:
- # 重命名
- os.rename(film_img_disk_url1, film_img_disk_url)
- except Exception as error:
- Logger(logLevel='error').getLogger().error("图片重命名失败, 异常信息:%s" % error)
- pass
- return item
3, 数据存数据库
还是写在同一个 "管道" 文件里面.
"管道" 配置为:'Douban.pipelines.DoubanDBPipeline': 400
settings.py 中配置数据库信息:
- # MySQL 设置
- MYSQL_HOST = 'localhost'
- MYSQL_PORT = 3380
- MYSQL_DBNAME = 'scrapy'
- MYSQL_USER = 'root'
- MYSQL_PASSWD = 'root'
代码理解也不困难, 见注释即可, 此处进行了简单的查重处理.
- class DoubanDBPipeline(object):
- """
- 数据存入 mysql
- """
- def __init__(self):
- # 连接数据库
- self.connect = pymysql.connect(
- host=settings.MYSQL_HOST,
- port=settings.MYSQL_PORT,
- db=settings.MYSQL_DBNAME,
- user=settings.MYSQL_USER,
- passwd=settings.MYSQL_PASSWD,
- charset='utf8mb4',
- use_unicode=True
- )
- self.cursor = self.connect.cursor()
- def process_item(self, item, spider):
- try:
- # 数据库查重
- self.cursor.execute(
- """select film_name from douban_movie_top_250 where film_name = %s and film_img_url = %s""",
- (item['film_name'], item['film_img_url'])
- )
- # 查重
- repetition = self.cursor.fetchone()
- if repetition:
- # 数据重复
- Logger().getLogger().info("数据重复, film_name: %s,film_img_url:%s" % (item['film_name'], item['film_img_url']))
- pass
- else:
- # 插数据
- self.cursor.execute(
- """insert into douban_movie_top_250(film_name, director_performer_name, film_year, film_country, film_type, film_rating, film_reviews_num, film_quato, film_img_url)
- VALUE (%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
- (
- item['film_name'],
- item['director_performer_name'],
- item['film_year'],
- item['film_country'],
- item['film_type'],
- item['film_rating'],
- item['film_reviews_num'],
- item['film_quato'],
- item['film_img_url']
- )
- )
- # sql 提交
- self.connect.commit()
- except Exception as error:
- Logger(logLevel='error').getLogger().error("数据插入数据库失败", error)
- return item
- def close_spider(self, spider):
- # 关闭数据库连接
- self.connect.close()
八, 简单的反爬虫
1, 添加用户代理
即添加 USER_AGENT, 用于伪装浏览器
在 settings.py 中进行配置:
- # Crawl responsibly by identifying yourself (and your website) on the user-agent
- USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
2, 不遵守 robots 协议
在 settings.py 中进行配置:
- # Obey robots.txt rules
- ROBOTSTXT_OBEY = False
3, 请求间隙
即防止请求过于频繁.
在 settings.py 中进行配置, 单位是秒:
DOWNLOAD_DELAY = 0.25
4, 设置 cookies
此处 Douban 的爬虫我们没有进行设置, GitHub 中拉勾网的爬虫中有最简单设置.
即在爬虫代码中设置 cookie, 然后在每个 Request 请求中直接添加.
yield scrapy.Request(job_url, cookies=self.cookie, meta={'item': item}, callback=self.parse_url)
或者完美一点的做法, 应该是在中间件中设置, 即在 middlewares.py 文件中进行配置.
九, 日志
日后写一篇详细的介绍, 此处暂不进行细说.
自定义的简单的日志模块为项目中的 logger.py 文件, 使用方法见注释.
十, 运行
1, 查找可运行的 scrapy 项目
scrapy list
2, 运行爬虫
scrapy crawl douban
3, 运行爬虫并将 item 信息输出至文件
scrapy crawl douban -o doubanData.JSON
4, 新建执行文件
新建 run.py 执行文件
文件内容为:
- from scrapy.cmdline import execute
- execute(['scrapy', 'crawl', 'douban'])
以后直接执行这个 python 文件即可.
十一, 其他
来源: http://www.jianshu.com/p/87cc8160c29b