scrapy整体流程
scrapy的官方文档: https://docs.scrapy.org/en/latest/
引擎(engine)
scrapy的核心, 所有模块的衔接, 数据流程梳理.
调度器(scheduler)
本质上这东西可以看成是一个队列. 里面存放着一堆我们即将要发送的请求. 可以看成是一个url的容器. 它决定了下一步要去爬取哪一个url. 通常我们在这里可以对url进行去重操作.
下载器(downloader)
它的本质就是用来发动请求的一个模块. 小白完全可以把它理解成是一个get_page_source()的功能. 只不过这货返回的是一个response对象.
爬虫(spider)
负责解析下载器返回的response对象.从中提取到我们需要的数据.
管道(pipeline)
主要负责数据的存储和各种持久化操作.
1.创建工程
首先,打开控制台,cd到你想创建工程的文件夹下
scrapy startproject 项目名
提示创建成功
cd到spider文件夹内创建爬虫
scrapy genspider 爬虫名 要爬取的网站域名
这里项目名是你爬虫整个项目的名称
爬虫名是spider文件名,注意区分
这两个名称不允许一样,否则会报错
经过上述步骤之后,会出现下面这样的一系列文件夹
├── mySpider_2
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ ├── __init__.py
│ └── 爬虫名.py # 多了一个这个.
└── scrapy.cfg
说明你这个爬虫项目 成功创建了
2 进行相关设置
打开settings,发现其中有很多参数被注释了
我的习惯是五步走
(1)不看日志:LOG_LEVEL = 'WARNING'
(2)不遵守robot_text协议:ROBOTSTXT_OBEY = False
(3)设置代理ip,这部分后边细说
(4)降低请求数 CONCURRENT_REQUESTS = 4 不要太高容易崩
(5)在文件夹最外层创建spider-run文件,省的每次都要scrapy crawl
1.日志很多看起来很乱,编写的时候不需要看,维护的时候尽量打开,比较容易发现哪里出错
2.如果遵守协议你什么也拿不到
3.下边详细说
4.请求数不要太高,很容易被网站检测出你是爬虫,从而拒绝你的请求
5.spider_run的功能,就是运行爬虫,如果没有这个文件每次都要在终端里scrapy crawl
spider_run代码如下
from scrapy import cmdline
cmdline.execute("scrapy crawl spider_name".split())
建立完spider_run后,文件结构如下(spider_run一定会要建立在最外层,否则不生效)
├── mySpider_2
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ ├── __init__.py
│ └── 爬虫名.py # 多了一个这个.
└── scrapy.cfg
└── spider_run.py
3.静态页面spider
在写spider之前,你首先应该观察你要爬的界面(右键检查),是静态还是动态,
静态直接HTML获取对应字段,
动态需要抓包
完成创建后,打开spider文件会发现以下代码
import scrapy
class YouxiSpider(scrapy.Spider):
name = 'youxi' # 该名字非常关键, 我们在启动该爬虫的时候需要这个名字
allowed_domains = ['4399.com'] # 爬虫抓取的域.单网站爬取可以注释掉
start_urls = ['http://www.4399.com/flash/'] # 起始页
def parse(self, response, **kwargs):
# response.text # 页面源代码
# response.xpath() # 通过xpath方式提取
# response.css() # 通过css方式提取
# response.json() # 提取json数据
# 用我们最熟悉的方式: xpath提取游戏名称, 游戏类别, 发布时间等信息
li_list = response.xpath("//ul[@class='n-game cf']/li")
for li in li_list:
name = li.xpath("./a/b/text()").extract_first()
category = li.xpath("./em/a/text()").extract_first()
date = li.xpath("./em/text()").extract_first()
# 存储为字典
dic = {
"name": name,
"category": category,
"date": date
}
# 将提取到的数据提交到管道内.
yield dic # 注意, 这里只能返回 request对象, 字典, item数据, or None
这个spider中有三个地方需要注意
(1)解析
解析页面,自己去看xpath css 正则表达式
(2)parse函数间数据的传递
当一个参数需要再每个函数之间传递时,在一个parse函数末尾,现将传递参数内容和他的参数名对应起来,再使用mate函数进行传递
for ind, url in enumerate(main_category_list):
main_category_name = main_category_names[ind]
# meta用于再不同函数之间传递参数
yield scrapy.Request(url, callback=self.keshi_detail, meta={'main_category_name': main_category_name})
信息传递出去了肯定要在keshi_detail函数中接收啊
def keshi_detail(self, response):
main_category_name = response.meta['main_category_name']
(3)传递数据给item时的格式
当你将数据yield item时,请注意,一定要是item,不要用字典dic或者list
最简便的方法就是直接传到对应字段,参考以下代码
item['字段名']= response.xpath(.........)
直接将items中的字段名复制到这个位置,标准又完美,其实就是把数据结构定义好
这样你爬到的数据直接被yield到item中的对应字段
3.动态页面spider
对于动态页面有一个很明显的特征,在同一网址下的子链接,无论如何刷新都是同一个链接,这个链接,你对他直接进行请求,网站会拒绝你的请求,返回状态码400
这时,你要组装好你的headers,也就是请求头,网页拒绝你的请求就是因为你的请求头不对,请求头包含很多种参数,有些网页甚至要求你一一对应才会允许你的请求
这里我列一个比较稳妥的方案,你去要爬取的网页,右键检查,找出请求头的所有信息,全部复制下来,最后组装成一个字典,这样网页要检查哪个,随它去
request_headers = b"""
:authority: www.xxxxx.cn
:method: GET
:path: /work/index.htm?from=index
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: max-age=0
cookie: _ga=GA1.2.1425286857.1577928625; route=2bf12a7e3d18498383e2970557db636f; dxy_da_cookie-id=4fd84d1297e8a4c397d35355c62f03131578878840013; Hm_lvt_17521045a35cb741c321d016e40c7f95=1578561472,1578620137,1578626115,1578878841; _gid=GA1.2.1118274719.1578878841; JOSESSIONID=459F925E8F455EB53F24BE736297EEF5-n2; _gat=1; Hm_lpvt_17521045a35cb741c321d016e40c7f95=1578894839
referer: https://www.jobmd.cn/work/index.htm?from=index
sec-fetch-mode: navigate
sec-fetch-site: same-origin
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
"""
headers = headers_raw_to_dict(request_headers)
尤其注意这里有一个关键的字段cookies,这个字段带着请求的身份信息,你复制登录的请求cookie,可以免密码登录,有的网页的cookie带时间戳需要自己组装
写好了请求头,解析界面发现是一堆乱码,什么也没有,
这是因为网页要知道你要请求哪些数据,才能给你返回,也就是常说的带数据返回
4.items.py
从上边spider中拿到的数据,无一例外最终都yield item,这个item可以理解为是一个数据库的自定义表,在数据库中有每个字段相应的位置
这部分很容易,格式是一样的
里面有你获取到的每个字段名字,
字段名 = scrapy.Field()
存储你每个字段的数据然后传递给pipline(管道)
itrem.py文件中的代码格式:
import scrapy
class GameItem(scrapy.Item):
# 定义数据结构
name = scrapy.Field()
category = scrapy.Field()
date = scrapy.Field()
这里需要注意的是,当你设置了很多个item时,
有一个问题,都是yield item 那程序怎么知道那个字段要进入哪个item呢?
你要先声明。你导入的是哪个item,我这里声明的是我上边在item里定义好的,那我后续进行传递参数,就会往这个GameItem里传递
item = GameItem()
item['name']= response.xpath(.........)
item['category']= response.xpath(.........)
item['date']= response.xpath(.........)
yield item
何必是item呢
大汉堡 = GameItem()
大汉堡['name']= response.xpath(.........)
大汉堡['category']= response.xpath(.........)
大汉堡['date']= response.xpath(.........)
yield 大汉堡
开玩笑,中文相当不专业。。。
5. pipline.py(管道)
你想用pipline 请先去设置(settings)里打开pipline,打开pipline,打开pipline,重要的说三遍,不打开是不生效的
ITEM_PIPELINES = {
'caipiao.pipelines.CaipiaoFilePipeline': 300,
}
从item中的传出来的数据进入了pipline. pipline主要负责数据的存储.你可以存为任何你想要的格式,
(1) 存储为csv文件
写入文件是一个非常简单的事情. 直接在pipeline中开启文件即可.
class CaipiaoFilePipeline:
def process_item(self, item, spider):
with open("pig.txt", mode="a", encoding='utf-8') as f:
# 写入文件
f.write(f"{item['name']}, {'_'.join(item['age'])}, {'_'.join(item['sex'])}\n")
return item
但是你这个代码写的意思是,你没存储一条数据就要打开一次文件,如果我有上千万条数据岂不是要打开上千次
我希望的是, 只打开一次这个文件, 我可以在pipeline中创建两个方法, 一个是open_spider(), 另一个是close_spider(). 看名字也能明白其含义:
open_spider(), 在爬虫开始的时候执行一次
close_spider(), 在爬虫结束的时候执行一次
这样我就能打开一次文件,然后存储,爬虫运行完毕,关闭这个文件
class CaipiaoFilePipeline:
def open_spider(self, spider):
self.f = open("caipiao.txt", mode="a", encoding='utf-8')
def close_spider(self, spider):
if self.f:
self.f.close()
def process_item(self, item, spider):
# 写入文件
self.f.write(f"{item['name']}, {'_'.join(item['age'])}, {'_'.join(item['sex'])}\n")
return item
(2)mysql数据库写入
首先, 在open_spider中创建好数据库连接. 在close_spider中关闭链接. 在proccess_item中对数据进行保存工作.
先把mysql相关设置丢到settings里
# MYSQL配置信息
MYSQL_CONFIG = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "test123456",
"database": "spider",
}
from caipiao.settings import MYSQL_CONFIG as mysql
import pymysql
class CaipiaoMySQLPipeline:
def open_spider(self, spider):
self.conn = pymysql.connect(host=mysql["host"], port=mysql["port"], user=mysql["user"], password=mysql["password"], database=mysql["database"])
def close_spider(self, spider):
self.conn.close()
def process_item(self, item, spider):
# 写入文件
try:
cursor = self.conn.cursor()
sql = "insert into caipiao(qihao, red, blue) values(%s, %s, %s)"
red = ",".join(item['red_ball'])
blue = ",".join(item['blue_ball'])
cursor.execute(sql, (item['qihao'], red, blue))
self.conn.commit()
spider.logger.info(f"保存数据{item}")
except Exception as e:
self.conn.rollback()
spider.logger.error(f"保存数据库失败!", e, f"数据是: {item}") # 记录错误日志
return item
别忘了把pipeline设置一下
ITEM_PIPELINES = {
'caipiao.pipelines.CaipiaoMySQLPipeline': 301,
}
(3) mongodb数据库写入
mongodb数据库写入和mysql写入如出一辙…不废话直接上代码吧
MONGO_CONFIG = {
"host": "localhost",
"port": 27017,
'has_user': True,
'user': "python_admin",
"password": "123456",
"db": "python"
}
from caipiao.settings import MONGO_CONFIG as mongo
import pymongo
class CaipiaoMongoDBPipeline:
def open_spider(self, spider):
client = pymongo.MongoClient(host=mongo['host'],
port=mongo['port'])
db = client[mongo['db']]
if mongo['has_user']:
db.authenticate(mongo['user'], mongo['password'])
self.client = client
self.collection = db['caipiao']
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.collection.insert({"qihao": item['qihao'], 'red': item["red_ball"], 'blue': item['blue_ball']})
return item
ITEM_PIPELINES = {
# 三个管道可以共存~
'caipiao.pipelines.CaipiaoFilePipeline': 300,
'caipiao.pipelines.CaipiaoMySQLPipeline': 301,
'caipiao.pipelines.CaipiaoMongoDBPipeline': 302,
}
4. 文件保存
这里面对的场景是,某个网站上有一个文件,我想要讲这些文件批量的下载下来,
在网页上,点击下载,就能下载这个文件,但背后的原理是,你点击的时候,相当于发送了一个请求,服务器会响应给你下载链接。这样电脑就会下载,我们需要拿到的也是这个下载链接,我曾经爬药监局的审批文件就是这个套路
(1)首先,建好项目, 在items中 定义好数据结构
class MeinvItem(scrapy.Item):
name = scrapy.Field()
img_url = scrapy.Field()
img_path = scrapy.Field()
(2)完善spider, 注意看yield scrapy.Request()
import scrapy
from meinv.items import MeinvItem
class TupianzhijiaSpider(scrapy.Spider):
name = 'tupianzhijia'
allowed_domains = ['tupianzj.com']
start_urls = ['https://www.tupianzj.com/bizhi/DNmeinv/']
def parse(self, resp, **kwargs):
li_list = resp.xpath("//ul[@class='list_con_box_ul']/li")
for li in li_list:
href = li.xpath("./a/@href").extract_first()
# 拿到href为了什么? 进入详情页啊
"""
url: 请求地址
method: 请求方式
callback: 回调函数
errback: 报错回调
dont_filter: 默认False, 表示"不过滤", 该请求会重新进行发送
headers: 请求头.
cookies: cookie信息
"""
yield scrapy.Request(
url=resp.urljoin(href), # scrapy的url拼接
method='get',
callback=self.parse_detail,
)
# 下一页
next_page = resp.xpath('//div[@class="pages"]/ul/li/a[contains(text(), "下一页")]/@href').extract_first()
if next_page:
yield scrapy.Request(
url=resp.urljoin(next_page),
method='get',
callback=self.parse
)
def parse_detail(self, resp):
img_src = resp.xpath('//*[@id="bigpic"]/a[1]/img/@src').extract_first()
name = resp.xpath('//*[@id="container"]/div/div/div[2]/h1/text()').extract_first()
meinv = MeinvItem()
meinv['name'] = name
meinv['img_url'] = img_src
yield meinv
关于Request()的参数:
url: 请求地址
method: 请求方式
callback: 回调函数
errback: 报错回调
dont_filter: 默认False, 表示"不过滤", 该请求会重新进行发送
headers: 请求头.
cookies: cookie信息
接下来就是下载问题了. 如何在pipeline中下载一张图片呢?
Scrapy早就帮你准备好了. 在Scrapy中有一个ImagesPipeline可以实现自动图片下载功能.
from scrapy.pipelines.images import ImagesPipeline, FilesPipeline
import pymysql
from meinv.settings import MYSQL
import scrapy
class MeinvPipeline:
def open_spider(self, spider):
self.conn = pymysql.connect(
host=MYSQL['host'],
port=MYSQL['port'],
user=MYSQL['user'],
password=MYSQL['password'],
database=MYSQL['database']
)
def close_spider(self, spider):
if self.conn:
self.conn.close()
def process_item(self, item, spider):
try:
cursor = self.conn.cursor()
sql = "insert into tu (name, img_src, img_path) values (%s, %s, %s)"
cursor.execute(sql, (item['name'], item['img_src'], item['img_path']))
self.conn.commit()
except:
self.conn.rollback()
finally:
if cursor:
cursor.close()
return item
class MeinvSavePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
# 发送请求去下载图片
# 如果是一堆图片. 可以使用循环去得到每一个url, 然后在yield每一个图片对应的Request对象
return scrapy.Request(item['img_url'])
def file_path(self, request, response=None, info=None):
# 准备好图片的名称
filename = request.url.split("/")[-1]
return f"img/{filename}"
def item_completed(self, results, item, info):
# 文件存储的路径
ok, res = results[0]
# print(res['path'])
item['img_path'] = res["path"]
return item
最后, 需要在settings中设置以下内容:
MYSQL = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "test123456",
"database": 'spider'
}
ITEM_PIPELINES = {
'meinv.pipelines.MeinvPipeline': 303,
'meinv.pipelines.MeinvSavePipeline': 301,
}
# 图片保存路径 -> ImagesPipeline
IMAGES_STORE= './my_tu'
# 文件保存路径 -> FilesPipeline
FILES_STORE = './my_tu'