第一步:创建项目
需要用到虚拟环境的话,可以按照网上教程自行配置。
我们跳过这一步,首先在桌面创建一个合适的文件夹并命名为自己想取的名字
第二步:使用命令终端,然后cd到桌面我们创建的文件夹。
第三步:在该文件夹目录下创建项目,运行命令:scrapy startproject myApp(根据自己项目命名)
接下来我们就可以打开pycharm查看项目的目录,可以看到如下结构:
(base) MACdeMBP:ArticleSpider mac$ tree
.
|____ArticleSpider
| |____spiders
| | |______init__.py
| | |____jobbole.py
| | |______pycache__
| | | |____jobbole.cpython-37.pyc
| | | |______init__.cpython-37.pyc
| |______init__.py
| |______pycache__
| | |____settings.cpython-37.pyc
| | |______init__.cpython-37.pyc
| |____middlewares.py
| |____settings.py
| |____items.py
| |____pipelines.py
|____scrapy.cfg
|____main.py
现在我们来理解项目的整体目录结构,以及每个py文件的作用。
1,jobbole.py:爬虫项目中主要负责网页中字段和url提取和解析
2,items.py:负责jobbole中提取数据的处理,这里我们可以引入Itemloader类,ItemLoader 是分离数据的另一种方式,可以将数据的分离和提取分为两部分, 默认使用xpath,css数据提取方式,让代码更加整洁,更加清晰。
3,pipelines.py:用于存储爬取的数据。
4,middlewares.py:下载器中间件,作用:更换代理IP,更换Cookies,更换User-Agent,自动重试
5,settings.py:配置文件。
6,main.py:这是我自己创建方便运行管理的文件
接下来我们认识一下每一个py文件里所做的内容:
# -*- coding: utf-8 -*-
import scrapy
import re,time,datetime
from scrapy.http import Request
from urllib import parse
from ArticleSpider.items import JobBoleArticleItem
from ArticleSpider.utils.common import get_md5
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['blog.jobbole.com']
start_urls = ['http://blog.jobbole.com/all-posts/']
def parse(self, response):
'''
1,获取文章列表页中的文章url并交给解析函数进行具体字段的解析
2,获取下一页的url并交给scrapy进行下载,下载完成后交给parse函数
'''
# 解析列表页中的所有文章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('')
#url拼接好后使用yield关键字返回给scrapy进行下载
yield Request(url=parse.urljoin(response.url,post_url),meta={"front_image_url":image_url},callback=self.parse_detail)
# 提取下一页并交给scrapy进行下载
time.sleep(1)
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)
def parse_detail(self,response):
'''
处理每个url的方法,解析具体文章的逻辑
提取文章的具体字段
'''
article_item = JobBoleArticleItem()
#文章封面图
front_image_url = response.meta.get('front_image_url','')
#提取文章标题
title = response.xpath('//div[@class="entry-header"]/h1/text()').extract()[0]
#提取日期
create_date = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/text()').extract()[0].strip().replace(' ·','')
#提取点赞数目
praise_nums = response.xpath('//span[contains(@class,"vote-post-up")]/h10/text()').extract()[0]
#提取收藏数目(^(\d)* 匹配任意数字
fav_nums = response.xpath('//span[contains(@class,"bookmark-btn")]/text()').extract()[0].strip()
match_re = re.match(r'^(\d)*',fav_nums)
if match_re:
fav_nums = match_re.group()
else:
fav_nums = 0
#提取文章评论数
#comment = response.xpath('//a[@href="#article-comment"]/text()').extract()[0].strip()
comment = response.css('a[href="#article-comment"] span::text').extract()[0].strip()
match_re = re.match(r'^(\d)*',comment)
if match_re:
comment = match_re.group()
else:
comment = 0
#提取网页文章的内容
content = response.xpath('//div[@class="entry"]').extract()[0]
#这一步主要为了提取文章的所属类型
tag_list = response.xpath('//p[@class="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)
article_item["url_object_id"] = get_md5(response.url)
article_item["title"] = title
article_item["url"] = response.url
#转换时间格式,由取得的字符串处理成时间格式,方便存在数据库中
try:
create_date = datetime.datetime.strftime(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["fav_nums"] = fav_nums
article_item["comment"] = comment
article_item["tags"] = tags
article_item["content"] = content
yield article_item
#print(title,create_date,praise_nums,fav_nums,comment,tags,front_image_url)
这里我们定义了一个md5加密函数,这个函数是后来添加的文件utils文件夹中的,它是项目里面的一个包,里面定义了md5解密的函数,可以使用from ArticleSpider.utils.common import get_md5来应用这个方法。字段介绍在代码中已经有具体注释,这里我们以伯乐在线网站为范例。这个py文件主要负责url解析,以及关键字段提取。下面是md5加密函数
import hashlib
def get_md5(url):
if isinstance(url,str):
url = url.encode('utf-8')
m = hashlib.md5()
m.update(url)
return m.hexdigest()
if __name__ == '__main__':
print(get_md5("http://jobbole.com".encode('utf-8')))
python3中所有的str类型可以看做是unicode编码。在python2中,就不需要使用解码了。
pipeline.py定制管道。可以用它来定制自己保存图片的路径,还可以使用它和数据库进行交互,下面与数据库mysql交互用到了两种方法,同步和异步,异步框架twisted中已经给我们定制好了异步机制,和处理异步带来的可能出错的处理,这个可以参照它来写。具体如下。异步可以防止大量数据存储和爬虫速度不同步问题。
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
import codecs,json
import MySQLdb
import MySQLdb.cursors
from scrapy.pipelines.images import ImagesPipeline
from scrapy.exporters import JsonItemExporter
from twisted.enterprise import adbapi
class ArticlespiderPipeline(object):
def process_item(self, item, spider):
return item
class JsonWithEncodingPipeline(object):
# 自定义导出json文件
def __init__(self):
# json文件保存和格式
self.file = codecs.open('article.json','w',encoding='utf-8')
def process_item(self,item,spider):
#调用json.dumps方法以字典形式保存数据
lines = json.dumps(dict(item),ensure_ascii=False)
self.file.write(lines)
return item
def spider_close(self,spider):
self.file.close()
class JsonExporterPipeline(object):
#调用scrapy提供的json_export导出json文件
def __init__(self):
self.file = open('articleexporter.json','wb')
self.exporter = JsonItemExporter(self.file,encoding='utf-8',ensure_ascii=False)
self.exporter.start_exporting()
def close_spider(self,spider):
self.exporter.finish_exporting()
self.file.close()
def process_item(self,item,spider):
self.exporter.export_item(item)
return item
class MysqlPipeline(object):
'''
采用同步的机制插入数据库
'''
def __init__(self):
self.conn = MySQLdb.connect('localhost','root','maple','test2',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,fav_nums,url_object_id,
front_image_url,front_image_path,comment,praise_nums,tags,content)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
self.cursor.execute(insert_sql,(item['title'],item['url'],item['create_date'],item['fav_nums'],
item['url_object_id'],item['front_image_url'],item['front_image_path'],
item['comment'],item['praise_nums'],item['tags'],item['content']))
self.conn.commit()
class MysqlTwistedPipeline(object):
'''
twisted 异步api写入数据到数据库
'''
def __init__(self,dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls,settings):
#从settings文件中引入mysql基础配置
dbparms = dict(
host = settings['MYSQL_HOST'],
db = settings['MYSQL_DBNAME'],
user = settings['MYSQL_USER'],
passwd = settings['MYSQL_PASSWORD'],
charset = 'utf8',
cursorclass = MySQLdb.cursors.DictCursor,
use_unicode = True,
)
dbpool = adbapi.ConnectionPool("MySQLdb",**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)
def handle_error(self,failure):
# 处理异步插入的异常
print(failure)
def do_insert(self,cursor,item):
# 执行具体插入
insert_sql = """
insert into jobbole_article(title,url,create_date,fav_nums,url_object_id,
front_image_url,comment,praise_nums,tags,content)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
cursor.execute(insert_sql, (item['title'], item['url'], item['create_date'], item['fav_nums'],
item['url_object_id'], item['front_image_url'],
item['comment'], item['praise_nums'], item['tags'], item['content']))
class ArticleImagePipeline(ImagesPipeline):
'''
处理图片,保存在自己设置的path
'''
def item_completed(self, results, item, info):
for ok,value in results:
image_file_path = value['path']
item['front_image_path'] = image_file_path
return item