python爬蟲--獲取天貓店鋪商品價格及銷量
(一)專案目標
1. 獲取天貓店鋪 “探路者官方旗艦店” 所有商品的名稱、價格以及銷量。
說明:本次專案目標是從一個熱門店鋪排行榜中隨機選擇的,沒有任何針對性的含義。
該排行榜的網址為: http://www.xlphb.cn/index.php?c=shop
2. 該店鋪的截圖如下:
3. 左上角有一個 “所有商品” 的連結,點選進入如下截圖:
4. 下方截圖可以看到資料仍然是分頁排列的,一共有14頁的資料。
(二)網頁分析
1. 首先還是開啟charles,重新整理頁面,通過charles的搜尋功能,找到目標資料的請求
2. 確認該請求是否全部包含目標資料
從下方截圖可以看到,返回的資料格式是html,經手動確認,該請求包含該頁的所有商品資訊。
3. 分析請求的具體情況
從下方截圖可以看到,該請求的具體資訊是:
url: https://toread.tmall.com/i/asynSearch.htm?_ksTS=1529821691770_124&callback=jsonp125&mid=w-18307703560-0&wid=18307703560&path=/search.htm&search=y&spm=a1z10.3-b-s.w4011-18307703560.430.4ee0605f0KyPWs&scene=taobao_shop&pageNo=3&tsearch=y
請求型別: GET
4. 可以看到,這個請求有非常多的query引數。
經手動測試,有一些引數即使沒有,也可以拿到資料,最終精簡到如下url:
https://toread.tmall.com/i/asynSearch.htm?mid=w-18307703560-0&pageNo=2
注意,最後一個pageNo是指的頁數,這樣我們就可以直接通過改變pageNo,就能獲得到不同頁面的資料了。
5. 然後就是對頁面資料進行解析時有一個坑。
一般來講,我比較擅長使用css和正則表示式語法來選擇資料,但是請看以下截圖:
目標資料就在這些div和dl裡面,但是淘寶設定的class 使用了這樣的形式 "\"item" \", 這樣我在用pyquery解析的時候總是要麼要錯,要麼拿到資料。
但是沒關係,下面的程式碼中,我使用了xpath語法來選擇,躲開這個坑。
6. 網頁基本分析完畢,請求只有一個,比較簡單。但是要注意一下兩點:
- 不知道淘寶的具體規則是什麼,傳送一個上面的請求並不一定能夠獲取到目標資料,但是隻要重複不斷髮送,就能獲取到。
- headers一定要寫全。
(三)核心程式碼實現
1. 一些需要使用的模組和常量
import requests
from requests.exceptions import RequestException
from scrapy.selector import Selector
import csv
import random
from requests.exceptions import ConnectionError
s = requests.session()
# csv結果檔案的儲存檔名
filename = '爬取結果.csv'
# 定義代理池url,可以從代理池專案檔案中找到介面
PROXY_POOL_URL = 'http://127.0.0.1:5555/random'
2. 隨機切換User-Agent:
我是在專案中加入了一個agent.txt檔案,裡面儲存了一些User-Agent可供使用。
# ag作為開關,僅第一次讀取,之後就從ag裡面拿
ag = None
def change_agent():
global ag
if not ag:
with open('agent.txt') as f:
ag = f.readlines()
return ag[random.randint(1,866)].strip()
3. 使用代理池
代理池的程式碼就不貼出來了,我是從github找的其他大神寫的,從公開渠道獲取免費代理後儲存入redis的一個專案。
下面的程式碼使用前,我已經打開了redis和代理池的程式碼。
# 定義獲取代理的函式
def get_proxy():
"""從代理池中取出一個代理"""
try:
response = requests.get(PROXY_POOL_URL)
if response.status_code == 200:
return response.text
return None
except ConnectionError:
return None
def build_proxy():
"""將代理池中取出的代理構造成完整形式"""
proxy = get_proxy()
if proxy:
return {
'http': 'http://' + proxy,
#'https': 'https://' + proxy
}
else:
return None
4. 上面網頁分析的部分有說到,傳送該請求並不一定會獲取到目標資料,如果沒有獲取到,需要重新發送。
這裡定義個一個輔助函式,用於判斷獲取到的網頁是否包含目標資料。
注意這裡用的scrapy庫裡面的Selector模組,方便使用xpath語法選取資料。
當然還有其他庫可以使用xpath語法,我對scrapy比較熟悉,所以使用這個。
def decide_if_loop(html):
"""通過解析要拿到的頁面第一個資料,判斷是否拿到真正的頁面,如果假的頁面,就返回False"""
selector = Selector(text=html)
data = selector.xpath('/html/body/div/div[3]/div[1]/dl[1]/dd[2]/a/text()').extract_first()
return False if not data else selector
5. 下面是獲取頁面的函式:
注意該函式內,呼叫了上面的切換User-Agent 和 使用代理池的函式。
def get_page(url):
"""
1. user-agent 不斷切換
2. 直接使用代理池中的代理來請求
"""
headers = {
'User-Agent':change_agent(),
'Referer':'https://toread.tmall.com',
'accept':'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01',
'x-requested-with':'XMLHttpRequest',
'accept-encoding':'gzip, deflate, br',
'accept-language':'zh-CN,zh;q=0.9,en;q=0.8'
}
try:
response = s.get(url, headers=headers, verify = False, proxies=build_proxy())
except:
response = s.get(url, headers=headers, verify = False)
if response.status_code == 200:
return response.text
else:
print("請求錯誤:{}".format(response.status_code))
6. 獲取到真實頁面之後,就需要進行頁面解析
注意傳入的引數是selector,就是前面的判斷是否需要迴圈的函式中的返回值。
因為該函式內已經對頁面進行解析後得到selector了,所以這裡就不再重複,直接傳入使用。
def parse_detail(selector):
"""從拿到的真實頁面中,解析出商品名,銷量和價格"""
data = []
# 兩個for迴圈解析一個html頁面
for i in range(1,13):
for j in range(1, 6):
title = selector.xpath('/html/body/div/div[3]/div['+str(i)+']/dl['+str(j)+']/dd[2]/a/text()').extract_first()
price = selector.xpath('/html/body/div/div[3]/div['+str(i)+']/dl['+str(j)+']/dd[2]/div/div[1]/span[2]/text()').extract_first()
num = selector.xpath('/html/body/div/div[3]/div['+str(i)+']/dl['+str(j)+']/dd[2]/div/div[3]/span/text()').extract_first()
# 這個判斷用於防止最後一頁商品不全時,或者頁面出現任何錯誤,值可能為空的情況
if title and price and num:
data.append([title.strip(), price.strip(), num.strip()])
return data
7. 下面定義了兩個函式,將解析到的資料儲存本地csv檔案。
def save_to_csv(rows, filename):
if rows:
with open(filename, "a") as f:
f_csv = csv.writer(f)
for row in rows:
f_csv.writerow(row)
def write_csv_headers(filename):
csv_headers = ["商品名稱", "價格", "銷量"]
with open(filename, "a") as f:
f_csv = csv.writer(f)
f_csv.writerow(csv_headers)
8. 下面的一些函式將整個程式碼串在一起
def loop(url):
html = get_page(url)
selector = decide_if_loop(html)
if not selector:
loop(url)
else:
data = parse_detail(selector)
save_to_csv(data, filename)
def get_urls():
urls = []
base_url = 'https://toread.tmall.com/i/asynSearch.htm?mid=w-18307703560-0&pageNo='
for i in range(1, 15):
urls.append(base_url + str(i))
return urls
def main():
write_csv_headers(filename)
for url in get_urls():
loop(url)
if __name__=="__main__":
main()
(四)專案結果以及經驗教訓
經過不斷的失敗後重試,專案終於成功的獲取了所有的14頁的資料,共700多條,也就是該天貓店鋪所有商品的名稱、價格以及銷量。展示截圖如下:
經驗教訓:
淘寶的坑還是比較多的,在嘗試的過程中:
1. 如果直接請求頁面的url,可以直接獲得到資料,但是全部是假資料;
2. 真實的請求也需要多次獲取才能拿到目標資料;
3. 如果不使用代理池,自己的ip很容易被封掉。
4. 專案程式碼重用率比較低,如果要爬其他店鋪的商品資訊,如要重新進行分析。
本文僅供學習交流使用,請勿將其用於違法目的。