1. 程式人生 > >python爬蟲--獲取天貓店鋪商品價格及銷量

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. 專案程式碼重用率比較低,如果要爬其他店鋪的商品資訊,如要重新進行分析。

本文僅供學習交流使用,請勿將其用於違法目的。