前情提要:須要爬取搜狐汽車的全部配置信息,具體配置對應的參數. 以及在動態圖表上的歷史銷量。javascript
好比: 一汽奧迪旗下Q5L 的《40 TFSI 榮享進取型 國VI 》的歷史銷量和該配置的參數信息。html
所以總體分兩個大塊,一個是配置參數,一個是歷史銷量。java
下面開始正文python
第一步:mysql
首先觀察網頁:http://db.auto.sohu.com/home/ sql
在搜狐汽車主頁查看源代碼,找到對應的配置信息連接所在DIV:mongodb
(能夠任選一個汽車的配置信息頁好比奧迪A4 發現連接爲 http://db.auto.sohu.com/yiqiaudi/2374瀏覽器
而其配置信息的頁面爲 http://db.auto.sohu.com/yiqiaudi/2374/trim.html .安全
故須要找的是主頁內每一款車的具體連接所在的div模塊 ,有了這個每次加上trim.html 進行循環爬取便可。)app
對應DIV模塊爲:
所以利用xpath定位到具體的 a標籤下 class="model-a"便可鎖定全部車型的連接,代碼以下:
import requests import pandas as pd import re from lxml import etree import numpy as np import collections import pickle # 總頁面的根據字母提取子url和對應品牌 # treeNav brand_tit url_all='https://db.auto.sohu.com/home/' req_all=requests.get(url_all) wb_all=req_all.text; #網頁源碼 html_all = etree.HTML(wb_all) # model-a 先獲取全部車型連接反推創建上層 或根據連接分析創建(英文不推薦) js=html_all.xpath('//a[@class="model-a"]') h=[] for i in js: h.append(i.xpath('@href')[0])
所獲得的h 即爲全部子連接的list;
第二步:
觀察某一個車型子連接下配置頁面的源代碼, 尋找配置的具體參數命名和具體參數的值。 以下圖
<table id="trimArglist" cellspacing="0" cellpadding="0"> <tbody> <tr id="SIP_C_102"> <th class="th1"> <div class="th1_div"> <a href="http://db.auto.sohu.com/baike/244.shtml" target="_blank">廠商指導價</a>: </div> </th> <td class="th2"></td> <td class="th3"> </td> <td class="th4"> </td> <td class="th5"> </td> </tr> <tr id="SIP_C_101" class="new-energy-car"> <th class="th1">補貼後售價: </th> <td class="th2"></td> <td class="th3"> </td> <td class="th4"> </td> <td class="th5"> </td> </tr> <tr id="SIP_C_103"> <th class="th1"> <a href="http://db.auto.sohu.com/baike/247.shtml" target="_blank">4S店報價</a>: </th> <td class="th2"> </td> <td class="th3"> </td> <td class="th4"> </td> <td class="th5"> </td> </tr> <tr id="ttr_0"> <th colspan="60" class="colSpan6" style="border-top: 0px"> <span>車輛基本參數</span> <span class="sqsq">收起</span> </th> </tr> <tr id="SIP_C_105"> <th class="th1"> <a href="http://db.auto.sohu.com/baike/249.shtml" target="_blank">級別</a>: </th> <td class="th2"> </td> <td class="th3"> </td> <td class="th4"> </td> <td class="th5"> </td> </tr> <tr id="SIP_C_109"> <th class="th1"> <a href="http://db.auto.sohu.com/baike/249.shtml" target="_blank">上市時間</a>: </th> <td class="th2"> </td> <td class="th3"> </td> <td class="th4"> </td> <td class="th5"> </td> </tr>
通過觀察上圖看到,具體的配置名稱能夠經過xpath定位到 table[@id="trimArglist"] 後提取內中全部 a標籤的名稱。 可是此處並未有具體配置的值信息。所以只是一個配置名稱的集合。
而連接配置名稱與配置參數值的樞紐是他的 tr id 好比上圖 中 : 上市時間的表格 id
SIP_C_109 發現該值出如今js的一個參數裏: 見下圖
即調用js的參數對錶格進行賦值。連接樞紐是表格id表明其物理意義。
而後咱們即提取id 而後搜尋js的這個賦值參數便可。
因在js中全部配置的參數賦值自己就是字典形式,即var trim={SIP_C_103:xxx,SIP_C_104:xxx}
所以直接在python中執行這一js賦值語句便可獲得一個字典,而後再將這些
id號好比SIP_C_103對應的網頁div表格下a標籤的中文進行替換便可
代碼以下:
# 全部車 df={} df=collections.OrderedDict() for o in h: ############################################## 整車配置 ################################################# url='https:'+o+'/trim.html' req=requests.get(url) wb_data=req.text #網頁源碼 # xpath定位至其js賦給車輛頁面參數的地方 html = etree.HTML(wb_data) js=html.xpath('//script[@type="text/javascript"]') # 這裏有不少js 尋找js內存在參數配置備註的這一條 k=[] for i in range(len(js)): if js[i].text!=None: if len(re.findall('// 參數配置',js[i].text))!=0: k.append(js[i]); js=k.copy() js=k.copy() sss=js[0].text # 定位到具體js的某一個變量 trimParam 順便處理js賦值中TRUE 和false在python中會報錯 所以定義爲字符。 sss=sss[sss.find('trimParam'):] sss=sss.replace('false','"false"') sss=sss.replace('true','"true"') # 直接調用js的賦值. 某些車輛停售或暫未發售無參數默認就繼續循環執行(continue) exec(sss) if len(trimParam)==0: continue # js對參數賦值時對應的代號的物理意義:好比 SIP_C_103的意義多是爲 續航里程,把代號換掉 c=[] TB=html.xpath('//table[@id="trimArglist"]') for i in list(trimParam[0]['SIP_T_CONF'].keys()): tbname=TB[0].xpath('//table//tr[@id=\"'+i+'\"]//th[@class="th1"]') for j in range(len(trimParam)): if len(tbname)!=0: if tbname[0].text.replace(' ','')=='\n': tbname=TB[0].xpath('//tr[@id=\"'+i+'\"]//th[@class="th1"]//a') c.append(tbname[0].text) trimParam[j]['SIP_T_CONF'][tbname[0].text] = trimParam[j]['SIP_T_CONF'].pop(i) try: trimParam[j]['SIP_T_CONF'][tbname[0].text]=trimParam[j]['SIP_T_CONF'][tbname[0].text]['v'] except: trimParam[j]['SIP_T_CONF'][tbname[0].text]=''; #車輛沒有的配置數據不進行記錄 if (trimParam[j]['SIP_T_CONF'][tbname[0].text]=='-') | (trimParam[j]['SIP_T_CONF'][tbname[0].text]==''): # 車輛配置裏-表明車無此配置廠商也沒法進行安裝此配置 del trimParam[j]['SIP_T_CONF'][tbname[0].text] else: # 某些配置在js中沒有參數進行賦值,發現是一些複寫的參數好比已有長寬高的信息和參數值,可是存在名字爲長的信息但沒有賦值,所以不要 c.append(np.nan) del trimParam[j]['SIP_T_CONF'][i] trimParam_dict={} for i in range(len(trimParam)): trimParam_dict[trimParam[i]['SIP_T_NAME']]=trimParam[i]; # 反推創建數據字典 if trimParam[0]['brandName'] not in df.keys(): df[trimParam[0]['brandName']]={} if trimParam[0]['subbrandName'] not in df[trimParam[0]['brandName']].keys(): df[trimParam[0]['brandName']][trimParam[0]['subbrandName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']][trimParam[0]['modelName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']][trimParam[0]['modelName']]['配置參數']=trimParam_dict
最後反推創建字典是根據配置裏的品牌,子品牌,車輛配置名稱信息創建上層字典的key來定位自身。
至此配置信息的字典格式就完成了,由於訪問每個車型時都會進行數據處理,所以訪問間隔不會過短致使被反爬機制封掉。
接下來是動態圖表的銷量信息 ,咱們但願承接上文,在每個子品牌的車型旗下直接新建一個key(原本只有上文的配置參數key),讓他記錄歷史的銷量信息。
首先動態圖表的歷史數據在網頁源碼上搜不到,那麼咱們調用瀏覽器的控制檯來觀察他在動態圖表上顯示數據的整個響應過程,經過這個來找圖表調用的數據來源是什麼。
打開控制檯觀察一個車型子連接的銷量頁面。見下圖:
左側爲動態圖表,右側爲控制檯,如今咱們點一下所有數據
響應的信息數據出現了,見右側控制檯xml參數,觀察xml的header(當前是response返回的數據)
不難發現數據是從這個 連接獲得的,以下圖:
這跟咱們車型的關係樞紐就是model後的那一串數字 即爲車型id號的連接,那麼每一款車型的id號知道了,就能獲取每個銷量數據的連接,
車型id號 剛好咱們在調用js賦值時發現是有的 那麼在以前的循環中 提取id號 而後處理銷量數據便可,代碼以下面的銷量數據部分:
for o in h: ############################################## 整車配置 ################################################# url='https:'+o+'/trim.html' req=requests.get(url) wb_data=req.text #網頁源碼 # xpath定位至其js賦給車輛頁面參數的地方 html = etree.HTML(wb_data) js=html.xpath('//script[@type="text/javascript"]') # 這裏有不少js 尋找js內存在參數配置備註的這一條 k=[] for i in range(len(js)): if js[i].text!=None: if len(re.findall('// 參數配置',js[i].text))!=0: k.append(js[i]); js=k.copy() js=k.copy() sss=js[0].text # 定位到具體js的某一個變量 trimParam sss=sss[sss.find('trimParam'):] sss=sss.replace('false','"false"') sss=sss.replace('true','"true"') # 直接調用js的賦值. exec(sss) if len(trimParam)==0: continue # js對參數賦值時對應的代號的物理意義:好比 SIP_C_103的意義多是爲 續航里程,把代號換掉 c=[] TB=html.xpath('//table[@id="trimArglist"]') for i in list(trimParam[0]['SIP_T_CONF'].keys()): tbname=TB[0].xpath('//table//tr[@id=\"'+i+'\"]//th[@class="th1"]') for j in range(len(trimParam)): if len(tbname)!=0: if tbname[0].text.replace(' ','')=='\n': tbname=TB[0].xpath('//tr[@id=\"'+i+'\"]//th[@class="th1"]//a') c.append(tbname[0].text) trimParam[j]['SIP_T_CONF'][tbname[0].text] = trimParam[j]['SIP_T_CONF'].pop(i) try: trimParam[j]['SIP_T_CONF'][tbname[0].text]=trimParam[j]['SIP_T_CONF'][tbname[0].text]['v'] except: trimParam[j]['SIP_T_CONF'][tbname[0].text]=''; #車輛沒有的配置數據不進行記錄 if (trimParam[j]['SIP_T_CONF'][tbname[0].text]=='-') | (trimParam[j]['SIP_T_CONF'][tbname[0].text]==''): # 車輛配置裏-表明車無此配置廠商也沒法進行安裝此配置 del trimParam[j]['SIP_T_CONF'][tbname[0].text] else: # 某些配置在js中沒有參數進行賦值,發現是一些複寫的參數好比已有長寬高的信息和參數值,可是存在名字爲長的信息但沒有賦值,所以不要 c.append(np.nan) del trimParam[j]['SIP_T_CONF'][i] trimParam_dict={} for i in range(len(trimParam)): trimParam_dict[trimParam[i]['SIP_T_NAME']]=trimParam[i]; # 反推創建數據字典 if trimParam[0]['brandName'] not in df.keys(): df[trimParam[0]['brandName']]={} if trimParam[0]['subbrandName'] not in df[trimParam[0]['brandName']].keys(): df[trimParam[0]['brandName']][trimParam[0]['subbrandName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']][trimParam[0]['modelName']]={} df[trimParam[0]['brandName']][trimParam[0]['subbrandName']][trimParam[0]['modelName']]['配置參數']=trimParam_dict ############################################## 銷量數據 ################################################# vehicle_model_id= trimParam[0]['SIP_T_MODELID'] url='https://db.auto.sohu.com/cxdata/xml/sales/model/model'+str(vehicle_model_id)+'sales.xml' req=requests.get(url) wb_data=req.text #網頁源碼 sales=re.findall(r'(?<=<sales).*?(?=/>)',wb_data) if len(sales)==0: continue; else: df[trimParam[0]['brandName']][trimParam[0]['subbrandName']][trimParam[0]['modelName']]['歷史銷量']={} for i in sales: df[trimParam[0]['brandName']][trimParam[0]['subbrandName']][trimParam[0]['modelName']]['歷史銷量'][re.findall(r'(?<=date=").*?(?=")',i)[0]]=int(re.findall(r'(?<=salesNum=").*?(?=")',i)[0]) print(trimParam[0]['subbrandName']+trimParam[0]['modelName']+'--num'+str(h.index(o))+'--total:'+str(len(h)))
至此整個字典就定義好了,最上層爲品牌,其次是子品牌,而後是配置,最後分銷量和配置信息。
接下來要麼就已字典的格式利用pymongo存到mongodb裏去,要麼改爲dataframe格式存入sql均可。 須要注意的是mongodb存入的過程當中 字典key不能夠出現" ."點的符號 所以須要替換。
給出一個替換函數供參考
# fix 字典內keys含有.並替換 def fix_dict(data, ignore_duplicate_key=True): """ Removes dots "." from keys, as mongo doesn't like that. If the key is already there without the dot, the dot-value get's lost. This modifies the existing dict! :param ignore_duplicate_key: True: if the replacement key is already in the dict, now the dot-key value will be ignored. False: raise ValueError in that case. """ if isinstance(data, (list, tuple)): list2 = list() for e in data: list2.append(fix_dict(e)) # end if return list2 if isinstance(data, dict): # end if for key, value in data.items(): value = fix_dict(value) old_key = key if "." in key: key = old_key.replace(".", "_") if key not in data: data[key] = value else: error_msg = "Dict key {key} containing a \".\" was ignored, as {replacement} already exists".format( key=key_old, replacement=key) if force: import warnings warnings.warn(error_msg, category=RuntimeWarning) else: raise ValueError(error_msg) # end if # end if del data[old_key] # end if data[key] = value # end for return data # end if return data # end def df_2=fix_dict(df);
我這裏作成一個首字母的key在品牌以前,而後按照pkl的格式保存到本地
#按照首字母檢索的字典 for letter in range(65,91): df2[chr(letter)]={} for i in df.keys(): df2[lazy_pinyin(i)[0][0].upper()][i]=df[i] #本地文件保存 output = open('soho_vehicle.pkl', 'wb') pickle.dump(df2, output) output.close()
後續也能夠再處理存到sql 並另存爲csv或excel 用於查看。
import pandas as pd import numpy as np import pickle output = open('soho_vehicle.pkl', 'wb') df=pickle.load(output) output.close() # 配置信息整理 a=[] for o in df.keys(): for i in df[o].keys(): for j in df[o][i].keys(): for k in df[o][i][j]['配置參數'].keys(): df[o][i][j]['配置參數'][k]['SIP_T_CONF']['子品牌']=df[o][i][j]['配置參數'][k]['subbrandName'] df[o][i][j]['配置參數'][k]['SIP_T_CONF']['品牌']=df[o][i][j]['配置參數'][k]['brandName'] df[o][i][j]['配置參數'][k]['SIP_T_CONF']['款式']=df[o][i][j]['配置參數'][k]['modelName'] df[o][i][j]['配置參數'][k]['SIP_T_CONF']['配置名稱']=df[o][i][j]['配置參數'][k]['SIP_T_NAME'] df[o][i][j]['配置參數'][k]['SIP_T_CONF']['是否電動']=df[o][i][j]['配置參數'][k]['SIP_C_ISELECTRIC'] a.append(pd.Series(df[o][i][j]['配置參數'][k]['SIP_T_CONF'])) df_trim=pd.DataFrame(a) df_trim=df_trim.replace(np.nan,'---'); cols = list(df_trim) for i in cols: df_trim[i]=df_trim[i].str.strip(); df_trim[i]=df_trim[i].apply(lambda x:x.replace('m³','立方米')) df_trim[i]=df_trim[i].apply(lambda x:x.replace('\xa0',' ')) #df_trim['配置名稱']=df_trim['配置名稱'].apply(lambda x:x.replace('m³','立方米')) cols=list(pd.Series(cols).str.strip()); cols.insert(0, cols.pop(cols.index('保修政策'))) cols.insert(0, cols.pop(cols.index('車聯網:'))) cols.insert(0, cols.pop(cols.index('自動泊車入位'))) cols.insert(0, cols.pop(cols.index('車身穩定控制'))) cols.insert(0, cols.pop(cols.index('車載信息服務'))) cols.insert(0, cols.pop(cols.index('車道保持輔助系統:'))) cols.insert(0, cols.pop(cols.index('車道偏離預警系統:'))) cols.insert(0, cols.pop(cols.index('倒車車側預警系統:'))) cols.insert(0, cols.pop(cols.index('主動剎車/主動安全系統'))) cols.insert(0, cols.pop(cols.index('中央差速器結構'))) cols.insert(0, cols.pop(cols.index('底盤結構'))) cols.insert(0, cols.pop(cols.index('轉向助力'))) cols.insert(0, cols.pop(cols.index('輪轂材料'))) cols.insert(0, cols.pop(cols.index('進氣形式:'))) cols.insert(0, cols.pop(cols.index('每缸氣門數(個)'))) cols.insert(0, cols.pop(cols.index('氣門結構'))) cols.insert(0, cols.pop(cols.index('汽缸容積(cc)'))) cols.insert(0, cols.pop(cols.index('汽缸排列形式'))) cols.insert(0, cols.pop(cols.index('最大馬力(ps)'))) cols.insert(0, cols.pop(cols.index('最大扭矩(N·m/rpm)'))) cols.insert(0, cols.pop(cols.index('最大功率(kW/rpm)'))) cols.insert(0, cols.pop(cols.index('擋位個數'))) cols.insert(0, cols.pop(cols.index('變速箱類型'))) cols.insert(0, cols.pop(cols.index('變速箱'))) cols.insert(0, cols.pop(cols.index('壓縮比'))) cols.insert(0, cols.pop(cols.index('發動機電子防盜'))) cols.insert(0, cols.pop(cols.index('發動機型號'))) cols.insert(0, cols.pop(cols.index('發動機啓停技術'))) cols.insert(0, cols.pop(cols.index('發動機'))) cols.insert(0, cols.pop(cols.index('工信部油耗(L/100km)'))) cols.insert(0, cols.pop(cols.index('排放標準'))) cols.insert(0, cols.pop(cols.index('供油方式'))) cols.insert(0, cols.pop(cols.index('整車最大扭矩(N·m):'))) cols.insert(0, cols.pop(cols.index('整車最大功率(kW):'))) cols.insert(0, cols.pop(cols.index('軸距(mm)'))) cols.insert(0, cols.pop(cols.index('整備質量(kg)'))) cols.insert(0, cols.pop(cols.index('長x寬x高(mm)'))) cols.insert(0, cols.pop(cols.index('車體結構'))) cols.insert(0, cols.pop(cols.index('官方最高車速(km/h)'))) cols.insert(0, cols.pop(cols.index('官方0-100加速(s)'))) cols.insert(0, cols.pop(cols.index('快充時間(小時):'))) cols.insert(0, cols.pop(cols.index('快充電量(%):'))) cols.insert(0, cols.pop(cols.index('充電兼容性:'))) cols.insert(0, cols.pop(cols.index('充電方式:'))) cols.insert(0, cols.pop(cols.index('電池種類:'))) cols.insert(0, cols.pop(cols.index('電池容量(kWh):'))) cols.insert(0, cols.pop(cols.index('電動機總功率(kW):'))) cols.insert(0, cols.pop(cols.index('電動機總扭矩(N·m):'))) cols.insert(0, cols.pop(cols.index('電動機總扭矩(N·m):'))) cols.insert(0, cols.pop(cols.index('電機佈局:'))) cols.insert(0, cols.pop(cols.index('電機數:'))) cols.insert(0, cols.pop(cols.index('電機類型:'))) cols.insert(0, cols.pop(cols.index('上市時間'))) cols.insert(0, cols.pop(cols.index('動力類型:'))) cols.insert(0, cols.pop(cols.index('驅動方式'))) cols.insert(0, cols.pop(cols.index('補貼後售價:'))) cols.insert(0, cols.pop(cols.index('4S店報價'))) cols.insert(0, cols.pop(cols.index('廠商指導價'))) cols.insert(0, cols.pop(cols.index('級別'))) cols.insert(0, cols.pop(cols.index('百千米耗電量(kWh/100km):'))) cols.insert(0, cols.pop(cols.index('是否電動'))) cols.insert(0, cols.pop(cols.index('配置名稱'))) cols.insert(0, cols.pop(cols.index('款式'))) cols.insert(0, cols.pop(cols.index('子品牌'))) cols.insert(0, cols.pop(cols.index('品牌'))) df_trim = df_trim.ix[:, cols] df_trim=df_trim.replace(np.nan,'---'); df_trim=df_trim.drop(['高度(mm)','長度(mm)','寬度(mm)'],axis=1) df_trim=df_trim.drop(['車門數(個)','4S店報價'],axis=1) df_trim.to_csv('soho_veh_trim_para.csv',encoding='gbk') #銷量 a=[] for o in df.keys(): for i in df[o].keys(): for j in df[o][i].keys(): try: k=list(df[o][i][j]['配置參數'].keys())[0]; df[o][i][j]['銷量']['子品牌']=df[o][i][j]['配置參數'][k]['subbrandName'] df[o][i][j]['銷量']['品牌']=df[o][i][j]['配置參數'][k]['brandName'] df[o][i][j]['銷量']['款式']=df[o][i][j]['配置參數'][k]['modelName'] a.append(pd.Series(df[o][i][j]['銷量'])) except: continue df_sales=pd.DataFrame(a) cols = list(df_sales) cols.reverse() cols.insert(0, cols.pop(cols.index('款式'))) cols.insert(0, cols.pop(cols.index('子品牌'))) cols.insert(0, cols.pop(cols.index('品牌'))) df_sales = df_sales.ix[:, cols] df_sales=df_sales.fillna(0) df_sales.to_csv('soho_veh_sales.csv',encoding='gbk') #存入 sql from sqlalchemy import create_engine import pandas as pd import numpy as np from sqlalchemy.types import VARCHAR host = '127.0.0.1' port= 3306 db = 'soho_vehicle' user = 'root' password = 'twz1478963' engine = create_engine(str(r"mysql+mysqldb://%s:" + '%s' + "@%s/%s?charset=utf8") % (user, password, host, db)) df_sales.to_sql('soho_veh_sales', con=engine, if_exists='append', index=False) #如量級過大使用chunksize df_trim=df_trim.drop(['內飾可選顏色','車身可選顏色'],axis=1) df_trim.to_sql('soho_veh_trim', con=engine, if_exists='append', index=False) #如量級過大使用chunksize
歡迎交流!