1、创建项目:scrapy startproject tubatu_scrapy_project
2、梳理需要爬取的内容:
爬取土巴兔
- url:https://xiaoguotu.to8to.com/tuce/【土巴兔的装修图册】
- 爬取的字段:在
items.py
文件声明需要爬取的字段👇
content_name = scrapy.Field() # 装修名称
content_id = scrapy.Field() # 装修id
content_url = scrapy.Field() # 请求的url地址
nick_name = scrapy.Field() # 上传图片人的昵称
pic_url = scrapy.Field() # 图片的url地址
pic_name = scrapy.Field() # 图片名称
- 通过模板生成
spider
文件:scrapy genspider tubatu xiaoguotu.to8to.com/tuce/
3、编写如下代码:
1》通过xpath
获取该装修页面的所有信息块,如下图(各个信息块)所示:
2》遍历这些信息块,获取各个信息块的名称
和url
(代码中粉色部分)
3》通过yield
发送异步请求,传递两个参数:url
和回调函数
。使用break
(即只输出第一个信息块的内容),并在回调函数
中输出第一个信息块的网页源码
4、进入到第一个信息块页面继续分析:
》第一个信息块页面的地址:https://xiaoguotu.to8to.com/c2967148.html 而该网址的后面一串数字编码就是装修id。该页面如下图所示👇:
》最终发现,若想要获取图片的名称、图片的url、上传图片人的昵称等信息需要通过浏览器的调式工具中的Network
获取相应的json
数据,里面便是需要用到的信息。
》该json
数据地址:https://xiaoguotu.to8to.com/case/list?a2=0&a12=&a11=2967148&a1=0 【且该地址中的最后一串数字也是装修id】因此,我们的抓取数据的url地址更改为此json
数据地址。该页面如下图所示👇
》最终编写如下代码:
- 其中用正则获取网页上的装修id、并将
json
数据地址作为请求的url地址(在yield
异步请求中作为url参数) - 导入之前编写的
items.py
中的字段,用for循环
遍历json
数据地址的内容,从而获取相应的字段数据,该代码爬取到相应的上传图片人的昵称、图片的url地址 和 图片名称 - 但输出的内容有20条、且除了第一个信息块的信息完整以外,其他的信息块的信息都缺少图片名称。因此还需要继续完善。
# -*- coding: utf-8 -*-
import scrapy
import re
import json
from tubatu_scrapy_project.items import TubatuScrapyProjectItem
class TubatuSpider(scrapy.Spider):
name = 'tubatu' # 爬虫项目名称
allowed_domains = ['xiaoguotu.to8to.com'] # 允许爬取的网址
start_urls = ['https://xiaoguotu.to8to.com/tuce/p_1.html'] # 从哪个网页开始爬取
# 默认的解析方法
def parse(self, response):
# print(response.text) # 打印请求的网页的源码
pic_item_list = response.xpath("//div[@oldaid]") # 获取页面上的所有信息块
# 因为response就是一个Html对象,所以response后面可以直接使用xpath方法
# 使用正则提取url地址,以备从中获取id
content_id_search = re.compile(r"(\d+)\.html")
for item in pic_item_list:
# 获取项目的名称
content_name = item.xpath("./div/a/text()").extract_first()
# extract_first()是xpath语句的方法,返回的是一个string字符串,是list数组里面的第一个字符串
# 获取项目的url 该url地址包含id的信息,且该id有用处,所以需要在前面使用正则来提取
content_url = "http://"+item.xpath("./div/a/@href").extract_first()
# 使用正则提取id
content_id = content_id_search.search(content_url).group(1)
content_ajax_url = "https://xiaoguotu.to8to.com/case/list?a2=0&a12=&a11={}&a1=0&a17=1".format(content_id)
# 使用yield来发送异步请求
# 使用scrapy.Request()来发送请求,传递两个参数,其中callback(回调函数),只写方法的名称即可
yield scrapy.Request(url=content_ajax_url, callback=self.handle_pic_parse)
break
def handle_pic_parse(self, response):
# print(response.text) 返回的是json格式
pic_dict_data = json.loads(response.text)["dataImg"] # json.loads()方法将json格式数据转换为字典
for pic_item in pic_dict_data:
for item in pic_item["album"]:
tubatu_info = TubatuScrapyProjectItem() # 使用在items.py中定义的字段,因此需要导入相应的TubatuScrapyProjectItem包
tubatu_info["nick_name"] = item["l"]["n"] # 上传图片人的昵称
tubatu_info["pic_url"] = "https://pic1.to8to.com/case/" + item["l"]["s"] # 图片的url地址
tubatu_info["pic_name"] = item["l"]["t"] # 图片名称
print(tubatu_info)
5、完善代码:
- 将其他字段信息也引入。即:声明
info
字典,对其他字段封装到字典中,并在yield
请求中传入meta
参数 - 并对
json
数据地址的内容进行条件判断,只获取有效的信息(有图片名称的信息才要,即:只获取第一个信息块【因为第一个信息块才有图片名称】)。
》因此代码如下:
# -*- coding: utf-8 -*-
import scrapy
import re
import json
from tubatu_scrapy_project.items import TubatuScrapyProjectItem
class TubatuSpider(scrapy.Spider):
name = 'tubatu' # 爬虫项目名称
allowed_domains = ['xiaoguotu.to8to.com'] # 允许爬取的网址
start_urls = ['https://xiaoguotu.to8to.com/tuce/p_1.html'] # 从哪个网页开始爬取
# 默认的解析方法
def parse(self, response):
# print(response.text) # 打印请求的网页的源码
pic_item_list = response.xpath("//div[@oldaid]") # 获取页面上的所有信息块
# 因为response就是一个Html对象,所以response后面可以直接使用xpath方法
# 使用正则提取url地址,以备从中获取id
content_id_search = re.compile(r"(\d+)\.html")
for item in pic_item_list:
info = {} # 创建字典,把需要的字段封装到字典中,如下:info["content_name"]等3个
# 获取项目的名称
info["content_name"] = item.xpath("./div/a/text()").extract_first()
# extract_first()是xpath语句的方法,返回的是一个string字符串,是list数组里面的第一个字符串
# 获取项目的url 该url地址包含id的信息,且该id有用处,所以需要在前面使用正则来提取
info["content_url"] = "http://"+item.xpath("./div/a/@href").extract_first()
# 使用正则提取id
info["content_id"] = content_id_search.search(info["content_url"]).group(1)
content_ajax_url = "https://xiaoguotu.to8to.com/case/list?a2=0&a12=&a11={}&a1=0&a17=1".format(info["content_id"])
# 使用yield来发送异步请求
# 使用scrapy.Request()来发送请求,传递两个参数,其中callback(回调函数),只写方法的名称即可
yield scrapy.Request(url=content_ajax_url, callback=self.handle_pic_parse, meta=info) # 传递meta参数,这样在回调函数中就可以获取到info
break
def handle_pic_parse(self, response):
# print(response.request.meta) 返回前面info中的字段
# print(response.text) 返回的是json格式
pic_dict_data = json.loads(response.text)["dataImg"]
for pic_item in pic_dict_data:
for item in pic_item["album"]:
if item["l"]["cid"] == int(response.request.meta["content_id"]):
tubatu_info = TubatuScrapyProjectItem() # 使用在items.py中定义的字段,因此需要导入相应的TubatuScrapyProjectItem包
tubatu_info["nick_name"] = item["l"]["n"] # 上传图片人的昵称
tubatu_info["pic_url"] = "https://pic1.to8to.com/case/" + item["l"]["s"] # 图片的url地址
tubatu_info["pic_name"] = item["l"]["t"] # 图片名称
tubatu_info["content_name"] = response.request.meta["content_name"]
tubatu_info["content_id"] = response.request.meta["content_id"]
tubatu_info["content_url"] = response.request.meta["content_url"]
print(tubatu_info)
- 以上代码获取到的仅仅是第一个信息块的字段内容,若要获取首页中第一页的所有信息块内容,那么将
yeild
后面的break
删除即可。
6、页码逻辑编写:
(该代码在for循环外面编写)
1》获取网页的页码,判断是否存在下一页
2》用xpath
语法获取当前页面,并使用extract_first()
获取,再用int()
进行转型。并将当前页码数+1
得到下一页的url地址
3》再次使用yield
方法,url地址为下一页的url
,回调函数为parse
函数
# 页码编辑
if response.xpath("//a[@id='nextpageid']"):
now_page = int(response.xpath("//div[@class='pages']/strong/text()").extract_first())
next_page = "https://xiaoguotu.to8to.com/tuce/p_{}.html".format(now_page+1)
yield scrapy.Request(url=next_page, callback=self.parse)
7、代码的存储:
在最后返回的数据中调用yield
方法,将数据返回给pipelines.py
,在pipelines.py
中编写代码的存储操作
tubatu_info["content_id"] = response.request.meta["content_id"]
tubatu_info["content_url"] = response.request.meta["content_ajax_url"]
yield tubatu_info
而且需要在settings.py
文件中对pipelines
的设置项进行打开,如下图所示:👇
在pipelines.py
中编写如下代码即可连接mongodb
,从而将数据存储在mongodb
中
import pymongo
class TubatuScrapyProjectPipeline(object):
def __init__(self): # 初始化mongodb方法
client = pymongo.MongoClient("mongodb://localhost:27017")
client.admin.authenticate("admin", "admin") # 因为自己设置了mongodb的权限认证,所有需要额外加上这句,如果没有设置权限认证就不用添加这一段代码
db = client["db_tubatu"]
self.collection = db["collection_tubatu"]
def process_item(self, item, spider):
data = dict(item) # 转为字典格式
self.collection.insert(data)
return item
8、伪装爬虫:
1)通过修改请求头User-Agent
- 在
middlewares.py
文件中自定义请求头中间件,编写如下代码:
# 自定义爬虫中间件(修改请求头)
class MyMiddleware(object):
def process_request(self, request, spider):
# 网上搜索user_agent,封装成一个列表
user_agent_list = [
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
....(省略其他user_agent)
]
agent = random.choice(user_agent_list) # 导入random模块,使用random.choice()随机选取列表中的一个数据
request.headers["User-Agent"] = agent # 设置请求头
- 再到
settings.py
中开启中间件选项,并把自定义中间件的名称(MyMiddleware)
添加上,后面的数值表示优先级,数值越小,优先级越大
2)通过设置代理
- 在
middlewares.py
文件中自定义代理中间件,编写如下代码:
# 自定义爬虫中间件(设置ip代理)
class MyProxy(object):
def process_request(self, request, spider):
request.meta["proxy"] = "b5.t.16yun.cn:6460" # 代理ip:端口号
proxy_name_pass = 'admin:123321'.encode("utf-8") # 代理用户名:密码
encode_pass_name = base64.b64encode(proxy_name_pass) # 导入b64模块,使用b64进行加密处理
# 将代理信息设置到头部 【注意!在Basic后面有一个空格, 并使用decode()转化为字符串格式】
request.headers["Proxy-Authorization"] = "Basic "+encode_pass_name.decode()
- 同样的,再到
settings.py
中把自定义中间件的名称(MyProxy)
添加上
9、图片存储:
1》在items.py
文件中使用如下字段,用于存储图片(在pic_url
后面添加)
pic_url = scrapy.Field() # 图片的url地址
image_urls = scrapy.Field() # 若要下载图片保存到磁盘中,那么该字段是必须的!!!
2》在spider
文件中使用如下字段,同样的在pic_url
后面添加
tubatu_info["pic_url"] = "https://pic1.to8to.com/case/" + item["l"]["s"] # 图片的url地址
# 必须使用该字段,且后面的数据要改成列表的格式(加多个[]即可)
tubatu_info["image_urls"] = ["https://pic1.to8to.com/case/" + item["l"]["s"]]
3》在pipelines.py
文件中自定义图片下载类,进行图片存储的代码编写,实现图片存储在不同的文件夹中
import scrapy
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline
# 自定义图片下载类,且继承于ImagesPipeline
class TubatuImagePipeline(ImagesPipeline):
def get_media_requests(self, item, info):
# 根据image_urls中指定的url进行爬取
for img_url in item['image_urls']:
# 获取到item中的image_urls数据,传递到下面进行下载
# 再将item中的数据传入到meta参数中,用于后面存储图片时的命名
yield scrapy.Request(img_url, meta={'item': item})
# 该方法默认即可,一般不做修改(用于下载图片)
def item_completed(self, results, item, info):
# 图片下载完成之后,处理结果的方法,返回的是一个二元组
# 返回的格式:(success, image_info_or_failure) 第一个元素表示图片是否下载成功;第二个元素是一个字典,包含了image的信息
image_paths = [x["path"] for ok, x in results if ok] # 通过列表生成式
if not image_paths:
raise DropItem("Item contains no images") # 抛出异常,图片下载失败(注意要导入DropItem模块)
return item
# 用于给下载的图片设置文件名称和路径
def file_path(self, request, response=None, info=None):
item = request.meta['item'] # 从上面的get_media_requests()方法中获取到item
folder = item['content_name'] # 获取爬取到的数据的content_name
folder_strip = folder.strip() # 去除空格
image_guid = request.url.split('/')[-1] # request.url 获取到如下的图片地址
# 如:"https://pic.to8to.com/case/2018/08/27/20180827165930-fac62168_284.jpg"
# 拆分后为:20180827165930-fac62168_284.jpg
filename = u'images/{}/{}'.format(folder_strip, image_guid)
return filename
-----解释上面的第13行代码:image_paths = [x['path'] for ok, x in results if ok]
,(其实这是列表推导式,只是python中的简写写法)
- result是一个包含tuple的容器
- 容器中每个元素包含两个值,第一个代表状态True/False,第二个值是一个dict
- 如果元素中状态为True则取dict中的path值
拆开来写就是:
for ok, x in results:
if ok:
print(x['path'])
4》最后在settings.py
文件中进行如下配置
效果如下图: