爬虫实战!Python采集二手车数据并进行数据分析

蚂蚁学Python

共 17682字,需浏览 36分钟

 ·

2022-02-28 11:30

前言

前段时间给朋友采集某子二手车平台的数据,虽然最后搞定了js加密,也通过价格对比短时间内算是搞定了字体反爬,但总是感觉很不完美,网站字体一共有5种对应关系,每次请求后都用价格对比,严重拖慢了系统运行速度不说,还容易出错。这次换一个平台,采集数据后进行简单的数据分析。

1.数据采集

1.1车辆列表页面分析

通过对网页的url分析发现,URL地址p后面的数字是用来的控制翻页,plog_id可以不用添加。继续分析网页源代码:

发现车辆的信息就在网页源代码里,同时也发现网页有字体反爬,继续在网页源代码里搜索网页字体的URL。

根据每辆车详情页面的分析,还要拿到每辆车的data-car-id。

1.2车辆详情页面分析

可以看到网页的URL地址拼接了车辆的data-car-id,同样后面的plog_id也可以省略。

同样所有的数据也都在网页源代码里,同样也有字体文件的URL。

1.3实施步骤

  • 请求字体URL,手动建立对应关系 这里请求过程很简单,用requests的get方法就可以拿到文件,打开后观察字体的对应关系。

手动建立对应关系字典(根据观察,这个网站每天会更换一次字体对应关系,还没有瓜子那么BT)

# 字体对应关系
relation_table = {"zero""0""one""2""two""1""four""3""three""4""five""8""seven""7",
                  "nine""9""six""6""eight""5"}
  • 请求列表页面,拿到车辆的data-car-id
def get_carid(index):
    '''获取车辆id'''
    response 
= session.get(f'https://www.renrenche.com/bj/ershouche/p{index}/', headers=headers)
    html = etree.HTML(response.text)
    car_id = html.xpath('//*[@id="search_list_wrapper"]/div/div/div[1]/ul/li/a/@data-car-id')  # 获取车辆id列表
    return car_id

需要注意的是,返回的car_id是一个列表。

  • 处理字体,获取真实的对应关系,方便后续替换
def woff_font(font_url):
    '''获取字体真实对应关系'''
    newmap 
= {}
    resp = session.get(font_url)  # 请求字体链接
    woff_data = BytesIO(resp.content)
    font = TTFont(woff_data)  # 读取woff数据
    cmap = font.getBestCmap()  # 获取xml文件字体对应关系
    font.close()
    for k, v in cmap.items():
        value = v
        key = str(k - 48)  # 获取真实的key
        try:
            get_real_data = relation_table[value]
        except:
            get_real_data = ''
        if get_real_data != '':
            newmap[key] = get_real_data  # 将字体真实结果对应
    return newmap
  • 请求每辆车的详情页面,拿到所有数据,并写入csv文件
def get_data(url):
    '''提取数据'''
    resp 
= session.get(url, headers=headers)
    html = etree.HTML(resp.text)
    font_text = "".join(html.xpath('/html/head/style[2]/text()')).strip()  # 获取字体链接
    font_url = re.search(r"https.*?woff", font_text).group()
    font = woff_font(font_url)
    title = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[1]/h1/text()')).strip()  # 获取原始标题
    trans_title = "".join([i if not i.isdigit() else font[i] for i in title])  # 替换错误字体,获取真实标题
    mil = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[4]/ul/li[1]/div/p[1]/strong/text()'))  # 获取原始行驶里程
    trans_mil = "".join([i if not i.isdigit() else font[i] for i in mil])  # 获取真实行驶里程
    lice_time = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[9]/span[2]/text()')).strip()  # 获取上牌时间
    emi_level = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[4]/ul/li[4]/div/p[1]/strong/text()')).strip()  # 获取排放等级
    trans = "".join(html.xpath('//*[@id="basic-parms"]//tr[3]/td[2]/div[2]/text()')).strip()  # 获取变速箱形式
    c_name = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[4]/ul/li[6]/p[1]/strong/text()')).strip()  # 获取过户次数
    colo = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[6]/span[2]/text()')).strip()  # 获取车身颜色
    fu_type = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[13]/span[2]/text()')).strip()  # 获取燃料类型
    out_time = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[11]/span[2]/text()')).strip()  # 获取出厂日期
    model = "".join(html.xpath('//*[@id="basic-parms"]//tr[2]/td[1]/div[2]/text()')).strip()  # 获取车型
    engine = "".join(html.xpath('//*[@id="engine"]//tr[2]/td[1]/div[2]/text()')).strip()  # 获取发动机
    struc = "".join(html.xpath('//*[@id="basic-parms"]//tr[4]/td[2]/div[2]/text()')).strip()  # 获取车身结构
    guid_price = "".join(html.xpath('//*[@id="zhimaicar-no-discount"]/div[1]/span/text()')).strip()  # 获取指导价
    price = "".join(html.xpath('//*[@id="zhimaicar-no-discount"]/p/text()')).strip()  # 获取售价
    if len(trans_title) !0 and len(lice_time) == 10 and len(out_time) == 10:
        print(trans_title, trans_mil, lice_time, emi_level, trans, c_name, colo, fu_type, out_time, model, engine, struc, guid_price, price)
        csv_writer.writerow([trans_title, trans_mil, lice_time, emi_level, trans, c_name, colo, fu_type, out_time, model, engine, struc, guid_price, price])  # 写入csv文件

