版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_1290259791/article/details/82318130
Scrapy爬取伯乐在线所有文章
爬取网站伯乐在线
1、目标分析
爬取内容:所有的文章及文章标题、发布时间、文章内容、文章url、文章图片url及下载、评论数目等…
处理内容:
- 将url进行md5处理后保存到mysql。
- 异步插入数据到mysql。
- 对文章内容进行简单处理。
- 自定义图片下载的管道。
使用方法:
- 在spider文件中使用ArticleItem类,来规范Item里的内容。
- 在spider文件中进行url的md5加密。
- 在items使用模块
scrapy.loader.processors
中的MapCompose、TakeFirst、Join类。 - 在items使用模块
scrapy.loader
中的ItemLoader类,来定义一个Item实例。 - 在main.py中定义启动方法。
- 在pipelines中使用ImagesPipeline图片下载管道、
JsonItemExporter
JSON文件的保存、adbapi来将数据异步保存在Mysql。
使用模块
Scrapy--1.5.1
PyMySQL--0.9.2
Pillow--5.2.0
文件结构
├── bole
│ ├── __init__.py
│ ├── images # 图片存取
│ │ └── full
│ ├── items.py # 结构体定义
│ ├── main.py # 启动文件
│ ├── middlewares.py # 中间件
│ ├── pipelines.py # 管道
│ ├── settings.py # 配置文件
│ ├── spiders # 爬虫文件编写
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ └── boleblog.py
│ └── utils # 自定义的方法
│ ├── __init__.py
│ ├── __pycache__
│ └── common.py
└── scrapy.cfg
2、Spiders的编写
2.1、网站结构分析
我们爬取的是所有文章的url,在最新文章中我们发现这里是所有的文章,所以获取当前页面的文章的url,然后继续爬取下一页。
2.2、获取当页文章URL
我们使用Xpath来获取页面的所有URL。
2.3、获取文章的信息
首先获取文章的所有内容。
然后继续获取标题、文章内容等信息。
这是获取标题的xpath,获取其他信息类似。
2.4、文章列表下一页
继续编写Xpath获取下一页。
2.4、编写spiders.py
这里编写的是获取当前页面文章的url,和下一页的url,以及重复调用。
# -*- coding: utf-8 -*-
import scrapy
from scrapy import Request
from urllib import parse
from bole.items import ArticleItem, ArticleItemLoader
from bole.utils.common import get_md5
class BoleblogSpider(scrapy.Spider):
name = 'boleblog'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/']
def parse(self, response):
"""
1、获取文章列表中的url,并交给解析函数进行解析
2、获取下一页的url并交给scrapy进行下载
:param response:
:return:
"""
# 分析当前页面的文章url
post_nodes = response.xpath('//*[@id="archive"]//div[@class="post-thumb"]') # 首先获取文章的所有url
for post_node in post_nodes:
post_url = post_node.xpath('.//a/@href').extract_first() # 文章的地址
image_url = post_node.xpath('.//img/@src').extract_first() # 文章图片的地址
image_url = parse.urljoin(response.url, image_url) #以前的文章图片是在本域名下,所以拼接一下。
yield Request(url=parse.urljoin(response.url, post_url), callback=self.parse_detail,
meta={'image_url': image_url}) # 用回调函数分析文章页面的元素
# 提取下一页的url
next_url = response.xpath('//*[@id="archive"]//a[contains(@class,"next")]/@href').extract_first()
if next_url:
yield Request(url=next_url, callback=self.parse)
先说获取文章数据的结构再来,继续爬取页面。
3、Item爬取数据结构的定义
- 在提取的字段数很多时,各种提取规则会越来越多,维护也会变困难。
- scrapy就提供了ItemLoader这样一个容器,在这个容器里面可以配置item中各个字段的提取规则。可以通过函数分析原始数据,并对Item字段进行赋值。
**Item和Itemloader区别:**Item提供保存抓取到数据的容器,而 Itemloader提供的是填充容器的机制。
3.1、Spiders编写
def parse_detail(self, response):
# 通过Itemloader加载Item
image_url = response.meta.get('image_url') # 传递图片的url
item_loader = ArticleItemLoader(item=ArticleItem(), response=response) # 将类设置为自定义的Itemloader类
item_loader.add_xpath('title', '//*[@class="entry-header"]/h1/text()') # 通过xpath来提取数据
item_loader.add_value('url', response.url) # 直接添加值
item_loader.add_value('url_object_id', get_md5(response.url))
item_loader.add_xpath('create_date', '//p[@class="entry-meta-hide-on-mobile"]/text()[1]')
item_loader.add_value('image_url', [image_url])
item_loader.add_xpath('praise_nums', "//span[contains(@class,'vote-post-up')]/h10/text()")
item_loader.add_xpath('fav_nums', "//span[contains(@class,'bookmark-btn')]/text()")
item_loader.add_xpath('comment_nums', "//a[@href='#article-comment']/span/text()")
item_loader.add_xpath('content', '//*[@class="entry"]/p | //*[@class="entry"]/h3 | //*[@class="entry"]/ul')
article_item = item_loader.load_item() # 将规则进行解析,返回的是list
yield article_item
- ArticleItemLoader是自定义的一个继承
ItemLoader
的类,其中的参数。
- 第一个参数:Item对象是自己定义的Item传递的是一个实例。
- 第二个参数:response就是提取的数据源。
- 生成的item_loader用了两个对象
- add_xpath:第一个参数字段名,第二个参数提取的xpath规则。
- add_value:第一个参数字段名,第二个参数直接添加值。
- 这些字段默认情况下填充的都是list类型。
- 注意的是最后要调用load_item()方法,返回刚刚提取的数据。
- 我们还要对获取的字段做进一步处理。
3.2、Item自定义的类
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
import datetime
import re
import scrapy
from scrapy import Field
from scrapy.loader.processors import MapCompose, TakeFirst, Join
from scrapy.loader import ItemLoader
class BoleItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
def add_name(value): # 接受的值就是title所有值,这值是列表的遍历
return value + 'ok'
def date_convert(value):
# 对时间进行处理格式
value = value.strip().replace('·', '').strip()
try:
create_data = datetime.datetime.strptime(value, '%Y/%m/%d').date()
except Exception as e:
create_data = datetime.datetime.now().date()
return create_data
def get_nums(value):
# 处理评论和点赞的数
math_re = re.search(r'(\d+)', value)
if math_re:
value = math_re.group(1)
else:
value = 0
return value
class ArticleItemLoader(ItemLoader):
# 自定义Itemloader
default_output_processor = TakeFirst() # 自定义ouput_processor
class ArticleItem(scrapy.Item):
# 标题
title = Field(
input_processor=MapCompose(add_name, lambda x: x + 'no') # 可以传递任意多的函数进行从左到右处理。
)
# 时间
create_date = Field(
input_processor=MapCompose(date_convert),
)
# 文章url
url = Field()
# 对url进行md5
url_object_id = Field()
# 文章图片
image_url = Field( # 因为要求返回list,不能用str
output_processor=MapCompose(lambda x: x) # 变为一个list,但是在插入mysql的时候要求是str。
)
image_path = Field()
# 点赞数
praise_nums = Field()
# 评论数
comment_nums = Field(
input_processor=MapCompose(get_nums)
)
# 点赞数
fav_nums = Field(
input_processor=MapCompose(get_nums)
)
# 内容
content = Field(
output_processor=Join('\n') # 不选择第一个使用Join来进行链接
)
- 定义的字段结构体ArticleItem,其中Field字段有两个参数。
- 输入处理器:input_process当传入字段值时,在传进来的时候对字段进行处理。
- 输出处理器:output_process当字段处理完后,输出前的处理。
- TakeFirst是Scrapy提供的内置处理器,提取字段List中的第一个非空元素,用于输出。
- MapCompose能把多个函数执行的结果按顺序组合起来,产生最终的输出,通常用于输入处理器。
- Identity不进行任何处理,直接返回原来的数据,因为所有字段默认去取第一个非空元素,当我们要保留前面的空元素,就是用这个。
- Join返回用分隔符连接后的值,分隔符默认为空格。
- Compose用于多个函数的组合。List对象,依次被传递到第一个参数(就是处理函数),然后再穿入到第二个参数,默认情况遇到Node(List中有None值)就停止处理,参数stop_on_none = False取消停止。
- MapCompose用于多个函数的组合。List对象中的元素,List中的每一个元素依次传到第一个参数,然后第二个参数。返回None(会被下一个函数所忽略)。
- 因为返回的是列表每次都要输出第一个,所以我们定义一个ItemLoader类继承ItemLoader,重写default_output_processor,当我们重写output_processor时继承的就会失效。
4、启动函数
from scrapy.cmdline import execute
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute('scrapy crawl boleblog'.split())
5、保存文件到Mysql
主要说异步插入数据,暂时只支持关系数据库。
import pymysql
from twisted.enterprise import adbapi
class MysqlTwistedPipeline(object):
"""
异步插入数据,暂时支持关系数据库。
"""
def __init__(self, dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls, settings): # 该方法就是获取setting中的配置信息
dbparms = dict(
host=settings['MYSQL_HOST'],
db=settings['MYSQL_DBNAME'],
user=settings['MYSQL_USER'],
cursorclass=pymysql.cursors.DictCursor,
charset='utf8',
use_unicode=True,
)
dbpool = adbapi.ConnectionPool('pymysql', **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) # 处理异常
return item
def handle_error(self, failure):
# 处理异步插入的异常
print('出现异常')
def do_insert(self, cursor, item):
# 执行具体的插入
insert_sql = """
insert into article(title,url,url_object_id,create_date,fav_nums) values(%s,%s,%s,%s,%s)
"""
cursor.execute(insert_sql,
(item['title'], item['url'], item['url_object_id'], item['create_date'], item['fav_nums']))
- 调用from twisted.enterprise import adbapi支持异步的adbapi。
- from_settings获取setting的配置信息。
- dbpool = adbapi.ConnectionPool(‘pymysql’, **dbparms)调用连接线程池使用pymysql将连接信息进行解包传入。
- self.dbpool.runInteraction(self.do_insert, item)调用启动的方法插入数据库。
总结
- 在使用Xpath选择元素的时候,在不确定的情况下最好不要用数组选择
//span[4]
因为你不确定其他以前的历史界面,最好还是用属性定位获取的内容。 - 使用ItemLoader来定义数据结构,以及TakeFirst、MapCompose、Join和input_process()\output_process()方法。
- 以及异步插入数据到Mysql。