本文旨在通过爬取一系列博客网站技术文章的实践,介绍一下scrapy
这个python语言中强大的整站爬虫框架的使用。各位童鞋可不要用来干坏事哦,这些技术博客平台也是为了让我们大家更方便的交流、学习、提高的,大家千万要珍惜哦(-_-)。
0、开发环境
本文环境:
Win7 64位
Python 版本:3.6.5
pip 版本:18.1
scrapy 版本:1.5
pymysql 版本:0.9.2
Visual Code 版本: 1.28.2
Mysql 5.7 (不会安装的自行百度,这里就不岔话题了)
至于初始化Visual Code(后续文中统一简称:vscode)的Python编程环境,请参考博文:建议通过Anaconda3 来搭建python开发环境,省心。
【注】:文中源码有关XPath解析页面内容提取文章元素的解读,本文不再啰嗦重复了,可以参考前面的博文:(2.1.3节有介绍XPath使用)
1、目标介绍
今天的目标网站主要分为两类:
- 一类是页面通过Ajax异步请求的方式获取翻页文章列表
比如强大的csdn博客、oschina开源中国就是属于第一类通过Ajax异步加载的方式获取更多内容的
- 另一类是直接通过界面的上一页、下一页、跳转到第几页的方式直接加载下一页的文章列表
比如cnblog博客园、51cto、iteye、itpub、jobbole伯乐在线等几个是直接通过页面导航翻页的。
- > 比如cnblog博客园、51cto、iteye、itpub、jobbole伯乐在线等几个是直接通过页面导航翻页的。
下面让我们来一个一个分析、实现爬取技术文章保存到本地。
2、爬取目标
2.1、csdn博客
2.1.1 如何判断是否为Ajax方式异步获取的?
有很多种方式,比如,我们打开火狐浏览器(其它浏览器也一样),按F12打开浏览器的调试模式,选中【网络】,点击【XHR】来过滤异步请求,
然后输入csdn的网址:,我们看到列表有很多异步的请求,别慌,我们找一下很容易发现其中一个返回json格式的传输数据大小28KB的是我们的目标,点开这个请求发现右边返回的确实是文章列表:
我们通过关键字articles过滤一下这个请求,再往下滑动发现又发了同样的请求,不过参数不一样:
分析下这几个网址:
1541633731306080``1541579172175664``1541572185652051
发现其中不一样的就是最后一个shown_offset参数值,这个看起来像时间戳,但是是什么时间戳呢?当前系统时间?上次返回数据的最后一个时间戳?让我们看下响应里面的数据:
分析了下可以知道,第二个请求的这个时间戳,正是第一个请求返回数据的最后一个里面的这个值,我们再分析后面几个请求发现都是符合这个规律的,OK了,那接下来就是写爬虫去获取数据了。
2.1.2 爬虫实现
打开vscode终端定位到工程目录,例如d:/tmp/csdnnews 输入:
scrapy startproject csdnnews
提示成功后,进入到csdnnews目录cd csdnnews
然后输入以下命令创建爬虫:scrapy genspider csdnspider www.youkuaiyun.com
其中csdnspider 是爬虫类的名字, 是我们要爬虫允许的域名地址 此时目录如下(db目录是后面加的数据库操作工具类的文件夹)这里面我们要改几个地方来实现我们的功能。
2.1.2.1 修改spider接口实现
主要修改的内容有:
url
:开始爬虫的首页allow_domians
:允许爬虫的网站域名def parse(self, response):
这里处理爬虫返回的网页内容,抓取数据 第一次的url中要拼接一个16位的时间戳,类中提供了方法,后续的这个offset都是根据上一次返回的数据最后一个里面的offset来赋值即可。 这个spider类的主要内容如下:class tbmmSpider(Spider): # url = "https://www.youkuaiyun.com/api/articles?type=more&category=home&shown_offset=" url = 'https://www.youkuaiyun.com/api/articles?type=more&category=newarticles&shown_offset=' name = "csdnspider" allow_domians = ["www.youkuaiyun.com"] def get_time_stamp16(self): # 生成16时间戳 eg:1540281250399895 -ln datetime_now = datetime.datetime.now() # 10位,时间点相当于从1.1开始的当年时间编号 date_stamp = str(int(time.mktime(datetime_now.timetuple()))) # 6位,微秒 data_microsecond = str("%06d"%datetime_now.microsecond) date_stamp = date_stamp+data_microsecond return int(date_stamp) def start_requests(self): curl = self.url + str(self.get_time_stamp16()) yield Request(curl, headers=self.headers) def parse(self, response): datas = json.dumps(response.text, ensure_ascii= False, indent=4, separators=(',', ': ')) json_data = json.loads(datas) json_data = json_data.replace('null', '\"\"').replace(u'None', u'\"\"').replace(u'false', 'False').replace(u'true', 'True') dict = eval(json_data) articles = dict['articles'] if articles and len (articles) > 0: for article in articles: item = CsdnnewsItem() item['avatar'] = article['avatar'] item['title'] = article['title'] item['category'] = article['category'] item['category_id'] = article['category_id'] item['channel'] = article['channel'] item['show_datetime'] = article['created_at'] item['cur_id'] = article['id'] item['user_name'] = article['user_name'] item['nickname'] = article['nickname'] item['user_url'] = article['user_url'] item['showtime'] = article['shown_time'] item['source_from'] = 'csdn' item['summary'] = article['summary'] item['tag'] = article['tag'] item['type'] = article['type'] item['detail_url'] = article['url'] item['views_count'] = article['views'] item['comments_count'] = article['comments'] shown_offset = article['shown_offset'] yield item #如果datas存在数据则对下一页进行采集 time.sleep(0.5) next_url = self.url + str(shown_offset) yield Request(next_url, headers=self.headers)
2.1.2.2 修改items.py文件
声明接收抓取内容的item类属性 内容如下:
class CsdnnewsItem(scrapy.Item): # define the fields for your item here like: # 头像 avatar = scrapy.Field() # 标题 title = scrapy.Field() # 分类文本 category = scrapy.Field() # 分类id category_id = scrapy.Field() # 渠道 channel = scrapy.Field() # 创建时间 created_time = scrapy.Field() # 当前id cur_id = scrapy.Field() # 用户名 user_name = scrapy.Field() # 作者昵称 nickname = scrapy.Field() # 用户详情url user_url = scrapy.Field() # 展示时间 showtime = scrapy.Field() # 展示时间,比如2018年8月、19小时前、2天前 show_datetime = scrapy.Field() # 来源 source_from = scrapy.Field() # 摘要 summary = scrapy.Field() # 标签(多个标签以|分割) tag = scrapy.Field() # 类型(blog、news、article) type = scrapy.Field() # 详情url detail_url = scrapy.Field() # 阅读数量 views_count = scrapy.Field() # 评论数量 comments_count = scrapy.Field()
2.1.2.3 修改pipeline.py文件
实现保存数据到mysql数据库
class CsdnnewsPipeline(object): def __init__(self): self.db = DBHelper() def process_item(self, item, spider): self.db.insert(item) return item def close_spider(self, spider):
2.1.2.4 修改settings.py文件
声明pymysql数据库连接信息等
BOT_NAME = 'csdnnews' SPIDER_MODULES = ['csdnnews.spiders'] NEWSPIDER_MODULE = 'csdnnews.spiders' #mysql-连接配置 MYSQL_HOST = '127.0.0.1' MYSQL_DBNAME = 'voanews' MYSQL_USER = 'news' MYSQL_PASSWD ='123456' MYSQL_PORT = 3306 # 下面这个要打开,否则无法通过pipe管道存储到数据库 ITEM_PIPELINES = {<!-- --> 'csdnnews.pipelines.CsdnnewsPipeline': 300, }
2.1.2.5 修改dbhelper.py文件
实现保存到数据库
# -*- coding: utf-8 -*- import pymysql from twisted.enterprise import adbapi from scrapy.utils.project import get_project_settings #导入seetings配置 class DBHelper(): def __init__(self): settings = get_project_settings() #获取settings配置,设置需要的信息 dbparams = dict( host=settings['MYSQL_HOST'], #读取settings中的配置 db=settings['MYSQL_DBNAME'], user=settings['MYSQL_USER'], passwd=settings['MYSQL_PASSWD'], charset='utf8', #编码要加上,否则可能出现中文乱码问题 cursorclass=pymysql.cursors.DictCursor, use_unicode=False, ) #**表示将字典扩展为关键字参数,相当于host=xxx,db=yyy.... dbpool = adbapi.ConnectionPool('pymysql', **dbparams) self.__dbpool = dbpool def connect(self): return self.__dbpool #插入数据 def insert(self, item): #这里定义要插入的字段 sql = "insert into news(avatar, title, category, category_id, channel,show_datetime,\ cur_id, user_name, nickname, user_url, showtime, source_from, summary, tag, type, detail_url, views_count, comments_count)\ values(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" #调用插入的方法 query = self.__dbpool.runInteraction(self._conditional_insert, sql, item) #调用异常处理方法 query.addErrback(self._handle_error) return item #写入数据库中 def _conditional_insert(self, canshu, sql, item): #取出要存入的数据,这里item就是爬虫代码爬下来存入items内的数据 params = (item['avatar'], item['title'], item['category'], item['category_id'], item['channel'], item['show_datetime'], item['cur_id'], item['user_name'], item['nickname'], item['user_url'], item['showtime'], item['source_from'], item['summary'], item['tag'], item['type'], item['detail_url'], item['views_count'], item['comments_count']) canshu.execute(sql, params) #错误处理方法 def _handle_error(self, failue): print('--------------database operation exception!!-----------------') print(failue) def __del__(self): try: self.__dbpool.close() except Exception as ex: print(ex)
2.1.3 启动爬虫
在启动前,先初始化
mysql
的表结构,源码中db目录有个init.sql
文件,执行后会创建一个存储爬取内容的表结构,同时要把setting
中配置的用户
名,在mysql中创建相应的用户以及授权
给刚才新建的表。不清楚的可以评论回复,这里不岔话题了。 我们在刚才的spider目录的同级路径,输入以下命令即可启动爬虫:scrapy crawl csdnspider
2.1.4 源码下载
本节结束,爬取csdn博文的源码下载:
2.2、cnblog博客园
2.2.1、页面分析
我们打开博客园的java分类,地址是: 可以看到这个网站跟前面的csdn不一样的是,这个的文章列表内容是直接在源码里面的,这种是最容易爬取的,我们在页面右键查看源码内容如下:
刚好对应了页面的前两篇文章:
下面让我们来看下页面的文章列表布局:
所有的文章都是在一个
id
值为post_list
的div
里面,每一个文章布局都是包裹在一个class
属性值为post_item
的div
布局里面。我们再看下底部的翻页跳转导航内容:
翻页导航布局是在一个
class
属性值为pager
的div
里面,每一个a
标签表示一页导航地址。找到页面的规律后,接下来让我们编写爬虫实现文章的提取。
2.2.2、爬虫实现
2.2.2.1、创建项目
打开vscode终端定位到工程目录,例如d:/tmp/cnblog 输入:
scrapy startproject cnblog
提示成功后,进入到cnblog目录cd cnblog
然后输入以下命令创建爬虫:scrapy genspider -t crawl cnblogspider www.cnblogs.com
其中cnblogspider
是爬虫类的名字,www.cnblogs.com
是我们要爬虫允许的域名地址 此时目录如下(db目录是后面加的数据库操作工具类的文件夹)。这里跟前面一节实现爬取csdn博文的爬虫不一样的是,在创建爬虫的时候,加了参数
-t crawl
,就是以crawl
模板来创建一个继承自CrawlSpider
的spider
。2.2.2.2、spider实现
spider
的内容如下: 其中start_urls
是代表要爬取的首页列表,这里选择了java、python、linux
等分类; parse_item是我们处理数据的回调,这里解析页面提取文章各元素; rules是告诉爬虫应该如何提取翻页导航,这里是以xpath方式定位翻页导航元素,根据2.2.2.1节里面的页面分析很容易理解。class CnblogspiderSpider(CrawlSpider): name = 'cnblogspider' allowed_domains = ['www.cnblogs.com'] start_urls = [ 'https://www.cnblogs.com/cate/java/', 'https://www.cnblogs.com/cate/python/', 'https://www.cnblogs.com/cate/job/', 'https://www.cnblogs.com/cate/algorithm/', 'https://www.cnblogs.com/cate/linux/', 'https://www.cnblogs.com/cate/mysql/', 'https://www.cnblogs.com/cate/cpp/', 'https://www.cnblogs.com/cate/go/' ] rules = ( Rule(LinkExtractor(restrict_xpaths=('//div[@id="pager_bottom"]/div[@id="paging_block"]/div[@class="pager"]/a', )), callback='parse_item', follow=True), ) def parse_item(self, response): for article in response.xpath('//div[@id="post_list"]/div[@class="post_item"]'): item = CnblogItem() try: item['title'] = article.xpath('./div[@class="post_item_body"]/h3/a[@class="titlelnk"]/text()').extract_first() item['summary'] = article.xpath('./div[@class="post_item_body"]/p[@class="post_item_summary"]/text()').extract_first() item['detail_url'] = article.xpath('./div[@class="post_item_body"]/h3/a[@class="titlelnk"]/@href').extract_first() item['logo_url'] = article.xpath('./div[@class="post_item_body"]/p[@class="post_item_summary"]/a/img/@src').extract_first() item['source_from'] = '博客园' item['show_datetime'] = article.xpath('./div[@class="post_item_body"]/p/div[@class="post_item_foot"]/text()').extract_first() item['user_name'] = article.xpath('./div[@class="post_item_body"]/p/div[@class="post_item_foot"]/a/text()').extract_first() item['nickname'] = article.xpath('./div[@class="post_item_body"]/p/div[@class="post_item_foot"]/a/text()').extract_first() item['user_url'] = article.xpath('./div[@class="post_item_body"]/p[@class="post_item_summary"]/a/@href').extract_first() item['cur_id'] = '' item['views_count'] = article.xpath('./div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_view"]/a/text()').extract_first() item['views_count'] = re.findall("\((\d*?)\)", item['views_count'])[0] item['comments_count'] = article.xpath('./div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_comment"]/a/text()').extract_first() item['comments_count'] = re.findall("\((\d*?)\)", item['comments_count'])[0] print(item['views_count']) print(item['comments_count']) except Exception as ex: print(ex) yield item
2.2.2.3、items实现
跟前面2.1节爬取csdn的一样,定义了一样的元素,只不过不同的博客文章,所能提取的字段不一样,只是这里定义的一部分而已。这里就不重复贴代码了。
2.2.2.4、pipeline、settings、dbhelper的实现
这几个都跟2.1节爬取csdn的差不多,只是在dblhelper里面存储的字段不一样而已,具体直接看源码吧。
2.2.3、启动爬虫
如果已经按照2.1爬取csdn的章节里面初始化过mysql数据库了,这一步可以省略。
在启动前,先初始化
mysql
的表结构,源码中db目录有个init.sql
文件,执行后会创建一个存储爬取内容的表结构,同时要把setting
中配置的用户
名,在mysql中创建相应的用户以及授权
给刚才新建的表。不清楚的可以评论回复,这里不岔话题了。我们在刚才的spider目录的同级路径,输入以下命令即可启动爬虫:
scrapy crawl cnblogspider
2.2.4、源码下载
本节结束,爬取cnblog博客园文章的源码下载:
2.3、51cto博客
2.3.1 页面分析
我们打开技术类的推荐分类:
2.3.2、启动爬虫
如果已经按照2.1爬取csdn的章节里面初始化过mysql数据库了,这一步可以省略。
在启动前,先初始化
mysql
的表结构,源码中db目录有个init.sql
文件,执行后会创建一个存储爬取内容的表结构,同时要把setting
中配置的用户
名,在mysql中创建相应的用户以及授权
给刚才新建的表。不清楚的可以评论回复,这里不岔话题了。我们在刚才的spider目录的同级路径,输入以下命令即可启动爬虫:
scrapy crawl ctospider
2.3.3、源码下载
本节结束,爬取51CTO博客文章的源码下载:
2.4、jobbole伯乐在线
2.4.1 页面分析
我们打开伯乐在线的全部文章页面: 可以看到底部有分页导航,是不是很爽…克制一点,以学习为目的,不要耍流氓哈,不要干坏事。
创建工程、创建爬虫、编写spider等就不重复了,跟前面的一样一样的,直接看源码会更直接。
2.4.2、启动爬虫
如果已经按照2.1爬取csdn的章节里面初始化过mysql数据库了,这一步可以省略。
在启动前,先初始化
mysql
的表结构,源码中db目录有个init.sql
文件,执行后会创建一个存储爬取内容的表结构,同时要把setting
中配置的用户
名,在mysql中创建相应的用户以及授权
给刚才新建的表。不清楚的可以评论回复,这里不岔话题了。我们在刚才的spider目录的同级路径,输入以下命令即可启动爬虫:
scrapy crawl jobbolespider
2.4.3、源码下载
本节结束,爬取伯乐在线博客文章的源码下载:
2.5、itpub博客(待续)
2.5.1、页面分析
我们打开itpub的linux专题:
我们右键查看源码,可以看到文章内容就在源码中,只不过这个网站的翻页导航不是跟前面的一样有直接的第1页、第2页、下一页的形式,而是在底部有个“点击加载更多”的布局,其实也是一个a标签,跟其它的分页导航没有本质的区别。
创建工程、创建爬虫、编写spider等就不重复了,跟前面的一样一样的,直接看源码会更直接。
2.5.2、启动爬虫
如果已经按照2.1爬取csdn的章节里面初始化过mysql数据库了,这一步可以省略。
在启动前,先初始化
mysql
的表结构,源码中db目录有个init.sql
文件,执行后会创建一个存储爬取内容的表结构,同时要把setting
中配置的用户
名,在mysql中创建相应的用户以及授权
给刚才新建的表。不清楚的可以评论回复,这里不岔话题了。我们在刚才的spider目录的同级路径,输入以下命令即可启动爬虫:
scrapy crawl itpubspider
2.5.3、源码下载
本节结束,爬取ITPUB博客文章的源码下载:
2.6、oschina开源中国博客
2.6.1、页面分析
开源中国的博客就比较有意思,他的翻页比较隐藏,但是还是可以分析得到的。我们打开编程语言的首页,并且按F12打开调试模式查看Ajax过滤异步请求:
可以看到这里是有异步请求获取内容的,并且从过滤的Ajax请求的内容一看便知,在当前分类(比如截图中的编程语言分类是
classification=428609
)定了的前提下,p=2
就是表示获取第二页的数据了,type=ajax
表示以异步的方式返回文章列表。再让我们看下源码,在当前页面右键查看源码,如下图:可以看到它的内容在源码里面的:
但是它页面上又看不到上一页、下一页、或者第1页,第2页之类的分页导航的布局。从下图可以看到,其实是有的,只不过它的布局元素都设置了显示属性为隐藏的:
style="display: none"
,所以我们看不到。 这样我们就知道在crawl爬虫中如何获取爬虫所需下一页的布局元素了。那还有个问题:oschina开源中国这个网站的博客页面,跟前面的几个可以通过页面翻页导航自动爬取同系列的博客页面不一样的是,这里没有告诉我们它的最后一页是多少,如何判断呢?
我们这里采取一个策略:如果当前这次请求,没有返回任何博客文章内容了,就认为到最后一页了。
2.6.2、爬虫实现
ok,前面分析过页面结构、翻页导航定位、判断最后一页的策略了,这里就具体实现爬虫模块。
这里我们把爬虫访问的首页定为p=1,链接就是前面2.6.1分析的Ajax异步请求里面的内容,只是把
p=2
改为p=1
就是访问第一页了。 下一页的定位规则也很容易理解,通过XPath定位到2.6.1分析的那个隐藏的翻页的a标签即可。start_urls = ['https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=0&type=ajax&p=1'] rules = ( Rule(LinkExtractor(restrict_xpaths=('//p[@class="pagination"]/a[contains(@class, "pagination__next")]', )), callback='parse_item', follow=True), )
解析文字列表的方法如下:这个直接看代码就行了,主要是根据XPath提取我们需要的字段,如果对XPath不熟悉,可以参考我的另一篇文章有详细介绍使用示例: 的
2.1.3
节有介绍。def parse_item(self, response): for article in response.xpath('//div/div[contains(@class, "blog-item")]'): item = OschinaItem() try: item['title'] = article.xpath('./div[@class="content"]/a[@class="header"]/@title').extract_first() item['summary'] = article.xpath('./div[@class="content"]/div[@class="description"]/p/text()').extract_first() item['detail_url'] = article.xpath('./div[@class="content"]/a[@class="header"]/@href').extract_first() item['source_from'] = '开源中国' item['show_datetime'] = article.xpath('./div[@class="content"]/div[@class="extra"]/div/div[2]/text()').extract_first() item['user_name'] = article.xpath('./div[@class="content"]/div[@class="extra"]/div/div[1]/a/text()').extract_first() item['nickname'] = article.xpath('./div[@class="content"]/div[@class="extra"]/div/div[1]/a/text()').extract_first() item['user_url'] = article.xpath('./div[@class="content"]/div[@class="extra"]/div/div[1]/a/@href').extract_first() item['cur_id'] = article.xpath('./@data-id').extract_first() item['views_count'] = article.xpath('./div[@class="content"]/div[@class="extra"]/div/div[3]/text()').extract_first() view_count_str = str(item['views_count']) # 因为页面上的浏览数,这个网站返回的是2k,100之类的,要统一转为整型存储到数据库 if view_count_str.find('K') > -1: view_count_str = view_count_str.lstrip().rstrip() item['views_count'] = int(float(view_count_str.replace('K',''))*1000.0) if view_count_str.find('w') > -1: item['views_count'] = str(float(view_count_str.replace('w',''))*10000.0) item['comments_count'] = article.xpath('./div[@class="content"]/div[@class="extra"]/div/div[4]/a/text()').extract_first() except Exception as ex: print(ex) yield item
OK,主要的就上面这个代码,其它的跟前面章节的都大同小异,直接看源码即可。
2.6.3、启动爬虫
如果已经按照2.1爬取csdn的章节里面初始化过mysql数据库了,这一步可以省略。
在启动前,先初始化
mysql
的表结构,源码中db目录有个init.sql
文件,执行后会创建一个存储爬取内容的表结构,同时要把setting
中配置的用户
名,在mysql中创建相应的用户以及授权
给刚才新建的表。不清楚的可以评论回复,这里不岔话题了。我们在spider目录的同级路径,输入以下命令即可启动爬虫:
scrapy crawl oschinaspider
2.6.4、源码下载
本节结束,爬取OSChina开源中国博客文章的源码下载:
全部内容完毕,这里只是总结下自己最近学习scrapy爬虫的几个实践,通过这几个实践,一般的类似需求应该都可以搞定,当然这里没有涉及到反爬虫厉害的站点的处理,比如浏览器header伪装、ip代理、爬虫时间间隔等,后续有机会再实践下。 本文内容如有错误,恳请斧正,如有更好的技术,欢迎指点一二,谢谢。
3、参考资料
[1]: [2]: [3]: [4]: [5]: [6]:
转载于:https://www.cnblogs.com/xiaocy66/p/10589254.html