通过对请求后的数据分析,由于网页结构差异,有些返回的数据是空,容易导致写入错误数据,为了方便后期处理,加了判断。同时这里需要注意,Chrome浏览器tbody的坑。

1.4爬虫部分全部代码

from lxml import etree
import requests
import re
from fontTools.ttLib import TTFont
from io import BytesIO
from multiprocessing import Queue
from concurrent.futures import ThreadPoolExecutor
import csv

queue_list = Queue()

session = requests.session()
headers = {
    'User-Agent''Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
    'Referer''https://www.renrenche.com/bj/ershouche/?plog_id=3978670bd754fae06b75b0bc33540592',
}
# 字体对应关系
relation_table = {"zero""0""one""2""two""1""four""3""three""4""five""8""seven""7",
                  "nine""9""six""6""eight""5"}


def get_carid(index):
    '''获取车辆id'''
    response 
= session.get(f'https://www.renrenche.com/bj/ershouche/p{index}/', headers=headers)
    html = etree.HTML(response.text)
    car_id = html.xpath('//*[@id="search_list_wrapper"]/div/div/div[1]/ul/li/a/@data-car-id')  # 获取车辆id列表
    return car_id


def woff_font(font_url):
    '''获取字体真实对应关系'''
    newmap 
= {}
    resp = session.get(font_url)  # 请求字体链接
    woff_data = BytesIO(resp.content)
    font = TTFont(woff_data)  # 读取woff数据
    cmap = font.getBestCmap()  # 获取xml文件字体对应关系
    font.close()
    for k, v in cmap.items():
        value = v
        key = str(k - 48)  # 获取真实的key
        try:
            get_real_data = relation_table[value]
        except:
            get_real_data = ''
        if get_real_data != '':
            newmap[key] = get_real_data  # 将字体真实结果对应
    return newmap


def get_data(url):
    '''提取数据'''
    resp 
= session.get(url, headers=headers)
    html = etree.HTML(resp.text)
    font_text = "".join(html.xpath('/html/head/style[2]/text()')).strip()  # 获取字体链接
    font_url = re.search(r"https.*?woff", font_text).group()
    font = woff_font(font_url)
    title = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[1]/h1/text()')).strip()  # 获取原始标题
    trans_title = "".join([i if not i.isdigit() else font[i] for i in title])  # 替换错误字体,获取真实标题
    mil = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[4]/ul/li[1]/div/p[1]/strong/text()'))  # 获取原始行驶里程
    trans_mil = "".join([i if not i.isdigit() else font[i] for i in mil])  # 获取真实行驶里程
    lice_time = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[9]/span[2]/text()')).strip()  # 获取上牌时间
    emi_level = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[4]/ul/li[4]/div/p[1]/strong/text()')).strip()  # 获取排放等级
    trans = "".join(html.xpath('//*[@id="basic-parms"]//tr[3]/td[2]/div[2]/text()')).strip()  # 获取变速箱形式
    c_name = "".join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[4]/ul/li[6]/p[1]/strong/text()')).strip()  # 获取过户次数
    colo = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[6]/span[2]/text()')).strip()  # 获取车身颜色
    fu_type = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[13]/span[2]/text()')).strip()  # 获取燃料类型
    out_time = "".join(html.xpath('//*[@id="report"]/div/div[3]/div[2]/ul/li[11]/span[2]/text()')).strip()  # 获取出厂日期
    model = "".join(html.xpath('//*[@id="basic-parms"]//tr[2]/td[1]/div[2]/text()')).strip()  # 获取车型
    engine = "".join(html.xpath('//*[@id="engine"]//tr[2]/td[1]/div[2]/text()')).strip()  # 获取发动机
    struc = "".join(html.xpath('//*[@id="basic-parms"]//tr[4]/td[2]/div[2]/text()')).strip()  # 获取车身结构
    guid_price = "".join(html.xpath('//*[@id="zhimaicar-no-discount"]/div[1]/span/text()')).strip()  # 获取指导价
    price = "".join(html.xpath('//*[@id="zhimaicar-no-discount"]/p/text()')).strip()  # 获取售价
    if len(trans_title) !0 and len(lice_time) == 10 and len(out_time) == 10:
        print(trans_title, trans_mil, lice_time, emi_level, trans, c_name, colo, fu_type, out_time, model, engine, struc, guid_price, price)
        csv_writer.writerow([trans_title, trans_mil, lice_time, emi_level, trans, c_name, colo, fu_type, out_time, model, engine, struc, guid_price, price])  # 写入csv文件


