# 安装虚拟环境使用
pip install -i https://pypi.douban.com/simple/ virtualenv
# 切换到自定义目录
virtualenv scrapy_article
# 进入里面的Scripts文件夹
activate.bat # 激活环境
# 我的Windows中只有Python3,如果你想指定Python2虚拟环境,需要创建时通过 -p 参数指定py2安装位置
# 也有其他创建虚拟环境的方法,还可以建立workon环境变量,方便操作
pip install -i https://pypi.douban.com/simple/ virtualenvwrapper-win
workon # wrapper自带命令,列出所有虚拟环境
# 设置WORKON_HOME环境变量,指定创建目录
mkvirtualenv [name] # 创建虚拟环境
workon [name] # 进入指定虚拟环境并激活
deactivate # 退出当前虚拟环境
rmvirtualenv [name] # 删除虚拟环境
workon py3scrapy_article
scrapy startproject ArticleSpider # 命令行创建工程
# 在pycharm打开并导入,更换解释器为虚拟环境
# 新建main.py
from scrapy.cmdline import execute
import sys
import os
# dirname是获取父目录;file指定当前文件
sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 加入ArticleSpider目录到环境变量,方便运行scrapy命令
# 现在,就相当于我们可在cmd中 E:\pythonPackage\ArticleSpider> 下执行命令
execute(['scrapy','crawl','jobbole']) # 相当于执行:scrapy crawl jobbole
settings.py
中关闭robots协议(每个网站都有robots协议,过滤爬虫)HTTPERROR_ALLOWED_CODES = [404]
parse
中使用xpath提取信息了
id
、class
、name
等等|
符号表示前后两种匹配方式,两种方式不是一回事,别想着用前一种的前缀//
代表所有,得看前面是谁,前面没人,就是从整个文档下选取
/
后不加序号[]
即可键:值
start_urls
中,文章包含的信息如下 :浏览器右键Copy/Copy Xpath
即可
import scrapy
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['jobbole.com']
start_urls = ['http://www.jobbole.com/keji/qkl/170004.html'] # http://www.jobbole.com/keji
def parse(self, response):
headline = response.xpath('/ html / body / div[3] / div[1] / div[3] / div[1] / h1') # Chrome
pass
一直显示404:加了User-Agent之类的也不行,反爬?
INFO: Ignoring response <404 http://www.jobbole.com/keji/qkl/170004.html>
在cmd里面也提供了工具,激活环境:scrapy shell 网址
,即可开始调试
提取作者出版社等这一块信息:啃了半天
infos_span = response.xpath('//*[@id="info"]/span[@class="pl"]/text() | //*[@id="info"]/span/span[@class="pl"]/text()').extract()
# [' 作者', '出版社:', '出品方:', '副标题:', '原作名:', ' 译者', '出版年:', '页数:', '定价:', '装帧:', 'ISBN:'] 终于成功了!
# print(infos_span)
infos_text = response.xpath('//*[@id="info"]/text() | //*[@id="info"]/span/a/text() | //*[@id="info"]/a/text()').extract()
print(infos_text)
# 下面这部分是将数组中只含特殊字符(\n\t\r)的元素去除,如果包含空格之类的可以用strip()或者replace()
regex = '.*(\S+).*' # 匹配任何非空白字符
span = []
for i in range(len(infos_span)):
match = re.match(regex, infos_span[i])
if match:
span.append(infos_span[i])
text = []
for i in range(len(infos_text)):
# infos_text[i].replace('\r','').replace('\n','').replace('\t','') # 不能直接extract().strip()
match = re.match(regex, infos_text[i])
if match:
text.append(infos_text[i])
print(span)
print(text) # 可以拼接了
# 内容简介
simple_content = response.xpath('//*[@id="link-report"]/span[2]/div/div/p/text()').extract()
# 作者简介
author = response.xpath('//*[@id="content"]/div/div[1]/div[3]/div[2]/span[2]/div/p/text()').extract()
# 所有标签
tags = response.xpath('//*[@id="db-tags-section"]/div/span/a/text()').extract()
div/img/h1/span
,范围大#
.
. #
,注意格式,紧跟着写(大部分定位都用这俩)>
符号(组)+
就是下一个~
符号[]
表示了::text
,即xpath中的/text(),举个例子吧:提取书名header = response.css('span[property="v:itemreviewed"]::text').extract_first("")
# 这个提取方法的作用是防止数组为空直接报错!
response
,再进行parse字段的
::attr()
urls = response.css('#subject_list ul li .pic a::attr(href)').extract()
url_next_page = response.css('.paginator .next a::attr(href)').extract_first()
# '/tag/东野圭吾?start=20&type=T'
regex = '.*([?]start\S+)' # 写正则的经验不足啊!
match = re.match(regex, url_next_page)
# 完整代码
import scrapy
import re
from scrapy import Request
from urllib import parse
class JobboleSpider(scrapy.Spider):
name = 'jobbole'
allowed_domains = ['book.douban.com']
start_urls = ['https://book.douban.com/tag/%E4%B8%9C%E9%87%8E%E5%9C%AD%E5%90%BE'] # http://www.jobbole.com/keji
def parse(self, response, **kwargs):
'''
获取当前页所有URL,交给scrapy下载
'''
urls = response.css('#subject_list ul li .pic a::attr(href)').extract() # 提取某一属性,使用::attr()
# print(urls)
for url in urls:
# # callback是异步调用的,调试可以用run tu cursor 查看article_detail的使用情况
yield Request(url=url, callback=self.article_detail) # 下载完成后回调字段解析函数,注意,不需要加()调用
# 使用生成器(类似子进程中断,可以暂停,然后恢复继续执行)
'''
下一页URL,交给scrapy下载
'''
url_next_page = response.css('.paginator .next a::attr(href)').extract_first()
# print(url_next_page) # '/tag/东野圭吾?start=20&type=T'
regex = '.*([?]start\S+)' # 写正则的经验不足啊!
match = re.match(regex, url_next_page)
# print(match.group(1))
if match.group(1):
# 如果提供的url不完整,需要拼接主url
yield Request(url=parse.urljoin(response.url, match.group(1)), callback=self.parse) # 就形成循环,直到没有下一页!函数执行结束
pass
def article_detail(self, response):
'''
文章详情解析函数
:param response:
:return:
'''
headline = response.xpath('//*[@id="wrapper"]/h1/span/text()').extract()[0] # Chrome
# headline2 = response.xpath('/html/body/div[3]/div[1]/div[3]/div[1]/h1') # FireFox
# print(headline)
infos_span = response.xpath(
'//*[@id="info"]/span[@class="pl"]/text() | //*[@id="info"]/span/span[@class="pl"]/text()').extract()
# [' 作者', '出版社:', '出品方:', '副标题:', '原作名:', ' 译者', '出版年:', '页数:', '定价:', '装帧:', 'ISBN:'] 终于成功了!
# print(infos_span)
infos_text = response.xpath(
'//*[@id="info"]/text() | //*[@id="info"]/span/a/text() | //*[@id="info"]/a/text()').extract()
# print(infos_text)
# 下面这部分是将数组中只含特殊字符(\n\t\r)的元素去除,如果包含空格之类的可以用strip()或者replace()
regex = '.*(\S+).*' # 匹配任何非空白字符
span = []
for i in range(len(infos_span)):
match = re.match(regex, infos_span[i])
if match:
span.append(infos_span[i])
text = []
for i in range(len(infos_text)):
# infos_text[i].replace('\r','').replace('\n','').replace('\t','') # 不能直接extract().strip()
match = re.match(regex, infos_text[i])
if match:
text.append(infos_text[i])
# print(span)
# print(text)
simple_content = response.xpath('//*[@id="link-report"]/span[2]/div/div/p/text()').extract() # 内容简介
author = response.xpath('//*[@id="content"]/div/div[1]/div[3]/div[2]/div/div/p/text()').extract() # 作者简介
# print(simple_content)
# print(author)
tags = response.xpath('//*[@id="db-tags-section"]/div/span/a/text()').extract() # 所有标签
# print(tags)
run to cursor
# items.py
class DoubanArticleItem(scrapy.Item):
front_img_url = scrapy.Field()
front_img_path = scrapy.Field()
title = scrapy.Field()
infos_span = scrapy.Field()
infos_text = scrapy.Field()
simple_content = scrapy.Field()
simple_author = scrapy.Field()
tags = scrapy.Field()
scrapy,Field
,不像Django中对应数据库各种类型的数据# 解析函数中定义:
article_item = DoubanArticleItem()
article_item['front_img'] = [front_img]
article_item['title'] = title
article_item['infos_span'] = infos_span
article_item['infos_text'] = infos_text
article_item['simple_content'] = simple_content
article_item['tags'] = tags
# 在settings中打开ITEM_PIPELINES,会执行到pipelines中的process_item
# 中间件原理,底层中断监听实现
scrapy-djangoitem
,可以将Django的ORM集成到scrapymodel
的方法,这个扩展可以安装试试(如果你学过Django的话)site-packages\scrapy\pipelines\images.py
中可以看到# settings.py
ITEM_PIPELINES = {
'ArticleSpider.pipelines.ArticlespiderPipeline': 300,
'scrapy.pipelines.images.ImagesPipeline':1, # 数值越小越先处理
}
# 类会从这里取值,类似config文件
IMAGES_URLS_FIELD = 'front_img' # ImagesPipeline类会找item中的这个字段
# 图片的存放路径我们搞成相对定位
import os
projecct_path = os.path.abspath(os.path.dirname(__file__)) # ArticleSpider路径
IMAGES_STORE = os.path.join(projecct_path, 'images')
# 还要安装pillow库 pip install -i https://pypi.douban.com/simple pillow
class ArticleImagePipeline(ImagesPipeline):
def item_completed(self, results, item, info):
'''
重载
:param results: list,每个元素是字典 [{0:True/False},{1:{path:full/xxx.jpg}}]
:param item: 在parse中初始化的字段item
:param info:
:return:
'''
# print('result:',results)
if "front_img_url" in item:
for ok, value in results:
image_file_path = value["path"]
item["front_img_path"] = image_file_path
print(image_file_path)
# 获取的是图片保存路径,应该就是:图片已经下载好(item_completed),让item中的这个属性记录保存路径!
return item # settings中的下一个pipeline还要处理
front_img_path
字段啥时候存?别急!# pipeline.py
import codecs
import json
class JsonWithEncodingPipeline(object):
'''
处理item的各个字段,变为json格式并写入
主要逻辑写在process_item
别忘了返回item
在settings中注册
'''
def __init__(self):
self.file = codecs.open('article.json', 'w', encoding='utf-8')
def process_item(self, item, spider):
lines = json.dump(dict(item), ensure_ascii=False)+'\n' # 变成json格式的文件
self.file.write(lines)
return item
def spider_closed(self,spider): # 信号量(中间件)
self.file.close()
class MySQLPipeline(object):
'''
保存字段数据到数据库
'''
def __init__(self):
# 链接数据库
# 'host','user','password','dbname'
self.conn = MySQLdb.connect('127.0.0.1','root','123456', 'article',charset='utf8', use_unicode=True)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = """
insert into article_douban(title, front_img_url, infos_span, infos_text, simple_content, simple_author, tags) VALUES(%s, %s, %s, %s, %s, %s, %s)
"""
# 这里都用str()处理一下
self.cursor.execute(sql, (str(item['title']), str(item['front_img_url']), str(item['infos_span']), str(item['infos_text']), str(item['simple_content']), str(item['simple_author']), str(item['tags'])))
self.conn.commit()
return item
# 在settings中注册
# 异步IO:下载、上传、保存到数据库等,都可以考虑twisted
class MySQLTwistedPipeline(object):
def __init__(self, dbpool):
self.dbpool = dbpool
# 这是个类方法,scrapy初始化的时候就会调用,返回dbpool,当前定义的pipeline工作时,实例化后就会接收此返回值
@classmethod
def from_settings(cls, settings): # 会将当前spider的settings传过来,名称固定
dbparams = dict(
host = settings['MYSQL_HOST'],
user = settings['MYSQL_USER'],
passwd = settings['MYSQL_PASSWORD'],
db = settings['MYSQL_DBNAME'],
charset = 'utf8',
cursorclass = MySQLdb.cursors.DictCursor,
use_unicode = True
)
# 连接池,指明用哪个数据库,然后传参
dbpool = adbapi.ConnectionPool('MySQLdb', **dbparams)
# 容器的思想
return cls(dbpool)
def process_item(self, item, spider):
# 异步执行数据库操作
query = self.dbpool.runInteraction(self.do_insert, item)
# 异步的错误处理
query.addErrback(self.handle_error)
def handle_error(self, failure, item, spider):
print(failure)
def do_insert(self, cursor, item):
sql = """
insert into article_douban(title, front_img_url, infos_span, infos_text, simple_content, simple_author, tags) VALUES(%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(sql, (str(item['title']), str(item['front_img_url']), str(item['infos_span']), str(item['infos_text']),\
str(item['simple_content']), str(item['simple_author']), str(item['tags'])))
ItemLoader
,整理代码# 修改spider
from scrapy.loader import ItemLoader
# 使用itemloader加载item
item_loader = ItemLoader(item=DoubanArticleItem(), response=response)
item_loader.add_value("front_img_url", front_img) # 直接添加值即可,无需提取
# item名称,提取规则
item_loader.add_xpath('title', '//*[@id="wrapper"]/h1/span/text()')
item_loader.add_xpath('infos_span', '//*[@id="info"]/span[@class="pl"]/text() | //*[@id="info"]/span/span[@class="pl"]/text()')
item_loader.add_xpath('infos_text', '//*[@id="info"]/text() | //*[@id="info"]/span/a/text() | //*[@id="info"]/a/text()')
item_loader.add_xpath('simple_content', '//*[@id="link-report"]/div[1]/div/p/text()')
item_loader.add_xpath('simple_author', '//*[@id="content"]/div/div[1]/div[3]/div[2]/span[2]/div/p/text()')
item_loader.add_xpath('tags', '//*[@id="db-tags-section"]/div/span/a/text()')
article_item = item_loader.load_item()
yield article_item
# 代码是不是看着清爽多了?
# items.py
from scrapy.loader.processors import MapCompose, TakeFirst
def regex_infos(value):
'''
处理item提取出的内容
这里是每次将list中的一个元素传过来
:param value:
:return:
'''
regex = '.*(\S+).*' # 匹配任何非空白字符
match = re.match(regex, value)
if match:
return value
return None
class ArticleItemLoader(ItemLoader):
'''
如果我们都是取第一个元素,不用在Field()都写一遍
'''
# 自定义default
# default_output_processor = MapCompose(empty) # 会在前面报错!
class DoubanArticleItem(scrapy.Item):
front_img_url = scrapy.Field()
front_img_path = scrapy.Field()
title = scrapy.Field(
# 只取list第一个
# output_processor = TakeFirst()
)
infos_span = scrapy.Field(
# 这里也可以传递匿名函数
input_processor=MapCompose(regex_infos)
)
infos_text = scrapy.Field(
input_processor=MapCompose(regex_infos)
)
simple_content = scrapy.Field(
)
simple_author = scrapy.Field(
)
tags = scrapy.Field()