scrapy:爬取北邮人论坛所有帖的基本信息及正文

scrapy1
参考代码来自于这里,算是二次加工吧。但我其实基本上重写了所有代码,item的定义,spider的逻辑,pipline的数据处理存储等,不过论坛信息爬取的思路分析,模拟登陆及cookie传递都是受原始代码的启发而来,感谢原作者buptbill200。在腾讯的云服务器上跑过几次,基本功能能跑通,但还有不完善之处,比如帖子的主体信息处理与存储部分。

完整代码下载

  1. 通过git:git clone https://github.com/ryderchan/byrbbs.git
  2. 访问地址:https://github.com/ryderchan/byrbbs

功能

爬取北邮人论坛十个主要板块的板块信息,包括子版块信息,板块的总帖数。针对每一个板块,爬取板块内的所有帖子,包括作者,上传时间,回帖数等等。并获取帖子的主体内容。以上信息均存储与Mysql中。

环境与工具

  • windows 10 64位 专业版
  • java 版本 1.8.0_111
  • Mysql 版本 5.7
  • python 版本 2.7.12(anaconda4.2 64位)
  • scrapy
  • re
  • MySQLdb
  • functools

scrapy基本原理

scrapy有详细的使用文档,也有人做了翻译(中文版版本较低),且原理讲解中穿插有相当多的代码实例,有时间的话推荐仔细看下。以下简要介绍几个关键组件及其作用。

scrapy-framework

关键组件

Scrapy Engine

Scrapy引擎是用来控制整个系统的数据处理流程,并进行事务处理的触发。更多的详细内容可以看下面的数据处理流程。

Scheduler

调度程序从Scrapy引擎接受请求并排序列入队列,并在Scrapy引擎发出请求后返还给他们。

Downloader

下载器的主要职责是抓取网页并将网页内容返还给 Spiders。

Spiders

Spiders是由用户自己定义用来解析网页并抓取制定URL返回内容的类,每个Spider都能处理一个域名或一组域名。换句话说就是用来定义特定网站的抓取和解析规则。

Spider的整个抓取流程是这样的:

(1)首先获取第一个URL的初始请求,当请求返回后调取一个回调函数。第一个请求是通过调用start_requests()方法完成的。该方法默认从start_urls中的Url中生成请求,交由回调函数处理。当然也可以自己重写这些函数。

(2)回调函数解析网页响应并返回项目对象item或请求对象request的迭代。即使返回的是request,最终的处理结果也应该是可迭代的item。若是request,这些请求也将包含一个回调,交由下一个回调函数处理。如此循环。这一过程中cookie的传递,item数据的传递,由request中meta的对应参数完成,如

yield scrapy.Request(url, meta={'cookiejar': response.meta['cookiejar'],'item':item}, headers=HEADERS, callback=self.parse)

(3)在回调函数中,解析网站内容使用的是Xpath选择器,并生成解析的数据项,存储于item中。

(4)最后,由pipelines来处理返回的item,可以直接打印输出,存储到csv,json文件中,或存储到mysql中。

Item Pipeline

项目管道的主要责任是负责处理从网页中抽取的item,他的主要任务是清晰、验证和存储数据。当页面被蜘蛛解析后,将被发送到项目管道,并经过几 个特定的次序处理数据。每个项目管道的组件都是有一个简单的方法组成的Python类。他们获取了项目并执行他们的方法,同时他们还需要确定的是是否需要 在项目管道中继续执行下一步或是直接丢弃掉不处理。

项目管道通常执行的过程有:

  1. 清洗HTML数据
  2. 验证解析到的数据(检查项目是否包含必要的字段)
  3. 检查是否是重复数据(如果重复就删除)
  4. 将解析到的数据存储到数据库中

Downloader middlewares(下载器中间件)

下载中间件是位于Scrapy引擎和下载器之间的钩子框架,主要是处理Scrapy引擎与下载器之间的请求及响应。它提供了一个自定义的代码的方式 来拓展Scrapy的功能。下载中间器是一个处理请求和响应的钩子框架。他是轻量级的,对Scrapy尽享全局控制的底层的系统。

Spider middlewares(spider中间件)

spider中间件是介于Scrapy引擎和蜘蛛之间的钩子框架,主要工作是处理蜘蛛的响应输入和请求输出。它提供一个自定义代码的方式来拓展Scrapy 的功能。蛛中间件是一个挂接到Scrapy的蜘蛛处理机制的框架,你可以插入自定义的代码来处理发送给蜘蛛的请求和返回蜘蛛获取的响应内容和项目。

Scheduler middlewares(调度中间件)

调度中间件是介于Scrapy引擎和调度之间的中间件,主要工作是处从Scrapy引擎发送到调度的请求和响应。他提供了一个自定义的代码来拓展Scrapy的功能。

网站分析

模拟登陆

为了分析登陆时后台所做的操作,可以先输入错误的登陆信息,观察后台的响应,如果你也用的是chrome,按F12即可调出检测工具。

