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属性为eng的div元素 |
/div/* |
选取属于div元素的所有子节点 |
//* |
选取所有元素 |
//div[@*] |
选取所有带属性的div元素 |
//div/a | //div/p |
选取所有div元素的a和p元素 |
//span | //ul |
选取文档中的span和ul元素 |
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, }
pipelines.py文件代码
class ArticlespiderPipeline: def process_item(self, item, spider): return item
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中
在虚拟环境下安装: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的使用