if __name__ == '__main__':
    f = open('renrenche1.csv''w', encoding='utf-8', newline='')
    csv_writer = csv.writer(f)
    csv_writer.writerow(['车型''行驶里程''上牌时间''排放等级''变速箱形式''过户次数''车身颜色''燃料类型''出厂日期''车型''发动机型号''车身结构''新车含税价''到手价'])
    for i in range(151):
        car_id 
= get_carid(i)
        for id in car_id:
            car_url = f"https://www.*********.com/bj/car/{id}"
            queue_list.put(car_url)  # get_data(car_url,font)
    pool = ThreadPoolExecutor(max_workers=30)
    while queue_list.qsize() > 0:
        pool.submit(get_data, queue_list.get())

1.5获取的文件

2.简单的数据分析

2.1数据清洗

# 新增品牌列
df["品牌"] = df["车型"].apply(lambda x : x.split("-")[0])
df.duplicated().value_counts()  # 查找重复数据
#False    1330
#True       18
#dtype: int64
df.drop_duplicates(inplace=True) # 删除重复数据
df.duplicated().value_counts()  # 检查重复数据
#False    1330
#dtype: int64
df.dropna(axis=0,how="any",inplace=True)  # 删除重复值  

2.2选择数据,并进行数据处理

df_new = df[['品牌','出厂日期','上牌时间','行驶里程','过户次数','新车含税价','到手价']]
# 清理出厂日期和上牌时间列的异常值
time1 = df_new['出厂日期'].map(lambda x : len(x)==10)
time2 = df_new['上牌时间'].map(lambda x : len(x)==10)
df_1 = df_new[time1&time2]
# 转换行驶里程等列的数据格式
df_1.loc[:,"行驶里程"] = df_1["行驶里程"].map(lambda x : float(x.replace("万公里","")))
df_1.loc[:,"过户次数"] = df_1["过户次数"].map(lambda x : int(x.replace("次","")))
df_1.loc[:,"新车含税价"] = df_1["新车含税价"].map(lambda x : float(x.replace("万","")))
df_1['出厂日期'] = pd.to_datetime(df_1['出厂日期'])  
df_1['上牌时间'] = pd.to_datetime(df_1['上牌时间'])

2.3数据分析

  • Describe
df_1.describe()
  • 协方差矩阵和相关系数矩阵
df_1.cov()  # 协方差矩阵
df_1.corr() # 相关系数矩阵

  • 品牌占比分析
p_num = df_1["品牌"].value_counts()
p_num = p_num[p_num.values>10] # 选择数量大于10的品牌
from pyecharts import options as opts
from pyecharts.charts import Pie
datas = list(zip(p_num.index.to_list(), p_num.to_list()))
pie = Pie()
pie.add("",
        datas,
       radius=[0,200],)
pie.set_global_opts(
    title_opts=  opts.TitleOpts(title="二手车品牌占比"),
    legend_opts = opts.LegendOpts(pos_right="right")
)
pie.set_series_opts(label_opts= opts.LabelOpts(formatter="{b}, {c}, {d}%"))
pie.render_notebook()
  • 价格分析
bins = [0,5,15,30,50,130]
s_bar = pd.cut(df_1.到手价,bins).value_counts()
from pyecharts.charts import Bar
x_num = [str(i) for i in s_bar.index]
bar=(
    Bar()
     .add_xaxis(x_num)
     .add_yaxis(
         "",
         s_bar.values.tolist(),
         bar_width='20%',
     )
     .set_global_opts(
     title_opts=opts.TitleOpts(title="车型售价区间"),
         datazoom_opts=opts.DataZoomOpts(type_="inside")
     )
    )
bar.render_notebook()
  • 半成品漏斗图
from pyecharts.charts import Funnel
funnel = Funnel()
funnel.add("", datas)
funnel.set_global_opts(
    title_opts=  opts.TitleOpts(title="二手车保有量占比"),
    legend_opts = opts.LegendOpts(pos_right="right")
)
funnel.render_notebook()


推荐蚂蚁老师的全套Python课程

先保存图片,然后抖音打开扫码购买,提供代码、课件、答疑服务



浏览 197
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报