Python 3.7+PyCharm+Scrapy 2.4.1+MySQL 8.0 爬虫项目快速搭建,包括自定义Pipeline和自定义ImagesPipeline
Scrapy框架
Scrapy是一个用于抓取网站和提取结构化数据的应用程序框架,可用于广泛实用的应用程序,如数据挖掘、信息处理或历史存档。
尽管Scrapy最初是为web数据抓取而设计的,但它也可以使用API来提取数据(如Amazon Associates Web Services)或把它当作通用网络爬虫。
一、安装
- 支持的Python版本
Python 3.6+ - 安装/升级命令
- 安装:
pip install Scrapy
- 升级:
pip install -U Scrapy
二、创建爬虫工程
-
打开PyCharm创建一个空项目
输入项目名称
点击创建
-
打开PyCharm自带的Terminal
-
输入命令回到上一层目录
cd ..
- 创建Scrapy工程
scrapy startproject <project_name> [project_dir]
例:
scrapy startproject pythonProject pythonProject
在project_dir
下创建一个project_name
工程
如果未指定project_name
,它会与project_dir
相同
创建完成后按照提示进入工程目录
:
cd pythonProject
- 在
工程目录
下创建爬虫
scrapy genspider [-t template] <name> <domain>
例:
scrapy genspider mySpider target.url.com
支持模板:basic(默认)、crawl、csvfeed、xmlfeed
这里name
是爬虫的名字;domain
是设置allowed_domains
以及start_urls
,这两个属性可以在爬虫类中修改
三、编写爬虫
1. 编写items
根据网页确定好要爬取的字段,然后在items.py
中编写继承了scrapy.Item
的类,可以根据情况自定义多个类(都要继承scrapy.Item
)
以新闻为例,我想爬取新闻的文本内容与图片:
import scrapy
class NewsItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
article_id = scrapy.Field()
media = scrapy.Field()
url = scrapy.Field()
title = scrapy.Field()
tags = scrapy.Field()
content = scrapy.Field()
publish_time = scrapy.Field()
images = scrapy.Field() # 存放下载下来的磁盘路径
images_urls = scrapy.Field() # 存放要访问的远程路径
2. 编写spider
爬虫是scrapy的主体,在这个部分完成页面数据的抓取。爬虫需要注意的是网站的反爬虫策略,并根据需要绕过反爬虫策略。有些网站可能需要使用鼠标进行滑动或点击操作才能加载数据,这样需要借助selenium来模拟人为操作。
这里主要以理想化的、简单的情况作为示例:
import scrapy
import demjson
from newsdata.items import NewsItem
import time
class ChinanewsSpider(scrapy.Spider):
name = 'chinanews'
allowed_domains = ['这里填写允许访问的域名','包括所有请求的域名','如果某域名不填写可能会导致爬虫失效']
# 请求开始,这里主要是控制爬虫循环部分,根据页码进行访问
def start_requests(self):
for page in range(开始, 结束+1):
yield scrapy.Request(url='http://channel.chinanews.com/某某资源?pager={}&pagenum=100&其他参数={}'.format(page,参数)), callback=self.parse)
# 回调函数,对应上面的callback
def parse(self, response):
# 根据响应中对应的数据格式进行抽取,必要时使用demjson进行str2json格式转换
newsList = demjson.decode(response.text)
for news in newsList:
item = NewsItem()
item['article_id'] = news['id']
item['url'] = news['url']
item['title'] = news['title']
item['media'] = news['media']
item['publish_time'] = news['pubtime']
item['images'] = []
img_url = []
yield scrapy.Request(url=item['url'], callback=self.contentParse, cb_kwargs=dict(item=item, img_url=img_url))
# 处理单文档页面,提取内容等数据
def contentParse(self,response,item,img_url):
item['tags'] = response.xpath("xpath常用语法有:.本节点、/单节点、//所有节点、@单属性、contains(@单属性,'value')、string(抽取节点下的所有文本)、normalize-space(把文本的空格和换行去掉,多空格改为单空格)").get()
item['content'] = ''.join([ppath.xpath("normalize-space(string(.))").getall()[0] for ppath in response.xpath("//div[@class='正文节点']//p")])
# 没内容
if item['content'] == '':
return
img_url.extend(response.xpath("//div[@class='图片节点']//img/@src").getall())
# 去除gif图片
img_url = filter(lambda x:'gif' not in x,img_url)
item['images_urls'] = img_url
yield item
3. 编写settings
设置请求头:
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15',
'Accept-Language': 'zh-cn',
'Connection': 'keep-alive',
}
设置pipeline优先级:
ITEM_PIPELINES = {
'newsdata.pipelines.ImgsPipeline': 1, # 先下载图片
'newsdata.pipelines.NewsdataPipeline': 2, # 再存入数据库
}
设置图片的下载路径、允许重定向:
IMAGES_STORE = os.path.join(os.path.dirname(os.path.dirname(__file__)),'images')
MEDIA_ALLOW_REDIRECTS = True
遇到限制爬虫速率的网站要设置好(包括但不限于):
# 并发数
CONCURRENT_REQUESTS = 8 (default:16)
# 请求延迟
DOWNLOAD_DELAY = 0.5 (default:0)
# 代理设置
# CONCURRENT_REQUESTS_PER_DOMAIN = 16
# CONCURRENT_REQUESTS_PER_IP = 16
4. 设计数据库、编写数据库连接操作
在navicat中完成mysql数据库的数据表设计:
在scrapy中编写连接数据库操作,我们使用sqlalchemy作为数据访问层。创建一个Python Package: /工程目录/工程名/model(和spiders目录同级)
,在__init__.py
中初始化:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
HOST = 'localhost'
PORT = '3306'
DATABASE = '数据库名字'
USERNAME = '数据库账号'
PASSWORD = '数据库密码'
CHARSET = 'utf8mb4' # mysql8.0+使用utf8mb4, 5.0+使用utf8
# 创建对象的基类:
Base = declarative_base()
# 初始化数据库连接:
engine = create_engine('mysql+pymysql://{}:{}@{}:{}/{}?charset={}'
.format(USERNAME,PASSWORD,HOST,PORT,DATABASE,CHARSET))
#返回数据库会话
def loadSession():
Session = sessionmaker(bind=engine)
session = Session()
return session
新建一个orm模型:News.py
:
from sqlalchemy import Column,String,JSON,DateTime
from newsdata.model import Base
class News(Base):
__tablename__ = 'news' # 表名
# 字段
article_id = Column(String(20),primary_key=True)
media = Column(String(20))
url = Column(String(100))
title = Column(String(50))
tags = Column(JSON)
content = Column(String(20000))
publish_time = Column(DateTime)
images = Column(JSON)
# 构造函数
def __init__(self,article_id,media,url,title,tags,content,publish_time,images):
self.article_id = article_id
self.media = media
self.url = url
self.title = title
self.tags = tags
self.content = content
self.publish_time = publish_time
self.images = images
5. 编写pipelines
ImgsPipeline
完成图片下载到本地磁盘的工作,NewsdataPipeline
完成数据写入数据库的工作。在settings
中我们设置了优先级:ImgsPipeline
>NewsdataPipeline
,yield
过来的item
首先经过ImgsPipeline
下载好图片,保留好图片路径再传递到NewsdataPipeline
完成入库操作。
在入库的时候使用了session.merge(new_item)
,其作用是先按主键查找session
中对应的记录,如果没有则到数据库query
一条记录old_item
,若找到则加载到session
中,否则insert new_item
到数据库中。
针对找到的old_item
,执行merge
:针对双方都有的属性进行更新
,针对old_item
没有的属性进行新增
,针对old_item
有但new_item
没有的属性进行保留
。
import os
from urllib.parse import urlparse
from scrapy import Request
from itemadapter import ItemAdapter
from scrapy.pipelines.images import ImagesPipeline
from newsdata.model import loadSession
from newsdata.model.News import News
from newsdata.settings import IMAGES_STORE
# 图片pipeline
class ImgsPipeline(ImagesPipeline):
# 在settings中的主目录下,每张图片的具体位置与名字
# IMAGES_STORE = os.path.join(os.path.dirname(os.path.dirname(__file__)),'images')
def file_path(self, request, response=None, info=None, *, item=None):
adapter = ItemAdapter(item)
return '/{}/{}/{}'.format(adapter['publish_time'].split(' ')[0],adapter['article_id'],os.path.basename(urlparse(request.url).path))
# 下载图片
def get_media_requests(self, item, info):
adapter = ItemAdapter(item)
for image_url in adapter['images_urls']:
yield Request(image_url)
# 下载完成
def item_completed(self, results, item, info):
image_paths = ['{}{}'.format(IMAGES_STORE,image['path']) for ok, image in results if ok]
# if not image_paths:
# raise DropItem("Item contains no images")
adapter = ItemAdapter(item)
adapter['images'] = image_paths
return item
# 要写入数据库的数据pipeline
class NewsdataPipeline(object):
# 构造函数
def __init__(self):
self.session = loadSession()
self.count=0
# 处理传递过来的item
def process_item(self, item, spider):
adapter = ItemAdapter(item)
newNews = News(
article_id=adapter['article_id'],
media=adapter['media'],
url=adapter['url'],
title=adapter['title'],
tags=adapter['tags'],
content=adapter['content'],
publish_time=adapter['publish_time'],
images=adapter['images'],
)
try:
# 分batch commit 否则会出现因连接丢失导致数据包无法读取的问题
self.count += 1
if self.count % 10 == 0:
print("========提交到数据库========")
self.session.commit()
# 如果pk不存在则从db中query出来合并,如果db中没有则insert新instance
# query前会flush,如果insert语句堆积会导致连接丢失,必须要分batch commit
self.session.merge(newNews)
return item
except Exception as e:
self.session.rollback()
print("========提交/合并失败,出现异常,回滚。========\n原因:{}".format(e))
# 关闭爬虫
def close_spider(self,spider):
try:
print("========提交到数据库========")
self.session.commit()
except Exception as e:
print("========提交失败,出现异常,回滚。========\n原因:{}".format(e))
self.session.rollback()
finally:
print("========结束会话========")
self.session.close()
四、运行爬虫(IDE中或Terminal中)
在settings.py同级文件夹下,新建main.py文件。
- 一次运行多个爬虫(如果有在
服务器
运行脚本的需求,一定要把顶上三行代码写在顶部,否则get_project_settings
会找不到路径,项目中的爬虫也获取不到):
# 这里一定要写在文件顶部
import sys
import os
sys.path.append(os.path.dirname(sys.path[0])) # 这行代码对命令行运行脚本很重要,不然会找不到爬虫
from scrapy.crawler import CrawlerProcess
from scrapy.spiderloader import SpiderLoader
from scrapy.utils.project import get_project_settings
# 根据项目配置获取 CrawlerProcess 实例
process = CrawlerProcess(get_project_settings())
# 获取 spiderloader 对象,以进一步获取项目下所有爬虫名称
spider_loader = SpiderLoader(get_project_settings())
for spidername in spider_loader.list():
process.crawl(spidername)
# 执行
process.start()
- 单独运行某个爬虫:
# 根据项目配置获取 CrawlerProcess 实例
process = CrawlerProcess(get_project_settings())
# 根据name获得爬虫
process.crawl('爬虫1的name')
# 执行
process.start()
五、写在后面
本文是从官方开发文档中抽取的一些常用知识点,仅用于记录,便于本人开发,想了解和使用更多个性化设置请移步:这里