更新
时隔1年半,终于战胜了懒惰,更新了二手车价格预测的数据处理部分,后续的模型训练部分可能也在秘密基地更新,懂得都懂。
这里是目录
前言
浑浑噩噩的研一过去了,不知道自己都学了些什么东西,所以痛定思痛,想要做一些东西,正好看到阿里天池有一个二手车价格预测的问题,想自己搞一个数据集,再写个机器学习的模型预测一下二手车价格,因此有了这篇blog。
一、配置数据库
我采用的数据库是MongoDB非关系型数据库,MongoDB将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB文档类似于JSON 对象。字段值可以包含其他文档,数组及文档数组。
采用MongoDB的好处在哪呢,它将数据存储为Json格式,与Pandas中的Dataframe数据类似,在之后的数据处理和探索性数据分析时,会有很大的方便。
下载安装好数据库后,配置就很简单了,在项目列表里新建一个名为config.py的python文件,配置MongoDB_URL和要存入的数据库即可。
config.py
MONGO_URL = 'localhost'
MONGO_DB = 'used_car'
二、目标分析
我们要获取的是二手车的数据,首先去到官网:某车官网
我们应该找到买车页面,因为要进行预测二手车的价格,所以需要买车的一些数据和它的售价。点进买车的页面,我们可以看到有城市列表,下面有每辆车的图片和简介,点击图片即可查看车辆的详细信息,在页面的最底部有翻页按钮。
那么,我们大致可以明确需要做些什么了。
- 首先,我们需要获取一个城市列表,对每座城市二手车的数据都进行一个爬取。
- 其次,我们需要获取每个城市的二手车页面有多少页的信息,有些城市较少,最多为50页。
- 最后,我们需要将页面中的每一辆二手车的链接获取到。
这样,我们就算完成了第一步,获取到了二手车网站中,所有城市的所有二手车页面的链接,并将它们存入数据库中。
三、页面解析-获取城市链接
在分析完我们的目标后,我们开始对页面进行分析。
鼠标右键检查元素或F12,点击绿色框的按钮,到页面上选中“北京”,我们可以看到,被橙色框框中的地方,变成了选中状态,这说明我们要找的城市信息在这个区域。
进一步寻找,我在 div标签中的 class="area-city-letter"的这个标签下找到了城市列表。
找到城市列表所在的标签,我们开始写代码。首先要获取整个页面的文本信息。调用requests库,获取传过来的url的页面信息。
import requests
def get_page(self, url):
try:
res = requests.get(url)
if res.status_code == 200:
# print(res.text)
return res.text
except:
print("url出错了!")
接下来,我们写一下获取城市列表的方法。通过分析页面,我们可以知道,城市的名称和对应的链接都在class为"area-city-letter"的div标签中,这里我们直接用BeautifulSoup进行页面解析,BeautifulSoup可以直接从html的text中获取对应的标签,找到name=“div”, class="area-city-letter"所有的div标签,再从这个div标签中找到所有的 a 链接,a标签中的href直接调用方法a.get(“href”)就可以获取,城市的名字直接采用字典读取数据的方式即可a[“rrc-event-expand-tag_value”]。
from bs4 import BeautifulSoup
def get_city_url(self):
# 用于存放每个城市对应的url和城市名称
url_list = []
# 用于存放每个城市页面的url、城市名称、每个城市页面的页数,其中是字典格式
url_dict = []
# page_text获取页面内容,调用上面写好的get_page()方法
page_text = self.get_page(self.url)
# 解析页面
soup = BeautifulSoup(page_text, 'lxml')
# 查找页面中div的class="area-city-letter"的所有标签
div = soup.find_all(name="div", attrs={"class": "area-city-letter"})
# 再将查找到的div标签内容重新解析,这里要将上面的div进行一个字符串转换,因为BeautifulSoup方法里传参为str格式
soup2 = BeautifulSoup(str(div), 'lxml')
# 查找div标签中的所有超链接标签
urls = soup2.find_all(name="a")
# 将城市链接和城市名称存入list
for u in urls:
url_list.append((u.get('href'), u["rrc-event-expand-tag_value"]))
print("共抓取到"+str(len(url_list))+"条url链接!")
# 用于存放具体城市买车的链接和城市名称
city_url_list = []
for u in url_list:
city_url_list.append(["https://www.renrenche.com" + u[0] + "ershouche/", u[1]])
# 将最终的链接、城市名称、城市页数 存入结果集
for u in city_url_list:
# 这里是另一个方法,用来获取每个城市页面中一共有多少页买车页面,达到翻页的效果
pagecount = self.get_pagecount(u[0])
url_dict.append({"city_url": u[0], "city_name": u[1], "page_count": pagecount})
u.append(pagecount)
# 返回列表和字典
return city_url_list, url_dict
这里加上获取每个城市页面的页数代码。页码的解析和找城市列表类似的,我们会找到页码都存在一个ul标签下,它的class为"pagination js-pagination",在其中的li标签里。
def get_pagecount(self, url: str):
try:
# 先解析页面
page_text = self.get_page(url)
soup = BeautifulSoup(page_text, 'lxml')
ul = soup.find_all(name="ul", attrs={"class": "pagination js-pagination"})
soup2 = BeautifulSoup(str(ul), 'lxml')
pagecount = soup2.find_all(name="li")
# 这里解释一下为什么还要再解析pagecount[-2],我发现最大页码是在倒数第二个li标签下的,页码数字在其a标签下,因此有了进一步的解析
page = BeautifulSoup(str(pagecount[-2]), 'lxml')
a_content = page.find_all(name='a')[0].string
# print(a_content)
# for item in page.find_all("a"):
# print(item.string)
return int(a_content)
except Exception as res:
return 0
至此,我们已经可以获取到城市链接、城市名称、城市页面的页数,我们把这些数据存到数据库中,方便以后的调用。
from spider_renren.get_city_data.config import *
import pymongo
# 配置数据库
client = pymongo.MongoClient(MONGO_URL)
db = client[MONGO_DB]
def save_city_url(self, table, city_url_dict):
print("save to db[city_url]")
data_len = len(city_url_dict)
for item in city_url_dict:
db[table].save(item)
print("共存入"+str(data_len)+"条数据到"+table+"数据库!")
这里,我为了重复使用一些方法,我做了一个小小的封装,将存入数据库写成了一个类,将获取城市列表和页码写成了一个类,最后在另一个文件中,进行调用。以下是这几部分的代码汇总。
config.py
MONGO_URL = 'localhost'
MONGO_DB = 'used_car'
sava_to_db.py
from spider_renren.get_city_data.config import *
import pymongo
client = pymongo.MongoClient(MONGO_URL)
db = client[MONGO_DB]
class SaveData:
def save_city_url(self, table, city_url_dict):
print("save to db[city_url]")
data_len = len(city_url_dict)
for item in city_url_dict:
db[table].save(item)
print("共存入"+str(data_len)+"条数据到"+table+"数据库!")
spider_city_url.py
import requests
from bs4 import BeautifulSoup
class Get_City_Url:
def __init__(self, url):
self.url = url
"""
:param url:str, 用来抓取页面文本的url
:return res.text:str, 返回页面文本
"""
def get_page(self, url):
try:
res = requests.get(url)
if res.status_code == 200:
# print(res.text)
return res.text
except:
print("url出错了!")
def get_city_url(self):
url_list = []
url_dict = []
page_text = self.get_page(self.url)
soup = BeautifulSoup(page_text, 'lxml')
div = soup.find_all(name="div", attrs={"class": "area-city-letter"})
soup2 = BeautifulSoup(str(div), 'lxml')
urls = soup2.find_all(name="a")
for u in urls:
url_list.append((u.get('href'), u["rrc-event-expand-tag_value"]))
print("共抓取到"+str(len(url_list))+"条url链接!")
city_url_list = []
for u in url_list:
city_url_list.append(["https://www.renrenche.com" + u[0] + "ershouche/", u[1]])
for u in city_url_list:
pagecount = self.get_pagecount(u[0])
url_dict.append({"city_url": u[0], "city_name": u[1], "page_count": pagecount})
u.append(pagecount)
return city_url_list, url_dict
def get_pagecount(self, url: str):
try:
page_text = self.get_page(url)
soup = BeautifulSoup(page_text, 'lxml')
ul = soup.find_all(name="ul", attrs={"class": "pagination js-pagination"})
soup2 = BeautifulSoup(str(ul), 'lxml')
pagecount = soup2.find_all(name="li")
page = BeautifulSoup(str(pagecount[-2]), 'lxml')
a_content = page.find_all(name='a')[0].string
# print(a_content)
# for item in page.find_all("a"):
# print(item.string)
return int(a_content)
except Exception as res:
# 有些城市没有二手车数据,会抛出异常,我们做返回0的操作,方便以后的区分
return 0
start_spider.py
from spider_renren.get_city_data.save_to_db import *
from spider_renren.get_city_data.spider_city_url import Get_City_Url
if __name__ == '__main__':
gcu = Get_City_Url("https://www.renrenche.com/bj/ershouche/?&plog_id=bb97445b3ce898dec2c3c0f4f4f85ea3")
city_url_list, city_url_dict = gcu.get_city_url()
print(city_url_list, city_url_dict)
print(len(city_url_list), len(city_url_dict))
save_city_url = SaveData()
save_city_url.save_city_url("city_url", city_url_dict)
最后,我们看一下获取到的数据的样子~ 可以看到,在数据库中,数据形式为字典,每一条数据对应key-value对,其中_id是数据库自行生成的,city_url、city_name、page_count是我们通过获取存入的,接下来我们要将city_url和page_count进行一个拼接,这样就可以得到所有城市所有页码的链接了。
四、拼接城市链接和页码(获取所有城市所有页码的链接)
在上一章节我们成功的获取了城市链接、城市名称和城市页面的页数,要想获取二手车网站内的所有二手车数据,首先要将每个城市的页面都获取到,一个城市的页面中可能会有几十页数据,因此,而不同页码对应了不同的链接,因此我们将城市链接和页码进行一个拼接,这样就可以成功得到所有城市的所有二手车页面了。
以北京市为例,我们选择北京市的二手车页面,它的url是:https://www.renrenche.com/bj/ershouche/p1/ ,其中结尾的p1代表了这是北京市二手车页面的第一页,我试着把p1改成了p10,结果就跳转到了北京市二手车页面的第十页。因此,想要获得一个城市二手车的所有页面,只需要在城市链接后加上对应的页码就可以了。
拼接链接的代码十分简单,从数据库中调取数据,直接拼接好再存入数据库中就可以了,以下是我全部代码。
get_page_url.py
import pandas as pd
import pymongo
from spider_renren.get_city_data.config import *
from spider_renren.get_city_data.save_to_db import *
# 连接数据库
client = pymongo.MongoClient(MONGO_URL)
db = client[MONGO_DB]
# 读取数据库中的“city_url”表
table = db['city_url']
def get_page_url():
# 将数据表中的数据转化成pandas中DataFrame格式,方便数据处理
data = pd.DataFrame(list(table.find()))
# 不读取“_id”列
data = data[["city_url", "city_name", "page_count"]]
# 筛选出页码大于0得到数据
data = data[data["page_count"] > 0]
# 存放每个页码对应页面的url和城市名称
page_dict = []
for i in data.index:
for p in range(1, data.loc[i][2] + 1):
# 进行拼接和存放
page_dict.append({"page_url": data.loc[i][0] + 'p' + str(p), "city_name": data.loc[i][1]})
return page_dict
if __name__ == '__main__':
# print(get_page_url())
# 存入数据库中
save_page_url = SaveData()
save_page_url.save_page_url("page_url", get_page_url())
五、获取每辆二手车页面url
在上一章节,我们成功获取到了每个城市所有页码的url,接下来就是爬取每个页面中,每辆二手车对应的链接。
检查页面源码,发现每辆车的链接是再一个ul标签中的li标签里。
具体的代码就直接放了,和上面爬链接的方法是一样的。
import pandas as pd
import pymongo
import requests
import json
from bs4 import BeautifulSoup
from spider_renren.get_city_data.config import *
from spider_renren.get_city_data.save_to_db import *
# 连接数据库
client = pymongo.MongoClient(MONGO_URL)
db = client[MONGO_DB]
table = db['page_url']
def get_page_url():
# 读取上一节爬取到的每个城市对应的所有页面url
data = pd.DataFrame(list(table.find()))
data = data[["page_url", "city_name"]]
page_list = []
for i in data.index:
page_list.append([data.loc[i][1], data.loc[i][0]])
return page_list
# 解析网页
def get_page(url):
try:
res = requests.get(url)
if res.status_code == 200:
# print(res.text)
return res.text
except:
print("url解析错误,url:"+url)
# 开始获取每辆车对应的url
def get_url(url):
try:
url_list = []
page_text = get_page(url)
soup = BeautifulSoup(page_text, 'lxml')
ul = soup.find_all(name="ul", attrs={"class": "row-fluid list-row js-car-list"})
soup2 = BeautifulSoup(str(ul), 'lxml')
urls = soup2.find_all(name="a")
# 抓取单个车的url list
for u in urls:
url_list.append("https://www.renrenche.com"+u.get('href'))
# 这里对链接进行了以下排序,因为在爬取过程中发现,有一些广告是穿插在车辆信息里的,所以多了一些无效链接,这些链接有明显的特征,链接长度小于正常车辆链接。
url_list.sort()
# 取排序后链接的中间链接长度作为正确连接的长度进行判断
url_len = len(url_list[len(url_list)//2])
new_url_list = []
# for u in url_list:
# print(u)
# 获取正确链接
for i in range(len(url_list)):
if len(url_list[i]) == url_len:
new_url_list.append(url_list[i])
url_dict = []
# print(len(url_list))
for u in new_url_list:
url_dict.append({"every_car_url": u})
save_every_car_url = SaveData()
save_every_car_url.save_every_car_url("every_car_url", url_dict)
except:
print("error")
if __name__ == '__main__':
page_list = get_page_url()
# 开始爬每个页面的每个车辆连接
# 这里博主遇到了问题,在爬取过程中,频繁访问网站,再加上网络有时连接不好,所以经常会跳出错误,导致程序中断,因此我在爬的时候用索引作为迭代依据,输出每次的索引和链接,在出错的时候,将range的起始值做一次手动更新,效率较低,好在出错数据不多。
for i in range(0, len(page_list)):
print("index:", i)
print("开始"+page_list[i][0]+"爬取,链接:"+page_list[i][1])
get_url(page_list[i][1])
六、获取每辆车的详细信息
终于到了数据获取的最后一个阶段,点开一辆车的链接,进到页面我们发现有好多关于车辆的信息,在这里,我选取了我认为对二手车价格预测有用的一些信息。
以上三张图片中的内容就是我选取的信息,对其一一解析。
我将解析过程中需要注意的事情放在代码的注释中。
get_detail_from_car.py
import pandas as pd
import pymongo
import time
from multiprocessing import Pool
import requests
from bs4 import BeautifulSoup
from spider_renren.get_city_data.config import *
from spider_renren.get_city_data.save_to_db import *
# 连接数据库
client = pymongo.MongoClient(MONGO_URL)
db = client[MONGO_DB]
table = db['every_car_url']
def get_every_car_url():
data = pd.DataFrame(list(table.find()))
data = data["every_car_url"]
every_car_url_list = []
for i in data.index:
every_car_url_list.append(data.loc[i])
return every_car_url_list
def get_page(url):
try:
# time.sleep(1.5)
res = requests.get(url)
if res.status_code == 200:
# print(res.text)
return res.text
except:
time.sleep(10)
def get_cars_detail_info(url):
table_dict = {}
table_dict["url"] = url
try:
page_text = get_page(url)
# 解析页面
soup = BeautifulSoup(page_text, 'lxml')
# 这里爬取车辆售价,寻找售价在页面的哪个标签中的方法与之前一样
right_container = soup.find_all(name="div", attrs={"class": "right-container"})
middle_content = BeautifulSoup(str(right_container), 'lxml').find_all(name="div", attrs={"class": "middle-content"})
price = BeautifulSoup(str(middle_content), 'lxml').find(name="p", attrs={"class": "price detail-title-right-tagP"})
# 这里做一个判断,有些时候网页加载或者链接错误,导致爬出来的数据为None
if price is not None:
price = str(price.contents[0]).replace('\n', '').replace(' ', '')
table_dict["售价"] = price
# 这里爬取车辆的新车售价
new_car_price_span = BeautifulSoup(str(middle_content), 'lxml').find(name="div", attrs={
"class": "new-car-price detail-title-right-tagP"})
new_car_price = BeautifulSoup(str(new_car_price_span), 'lxml').find(name="span")
if new_car_price is not None:
new_car_price = str(new_car_price.get_text()).replace('\n', '').replace(' ', '')
table_dict["新车售价"] = new_car_price[:-1]
# 这里爬取车辆行驶里程
mileage_li = soup.find_all(name="li", attrs={"class": "kilometre"})
mileage = BeautifulSoup(str(mileage_li), 'lxml').find(name="strong")
if mileage is not None:
mileage = str(mileage.get_text()).replace('\n', '').replace(' ', '')
# print(mileage[:-3])
table_dict["行驶里程"] = mileage[:-3]
# 这里爬取车牌所在地
city_div = soup.find_all(name="div", attrs={"class": "licensed-city"})
city = BeautifulSoup(str(city_div), 'lxml').find(name="strong")
if city is not None:
city = str(city.get_text()).replace('\n', '').replace(' ', '')
table_dict["车牌所在地"] = city
# 这里爬取图2车辆基础信息的内容
info_ul = soup.find_all(name="ul", attrs={"class": "module-base-content clearfixnew"})
ul_soup = BeautifulSoup(str(info_ul), 'lxml')
for index, li in enumerate(ul_soup.find_all('li')):
li_soup = BeautifulSoup(str(li), 'lxml')
base_key = li_soup.find(name="span", attrs={"class": "module-base-content-key"})
base_value = li_soup.find(name="span", attrs={"class": "module-base-content-value"})
if base_key is not None and base_value is not None:
base_key = str(base_key.contents[0]).replace('\n', '').replace(' ', '')
base_value = str(base_value.contents[0]).replace('\n', '').replace(' ', '')
table_dict[base_key] = base_value
# 这里爬取最后一部分,车辆配置参数
js_parms_table = soup.find_all(name="div", attrs={"id": "js-parms-table"})
soup2 = BeautifulSoup(str(js_parms_table), 'lxml')
# 这些参数都在页面的table标签中,对table进行行和单元格的迭代,依次找出想要的信息
for index, table in enumerate(soup2.find_all('table')):
soup3 = BeautifulSoup(str(table), 'lxml')
for index2, tr in enumerate(soup3.find_all("tr")):
if index2 != 0:
tds = tr.find_all('td')
for index3, td in enumerate(tds):
soup_key = BeautifulSoup(str(td), 'lxml').find(name="div", attrs={"class": "item-name"})
soup_value = BeautifulSoup(str(td), 'lxml').find(name="div", attrs={"class": "item-value"})
if soup_key is not None and soup_value is not None:
dict_key = str(soup_key.contents[0]).replace('\n', '').replace(' ', '')
dict_value = str(soup_value.contents[0]).replace('\n', '').replace(' ', '')
table_dict[dict_key] = dict_value
save_car_detail = SaveData()
save_car_detail.save_car_detail("cars_info", table_dict)
except:
print("咱也不知道啥问题!我擦!")
def main(i):
try:
get_cars_detail_info(every_car_url_list[i])
except:
print("问题url:"+str(every_car_url_list[i]))
print("main里出问题了!我擦!")
every_car_url_list = get_every_car_url()
failed_url = []
if __name__ == '__main__':
# 每辆车的链接大概有不到10万条数据,因此在爬虫的过程中,会耗费大量的时间,这里做了一个简单的多线程,可以加快爬虫速度,Pool()里穿的是线程数量,一般笔记本跑就不要太大,太大会烧机器,卡死。。
pool = Pool(4)
try:
pool.map(main, [i for i in range(0, len(every_car_url_list))])
except:
print("多线程出问题了!我擦!")
pool.close()
pool.join()
七、将MongoDB里的数据导出成excel文件
放在数据库里的数据看起来不那么美观和清晰,因此,可以将数据库中的文件,导出成excel文件。
import pandas as pd
import pymongo
from spider_renren.get_city_data.config import *
from spider_renren.get_city_data.save_to_db import *
# 连接数据库
client = pymongo.MongoClient(MONGO_URL)
db = client[MONGO_DB]
table = db['cars_info']
# def get_table():
data = pd.DataFrame(list(table.find()))
data.to_excel("cars_info.xlsx", index=False)
【注意】如果想将数据全部导出成excel文件,可能会打不开,因为这是一个10万*200维的数据表,数据量还是很大的,在做数据处理的时候,读成DataFrame格式的数据也会占据不小的内存。所以可以进行分步读取,每次存一部分,先用小部分数据做数据预处理和探索性数据分析,最后流程熟悉后,再进行处理。
八、常见问题总结
-
在页面分析时,有些同学找不到想要的数据在哪里,我一般都是先在Elements下,用选取按钮找到大致范围,随后在其中寻找数据。
-
Elements中找不到怎么办?有些数据是动态存储的,开发者将这些数据用JavaScript进行渲染,所以在Elements页面中找不到这部分数据,但是它们都在Network中,以json的数据格式存储,我们可以切换到Network下,清楚当前的信息,进行页面刷新,这时会在Name栏出现很多条链接数据,点击某行,在右侧的Preview中会显示这个链接的预览情况,找到你想要的数据连接,点击Headers,你可以得到Request URL,通过这个连接就可以得到Elements中找不到的数据了。
-
代码的规范性。
博主在写代码的时候就不太注意规范性,有些变量和方法的命名随心所欲,导致经常会忘记,降低了编程效率。其次,有些方法可以进行重复调用的时候,最好写一个类将它封装起来,这样减少了许多工作量。 -
抛出异常!
这是我最想说的一点,在写代码的时候,一定要加上try,except,不加会导致什么后果呢?比如解析网页的时候遇到网络连接断开,啪!你的程序爆红了,终止运行了,当你爬取很大的数据时,你都不知道在哪里断开的,这个时候就得重新新建一个数据表,重新跑代码,费时费力。
九、结语
spider的知识还需要进一步学习,有许多更加便捷的方法我还不知道,在一步一步的学习中,你会发现,有些困扰你很久的东西,突然看到哪篇博客就能有一个更好的方法去解决。
接下来想用这些数据做一个二手车价格预测的模型,在建模之前肯定还会有一个数据预处理和探索性数据分析,应该还会再出两篇博客,不过我实在是太懒了,一次性敲这么多字也好费精力鸭,不知道什么时候会在更新。
最后,如果我的分析和代码出现什么问题,或者有更好的解决方法,欢迎各位同学进行指正,相互学习。