Scrapy入门爬取博客园新闻

一 环境搭建

  1. 创建虚拟环境
conda create -n ArticleSpider python=3.7
conda env list # 查看存在哪些虚拟环境
activate ArticleSpider  # 激活进入虚拟环境
  1. scrapy安装
pip install Scrapy # 安装Scrapy
scrapy startproject ArticleSpider # 创建scrapy项目 项目位置是C:\Users\userame\ArticleSpider
cd ArticleSpider
scrapy genspider cnblogs news.cnblogs.com # 要抓取的网址 二级域名

然后把C盘的项目复制粘贴到想要的地方,我这里放在D:\Code

  1. pycharm配置使用虚拟环境

打开D:\Code里创建好的ArticleSpider项目

配置使用虚拟环境

image-20220118112727996

  1. pycharm 中调试scrapy代码

新建main.py

from scrapy.cmdline import execute
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__))) // 找到当前文件的绝对路径
execute(["scrapy", "crawl", "cnblogs"])

200状态码表示访问成功

image-20220118143242257

二 分析与设计

2.1 需求分析

​ 抓取博客园新闻栏目的信息

2.2 表设计

三 选择器

选择器的作用是定位所需要的信息,然后提取。

xpath选择器

简介:xpath使用路径表达式在xm和htm中进行导航

语法

image-20220118144343459

spider中使用xpath示例

博客园-新闻文章

image-20220118145553278

spider中提取信息 /n/711240/

url = response.xpath('//*[@id="entry_711240"]/div[2]/h2/a/@href').extract_first(""); // 提取id=entry_711240 下第二个div中h2中a标签的href属性;extract_first("")表示取第一个,不存在则赋值""空。

显然,使用这种xpath方法写法是固定死的,需要修改。

​ 观察发现,不同文章的url是不同的,因此第一步是提取出各个文章的 /n/xxxxxx/

image-20220118150751226

​ 使用开发者模式观察/n/xxxxx/的位置

image-20220118151230814

​ 编写代码提取出该页面的所有 /n/xxx/ 信息列表

    def parse(self, response):
        url = response.xpath('//div[@id="news_list"]//h2[@class="news_entry"]/a/@href').extract()

​ 效果图

image-20220118151445339

css选择器

基本语法

image-20220118153925861

spider中使用

urls = response.css('#news_list h2 a::attr(href)').extract() // # 代表id, ::取属性

当然也可以使用如下代码,效果是相同的

from scrapy import Selector
sel = Selector(text = response.text)
urls = sel.css('#news_list h2 a::attr(href)').extract()

四 编码

parse方法需要实现的功能

  • 提取url,并交给scrapy下载,完成相应的解析
  • 获取下一页的url,交给scrapy下载,并使用parse继续更进

4.1 封面图和文章url

观察—开发者模式查看

image-20220118160030494

浏览器中关于属性的问题

image-20220118162439979

存在的问题

  • 可以发现,href=‘/n/711240/’,但实际上,浏览器会自动加上它的域名。同样,src=‘//images.,’,浏览器会自动加上http。

  • 但是有时候会出现 href=‘http:/baidu.com’ 这种完整链接的情况,因此我们需要处理这几种情况。

python中parse模块为我们提供了处理这种情况的简便方法。

url = parse.urljoin(response.url, post_url) # 如果post_url不是完整域名,则会从response中提取域名,然后进行组合

编写代码—图片和文章url

图片url分析

image-20220119221151561

有一些图片前面没有https:,因此需要进行处理

if image_url.startswith("//"):
	image_url = "https:" + image_url
post_nodes = response.css('#news_list .news_block')
        for post_node in post_nodes:
            image_url = post_node.css('.entry_summary a img::attr(src)').extract_first("")
            post_url = post_node.css('.news_entry a::attr(href)').extract_first("")
            yield Request(url=parse.urljoin(response.url, post_url), meta={"front_image_url": image_url},
                          callback=self.parse_detail)

编写代码—获取下一页url

网页中点击next就可以跳到下一页,开发者模式查看一下next按钮的位置。

image-20220118224819087

实现代码

next_url = response.xpath("//a[contains(text(), 'Next >')]/@href").extract_first("")
yield Request(url=parse.urljoin(response.url,next_url), callback=self.parse)

4.2 详情页信息

调试技巧

