Scrapy爬虫一

理解业务

画出爬取网站的url结构图

url结构图 链接结构图 ### 爬取方式 #### 深度优先 爬虫去重实现破除循环(深度优先) #### 通过翻页爬取

初始Scrapy项目

创建项目

在对应的目录下的cmd中输入scrapy startproject ArticleSpider创建项目 Scrapy项目组成结构

进入项目目录 cd ArticleSpider 输入scrapy genspider jobbole blog.jobbole.com创建爬虫module setting.py中如下修改

1
2
# 关闭过滤不符合ROBOTSTXT条例的url的功能
ROBOTSTXT_OBEY = False

调试项目

在项目目录文件下创建main.py文件专门用来调试项目爬虫

1
2
3
4
5
6
7
8
9
import sys
import os
from scrapy.cmdline import execute

# 获取项目当前位置
address = sys.path.append(os.path.dirname(os.path.abspath(__file__)))
print(address)
# 用execute调用命令打开爬虫,cmd中命令为`scrapy crawl jobbole`
execute(["scrapy", "crawl", "jobbole"])

调试单个网址

在cmd中输入scrapy shell url,url为目标地址 开始调试,如下 mark

获取网页内容

XPath

概念

XPath 是一门在xml文档中查找信息的语言,它可以在XML文档中对于原色和属性进行遍历。其内置了超过100个内建函数,这些函数用于对字符串值,数值、日期、时间进行比较遍历。 #### 基本使用语法 mark #### 两种查找方式 ##### 通过源码查找 ##### 通过浏览器开发者模式直接复制得到xpath位置 #### 提取内容信息 通过a/text()提取文字信息,通过a.extract()提取为数组,调用时只需调用数组的值 注:调用extract()后信息存储为数组形式,selector无法继续查询子条件 如a/div不可用 #### 爬取一篇文章的内容

1
2
3
4
5
6
7
8
9
10
11
12
 #提取文章的具体字段
#extract_first()提取第一个数组元素,默认为空,可防止空异常
title = response.xpath('//div[@class="entry-header"]/h1/text()').extract_first("")
# strip()去除空格 replace()把 · 替换为空
create_date = response.xpath("//p[@class='entry-meta-hide-on-mobile']/text()").extract()[0].strip().replace("·","").strip()
# xpath中的contains()函数获取包含该字符串的该元素的class
praise_nums = response.xpath("//span[contains(@class, 'vote-post-up')]/h10/text()").extract()[0]
fav_nums = response.xpath("//span[contains(@class, 'bookmark-btn')]/text()").extract()[0]
# re模块的正则表达式获取数值
match_re = re.match(".*?(\d+).*", fav_nums)
if match_re:
fav_nums = match_re.group(1)

CSS选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 通过css选择器提取字段
front_image_url = response.meta.get("front_image_url", "") #文章封面图
title = response.css(".entry-header h1::text").extract()[0]
create_date = response.css("p.entry-meta-hide-on-mobile::text").extract()[0].strip().replace("·","").strip()
praise_nums = response.css(".vote-post-up h10::text").extract()[0]
fav_nums = response.css(".bookmark-btn::text").extract()[0]
match_re = re.match(".*?(\d+).*", fav_nums)
if match_re:
fav_nums = int(match_re.group(1))
else:
fav_nums = 0

comment_nums = response.css("a[href='#article-comment'] span::text").extract()[0]
match_re = re.match(".*?(\d+).*", comment_nums)
if match_re:
comment_nums = int(match_re.group(1))
else:
comment_nums = 0

content = response.css("div.entry").extract()[0]

tag_list = response.css("p.entry-meta-hide-on-mobile a::text").extract()
tag_list = [element for element in tag_list if not element.strip().endswith("评论")]
tags = ",".join(tag_list)

XPath与CSS选择器的区别

CSS选择器编码比较简短,适合熟悉前端的人员使用,两者都可以采集到网站的信息

获取文章url列表(翻页)

通过request函数调用parse_detail方法,通过parse函数拼接url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def parse(self, response):
"""
1. 获取文章列表页中的文章url并交给scrapy下载后并进行解析
2. 获取下一页的url并交给scrapy进行下载, 下载完成后交给parse
"""
# 解析列表页中的所有文章url并交给scrapy下载后并进行解析
if response.status == 404:
self.fail_urls.append(response.url)
self.crawler.stats.inc_value("failed_url")

post_nodes = response.css("#archive .floated-thumb .post-thumb a")
for post_node in post_nodes:
# 获取文章封面
image_url = post_node.css("img::attr(src)").extract_first("")
post_url = post_node.css("::attr(href)").extract_first("")
# request函数调用parse_detail方法,parse函数拼接url,meta传递的是字典做异步处理把图片传给下一层
yield Request(url=parse.urljoin(response.url, post_url), meta={"front_image_url": image_url}, callback=self.parse_detail)

# 提取下一页并交给scrapy进行下载,调用自身
next_url = response.css(".next.page-numbers::attr(href)").extract_first("")
if next_url:
yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse)

定义Item中的类

Spider中的Item只有一种类型Field,表示可以接收任何类型

