(一)项目目标
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. 项目代码重用率比较低,如果要爬其他店铺的商品信息,如要重新进行分析。
本文仅供学习交流使用,请勿将其用于违法目的。