瓜子二手车的车辆信息爬取
一、爬取网址和爬取内容的分析
瓜子网址:https://www.guazi.com/www/buy
- 需要从该网址获取到:全国各城市、汽车各品牌、最新发布的汽车信息页面url地址
- 根据每辆车的url地址,获取:车源码,车名,价格、公里数、变速箱等信息
- 最终将获取信息保存在
Mongodb
中
二、开始爬取
①使用requests,对瓜子二手车的 "反爬策略分析 " 和 “破解反爬”
》反爬策略方法的分析:
1、发送的第一个请求,返回状态码是203,请求时是不带有任何信息
2、猫腻就在203返回的这个 页面里,这个页面中返回的数据里面带有js
3、这段代码经过我们分析,他是用来设置cookie, 如:Cookie: antipas=84381S2R54H236370l9820h6672
4、再次发送请求,要带着这个cookie,这样就能返回正常的html页面了
》破解反爬:
1、新建py文件,先用request请求该页面,输出源码信息,把返回的数据copy出来,发现,里面数据就包含在js中,如下:
eval(function(p,a,c,k,e,r){e=function©{return(c<62?’’:e(parseInt(c/62)))+((c=c%62)>35?.. …
这个叫js混淆2、去掉eval。把后面的代码 “(function…); ” 都复制出来,【复制到 " );" 结束," ; " 后面的代码先留着,后面会用到】。粘贴到浏览器的开发者工具里的console标签,并敲下回车!!!注意:后面的分号别吃了
3、解析出来的是半混淆的代码,能看到相关的函数了,但是还是一大串js代码
4、再将解析出来的代码粘贴到 https://beautifier.io/ 中进行js美化
5、复制美化后的代码到sublime中,再把第2步的" );" 后面的代码,一同复制到sublime中。如下:
var value=anti(‘vJVYqY4i6r/Lb3kBmJ9dlhIO0ceQlAK1NLQp=’,‘695872648925525723’);
var name=‘antipas’;
var url=’’;
xredirect(name,value,url,‘https://’);6、对该代码进行分析,发现上面的value值是调用的anti()方法,因此需要从js代码中找到该anti()方法,且发现该anti()方法有两个参数:string 和 key。
即:var value=anti(‘vJVYqY4i6r/Lb3kBmJ9dlhIO0ceQlAK1NLQp=’,‘695872648925525723’)中的值。
因此到py文件中使用正则表达式获取到这两个参数(string 和 key)7、将格式化后的js代码进行删减,删除anti()方法后面的其他代码【因为后面的代码没用到】,且另存到项目文件夹中(‘guazi.js’)。【此时该js代码已经被破解了】
8、回到py文件,使用with方法读取破解好的guazi.js文件;
9、在Python里如何调用js ,需要安装
pyexecjs
模块,方法:pip install pyexecjs
10、使用execjs模块来封装这段破解好的js代码;接着使用call()方法,往方法中传递
当前需要使用的 “anti” 方法,和两个方法中需要的参数,得到破解出来的anti码。11、向该anti码前添加 “antipas=” 字符串,封装成cookie值,接着将cookie设置到头部
12、再次请求该网址,输出正确的网页源码
13、破解成功!!!
根据请求到的新网页源码,提取相关的信息: 城市(中文、拼音),品牌(中文、拼音),url地址
1、将请求到的新网页源码复制到编辑器中(下文以:guazi.html为命名)。
2、在瓜子(https://www.guazi.com/www/buy)网站中,找到城市和品牌的关键字,再到(guazi.html)中搜索关键字。
3、发现城市的关键字封装在源码的js格式中,有城市拼音,但是城市中文却以unicode的格式出现。而品牌的关键字比较容易找到
城市的关键字:“domain”:“anji”,“name”:"\u5b89\u5409", (其中,name的值为unicode格式)
品牌的关键字:href = “/www/audi/c-1/#bread” > 奥迪4、使用正则获取出城市和品牌的所有信息,并分别存到列表变量中。
5、使用for循环将城市的中文进行遍历,将unicode格式转化为utf-8的格式,并存放在新的列表变量中。【需要使用
encode("utf-8").decode("unicode_escape")
。这里卡了好久,需要多注意!!!】6、根据该网址:https://www.guazi.com/anqing/dazhong/o1i7c-1(按照最新发布的信息来获取最新的汽车某品牌某地区的第一页数据网址),提取出城市拼音和品牌拼音。
用for循环,遍历输出: 城市、品牌和url地址(用字典表示) 再进行条件判断,只获取北京地区的所有品牌信息。7、在handle_mongo.py中编写mongodb逻辑(注意,要看!!!),最后在此py文件中调用mongodb文件,将城市名称、汽车品牌、列表页中每辆车的url地址 3个信息存储到Mongodb中
8、其中,mongodb文件中的save_task()方法是用于存储数据的,get_task()方法是用于读取数据的
②详细代码如下:
》handle_guazi.py:
import requests
import re
import execjs # 使用该模块解析js
from handle_mongo import mongo
url = "https://www.guazi.com/www/buy"
headers = {
"User-Agent": "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"
}
response = requests.get(url=url, headers=headers)
response.encoding = "utf-8"
# print(response.text) # 打印该页面源码,发现被该网页设置反爬了
# 对反爬虫策略进行分析!!!
if "正在打开中,请稍后" in response.text:
# 通过正则获取到相关的字段和值
value_search = re.compile(r"anti\('(.*?)','(.*?)'\);")
string = value_search.search(response.text).group(1)
key = value_search.search(response.text).group(2)
# print(string, key) # 输出anti中string和key的值
# 读取破解好的js文件
with open("guazi.js", "r") as f:
f_read = f.read()
# 使用execjs模块来封装这段js[即:实例化js],传入的是读取后的js文件
js = execjs.compile(f_read)
js_return = js.call("anti", string, key) # 传递的是当前需要使用的"anti"方法,和两个方法中需要的参数
# print(js_return) # 输出anti方法进行加密处理后的cookie值
cookie_value = "antipas="+js_return # 该cookie格式需要在前面加"antipas="字符串
headers["Cookie"] = cookie_value # 将cookie设置到头部
response_second = requests.get(url=url, headers=headers) # 再次请求网址
# print(response_second.text) # 输出解决反爬后的网址源码。 以上破解反爬成功
# 根据返回的 response_second.text 源码中的城市选项,发现封装在js中,
# 以该格式出现:【"domain":"anji","name":"\u5b89\u5409",】如下,获取城市的正则
city_re = re.compile(r'"domain":"(.*?)","name":"(.*?)",')
city_list_unicode = city_re.findall(response_second.text) # 获取所有城市列表信息
# print(city_list_unicode) # 但该输出的格式中,城市名称中文字符以unicode编码的格式出现
# 以下解决unicode编码问题,将unicode格式转化为utf-8
city_list = []
for pinyin, name in city_list_unicode:
info = {}
name = name.encode("utf-8").decode("unicode_escape") # unicode格式转化
info["city_pinyin"] = pinyin
info["city_name"] = name
city_list.append(info) # 将返回的新格式存储到列表中
# print(city_list) # 返回正确的格式
# 根据返回的 response_second.text 源码中的品牌选项:以该格式出现:【href = "/www/audi/c-1/#bread" > 奥迪 </a>】获取品牌的正则
brand_re = re.compile(r'href="\/www\/(.*?)\/c-1/#bread"\s+>(.*?)</a>')
brand_list = brand_re.findall(response_second.text) # 获取所以品牌列表信息
# print(brand_list)
for city in city_list:
if city["city_name"] == "北京": # 只抓取北京的所有品牌的信息
for brand in brand_list:
info = {}
# https://www.guazi.com/www/dazhong/o2i7c-1 经分析,按照最新发布的信息来获取最新的汽车某品牌某地区的第一页数据
info["task_url"] = "https://www.guazi.com/" + city["city_pinyin"] + "/" + brand[0] + "/o1i7c-1"
info["city_name"] = city["city_name"]
info["brand_name"] = brand[1]
# print(info)
mongo.save_task("guazi_task", info) # 调用 handle_mongo.py ,将数据保存到mongodb中
》handle_mongo.py:
import pymongo
class HandleMongo(object):
def __init__(self):
client = pymongo.MongoClient("mongodb://localhost:27017")
client.admin.authenticate("admin", "admin")
self.db = client["db_guazi"]
# 存储task的逻辑
def save_task(self, collection_name, task): # 传入数据表名称和爬取内容
print("当前存储的task为: %s" % task)
collection = self.db[collection_name]
task = dict(task)
collection.insert_one(task)
# 读取task的逻辑
def get_task(self, collection_name): # 传入数据表名称
collection = self.db[collection_name]
# 找出一个数据并删除,保证取出的数据不重复。(而删除后的数据再调用一次 handle_guazi.py 即可再生成数据)
task = collection.find_one_and_delete({})
return task
# 实例化方法
mongo = HandleMongo()
三、改用Scrapy框架进行爬取,获取数据信息
列表页url,例如:https://www.guazi.com/bj/buy/i7c-1/#bread
详情页url,例如:https://www.guazi.com/linyi/ce087865aade93b2x.htm
①存储数据
在上一步的handle_guazi.py文件中运行代码;传入 表名(guazi_task) 和 请求到的数据(两个参数) ,将数据保存到mongodb数据库的guazi_task表中
②创建scrapy项目
scrapy startproject guazi_scrapy_project
分析需要爬取的字段名称,包括:(车源号、车名、从哪个url抓取过来的数据、价格、上牌时间、公里数、排量信息、变速箱)
【该字段信息从车子的详情页面中抓取】
将字段编写在项目中的items.py文件,如下代码:
import scrapy
class GuaziScrapyProjectItem(scrapy.Item):
car_id = scrapy.Field() # 车源号,通过该id实现去重
car_name = scrapy.Field() # 车名
from_url = scrapy.Field() # 从哪个url抓取过来的数据
car_price = scrapy.Field() # 价格
license_time = scrapy.Field() # 上牌时间
km_info = scrapy.Field() # 公里数
license_location = scrapy.Field() # 上牌地
desplacement_info = scrapy.Field() # 排量信息
transmission_case = scrapy.Field() # 变速箱,手动挡还是自动挡
③创建爬虫文件,编写爬虫逻辑
1️⃣:进到项目中的 spiders文件夹,输入命令:scrapy genspider guazi guazi.com
创建爬虫文件(guazi.py)
2️⃣:在项目下创建 main.py
文件,编写如下代码:
from scrapy import cmdline
cmdline.execute("scrapy crawl guazi".split())
# 即可运行此代码从而实现scrapy爬虫框架的执行
3️⃣:进入爬虫文件(guazi.py),注释默认的请求连接,反而定义一个默认的请求函数来发送请求(start_requests()
)【因为我们已经在handle_guazi.py已经获取到数据并存入数据库了,因此使用数据库中guazi_task表的url来请求连接即可】,代码如下:
import scrapy
from handle_mongo import mongo
class GuaziSpider(scrapy.Spider):
name = 'guazi'
allowed_domains = ['guazi.com']
# start_urls = ['http://guazi.com/'] # 默认发送该请求
# 注释掉原本的发送请求,改用默认函数请求方法
def start_requests(self): # 因为我们已经在handle_guazi.py已经获取到数据并存入数据库了,因此使用数据库中的url来请求连接即可
task = mongo.get_task("guazi_task")
print("当前获取到的task地址为:{}".format(task))
yield scrapy.Request( # 该Request对象代表了一个http请求,会经由Downloader去执行,从而产生一个response
url=task["task_url"], # 请求数据库中的url
dont_filter=True, # 设置不要过滤
)
4️⃣:
1》接着对Downloader中间件的三个方法进行讲解。中间件讲解链接
2》在middlewares.py中自定义Downloader中间件
,将原本的handle_gauzi.py中破解js反爬策略的代码复制到这里,并修改cookie的取值(spider中要使用cookies的值,需要用字典的格式进行封装),设置好cookies的值后,返回request的请求(此时,该request已经获取到cookies的值,下次的请求中就会带有cookies的值进行请求的了,返回正确的数据)。接着对条件进行判断,重新获取状态码为200的网页信息。
代码如下:
import re
import execjs
# 自定义完中间件之后,一定要到setting.py中去开启该中间件
class GuaziDownloader(object):
def __init__(self):
# 读取破解好的js文件
with open("guazi.js", "r") as f:
self.f_read = f.read()
def process_response(self, request, response, spider):
# 1️⃣》获取到页面返回的请求,状态码为203,返回的response.text页面中带有js反爬策略
if "正在打开中,请稍后" in response.text:
# 通过正则获取到相关的字段和值
value_search = re.compile(r"anti\('(.*?)','(.*?)'\);")
string = value_search.search(response.text).group(1)
key = value_search.search(response.text).group(2)
# print(string, key) # 输出anti中string和key的值
# 使用execjs模块来封装这段js[即:实例化js],传入的是读取后的js文件
js = execjs.compile(self.f_read)
js_return = js.call("anti", string, key) # 传递的是当前需要使用的"anti"方法,和两个方法中需要的参数
# print(js_return) # 输出anti方法进行加密处理后的cookie值
cookie_value = {"antipas": js_return} # 该cookie格式需要在前面加"antipas="字符串,且在scrapy中使用cookie需要以字典的格式进行传递
print('当前所使用的cookie为:{}'.format(cookie_value))
request.cookies = cookie_value # 设置cookie
return request # 把请求重新放回调度器。下次的请求中就会带有cookies的值进行请求的了,返回正确的数据
# 2️⃣》对js反爬策略进行破解后,重新获取的页面请求,状态码为200
elif response.status == 200:
# 正常的返回
return response
3》还可以再自定义User-Agent的中间件
和代理中间件
,而设置好的中间件需要到settings.py文件中进行开启该中间件。
【若启动scrapy后,①遇到scrapy将相同的请求信息过滤掉,那么需要到爬虫文件中的yield方法的
dont_filter参数设置为True
,让它设置成不过滤;②遇到获取多个cookie值的情况,那么需要在middlewares.py中注释掉自定义的User-Agent中间件即可,而该User-Agent还可在settings.py中编辑使用】,这样再运行main.py文件,可以正确解析到页面信息
import base64
import random
# 自定义爬虫中间件(修改请求头)
class MyUserAgent(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 # 设置请求头
# 自定义爬虫中间件(设置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()
5️⃣:以上获取到的页面是列表页信息,接下来对该列表页进行分析处理:
1》创建自定义解析方法 handle_car_item
(用于获取列表页的信息),用xpath对该页面的 “车名称” 和 “详情页的url地址” 的数据进行获取,并存储在car_info列表中;若返回的页面出现"为您找到0辆好车",那么设置返回。
# 自定义解析方法【获取列表页的信息】
def handle_car_item(self, response):
if "为您找到0辆好车" in response.text:
return
car_item_list = response.xpath("//ul[@class='carlist clearfix js-top']/li") # 当前列表页所展示的二手车信息
for item in car_item_list:
car_info = {} # 创建一个字典用于存储 二手车的名称 和 详情页的url地址
# xpath返回的是一个列表,而extract_first()方法获取的是索引值为0的值
car_info["car_name"] = item.xpath("./a/h2/text()").extract_first()
car_info["car_url"] = "https://www.guazi.com"+item.xpath("./a/@href").extract_first()
print(car_info)
运行代码,如果使用代理请求可能会慢,因此可以到settings.py中设置相关配置。【如:设置10秒内若无法请求到数据,那么进行重试,重试3次,若重试完后还是没有拿到数据,那么就会报
Timeout异常
。而我们可以在爬虫文件的yield方法的errback参数
进行异常的处理!】
# 在settings.py中进行如下设置:
DOWNLOAD_DELAY = 0.3 # 爬虫的请求速度,一般设置为:0.1-0.5
DOWNLOAD_TIMEOUT = 10 # 默认为180秒,即若带着代理180秒内无法请求到数据,就会重新请求,一直重试到无法请求为止。这里我们设置为10秒
# 不可能一直设置它一直进行重试
RETRY_ENABLED = True # 是否进行重试
RETRY_TIMES = 3 # 重试的次数
# 以上的设置:设置10秒内若无法请求到数据,那么进行重试,重试3次,若重试完后还是没有拿到数据,那么就会报Timeout异常
# 而我们可以在爬虫文件的yield方法的errback参数进行异常的处理!!!
2》回到爬虫文件对自定义函数请求方法( start_requests()
中yield请求中使用errback参数 ),将报错回调方法传入( handle_err()
)【该方法的 failure是errback的参数,可用于获取失败的request请求】使用failure.request.meta 方法用于获取失败的task。
该task需要在start_requests()
中yield请求使用meta参数进行传递,且需要删除task中携带的id数据。这样该报错回调函数使用mongo.save_task()方法,就可以把失败的请求扔回task库,以便可以接着重新获取数据。
# 注释掉原本的发送请求,改用默认函数请求方法
def start_requests(self): # 因为我们已经在handle_guazi.py已经获取到数据并存入数据库了,因此使用数据库中的url来请求连接即可
task = mongo.get_task("guazi_task")
if '_id' in task:
task.pop('_id')
print("当前获取到的task地址为:{}".format(task))
yield scrapy.Request( # 该Request对象代表了一个http请求,会经由Downloader去执行,从而产生一个response
url=task["task_url"], # 请求数据库中的url
dont_filter=True, # 设置不要过滤
errback=self.handle_err, # 失败请求的处理函数
meta=task, # 携带task信息,到errback的处理函数中进行获取数据(该task以字典的形式)
)
# 报错回调方法
def handle_err(self, failure): # 该failure是errback的参数,可用于获取失败的request请求
print(failure) # 可以打印报错结果
# failure.request.meta用于获取失败的task
mongo.save_task("guazi_task", failure.request.meta) # 把失败的请求扔回task库,以便可以接着重新获取数据
3》最后到handle_car_item()函数
的yield方法中请求详情页的网址和函数等
def handle_car_item(self, response):
if "为您找到0辆好车" in response.text:
return
car_item_list = response.xpath("//ul[@class='carlist clearfix js-top']/li") # 当前列表页所展示的二手车信息
for item in car_item_list:
car_info = {} # 创建一个字典用于存储 二手车的名称 和 详情页的url地址
# xpath返回的是一个列表,而extract_first()方法获取的是索引值为0的值
car_info["car_name"] = item.xpath("./a/h2/text()").extract_first()
car_info["car_url"] = "https://www.guazi.com"+item.xpath("./a/@href").extract_first()
yield scrapy.Request(url=car_info["car_url"], callback=self.handle_car_info, dont_filter=True, meta=car_info, errback=self.handle_err)
6️⃣:接下来对该详情页进行分析处理:
- 需要从详情页面中获取车源码、车名、价格、上牌时间、公里数、排量信息、变速箱的信息。可以通过xpath语句来获取这些信息。
- 创建items内部定义的字段实例,将获取到的信息都封装到该实例中。这样详情页就编写完成了。
【xpath返回的是一个列表,而
extract_first()
方法可以获取索引值为0的值;且strip()
方法用于如果后面有空格则删除空格】。而车源码的获取可以通过正则,注意后面的\s+
;而车名和url在列表页中已经获取到了,只需要使用meta
方法就可以使用这两个信息
# 自定义解析方法【获取详情页的信息】
def handle_car_info(self, response):
car_id_re = re.compile(r"车源号:(.*?)\s+")
car_info = GuaziScrapyProjectItem() # 创建items内部定义的字段实例
car_info["car_id"] = car_id_re.search(response.text).group(1)
car_info["car_name"] = response.request.meta["car_name"]
car_info["from_url"] = response.request.meta["car_url"]
car_info["car_price"] = response.xpath("//span[@class='price-num']/text()").extract_first().strip() # strip()用于如果后面又空格则删除空格
car_info["license_time"] = response.xpath("//ul[@class='assort clearfix']/li[@class='one']/span/text()").extract_first().strip()
car_info["km_info"] = response.xpath("//ul[@class='assort clearfix']/li[@class='two']/span/text()").extract_first().strip()
car_info['desplacement_info'] = response.xpath("//ul[@class='assort clearfix']/li[@class='three']/span/text()").extract_first().strip()
car_info["transmission_case"] = response.xpath("//ul[@class='assort clearfix']/li[@class='last']/span/text()").extract_first().strip()
print(car_info)
7️⃣:接着运行代码,可能会出现错误!!!
1》在task库中可能会出现两种url地址(分别为列表页url和详情页url)。这是因为在 handle_guazi.py
和 guazi.py 文件中handle_err()函数
都有调用save_task()
方法。且请求页面时如果出现报错,从而调用了handle_err()
函数,将错误数据回调到task库中,因此出现两种url的情况
2》解决该问题,需要新建一个新的字段(item_type
)用于存储是list_item还是car_info_item。
【在爬虫文件的 handle_car_item() 函数中定义字段:
car_info["item_type"] = "car_info_item"
】;
【在handle_guazi.py文件中定义字段:info["item_type"] = "list_item"
】。
对于原始请求方法(start_requests( ))进行该字段(item_type
)的条件判断,
【若
task["item_type"] == "list_item"
,那么进行数据库中请求到的url的请求,且继续回调handle_car_item()函数】;
【若task["item_type"] == "car_info_item"
,那么进行列表页的请求,且继续回调handle_car_info()函数】。
还是不太了解的话,到下面的 Q&A :3)中进行分析查看
并将代码判断设置为:while True
,让它一直循环去取值,直到task的内容为空才停止。完整代码如下:
# 注释掉原本的发送请求,改用默认函数请求方法
def start_requests(self): # 因为我们已经在handle_guazi.py已经获取到数据并存入数据库了,因此使用数据库中的url来请求连接即可
while True:
task = mongo.get_task("guazi_task")
if not task: # task取空了,就停下来
break
if "_id" in task:
task.pop("_id") # 删除task中携带的id数据
print("当前获取到的task地址为:{}".format(task))
print(task["task_url"])
if task["item_type"] == "list_item":
yield scrapy.Request( # 该Request对象代表了一个http请求,会经由Downloader去执行,从而产生一个response
url=task["task_url"], # 请求数据库中的url
dont_filter=True, # 设置不要过滤
callback=self.handle_car_item, # 回调方法,回调自定义解析方法
errback=self.handle_err, # 失败请求的处理函数
meta=task, # 携带task信息,到errback的处理函数中进行获取数据(该task以字典的形式)
)
elif task["item_type"] == "car_info_item":
# 该url为handle_car_item()中的"car_url",该meta信息包含"car_name"和"car_url"
print(task["car_url"])
yield scrapy.Request(url=task["car_url"], callback=self.handle_car_info, dont_filter=True, meta=task, errback=self.handle_err)
3》清空原来的数据库信息,重新运行代码。可以在handle_err()
函数中打印failuer(报错结果)。 对于 “请求太频繁”的提示 进行处理,回到middlewares.py文件,将request请求再次返回。【若使用的是代理请求,则在setting.py中减少代理的请求次数】。这样就不会报错了。
8️⃣:接下来就是对“下一页”进行处理了:
- 使用xpath获取下一页的标签,使用正则匹配url的地址(获取里面的city、brand、页码),使用列表返回获取到的数据。
- 创建下一页的链接格式,并将它赋值到
response.request.meta["task_url"]
中,将task_url替换为下一页的url地址。 - 且使用yield请求连接,其中meta参数携带原本请求的meta信息(car_name、car_url和item_type,其中变的只有car_url,它变成下一页的请求了)
if response.xpath("//ul[@class='pageLink clearfix']//li[last()]//span/text()").extract_first() == "下一页":
# https://www.guazi.com/bj/audi/o2i7c-1/#bread
value_re = re.compile(r"https://www.guazi.com/(.*?)/(.*?)/o(\d+)i7c-1")
value = value_re.findall(response.url)[0] # 返回的是列表,列表里面包含的是元组
# 下一页的链接: 将下一页的值,赋值到response.request.meta["task_url"]中
# next_page = "https://www.guazi.com/{}/{}/o{}i7c-1/#bread".format(value[0], value[1], str(int(value[2])+1))
response.request.meta["task_url"] = "https://www.guazi.com/{}/{}/o{}i7c-1/#bread".format(value[0], value[1], str(int(value[2])+1))
# 该yield请求中meta参数携带原本请求的meta信息(car_name、car_url和item_type,其中变的只有car_url,它变成下一页的请求了)
yield scrapy.Request(url=response.request.meta["task_url"], callback=self.handle_car_item, dont_filter=True, meta=response.request.meta, errback=self.handle_err)
9️⃣:存储数据到mongodb:
- 在爬虫文件中使用
yield car_info
,将数据往 pipelines.py请求。 - 接着进入handle_mongo.py,添加新的存储逻辑
save_data()
,将数据更新到数据库中。 - 再到 pipelines.py 中进行调用该逻辑,并且需要到 setting.py 中开启
ITEM_PIPELINES
# handle_mongo.py
def save_data(self, collection_name, data):
print("当前存储的数据为: %s" % data)
collection = self.db[collection_name]
data = dict(data)
# 通过car_id 更新和去重;在data中的car_id去找;把data更新进去
collection.update({"car_id": data["car_id"]}, data, True)
# pipelines.py
from handle_mongo import mongo
class GuaziScrapyProjectPipeline(object):
def process_item(self, item, spider):
mongo.save_data("guazi_data", item)
# settings.py
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'guazi_scrapy_project.pipelines.GuaziScrapyProjectPipeline': 300,
}
Q&A解答
1)
Q: “guazi.py"中打印第21行的"当前获取到的 task 地址”?(根据实际项目代码中的行数)
A: 当前获取到的task地址为:{'task_url': 'https://www.guazi.com/bj/hanteng/o1i7c-1', 'city_name': '北京','brand_name': '汉腾', 'item_type':'list_item'}
2)
Q: 数据库存储中(在task_data),为什么 desplacement字段 的值为:1.3T/保定?
A: 有些详情页面中有上牌地和排量,而有些只有排量。因此冲突到了,暂时先不做处理
3)
Q: 为什么 "guazi_task"数据库 中会出现 task_url 和 car_url 两个url?
A: 查数据库”guazi_task“,发现只有:task_url; 且该值为:https://www.guazi.com/bj/hanteng/o1i7c-1;而没有:car_url。
- 只有当超时出现报错时,调用handle_err()函数才会触发handle_mongo.py中的save_task()方法[存储task逻辑],其中携带的参数("failure.request.meta"用于获取失败的task)。
- 即:如果handle_car_item()函数出现报错,那么car_name,car_url等字段将传入此参数中;而其他函数报错,也会将字段传入此参数中。
- 而如果car_name,car_url字段传入此参数中,那么task库中将会出现两个url!!!
- 打印的内容为:
当前存储的task为:{'car_name':'奥迪Q7 2016款 45 TFSI 技术型(进口)','car_url':'http://www.guazi.com/bj/988db8d0de79237bx.htm#fr_page=list&fr_pos=city&fr_no=33','depth':1,'download_timeout':10.0,'proxy':....}
(反正不是原本请求的数据)- 正常打印的内容为:
当前获取到的task为:{'_id': ObjectId('5e8c94c369553ec53d97777a'),'task_url': 'https://www.guazi.com/bj/lufeng/o1i7c-1', 'city_name':'北京', 'brand_name': '陆风',}
- 因此需要再定义一个item_type字段,用于区分两url
4)
Q: 可以不设置 "item_type"字段 吗?
A: 如果没有出现报错调用handle_err()函数,可以不设置 "item_type"字段。但是为了代码完整性(出现报错就要处理),还是需要设置该字段
5)
Q: 试一下不删除 task 中携带的 id 数据,即guazi.py中第20行?(根据实际项目代码中的行数)
A: 返回去看:5️⃣2》中的报错回调方法,该failure.request.meta
方法用于获取失败的task。
- 如果出现报错,则会调用该回调方法,把失败的请求扔回task库,以便可以接着重新获取数据。
- 但是获取到的失败task中还包含着它自身的id,如果将这个失败的请求扔回task库,那么数据库自身也会生成新的id,从而两个id会出现冲突!!
- 因此才需要增加那段代码,将id删除,以免报错。
6)
Q: def start_requests(self) 是自定义的函数吗?为什么要使用该函数而不用原始的 start_urls(默认请求发送方法)?
A: def start_requests(self) 是默认请求函数。一般是使用start_urls(默认请求发送方法),但如果需要请求其他链接【即从数据库读取task_url
来进行发送请求】,那么可以使用默认请求函数:def start_requests(self)
7)
Q: 在请求 “下一页” 的链接中,使用了该代码value = value_re.findall(resvponse.url)[0]
,它返回的数据是什么?
A: 打印该value的值,输出类似:value=('bj', 'audi', '1')
这样的元组
数据。
- 该代码用于查找所有获取到请求的数据的下一页地址,类似:
https://www.guazi.com/bj/audi/o2i7c-1/#bread
,其中02i7c-1中的02是页码数,它随着下一页的获取而变化。- 若改为该代码:
value =value_re.search(response.url)
,则只能查找一个结果,且每次只能获取到第一页的url地址(01i7c-1),而获取不到下一页。