1
2
3
4
5
6
7
8
9
10
11
12
13
# 只有一种类型Field
class JobBoleAeticleItem(scrapy.Item):
title = scrapy.Field()
create_date = scrapy.Field()
url = scrapy.Field()
url_object_id = scrapy.Field() # 把url的长度变成固定长度
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()

在jobbole中填充传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 填充传值
article_item["url_object_id"] = get_md5(response.url)
article_item["title"] = title
article_item["url"] = response.url
try:
create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()
article_item["create_date"] = create_date
article_item["front_image_url"] = [front_image_url]
article_item["praise_nums"] = praise_nums
article_item["comment_nums"] = comment_nums
article_item["fav_nums"] = fav_nums
article_item["tags"] = tags
article_item["content"] = content

# 把值传到pipelines中
yield article_item

settings

开启pipelines默认配置

在settings.py中把默认设置的

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

scrapy自带的图片下载

1
2
3
4
5
6
7
8
9
10
11
ITEM_PIPELINES = {
'scrapy.pipelines.images.ImagesPipeline': 1,
}
IMAGES_URLS_FIELD = "front_image_url"
# abspath获取当前目录 dirname获取目录名称 join拼接
project_dir = os.path.abspath(os.path.dirname(__file__))
IMAGES_STORE = os.path.join(project_dir, 'images')

# 设置选择要下载图片的规模
# IMAGES_MIN_HEIGHT = 100
# IMAGES_MIN_WIDTH = 100

继承ImagesPipeline获取图片地址

1
2
3
4
5
6
7
8
9
10
from scrapy.pipelines.images import ImagesPipeline
class ArticleImagePipeline(ImagesPipeline):
# results中保存的是图片的路径,是list形式
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

固定url格式

新建一个名为utils的文件 mark

1
2
3
4
5
6
7
8
def get_md5(url):
# 如果是unicode,转为utf-8
if isinstance(url, str):
url = url.encode("utf-8")
m = hashlib.md5()
m.update(url)
# 抽取返回摘要
return m.hexdigest()

数据写入sql

josn文件导出基本格式

  • codecs:这样用到codecs来打开文件,优点是可以避免打开过程中出现的一些问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class JsonWithEncodingPipeline(object):
    #自定义json文件的导出
    def __init__(self):
    self.file = codecs.open('articleexport.json', 'w', encoding='utf-8')

    def process_item(self, item, spider):
    lines = json.dumps(dict(item), ensure_ascii=False) + "\n"
    self.file.write(lines)
    return item

    def close_spider(self, spider):
    self.file.close()

调用scrapy提供的json export导出json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 process_item(self, item, spider):
self.exporter.export_item(item)
return item

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

设计数据库

create_date日期数据的设计

jobbole.py中调用datetime类把数据改为日期形式

1
2
3
4
try:
create_date = datetime.datetime.strptime(create_date, "%Y/%m/%d").date()
except Exception as e:
create_date = datetime.datetime.now().date()

各种数据

mark #### 安装mysqlclient windows下pip install mysqlclinet

MysqlPipeline

setting下配置MYSQL

1
2
3
4
5
6
7
8
MYSQL_HOST = "127.0.0.1"
MYSQL_DBNAME = "article_spider"
MYSQL_USER = "root"
MYSQL_PASSWORD = "root"


SQL_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
SQL_DATE_FORMAT = "%Y-%m-%d"

MysqlTwistedPipline(异步插入)

简化代码

通过scrapy的item loader传值

主要语法

  • item_loader.add_css( )
  • item_loader.add_xpath( )
  • item_loader.add_value( )
    1
    2
    item_loader = ItemLoader(item=, response=response)
    item_loader.add_css()

传值

1
2
3
4
5
6
7
8
9
10
11
12
# 通过item loader加载item
item_loader = ItemLoader(item=JobBoleArticleItem(), response=response)
item_loader.add_css("title", ".entry-header h1::text")
item_loader.add_value("url", response.url)
item_loader.add_value("url_object_id", get_md5(response.url))
item_loader.add_css("create_date", "p.entry-meta-hide-on-mobile::text")
item_loader.add_value("front_image_url", [front_image_url])
item_loader.add_css("praise_nums", ".vote-post-up h10::text")
item_loader.add_css("comment_nums", "a[href='#article-comment'] span::text")
item_loader.add_css("fav_nums", ".bookmark-btn::text")
item_loader.add_css("tags", "p.entry-meta-hide-on-mobile a::text")
item_loader.add_css("content", "div.entry")

通过scrapy的自定义的Item提取信息

MapCompose方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class JobBoleArticleItem(scrapy.Item):
create_date = scrapy.Field(
input_processor=MapCompose(date_convert),
# output_processor=TakeFirst() 获取第一个
)
url = scrapy.Field()
# 把url的长度变成固定长度
url_object_id = scrapy.Field()
front_image_url = scrapy.Field(
# 用TakeFirst()会出异常
output_processor=MapCompose(return_value)
)
front_image_path = scrapy.Field()
praise_nums = scrapy.Field(
input_processor=MapCompose(get_nums)
)
comment_nums = scrapy.Field(
input_processor=MapCompose(get_nums)
)

自定义