Python+PyCharm+Scrapy+MySQL+SQLAlchemy爬虫项目快速搭建(包括自定义Pipeline和自定义ImagesPipeline)

Scrapy框架

Scrapy是一个用于抓取网站和提取结构化数据的应用程序框架,可用于广泛实用的应用程序,如数据挖掘、信息处理或历史存档。

尽管Scrapy最初是为web数据抓取而设计的,但它也可以使用API来提取数据(如Amazon Associates Web Services)或把它当作通用网络爬虫。

一、安装

  1. 支持的Python版本
    Python 3.6+
  2. 安装/升级命令
  • 安装:
 pip install Scrapy
  • 升级:
pip install -U Scrapy

二、创建爬虫工程

  1. 打开PyCharm创建一个空项目
    输入项目名称
    点击创建
    在这里插入图片描述

  2. 打开PyCharm自带的Terminal
    在这里插入图片描述

  3. 输入命令回到上一层目录

cd ..
  1. 创建Scrapy工程
scrapy startproject <project_name> [project_dir]

例:

scrapy startproject pythonProject pythonProject

project_dir下创建一个project_name工程
如果未指定project_name,它会与project_dir相同
创建完成后按照提示进入工程目录

cd pythonProject
  1. 工程目录下创建爬虫
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>NewsdataPipelineyield过来的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文件。

  1. 一次运行多个爬虫(如果有在服务器运行脚本的需求,一定要把顶上三行代码写在顶部,否则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()
  1. 单独运行某个爬虫:
# 根据项目配置获取 CrawlerProcess 实例
process = CrawlerProcess(get_project_settings())
# 根据name获得爬虫
process.crawl('爬虫1的name')
# 执行
process.start()

五、写在后面

本文是从官方开发文档中抽取的一些常用知识点,仅用于记录,便于本人开发,想了解和使用更多个性化设置请移步:这里

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值