​ 如果我们每提取一个信息都使用pycharm运行代码debug一下,这会造成了过多了请求,因此我们使用scrapy内附的功能进行代码调试。

进入该项目的虚拟环境以后

activate ArticleSpider # 进入虚拟环境
scrapy shell # 进入shell命令
scrapy shell https://news.cnblogs.com/n/711220/

也有可能需要登录,无法抓取到信息,此时,我们需要使用cookie和headers,因为这里是测试提取信息,所以不做模拟登录,而是直接使用cookie,在浏览器中获取到cookie,然后使用下面代码把cookie转换为字典类型。

import re

strs = '_g...'
strList = re.split(r';\S*', strs)
cookie = {}
for items in strList:
    item = items.split('=')
    key, value = item[0], item[1]
    cookie[str(key)] = str(value)  # 字典类型
print(cookie)

继续在虚拟环境中执行下面命令,把获取到的cookie代替下面cookies中的值

url_ = 'https://news.cnblogs.com/n/711220/' # 指定请求目标的 URL 链接
headers_ = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36', 'referer': 'https://news.cnblogs.com/'} # 自定义 Header
cookies_ = {642520268.522.415.93881', ' __utma': '66375729.537120372.1640693759.16472'}
 # 构造需要附带的 Cookies 字典
req = scrapy.Request(url_, cookies=cookies_, headers=headers_) # 构造 Request 请求对象
fetch(req) # 发起 Request 请求
view(response) # 在系统默认浏览器查看请求的页面(主要为了检查是否正常爬取到内页)

信息的提取

文章的标题 css(’#news_title a::text’)

image-20220118234208704

response.css("#news_title a::text").extract_first("")

发布时间 css(’#news_info .time::text’)

image-20220119005428984

response.css(’#news_info .time::text’).extract_first("")

文章内容 css("#news_content")

image-20220119005646316

response.css(("#news_content").extract()[0]

**文章标签 **文章标签可能是列表,因此我们不使用extract_first("")

image-20220119010643225

tag_list = response.css("#news_more_info .news_tags a::text").extract()

mysql中没有存储list的,因此我们把它转换为字符串 tag = “,”.join(tag_list)

其他数据

分析——可以发现,response中是没有评论数和点赞数的

image-20220119003554868

原因:在请求该页面时候通过js发出了另外带这些info的请求

解决:我们需要在它发出的请求中找到这些信息。

image-20220119003848380

这里我们使用 requests 库中的get方法进行请求测试

虚拟环境中安装requests库

pip install requests

测试代码

scrapy shell
import requests
response = requests.get('https://news.cnblogs.com/NewsAjax/GetAjaxNewsInfo?contentId=711228')
response.text

效果图

image-20220119011921180

把它变成json

import json
json.loads(response.text)

代码编写

​ 抓取文章信息需要登录,此阶段我们先使用cookie进行登录

登录部分

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    allowed_domains = ['news.cnblogs.com']
    start_urls = ['http://news.cnblogs.com/']
    cookie = {'_gid': 'GA1'}# 这里的cookie转换成字典了
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
        'referer': 'https: // news.cnblogs.com / n / 711220 /'
    }
    def start_requests(self):
        # 这里的url要求是字符串,因此需要转换一下
        yield Request(url=''.join(self.start_urls), cookies=self.cookie, headers=self.headers, callback=self.parse)

文章基本信息及下一页信息提取

    def parse(self, response):
        # url = response.xpath('//div[@id="news_list"]//h2[@class="news_entry"]/a/@href').extract()
        # urls = response.css('#news_list h2 a::attr(href)').extract()
        # sel = Selector(text=response.text)
        post_nodes = response.css('#news_list .news_block')[:1]
        for post_node in post_nodes:
            image_url = post_node.css('.entry_summary a img::attr(src)').extract_first("")
            if image_url.startswith("//"):  #  部分images是 //images/...png 需统一为https://imagegs/..png
                image_url = "https:" + image_url
            post_url = post_node.css('.news_entry a::attr(href)').extract_first("")
            yield Request(url=parse.urljoin(response.url, post_url), meta={"front_image_url": image_url},
                          cookies=self.cookie, headers=self.headers, callback=self.parse_detail)
        #
        # next_url = post_nodes.xpath("//a[contains(text(), 'Next >')]/@href").extract_first("")
        # yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)

​ 每篇文章的评论数等数据的请求url是/NewsAjax/GetAjaxNewsInfo?contentId=711228。因此我们需要提取出文章的id,这里使用正则表达式提取。match_re = re.match(".*?(\d+.*)", response.url)

文章相信信息提取

    def parse_detail(self, response):
        match_re = re.match(".*?(\d+.*)", response.url)
        if match_re:
            # 标题
            title = response.css("#news_title a::text").extract_first("")
            # 发布时间
            create_time = response.css('#news_info .time::text').extract_first("")
            # 文章内容
            content = response.css("#news_content").extract()[0]
            # 标签列表
            tag_list = tag_list = response.css("#news_more_info .news_tags a::text").extract()
            tags = ",".join(tag_list)  # 列表抓获为字符串
            post_id = match_re.group(1).replace("/", "")  # 去除 \
            # print(parse.urljoin(response.url, "/NewsAjax/GetAjaxNewsInfo?contentId={}".format(post_id)))
            yield Request(url=parse.urljoin(response.url, "/NewsAjax/GetAjaxNewsInfo?contentId={}".format(post_id)),
                          callback=self.parse_nums)

​ 又因为requests是同步的,因此我们改为使用Scrapy框架的Requset 来异步处理评论数点赞数等信息

评论数点赞数的处理

    def parse_nums(self, response):
        j_data = json.loads(response.text)
        praise_nums = j_data["DiggCount"]  # 收藏数
        fav_nums = j_data["TotalView"]  # 阅读数
        comment_nums = j_data["CommentCount"]  # 评论数

4.3 使用item优化

问:为什么需要item?

答:Item是保存结构数据的地方,tem提供了类字典的API,并且可以很方便的声明字段。简而言之就是可以方便存储数据。

定义item类

class CnblogsArticleItem(scrapy.Item):
    title = scrapy.Field()
    create_date = scrapy.Field()
    url = scrapy.Field()
    url_object_id = scrapy.Field()
    front_image_url = scrapy.Field()
    # front_image_path = scrapy.Field()
    praise_nums = scrapy.Field()
    comment_nums = scrapy.Field()
    fav_nums = scrapy.Field()
    tags = scrapy.Field()
    content = scrapy.Field()

代码修改为使用item

上一步yield的front_image_url记得要拿出来,同时把item放到下一个yield的meta中

    def parse_detail(self, response):
        match_re = re.match(".*?(\d+.*)", response.url)
        if match_re:
            post_id = match_re.group(1).replace("/", "")  # 去除 \
            cnblogsArticleItem = CnblogsArticleItem()
            # 标题
            title = response.css("#news_title a::text").extract_first("")
            # 发布时间
            create_time = response.css('#news_info .time::text').extract_first("")
            match_re = re.match(".*?(\d+.*)", create_time)  # 发布于 2022-01-01
            if match_re:
                create_date = match_re.group(1)  # 2022-01-01

            # 文章内容
            content = response.css("#news_content").extract()[0]
            # 标签列表
            tag_list = tag_list = response.css("#news_more_info .news_tags a::text").extract()
            tags = ",".join(tag_list)  # 列表抓获为字符串
            # print(parse.urljoin(response.url, "/NewsAjax/GetAjaxNewsInfo?contentId={}".format(post_id)))
            cnblogsArticleItem["title"] = title
            cnblogsArticleItem["create_date"] = create_date
            cnblogsArticleItem["content"] = content
            cnblogsArticleItem["tags"] = tags
            cnblogsArticleItem["url"] = response.url
            cnblogsArticleItem["front_image_url"] = response.meta.get("front_image_url", "")  # 使用get可避免为空导致出错的情况
            yield Request(url=parse.urljoin(response.url, "/NewsAjax/GetAjaxNewsInfo?contentId={}".format(post_id)),
                          meta={"cnblogsArticleItem": cnblogsArticleItem}, callback=self.parse_nums)

为了生成文章的uuid,我们使用md5,放在utils.common下面

def get_md5(url):
    if isinstance(url, str):
        url = url.encode("utf-8")
    m = hashlib.md5()
    m.update(url)
    return m.hexdigest()

把item yield出去,pipeline中会自动获取到item

    def parse_nums(self, response):
        j_data = json.loads(response.text)
        praise_nums = j_data["DiggCount"]  # 收藏数
        fav_nums = j_data["TotalView"]  # 阅读数
        comment_nums = j_data["CommentCount"]  # 评论数
        cnblogsArticleItem = response.meta.get("cnblogsArticleItem")
        cnblogsArticleItem["praise_nums"] = praise_nums
        cnblogsArticleItem["comment_nums"] = comment_nums
        cnblogsArticleItem["fav_nums"] = fav_nums
        cnblogsArticleItem["url_object_id"] = common.get_md5(cnblogsArticleItem['url'])
        yield  cnblogsArticleItem

setting中 设置是否遵循爬取协议

ROBOTSTXT_OBEY = False # 不遵循

setting中 设置item_pipelines 优先速度,取消注释

ITEM_PIPELINES = {
   'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
}  # 数值越小,优先级越高

4.4 图片下载

scrapy中的配置

​ 配置pipeline可下载图片,参考官方文档:启用并设置目录—定义两个字段变量—设置images_urls_field

image-20220119214127154

启用

setting.py中配置如下

project_dir = os.path.dirname(os.path.abspath(__file__))
IMAGES_STORE = os.path.join(project_dir, 'images')

设置目录为项目的images文件夹

image-20220119212634550

item定义两个字段

    front_image_url = scrapy.Field()
    front_image_path = scrapy.Field()

setting.py中设置images_urls_field

IMAGES_URLS_FIELD = 'front_image_url'

​ 完成上述步骤后,在yield item后scrapy就会自动把front_image_url的图片下载到指定的目录(项目的images目录)

其他问题

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

解决:scrapy中要求图片的url的列表,因此需要 [images_url]

 if response.meta.get("front_image_url", ""):
    cnblogs_article_item["front_image_url"] = [response.meta.get("front_image_url", "")]  # 使用get可避免为空导致出错的情况
else:
    cnblogs_article_item["front_image_url"] = []

​ 另外,scrapy下载图片还需要pillow库,没有该库时候不能下载图片且没有保持提示

pip install pillow

图片信息的获取

​ 下载完成图片后,因为我们后期需要写入数据库,因此需要获取它的路径和文件名字。

自定义Pipeline重载item_completed方法

class ArticleImagePipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        if "front_image_url" in item:
            for ok, value in results:
                image_file_path = value["path"]
            item["front_image_path"] = image_file_path
        return item

setting.py中设置使用该Pipeline

ITEM_PIPELINES = {
    'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
    'ArticleSpider.pipelines.ArticleImagePipeline': 1,
}

4.5 数据写入json

https://docs.scrapy.org/en/latest/topics/exporters.html

定义Pipeine—启用

自定义版

import codecs
import json
class JsonWithEncodingPipeline(object):
    #自定义json文件的导出
    def __init__(self):
        self.file = codecs.open('article.json', 'w', encoding="utf-8")
    def process_item(self, item, spider):
        lines = json.dumps(dict(item), ensure_ascii=False, default=json_serial) + "\n"
        self.file.write(lines)
        return item
    def spider_closed(self, spider):
        self.file.close()

setting.py中启用设置

ITEM_PIPELINES = {
    'ArticleSpider.pipelines.ArticleImagePipeline': 1,
    'ArticleSpider.pipelines.JsonWithEncodingPipeline': 2,
    'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
}

效果图

image-20220119234734038

标准版

​ 使用官方文档的JsonItemExporter

from scrapy.exporters import JsonItemExporter
class JsonExporterPipleline(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 close_spider(self, spider):
        self.exporter.finish_exporting()
        self.file.close()

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

​ setting.py中启用该Pipeline

'ArticleSpider.pipelines.JsonExporterPipleline': 3,

4.6 数据写入mysql

表结构定义

CREATE TABLE `cnblog_article` (
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`url_object_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`front_image_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`front_image_ur` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`praise_nums` int(255) NOT NULL,
`comment_nums` int(255) NULL DEFAULT NULL,
`fav_nums` int(11) NULL DEFAULT NULL,
`tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`create_date` datetime NULL DEFAULT NULL,
PRIMARY KEY (`url_object_id`) 
)

ENGINE = InnoDB
AUTO_INCREMENT = 0
AVG_ROW_LENGTH = 0
DEFAULT CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci
KEY_BLOCK_SIZE = 0
MAX_ROWS = 0
MIN_ROWS = 0
ROW_FORMAT = Dynamic;

安装库

pip install mysqlclient

定义Pipeline

class MysqlPipeline(object):
    # 采用同步的机制写入mysql
    def __init__(self):
        self.conn = MySQLdb.connect('127.0.0.1', 'root', 'ya2269276', 'article_spider', charset="utf8",
                                    use_unicode=True)
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        insert_sql = """
            insert into cnblog_article(url_object_id,title, url, create_date, fav_nums, praise_nums)
            VALUES (%s, %s, %s, %s, %s, %s)
        """
        # self.cursor.execute(insert_sql, (item["title"], item["url"], item["create_date"], item["fav_nums"]))
        self.cursor.execute(insert_sql, (
            item.get("url_object_id", ""), item.get("title", ""), item["url"], item["create_date"], item["fav_nums"],
            item["praise_nums"]))
        self.conn.commit()
        return item

setting.py ‘ArticleSpider.pipelines.MysqlPipeline’: 4,

异步插入

​ 上面使用MySQLdb直接插入是同步操作,这会降低爬虫的速度,因此我们借助Twisted的api完成异步操作

from twisted.enterprise import adbapi
import MySQLdb.cursors
class MysqlTwistedPipline(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):
        #使用twisted将mysql插入变成异步执行
        query = self.dbpool.runInteraction(self.do_insert, item)
        query.addErrback(self.handle_error, item, spider) #处理异常
        return item

    def handle_error(self, failure, item, spider):
        #处理异步插入的异常
        print (failure)

    def do_insert(self, cursor, item):
        #执行具体的插入
        #根据不同的item 构建不同的sql语句并插入到mysql中
        # insert_sql, params = item.get_insert_sql() 用此方法需要在item里面添加get_insert_sql()方法
        insert_sql = """ insert into cnblog_article(url, tags) VALUES (%s, %s) """
        params.append(item.get("url",""))
        params.append(item.get("tags",""))
        cursor.execute(insert_sql, tuple(params)

setting.py中配置数据库信息

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

解决主键冲突

​ ON DUPLICATE KEY UPDATE :当主键存在时,我们选择更新某些字段

例如 insert into table (key, word) VALUES (%s, %s) ON DUPLICATE KEY UPDATE word=VALUES(word)

4.7 itemloader的使用

​ 在真实开发中,我们可能有几十上百个信息要提取,这样就有很多选择器,以及数据入库时候很多字段要管理,比较混乱,因此使用itemloader统一进行管理。

信息提取部分改成使用itemloader

from scrapy.loader import ItemLoader
def parse_detail(self, response):
    cnblogs_article_item = CnblogsArticleItem()
	item_loader = ItemLoader(item=cnblogs_article_item, 	response=response)

基本使用

add_css(“field”, cssSelector)

title = response.css("#news_titlea::text").extract_first("")
cnblogs_article_item["title"] = title
# 等价于
item_loader.add_css("title", "#news_titlea::text")

add_value

item_loader.add_value("url", response.url)
# 等价于
cnblogs_article_item["url"] = response.url

疑问:extract_first("")在loader中并没有体现啊,这样会导致后面的信息处理,插入数据库都出错吧?还有有时候用的正则表达式等等。

解答:是的,loader把所有的信息都变成了list,我们统一在定义item处为这些字段添加输入前或输出后的处理方法,把它们转换为想要的格式.

修改代码如下

def parse_detail(self, response):
match_re = re.match(".*?(\d+.*)", response.url)
if match_re:
    post_id = match_re.group(1).replace("/", "")  # 去除 \
    item_loader = ItemLoader(CnblogsArticleItem(), response=response)
                item_loader.add_css("title", "#news_titlea::text")
                item_loader.add_css("content", "#news_content")
                item_loader.add_css("tags", "#news_more_info .news_tags a::text")
                item_loader.add_css("create_date", '#news_info .time::text')
                item_loader.add_value("url", response.url)
                item_loader.add_value("front_image_url", response.meta.get("front_image_url", ""))
                cnblogs_article_item = item_loader.load_item()  # 把上面提取到的值放回到item

debug可发现,loader里面的value都是list

image-20220120022815375

解决数据处理

​ 上面提到,有一些数据我们需要list,有一些需要str,还有一些要做正则,我们同一在item定义里面进行处理

loader.processors的MapCompose方法

​ 这个方法可以帮助我们都loader里面的每一个元素都执行自定义个方法。

​ 比如,我们想让每个title的末尾都加上jojo;每个title的前面都加上test 【给MapCompose传入两个方法参数】

from scrapy.loader.processors import MapCompose
def add1(value):
	return value + 'jojo'
def add2 (value):
	return 'test' + value
class CnblogsArticleItem(scrapy.Item):
    title = scrapy.Field(
    input_processor = MapCompose(add1,add2))

测试

image-20220120024236172

title字段我们数据库定义的是字符串,因此在操作后把list变为str。我们使用TakeFirst() 获取第一个字符串

在这里插入图片描述

修改代码

from scrapy.loader.processors import TakeFirst
class CnblogsArticleItem(scrapy.Item):
    title = scrapy.Field(output_processor=TakeFirst())

效果图

image-20220120025305205

日期的处理

​ 在处理日期时候,我们使用了正则表达式提取出来

def date_convert(value):
    match_re = re.match(".*?(\d+.*)", value)
    if match_re:
        return match_re.group(1)
    else:
        return "0000-00-00"
create_date = scrapy.Field(
        input_processor=MapCompose(date_convert)
    )

标签的处理

​ tags在网站上是多个a标签里面的value,使用Loader.add_css获取它们时候得到的是一个list,如果使用TakeFirst(),根据源码可知,只会return第一个标签,即后面的标签会被丢失

剩余parse代码统一使用Loader

​ 其实也不一定全部统一的,哪种更好就用哪种,混合用也是可的

自定义ItemLoader

​ 假设,我们的字段有很多的str类型的,一个个取output = TakeFirst()过于麻烦,因此我们自定义一个itemloader,它继承原来提供的类,但是我们设置它默认输出的时候自动调用TakeFirst() 就可以达到目的了

from scrapy.loader import ItemLoader
class ArticleItemLoader(ItemLoader):
    # 自定义itemloader
    default_output_processor = TakeFirst()

实例化时候使用自定义的Loader

def parse_detail(self, response):
item_loader = ArticleItemLoader(CnblogsArticleItem(), response=response)

​ 但是,这又出现了一个问题,之前提到过,是大部分字段是str,但不是全部,因为自定义的loader默认输出时候自动调用takefirst(),因此所有字段都变成了str。故应在不需要str的字段进行设置不调用takefirst()

from scrapy.loader.processors import Identity
front_image_url = scrapy.Field(output_processor=Identity())

如果使用insert_sql, params = item.get_insert_sql()来化简sql语句,item需要添加get_insert_sql方法

class CnblogsArticleItem(scrapy.Item):
    title = scrapy.Field()
    create_date = scrapy.Field(input_processor=MapCompose(date_convert))
    url = scrapy.Field()
    url_object_id = scrapy.Field()
    front_image_url = scrapy.Field(output_processor=Identity())
    front_image_path = scrapy.Field()
    praise_nums = scrapy.Field()
    comment_nums = scrapy.Field()
    fav_nums = scrapy.Field()
    tags = scrapy.Field(output_processor=Join(separator=","))
    content = scrapy.Field()

    def get_insert_sql(self):
        insert_sql = """
            insert into cnblog_article
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE fav_nums=VALUES(fav_nums)
        """
        params = (
            self.get("title", ""),
            self.get("url", ""),
            self.get("url_object_id", ""),
            self.get("front_image_path", ""),
            self.get("front_image_url", ""),
            self.get("parise_nums", 0),
            self.get("comment_nums", 0),
            self.get("fav_nums", 0),
            self.get("tags", ""),
            self.get("content", ""),
            self.get("create_date", "0000-00-00"),
        )

        return insert_sql, params

一些警告

ScrapyDeprecationWarning: scrapy.loader.processors.TakeFirst is deprecated, instantiate itemloaders.processors.TakeFirst instead.
default_output_processor = TakeFirst()

from itemloaders.processors import TakeFirst, MapCompose, Join, Identity

五 总结

虚拟环境的搭建、使用shell调试技巧、使用cookie登录、评论点赞收藏数的获取、主键冲突处理、使用Twisted异步数据库插入

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值