爬取jobbole文章
一、环境
- window7
- scrapy框架
- pycharm
- MySQL数据库
二、简介
既然是第一个爬虫,那么很多爬虫技巧也都是初次使用,有待深入了解;
爬虫基于scrapy框架,使用了框架中的scrapy.Request负责向目标服务器发送相应请求,解析数据时使用了scrapy的ItemLoader类来统一解析,而并不是原来的直接继承scrapy.item方法,详情下面再说;数据采用异步方式上传到MySQL,能有效防止前面的请求下载的数据过多,插入数据库速度跟不上数据产生的速度,产生的堵塞现象。
数据维度:标题、文章链接、链接MD5压缩、标题图片链接、点赞数、评论数、收藏数、标签、文章内容、创建时间。
三、网站分析与编程实现
开始肯定是去你要爬取的网站逛了,带着你需要爬取的内容要求有目的的逛,我们这里要爬取文章内容,那就主要关注jobbole文章栏,分析发现jobbole中的文章->全部文章->最新文章里包含了全站中几乎全部文章,这就算是找到需要爬取的数据的出处了,接下来就要分析如何进行编程爬取。
最新文章页面中(url:http://blog.jobbole.com/all-posts/)已经存在了一条条文章标题,按照我们的习惯,点击进去就是文章的详细内容了,所以我们可以以该页面作为start_urls,
start_urls = ['http://blog.jobbole.com/all-posts/']
爬虫一开始会取start_urls进行爬取,其默认返回处理函数为parse,当然也可以重写start_request函数来指定callback。我们按F12查看网页源码可知,
可以在此页面上提取出每篇文章的url和title,进而模拟请求url得到文章详细信息,代码如下:
def parse(self, response):
"""
1、获取文章列表页中的文章url并交给scrapy下载后并进行解析
2、获取下一页的url并交给scrapy进行下载,下载完成后交给parse
"""
#解析列表页中的所有文章URL并交给scrapy下载后并进行解析
# le = LinkExtractor(restrict_css="#archive .floated-thumb .post-thumb a")#LinkExtractor取url包
# links = le.extract_links(response)
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("")
yield Request(url=parse.urljoin(response.url , post_url) ,#parse.urljoin()作用是当post_url仅为文章页数而不是完整url时,需要将当前域名与文章页数进行拼接
meta = {"front_image_url":image_url} , callback=self.parse_detail)#yield关键字自动将url交给scrapy下载
break
#提取下一页并交给scrapy下载
next_urls = response.css(".next.page-numbers::attr(href)").extract_first()
if next_urls:
yield Request(url = parse.urljoin(response.url , next_urls) , callback = self.parse)
代码逻辑是解析出整页文章,然后通过循环迭代遍历每篇文章,并给每篇文章的url发送请求,下一页的文章提取通过分析可知
其是通过请求下一页url得到的,这样就好办了,直接提取出来,然后判断是否还有下一页来决定向服务器发送下一页请求,因为一旦到了最后一页的话,下一页的url是不存在的。
接下来就是数据的解析了,其文章url请求的返回函数为parse_detail,负责解析出需要提取的文章信息;
def parse_detail(self , response):
#article_item = JobBoleArticleItem()
"""xpath提取字段
#re_selector1 = response.xpath("/html/body/div[1]/div[3]/div[1]/div[1]/h1/text()")
#re_selector2 = response.xpath('//*[@id="post-113923"]/div[1]/h1/text()') #第二、三种方法最保险
title = response.xpath('//div[@class="entry-header"]/h1/text()')
create_date = response.xpath('//p[@class = "entry-meta-hide-on-mobile"]/text()').extract()[0].strip().replace('·' , '').strip()
praise_num = response.xpath('//span[contains(@class , "vote-post-up")]/h10/text()').extract()[0] #点赞数
fav_nums = response.xpath('//span[contains(@class , "bookmark-btn")]/text()').extract()[0] #收藏数
match_re = re.match('.*(\d).*' , fav_nums)
if match_re:
fav_nums = match_re.group(1)
comment_nums = response.xpath('//span[contains(@class , "href-style hide-on-480")]/text()').extract()[0] # 评论数
match_re = re.match('.*(\d| ).*', comment_nums)
if match_re:
if ord(match_re.group(1)) == 32:
comment_nums = '0'
else:
comment_nums = match_re.group(1)
cotent = response.xpath('//div[@class = "entry"]').extract()
"""
"""通过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('·' , '')
# praise_num = response.css('.vote-post-up h10::text').extract()
# fav_nums = response.css('.bookmark-btn::text').extract()[0]
# match_re = re.match(".*(\d| ).*" , fav_nums)
# if match_re:
# if ord(match_re.group(1)) == 32:
# fav_nums = '0'
# else:
# fav_nums = match_re.group(1)
# comment_nums = response.css('a[href="#article-comment"] span::text').extract()[0]
# match_re = re.match(".*(\d| ).*", comment_nums)
# if match_re:
# if ord(match_re.group(1)) == 32:
# comment_nums = '0'
# else:
# comment_nums = match_re.group(1)
# content = response.css("div.entry").extract_first()
# #contentlist = str(title) + '\n' + str(create_date) + '\t\t\t' + str(praise_num) +' ' + str(fav_nums) + ' ' + str(comment_nums) + '\n' + str(content)
# #match = re.match(',*(\d).*' , str(response))
# #self.articleID += 1
# '''
# if match:
# articleID = match.group(1)
# '''
# #self.savefile(self.articleID , contentlist)
# 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() #当前时间
# article_item["create_date"] = create_date
# article_item["front_image_url"] = [front_image_url]
# article_item["praise_nums"] = praise_num
# article_item["comment_nums"] = comment_nums
# article_item["fav_nums"] = fav_nums
# article_item["content"] = content
# 通过item Loader加载item , 结合css字段解析和item值传递
item_loader = ArticleItemLoader(item = JobBoleArticleItem() , response = response) #item参数的传入实参要实例化,即JobBoleArticleItem()
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("content" , "div.entry")
item_loader.add_css("tags" , 'p.entry-meta-hide-on-mobile a::text')
article_item = item_loader.load_item()#解析以上规则
yield article_item
从代码中就可以看到,解析出来的数据通过item_loader统一打包,将会yield到pipeline中进行清洗后的数据处理,比如上传数据库或者保存成文档形式;说到清洗,就得讲到items中的数据清洗操作了,直接看代码
from scrapy.loader import ItemLoader
from scrapy.loader.processors import MapCompose , TakeFirst , Join #Ma..传入数据时对数据进行预处理
'''自定义class , 方便统一修改'''
class ArticleItemLoader(ItemLoader):
#自定义ItemLoader
default_output_processor = TakeFirst() #所有继承了该类的类都执行了该TakeFirst()默认输出预处理函数
class JobBoleArticleItem(scrapy.Item):
title = scrapy.Field() #Field类型表示接收任何类型数据
create_date = scrapy.Field(
input_processor = MapCompose(date_convert)
)
url = scrapy.Field()
url_object_id = scrapy.Field()
front_image_url = scrapy.Field( #返回值必须为列表,故调用returnValue函数覆盖掉默认的TakeFirst函数
input_processor=MapCompose(returnValue)
)
front_image_path = scrapy.Field()
praise_nums = scrapy.Field(
input_processor=MapCompose(fetchPraise_nums)
)
comment_nums = scrapy.Field(
input_processor=MapCompose(get_num)
)
fav_nums = scrapy.Field(
input_processor=MapCompose(get_num)
)
tags = scrapy.Field(
input_processor = MapCompose(remove_comment_tags) , #注意这有个','逗号连接
output_processor = Join(',') #scrapy中的Join函数,连接返回列表中的多个元素,返回字符串
)
content = scrapy.Field()
def get_insert_sql(self):
insert_sql = """INSERT IGNORE INTO jobbole_article(title , url , create_date , url_object_id , front_image_url
, praise_nums , comment_nums , fav_nums , content , tags)
VALUES (%s , %s , %s , %s , %s , %s , %s , %s , %s , %s)
"""
params = (self["title"] , self["url"] , self["create_date"], self["url_object_id"] , self["front_image_url"] , self["praise_nums"] ,
self["comment_nums"] , self["fav_nums"] , self["content"] , self["tags"])
return insert_sql , params
这里是自定义继承了ItemLoader的AtricleItemLoader类,并通过MapCompose调入处理方法函数来对数据进行管道化处理(自己的理解),get_insert_sql上传数据库的操作放在这里是为了当存在多个爬虫或多组数据需要上传数据库时方便分类管理。
最后就到了最终数据的归宿问题了,我们此次是直接上传到MySQL数据库中
import MySQLdb
import MySQLdb.cursors
from twisted.enterprise import adbapi #adbapi可将mysqldb操作变成异步化操作
class MysqlPipeline(object):
#采用同步机制写入mysql
def __init__(self):
self.conn = MySQLdb.connect(host = '127.0.0.1' , port = 3307 , user = 'root' , passwd = PASSWARD , db = '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 , create_date , url_object_id) " \
"value(%s , %s , %s , %s)"
self.cursor.execute(insert_sql ,
(item["title"] , item["url"] , item["create_date"] , item["url_object_id"])) #此句可以写入数据库 , 所以以上的问题可能是creat_date或长度问题;
self.conn.commit() #执行完这条语句后数据才实际写入数据库
#self.conn.close()
class MysqlTwistedPipline(object): #Twisted异步插入操作机制
def __init__(self , dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls , settings): #通过@classmethod , 调用settings中定义的值
loginSet = dict(
host=settings["MYSQL_HOST"],
db=settings["MYSQL_DBNAME"],
user=settings["MYSQL_USER"],
passwd=settings["MYSQL_PASSWORD"],
charset='utf8',
port=settings["MYSQL_PORT"],
cursorclass = MySQLdb.cursors.DictCursor,
use_unicode = True
)
dbpool = adbapi.ConnectionPool("MySQLdb" , **loginSet) #**将dbparms变成可变化参数,
return cls(dbpool) #实例化dbpool
def process_item(self , item , spider):
# 使用twisted将mysql插入变成异步执行
query = self.dbpool.runInteraction(self.do_insert , item)#defer延迟对象
query.addErrback(self.handle_error , item , spider) #处理异常
def handle_error(self , failure , item , spider):
#处理异步插入的异常,查找bug的关键点
print(failure)
def do_insert(self , cursor , item):
#执行具体插入
# insert_sql , params = item.get_insert_sql()
# cursor.execute(insert_sql , params)
insert_sql, params = item.get_insert_sql()
cursor.execute(insert_sql, params)
代码分别介绍了同步上传机制和异步上传机制,异步上传机制就是通过Twisted框架提供的api创建连接池实现异步化,在process_item方法中执行异步插入操作。
可参考这篇博文了解线程和Twisted;
更详细的Twisted讲解可以点击打开链接;
爬取效果:
标题:title 点赞数 : praise_nums 文章内容:content
文章链接 : url 评论数 : comment_nums 文章创建时间:create_date
链接MD5压缩:url_object_id 收藏数:fav_nums
图片链接 : front_image_url 标签:tags
最后总得象征性的总结一下:
- 毕竟是第一个爬虫,技术上的总结也就是以上所有用到的了
- jobbole这个网站对于刚接触爬虫的初学者来说算是比较适合爬取的,因为其反爬策略不多
- 随便说下jobbole里面的文章还是挺有质量的。