进入Network选项卡,然后输入错误登陆信息,发现有个ajax_login.json文件出现,而Headers中所传入的Form Data的id和passwd就是刚才输入的账号密码。可以确定此文件跟登陆有密切关系。

传递cookie

为了保持登陆,需要传递cookie。与request的值传递类似,cookie的传递也用到了meta,具体见代码。如果想看cookie的传递信息,可在settings中设置COOKIES_DEBIG=True。

板块相关文件与逻辑分析

既然要爬取所有帖子,首先就要有所有的板块链接。论坛的讨论区共有10个一级板块。

当打开第一个板块的“+”,会生成ajax_list.json?……文件(红框所示),打开第二个板块,对应文件的链接的sec为1

稍微熟悉些http/https的应该能分析出来,打开一级板块的“+”,后台会向ajax_list.json发起post请求,携带的信息(?后面部分)为uid和root,uid为用户名,root的形式为“sec-X”,X取值为0到9,对应第一到第十个一级板块。当然也可以不这么麻烦,直接构造出10个板块的请求链接,即修改sec-后面的数字。

一级板块有10个,每一个板块下可能还有类似的统领型板块(由几个小版块组成),如[北邮校园]下,[北邮教务处]之下就是具体帖子了,但[社团组织]下还能细分出很多板块。可以理解成我们想看一台电脑上10个文件夹下的所有文件内容,但这些文件下有的打开后就能看到文件,有的则是文件夹嵌套,需要打开很多次才能看到文件。至于判断一个板块否是统领型板块,这个可以通过ajax_list.json中的板块链接进行区分。

正式开始

新建项目与数据库

使用scrapy shell创建项目及其他常用命令

scrapy startproject byrbbs # 创建项目
scrapy genspider byr_section bbs.byr.cn #创建一个爬虫,一个项目中可创建多个
scrapy list #显示当前项目所包括的爬虫
scrapy crawl byr_section #启动爬虫byr_section

爬虫写好后,用crawl命令运行,如果你像我一样使用pycharm写的爬虫,不想要再开个shell窗口运行crawl,可以把这条命令写到一个.py文件中:

# -*- coding: utf-8 -*-
# command.py file
import os
#如果此文件就存于项目目录,可以注释掉下面这行更改工作目录的代码
# os.chdir(r'C:\Users\chen\PycharmProjects\byrbbs')
# os.system('scrapy crawl byr_section') # 运行爬虫byr_section
# os.system('scrapy crawl byr_article -o article_list.xml') # 运行爬虫byr_section,并将item存到xml文件中(在没写pipline前可以先在本地存储)
os.system('scrapy crawl byr_article')

创建数据库表格

CREATE TABLE `section` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`section_url` varchar(60) NOT NULL,
`section_name` varchar(50) NOT NULL,
`section_article_total` int(7) NOT NULL,
`top_section_name` varchar(50) NOT NULL,
`top_section_num` int(2) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
create TABLE `articleinfo` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`section_name` varchar(50) NOT NULL,
`article_title` varchar(80) NOT NULL,
`article_url` varchar(80) NOT NULL,
`article_createtime` date NOT NULL,
`article_comment` int(10) unsigned NOT NULL DEFAULT '0',
`article_author` varchar(50),
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `articlebody` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`article_url` varchar(80) NOT NULL,
`article_content` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

代码分析

文件目录结构

byrbbs/ #项目根目录
├── byrbbs/ #项目文件夹
├── spiders/ #爬虫程序文件夹
├── __init__.py #
├── byr_section.py #
├── byr_config.py #
├── item.py #
├── middlewares.py # 中间件,未作更改,并未使用
├── pipelines.py # 用于数据处理与存储
├── settings.py # 项目配置文件,如pipline的启用,LOG的设置等等
├── scrapy.cfg #自动生成,里面存有settings和deploy的配置
├── .gitignore #git忽略文件,写明了哪些文件不用同步与上传,如数据文件,.pyc文件
├── *****.jpg #截图文件,三幅图分别为mysql中三张表的截图
└── command.py #手动添加的文件,为了项目运行的方便

关键点分析

模拟登陆与cookie传递

def start_requests(self):
return [scrapy.FormRequest("http://bbs.byr.cn/user/ajax_login.json",
formdata=LOGIN_FORMDATA,
meta={'cookiejar': 1},
headers=HEADERS,
callback=self.logged_in)]
def logged_in(self, response):
for url in self.start_urls:
item = ByrSectionItem()
num = int(url[-1])
item['top_section_num'] = num + 1 # 使存储的类别号从1开始
item['top_section_name'] = self.top_section_name[num]
yield scrapy.Request(url, meta={'cookiejar': response.meta['cookiejar'],'item':item}, headers=HEADERS, callback=self.parse)

在scrapy文档中也写有这种方式登陆,将登陆数据,headers等信息传给FormRequest。后续cookie的传递与logged_in函数中的方法一致,通过meta中的cookiejar,而值传递通过其中的item。

文章的处理

