Scrapy爬取知名技术文章网站

scrapy安装以及目录结构介绍

创建有python3的虚拟环境

mkvirtualenv mkvirtualenv py3env

安装scrapy

进入虚拟环境py3env,把pip的源设置为豆瓣源。这个命令执行完毕后,以后使用pip安装Python包时就会从豆瓣源下载,速度会更快

pip config set global.index-url https://pypi.doubanio.com/simple

安装scrapy依赖包lxml、twisted

安装scrapy

pip install -i https://pypi.douban.com/simple/ scrapy

补充

进入虚拟环境: workon py3env

创建项目: scrapy startproject ArticleSpider

建立spider: scrapy genspider jobbole blog.jobbple.com

 PyCharm 调试scrapy 执行流程

scrapy目录结构:

  • scrapy.cfg:配置文件
  • setings.py:基本设置
    SPIDER_MODULES = ['ArticleSpider.spiders'] #存放spider的路径
    NEWSPIDER_MODULE = 'ArticleSpider.spiders' 
  • pipelines.py:做跟数据存储相关的东西
  • middilewares.py:自己定义的middlewares 定义方法,处理响应的IO操作
  • init.py:项目的初始化文件
  • items.py:定义我们所要爬取的信息的相关属性。Item对象是种类似于表单,用来保存获取到的数据

 此时,pycharm下articlespiders项目还没完全配置好:

 ①需要在pycharm的setting中的project interpreter下配置python:使用虚拟环境articlespider下的python3.6

 ②爬虫项目不像Django项目,没有自动配置好调试或运行相关,需要我们手动生成一个main.py文件,作为启动文件:

在PyCharm中运行爬虫文件

在ArticleSpider文件根目录下建立main.py文件

 

要学会用断点和DEBUG

在实战中操作

可能出现的错误

File "C:\Users\用户名\AppData\Local\Programs\Python\Python39\lib\site-packages\cryptography\exceptions.py", line 9, in <module>
from cryptography.hazmat.bindings._rust import exceptions as rust_exceptions
ImportError: DLL load failed while importing _rust: 找不到指定的程序。

解决方法:尝试过网上一系列解决方法,没有解决。所使用版本为Python3.9.0,换个版本问题解决。。。

xpath的用法

xpath简介

  • xpath使用路径表达式在xml和html中进行导航。
  • xpath包含标准函数库。
  • xpath是一个w3c的标准。
  • xpath速度要远远超beautifulsoup

xpath节点关系

  • 父节点
  • 子节点
  • 同胞节点
  • 先辈节点
  • 后代节点

xpath语法

 

表达式

说明

article

选取所有article元素的所有子节点

/article

选取根元素article

article/a

选取所有属于article的子元素的a元素

//div

选取所有div子元素(不论出现在文档任何地方)

article//div

选取所有属于article元素的后代的div元素,不管它出现在article之下的任何位置

//@class

选取所有名为class的属性

/article/div[1]

选取属于article子元素的第一个div元素

/article/div[last()]

选取属于article子元素的最后一个div元素

/article/div[last()-1]

选取属于article子元素的倒数第二个div元素

//div[@lang]

选取所有拥有lang属性的div元素

//div[@lang='eng']

选取所有lang属性为engdiv元素