# 处理列表,翻页问题
def parse_article_list_pre(self, response):
page_list_num = response.xpath('//*[@class="t-pre-bottom"]/div[1]/ul/li[1]/i/text()').extract()[0]
total_num = int(page_list_num)/self.article_per_list+1 #页数从1到total_num
first_list = response._get_url()
for i in xrange(1,total_num+1):
crawl_list_url = first_list+'?p='+str(i)
yield scrapy.Request(crawl_list_url, meta={'cookiejar': response.meta['cookiejar'],'item':response.meta['item']}, headers=HEADERS,callback=self.parse_article_list)
# 处理列表,获取列表上的每条文章信息与文章链接
def parse_article_list(self, response):
# print "parse_article_list "+response._get_url()
section_name = response.meta['item']['section_name']
sel_article = response.xpath('//*[@class="b-content"]/table/tbody/tr')
article_url = sel_article.xpath('td[2]/a/@href').extract()
article_title = sel_article.xpath('td[2]/a/text()').extract()
article_createtime = sel_article.xpath('td[3]/text()').extract()
article_author = sel_article.xpath('td[4]/a/text()').extract()
article_comment = sel_article.xpath('td[5]/text()').extract()
# 处理列表的每一行,即每一篇文章的信息,存入item
for index, url in enumerate(article_url):
item = ByrArticleItem()
item['section_name'] = section_name
item['article_title'] = article_title[index]
item['article_url'] = response.urljoin(article_url[index])
item['article_createtime'] = article_createtime[index]
item['article_author'] = article_author[index]
item['article_comment'] = article_comment[index]
yield scrapy.Request(item['article_url'], meta={'cookiejar': response.meta['cookiejar'],'item': item}, headers=HEADERS,callback=self.parse_article_content)
# 处理文章主体内容
def parse_article_content(self, response):
article = response.xpath('//div[3]/div[1]/table/tr[2]/td[2]/div[1]').extract()[0]
article = re.sub('</?(font|div).*?>', '', article)
article = re.sub('<br>', '\n', article)
item = response.meta['item']
item['article_content'] = article
yield item

MySQLdb的使用

DB_CONFIG = {'host': 'your_mysql_ip', 'user':'your_mysql_username', 'passwd':'your_mysql_password', 'db':'your_database_name', 'port': 3306, 'charset': 'utf8'}
con = MySQLdb.connect(**DB_CONFIG)
cur = con.cursor()
sql = 'insert into section(section_url,section_name) values(%s,%s)'
values = (item['section_url'], item['section_name'])
cur.execute(sql, values) # second parameter must be iterabale
con.commit()
cur.close()
con.close()

使用装饰器,为不同的item指定pipeline

spider爬取完数据并生成了item,就会传出给pipeline。当有多个爬虫时,就会有多个item,多个pipeline,而所有的item都会按照settings.py文件中设定的顺序依次经过每个pipeline处理。如果你不想让item经过所有的pipeline处理,就需要为item进行指定pipeline操作。这种类似于权限检查的功能可以用装饰器轻松完成。由于我的一个爬虫只对应一类item,所以我的标记是存在了spider上,当然也可以存在item上。

(1) settings.py中启用pipeline

ITEM_PIPELINES = {
'byrbbs.pipelines.ByrSectionPipeline': 300,
'byrbbs.pipelines.ByrArticlePipeline': 400,
}

(2) spider中指定此爬虫后续想要使用的pipeline,进行标记。

# byr_section.py file
class ByrSectionSpider(scrapy.Spider):
pipeline = ['ByrSectionPipeline']
# other code
# byr_article.py file
class ByrArticleSpider(scrapy.Spider):
pipeline = ['ByrArticlePipeline']
# other code

(3) 装饰器,通过spider上的标记完成检测

# pipelines.py file
import functools
def check_pipline(func):
@functools.wraps(func)
def wrapper(self, item, spider):
if self.__class__.__name__ in spider.pipeline:
return func(self, item, spider)
else:
return item
return wrapper
class ByrSectionPipeline(object):
@check_pipline
def process_item(self, item, spider):
# balabala
class ByrArticlePipeline(object):
@check_pipline
def process_item(self, item, spider):
# balabala

LOG的设置

可以在运行shell指令时添加附加设置,但我更习惯在settings.py中写清楚:

# settings.py file
#LOG_ENABLED = False #是否启用LOG,默认启用
LOG_LEVEL = "INFO" #LOG等级,由高到低依次为CRITICAL,ERROR,WARNING,INFO,DEBUG,默认为DEBUG
LOG_FILE = "log_file.txt" #LOG文件,不指定则不生成,LOG信息显示在运行窗口中
# shell command
--logfile "log_file.txt" #日志文件
--loglevel "INFO" or -L "INFO" #日志等级
--nolog #禁用日志

在代码中添加日志消息:

import logging
logging.warning("This is a warning")

在spider中添加日志消息:

import scrapy
class MySpider(scrapy.Spider):
name = 'myspider'
start_urls = ['http://scrapinghub.com']
def parse(self, response):
self.logger.info('Parse function called on %s', response.url)

执行结果

section table

articleinfo table

articlebody table

------ 本文结束 ------