/div/*

选取属于div元素的所有子节点

//*

选取所有元素

//div[@*]

选取所有带属性的div元素

//div/a | //div/p

选取所有div元素的ap元素

//span | //ul

选取文档中的spanul元素

article/div/p | //span

选取所有属于article元素的div元素的p元素 以及 文档中所有的span元素

 

contains()用法

response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0]

表示在span标签中class属性中含有 bookmark-btn 即为符合

正文保留html标签,以便后续研究

scrapy shell url 调试xpath

如果在py3中就都显示中文了

re.math(reg,html).group() #正则匹配

tag_list=['职场','2 评论','今昔'] [element for element in tag_list if not element.strip().endswith('评论')] #结果['职场', '今昔']

xpath提取元素

css选择器实现字段解析



 

编写spider完成抓取过程

parse.urljoin(url,post_url)的使用

实现模拟登陆(undetected_chromedriver)

上处代码使用undetected_chromedriver时会自动识别Chrome浏览器版本并自动下载对应的Chromedriver,运行时总是下载失败一直未解决,为了后续项目进行,采用selenium模拟登录,登录时会被cnblog反爬识别,采用qq登录即可

from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://account.cnblogs.com/signin")

完成页面爬取

 提取详情页信息

 item的使用

数据爬取的任务就是从非结构的数据中提取出结构性的数据。
items 可以让我们自定义自己的字段(类似于字典,但比字典的功能更齐全)
当我们爬取数据,获  取数据后需要将数据传给下个函数调用,则可以利用scrapy request下的meta属性,meta接收的是字典类型,当使用request爬取数据时,将需要传递给下个函数的数据放到meta中,meta中的数据会传给下个函数的response中去,可以被下个函数使用

关于scrapy request的meta:

Request中meta参数的作用是传递信息给下一个函数,使用过程可以理解成:

把需要传递的信息赋值给这个叫meta的变量,
但meta只接受字典类型的赋值,因此
要把待传递的信息改成“字典”的形式,即:
meta={'key1':value1,'key2':value2}

如果想在下一个函数中取出value1,
只需得到上一个函数的meta['key1']即可,
因为meta是随着Request产生时传递的,
下一个函数得到的Response对象中就会有meta,
即response.meta,

meta是一个dict,主要是用解析函数之间传递值,一种常见的情况:在parse中给item某些字段提取了值,但是另外一些值需要在parse_item中提取,这时候需要将parse中的item传到parse_item方法中处理,显然无法直接给parse_item设置而外参数。 Request对象接受一个meta参数,一个字典对象,同时Response对象有一个meta属性可以取到相应request传过来的meta
request meta

request meta

关于urllib parse.urljoin(url1,url2):

parse.url:能将相对路径,自动补充域名补全路径,前提主域名response能获取域名路径

一般爬取网上数据时,存在网页某些url是相对路径形式展示的,通过js代码等操作补全,此时我们要爬取完整url路径,就需要用到parse.urljoin方法了

如果你是相对路径,没有补全域名,我就从response里取出来补全,如果你有域名我则不起作用

items.py创建类

在jobbole.py parse_detail 中实例化 JobBoleArticleItem , 并将爬取到的数据放入 JobBoleArticleItem 对象中:

setting的相关设置

robot协议:目标网站禁止爬取的连接目录

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

pipelines管道

ITEM_PIPELINES:item的管道,配置好ArticlespiderPipeline路径,当执行代码 yield article_item 时,程序会转到pipelines.py文件中执行  ArticlespiderPipeline 类,
也就是通过 yield article_item 将article_item数据传给pipelines调用。并执行pipelines中的相关类,如: ArticlespiderPipeline  ,在 ArticlespiderPipeline 类中,我们可以通过代码编写,将数据存到数据库中去

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   "ArticleSpider.pipelines.ArticlespiderPipeline": 300,
}
ITEM_PIPELINES

pipelines.py文件代码

class ArticlespiderPipeline:
    def process_item(self, item, spider):
        return item
pipelines.py

 scrapy配置图片下载

配置setting.py

配置pipeline

ITEM_PIPELINES = { "ArticleSpider.pipelines.ArticlespiderPipeline": 300, "scrapy.pipelines.images.ImagesPipeline": 1 }

配置images文件夹的路径:

Scrapy中的pipelines提供了默认的文件、图片、媒体等下载保存方式,如上所述,将路径配置上去就会相应的执行scrapy/pipelines的相关函数 ,

另外,ITEM_PIPELINES是一个数据管道的登记表,每一项后面的具体数字代表它的优先级,数字越小,越早进入管道执行

接下来我们新建一个images文件夹用于保存图片,在setting中配置images文件夹的路径:

import sys
import os
project_dir = os.path.dirname(os.path.abspath(__file__))
IMAGES_STORE = os.path.join(project_dir, "images"):

 指定某字段(我们获取图片路径的字段)为我们的图片文件处理

IMAGES_URLS_FIELD = "front_image_url"   # 指定爬取的某(图片)字段作为图片处理

若报错ModuleNotFoundError:No module named 'PIL',是因为图片保存等需要pillow库来操作,解决方法在虚拟环境中安装pillow:pip install pillow

若报错ValueError:Missing scheme in request url:h

 

原因是'scrapy.pipelines.images.ImagesPipeline' 中对图片的要求是数组格式,而我们目前传递的图片类型(front_image_url)是字符串格式,因此我们需要将爬取的图片类型改成数组类型获取

article_item["front_image_url"] = front_image_url
# ↓改成数组形式
article_item["front_image_url"] = [front_image_url]

 

额外知识:

 scrapy下的pipelines目录下有files.py、images.py、media.py ,用于处理文件、图片、用户上传下载媒体等数据

IMAGES_URLS_FIELD = "front_image_url"  # 指定该字段作为图片处理对象

IMAGES_STORE = os.path.join(project_dir, 'images')  # 图片存放路径

IMAGES_MIN_HEIGHT = 100  # 图片最小高度
IMAGES_MIN_WIDTH = 100   # 图片最小宽度

 使用scrapy自带的image.py处理图片,只需要按要求命名一致,以及在ITEM_PIPELINES 配置好所使用的类即可:ImagesPipeline , 

使用files.py、media.py也是一样的道理。

数据保存相关

保存到本地json文件中

方式一:

在pipelines.py文件中新建类:JsonWithEncodingPipeline

打开文件时不直接使用 open ,用python自带的codecs包,可以避免一些编码方面的问题出现

使用json.dumps时,不使用ensure_ascii=False ,默认输出中文是ASCII字符码,要想正确输出中文,需要指定ensure_ascii=False

pipelines.py/JsonWithEncodingPipelines:

class JsonWithEncodingPipeline(object):
    # 自定义json文件的导出
    def __init__(self):
        self.file = codecs.open('article.json', 'w', 'utf-8')
    def process_item(self, item, spider):
        # 将数据转换为字符串
        lines = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(lines)
        return item
    def spider_closed(self, spider):
        self.file.close()

在setting中配置ITEM_PIPELINES:

ITEM_PIPELINES = {
   'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
}

当爬虫爬取数据,保存到 item 中,经过yield item ,转到pipelines中执行里面的类

方式二:

使用scrapy中自带的类保存文件:

# scrapy/exporters

['BaseItemExporter', 'PprintItemExporter', 'PickleItemExporter','CsvItemExporter', 'XmlItemExporter','JsonLinesItemExporter','JsonItemExporter',   # 保存json文件 'MarshalItemExporter']
from scrapy.exporters import JsonItemExporter

class JsonExporterPipeline(object):
    #调用scrapy提供的json export导出json文件
    def __init__(self):
        self.file = open('articleexport.json', 'wb')
        self.exporter = JsonItemExporter(self.file, encoding="utf-8", ensure_ascii=False)
        self.exporter.start_exporting()

    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item

    def spider_closed(self, spider):
        self.exporter.finish_exporting()
        self.file.close()

在setting中配置ITEM_PIPELINES:

ITEM_PIPELINES = {
  "ArticleSpider.pipelines.JsonExporterPipeline": 3,
}

将数据保存到mysql中

 在Navicat中新建数据库article_spider ,再新建表jobbole_article,填充数据类型

在虚拟环境下安装:mysqlclient

pip install mysqlclient

开始我们的数据保存到mysql操作

方式一: 利用pipelines保存数据到数据库(同步)

import MySQLdb
class MysqlPipeline(object):
    # 数据保存
    def __init__(self):
        """
        初始化构造函数,连接数据库
        """
        self.conn = MySQLdb.connect("127.0.0.1", "root", "123456", "article_spider", charset="utf8", use_unicode=True)
        self.cursor = self.conn.cursor()# 创建游标

    def process_item(self, item, spider):
        """
        处理数据
        """
        insert_sql = """
            insert into jobbole_article(title, url, url_object_id, front_image_url, front_image_path, parise_nums, comment_nums, fav_nums, tags, content, create_date)
            values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
        """
        params = list()
        params.append(item.get("title", ""))
        params.append(item.get("url", ""))
        params.append(item.get("url_object_id", ""))
        # 将list[]转换成字符串
        front_image = ",".join(item.get("front_image_url", []))
        params.append(item.get(front_image))
        params.append(item.get("front_image_path", ""))
        params.append(item.get("parise_nums", 0))
        params.append(item.get("content_nums", 0))
        params.append(item.get("fav_nums", 0))
        params.append(item.get("tags", ""))
        params.append(item.get("content", ""))
        params.append(item.get("create_date", "1970-01-01"))
        self.cursor.execute(insert_sql, (params))
        self.conn.commit()

        return item

在setting中配置ITEM_PIPELINES:

ITEM_PIPELINES = {
  "ArticleSpider.pipelines.MysqlPipeline": 4,
}

方式二:利用pipelines保存数据到数据库(twisted异步)

 因为我们的爬取速度可能大于数据库存储的速度,最好是异步操作(异步化操作mysql)

在setting.py中配置数据库连接的相关配置参数:

MYSQL_HOST = "127.0.0.1" 
MYSQL_DBNAME = "article_spider"
MYSQL_USER = "root"
MYSQL_PASSWORD = "123456"

在pipelines.py中新建类:MysqlTwistedPipline 执行顺序:from_settings → init → pross_item

import MySQLdb
import MySQLdb.cursors
from twisted.enterprise import adbapi
class MySQLTwistedPipeline(object):
    def __init__(self, dbpool):
        self.dbpool = dbpool

    # 重载方法
    @classmethod
    def from_settings(cls, settings):
        dbparms = dict(
            host=settings["MYSQL_HOST"],
            db=settings["MYSQL_DBNAME"],
            user=settings["MYSQL_USER"],
            passwd=settings["MYSQL_PASSWORD"],
            charset="utf8",
            cursorclass=MySQLdb.cursors.DictCursor,
            use_unicode=True,
        )
        # 连接池
        dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
        return cls(dbpool)

    def process_item(self, item, spider):
        # 将某个方法扔到池里运行
        query = self.dbpool.runInteraction(self.do_insert,item)
        # 报错的处理方法
        query.addErrback(self.handle_error, item, spider)

    def handle_error(self, failure, item, spider):
        print(failure)
    def do_insert(self, cursor, item):
        """
        入库逻辑
        """
        insert_sql = """
                    insert into jobbole_article(title, url, url_object_id, front_image_url, front_image_path, parise_nums, comment_nums, fav_nums, tags, content, create_date)
                    values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                """
        params = list()
        params.append(item.get("title", ""))
        params.append(item.get("url", ""))
        params.append(item.get("url_object_id", ""))
        # 将list[]转换成字符串
        front_image = ",".join(item.get("front_image_url", []))
        params.append(item.get(front_image))
        params.append(item.get("front_image_path", ""))
        params.append(item.get("parise_nums", 0))
        params.append(item.get("content_nums", 0))
        params.append(item.get("fav_nums", 0))
        params.append(item.get("tags", ""))
        params.append(item.get("content", ""))
        params.append(item.get("create_date", "1970-01-01"))

        cursor.execute(insert_sql, tuple(params))

在setting中配置ITEM_PIPELINES:

ITEM_PIPELINES = {
  "ArticleSpider.pipelines.MysqlTwistedPipeline": 4,
}

运行后出错AttributeError: module 'MySQLdb' has no attribute 'cursors'

 解决方法:修改from_setting方法

@classmethod
    def from_settings(cls, settings):
        from MySQLdb.cursors import DictCursor
        dbparms = dict(
            host=settings["MYSQL_HOST"],
            db=settings["MYSQL_DBNAME"],
            user=settings["MYSQL_USER"],
            passwd=settings["MYSQL_PASSWORD"],
            charset="utf8",
            cursorclass=DictCursor,
            use_unicode=True,
        )
        # 连接池
        dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
        return cls(dbpool)

 

问题:数据插入时遇到主键冲突问题

 解决方法:插入值的时候更新字段

values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE parise_nums = VALUES(parise_nums)

scrapy提供item loader机制提取信息

# 在parse_detail方法中配置
item_loader = ItemLoader(item=JobBoleArticleItem(), response=response)
            item_loader.add_css("title", "#news_title a::text")
            item_loader.add_css("create_date", "#news_info .time::text")
            item_loader.add_css("content", "#news_content")
            item_loader.add_css("tags", "news_tags a::text")
            item_loader.add_value("url", response.url)
            item_loader.add_value("front_image_url", response.meta.get('front_image_url', ""))
            # 加载item
            article_item = item_loader.load_item()

MapCompose,TakeFirst的使用

 

 

 

 

 

 

 

posted @ 2024-04-21 16:15  JJJhr  阅读(46)  评论(0编辑  收藏  举报