Scrapy爬虫 + Tkinter爬取天猫热门商品

每当双十一、双十二,看着淘宝、天猫、京东网页上琳琅满目的商品,经常有人会因为选择困难症不知道该购买什么好。而且购物网站商品的排列顺序经常会受到人为控制。因此,一个具有通过输入关键字,筛选相关热门产品并按热门程度排序的程序是有存在意义的。本程序以天猫为例,使用Python语言开发,利用Scrapy框架爬取网页信息,利用Tkinter框架构建程序GUI。源代码已上传至GitHub:https://github.com/HirojiSawatari/FindGoods
最终界面如下所示:

Screenshot

一、框架安装

Tkinter框架为内置模块,无需额外安装,而Scrapy框架安装方法见官方链接:https://doc.scrapy.org/en/latest/intro/install.html

二、Scrapy配置

构建项目后,由于天猫具有反爬取措施,首先需要在配置文件settings.py中输入以下代码:

1
2
3
4
5
6
7
8
# 绕过天猫robots.txt
ROBOTSTXT_OBEY = False
# 禁止cookies,防止被ban
COOKIES_ENABLED = False
# 伪装chrome
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36'

同时,为了输出爬取得到的信息,同样需要在setting.py中输入输出文件信息:

1
2
3
# Output .csv
FEED_URI = u'goods.csv'
FEED_FORMAT = 'CSV'

配置文件配置完毕。

三、开始爬取

首先建立spider,爬取之前,设置name,爬取网站url等信息,并设置爬取时间间隔防止被ban。

1
2
3
4
5
6
name = "FindGoods"
download_delay = 4
allowed_domains = ["tmall.com"]
start_urls = [
"https://www.tmall.com/"
]

确定我们需要爬取的商品相关信息。观察天猫搜素结果页,我们可以获取到的信息包括商品名称、店铺名称、价格、月成交量和评论数。为了实现之后点击item跳转购买界面的功能,我们还应当获取该商品购买界面的url信息。此外,我们还需预留分数字段以存储根据商品成交量和评论数计算得到的分数信息。确定爬取信息之后定义item.py如下:

1
2
3
4
5
6
7
name = Field()
shop = Field()
price = Field()
trading = Field()
review = Field()
url = Field()
score = Field()

继续观察天猫搜素结果页,我们可以通过审查页面要素得到我们需得到的商品信息在页面中的位置。Scrapy中页面位置信息通过xpath获取。由于天猫电器城情况特殊,我们还需要单独对电子产品页面进行单独分析。最终分析得到的要素爬取xpath信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gifts = sel.xpath('//*[@id="J_ItemList"]/div[@class="product "]')
for gift in gifts:
name = gift.xpath('div/p[@class="productTitle"]/a/@title').extract()
# 天猫电器城HTML结构不同
if not name:
name = gift.xpath('div/div[@class="productTitle productTitle-spu"]/a[1]/text()').extract()
shop = gift.xpath('div/div[@class="productShop"]/a[@class="productShop-name"]/text()').extract()
price = gift.xpath('div/p[@class="productPrice"]/em/@title').extract()
trading = gift.xpath('div/p[@class="productStatus"]/span[1]/em/text()').extract()
review = gift.xpath('div/p[@class="productStatus"]/span[2]/a/text()').extract()
url = gift.xpath('div/p[@class="productTitle"]/a/@href').extract()
if not url:
url = gift.xpath('div/div[@class="productTitle productTitle-spu"]/a[1]/@href').extract()

获取信息后需要存储,同时计算商品评分。我们此处评分标准采用(月交易量×2+评论数)的方法计算。因为在html中获取的月交易量信息和评论数信息可能带有中文字符,例如“万”、“笔”等,我们还需剔除这些无关信息后转换为float数据类型进行计算。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# sys.getfilesystemencoding()获得本地编码(mbcs编码)
item['name'] = [na.encode(sys.getfilesystemencoding()) for na in name]
# 去掉商店名末尾的\n换行符(有两个\n)
tempshop = str(shop[0].encode(sys.getfilesystemencoding()))
item['shop'] = tempshop.strip('\n')
item['price'] = price
item['url'] = 'https:' + url[0]
# 天猫电器城少数商品无交易量信息
tradnum = 0
if trading:
# 在搜索页无法获取交易量详细数字,需转化
tradstr = str(trading[0].encode(sys.getfilesystemencoding()))
item['trading'] = tradstr
# “笔”字在字符串中的下标
biindex = tradstr.index('\xb1\xca')
# 除去“笔”
tradstr = tradstr[:biindex]
# 判断是否有“万”字
if '\xcd\xf2' in tradstr:
# “万”字在字符串中的下标
wanindex = tradstr.index('\xcd\xf2')
# 除去“万”字
tradstr = tradstr[:wanindex]
tradnum = tradnum + string.atof(tradstr) * 10000
else:
# 没有“万”字
tradnum = tradnum + string.atof(tradstr)
# 天猫电器城无评论数信息
revinum = 0
if review:
# 在搜索页无法获取评论数详细数字,需转化
revistr = str(review[0].encode(sys.getfilesystemencoding()))
item['review'] = revistr
# 判断是否有“万”字
if '\xcd\xf2' in revistr:
# “万”字在字符串中的下标
wanindex2 = revistr.index('\xcd\xf2')
# 除去“万”字
revistr = revistr[:wanindex2]
revinum = revinum + string.atof(revistr) * 10000
else:
# 没有“万”字
revinum = revinum + string.atof(revistr)
# 计算评分
score = revinum + (tradnum * 2)
item['score'] = round(score)
yield(item)

一页商品信息是不够的,因此我们观察后两页url可以发现,“s=60”表示第二页,“s=120”表示第三页,且“q=”后面的信息为商品关键字信息。因此我们可以依此设置后两页url并进行递归实现对后两页商品信息的自动爬取。

1
2
3
4
5
6
7
8
9
# 提取商品名
good = response.url[(response.url.index("q=") + 2):response.url.index("&type=p&v")]
next_page_urls = [
"https://list.tmall.com/search_product.htm?spm=a220m.1000858.0.0.0HVJLN&s=60&q=" + good + "&sort=s&style=g&from=mallfp..pc_1_searchbutton&type=pc#J_Filter",
"https://list.tmall.com/search_product.htm?spm=a220m.1000858.0.0.Zt2HlG&s=120&q=" + good + "&sort=s&style=g&from=mallfp..pc_1_searchbutton&type=pc#J_Filter"
]
# 递归获取后两页
for next_page_url in next_page_urls:
yield Request(next_page_url, callback=self.parse)

爬虫构建完毕,在根目录建立runscrapy.py并输入以下代码后,执行该文件即可实现爬取相关信息并存储信息至根目录goods.csv的功能。

1
2
3
from scrapy import cmdline
cmdline.execute("scrapy crawl FindGoods".split())

爬虫功能构建完毕。

四、GUI构建

为了界面美观,我们使用Tkinter为该爬虫程序构建了一个GUI,即main.py。需包括以下功能:

1、关键词输入与执行爬虫

关键词输入框构建代码如下:

1
2
3
# 关键字输入框
var = StringVar()
e = Entry(root, textvariable=var).grid(row=2)

确认按钮构建代码如下:

1
2
# 确认按钮
Button(root, text="开始查询", command=startSpider).grid(row=3)

爬虫执行函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def startSpider():
# 获取文本框内容
good = var.get()
# 关键字保存至临时文件
temp = open('tempgoods.temp', 'w')
temp.write(good.encode(sys.getfilesystemencoding()))
temp.close()
# 清空goods.csv
if os.path.exists('goods.csv'):
csvfile = open('goods.csv', 'w')
csvfile.truncate()
# 开始爬虫程序
os.system("runscrapy.py")

同时,为了让爬虫按照给定的关键字进行爬取,需要让爬虫读取通过该临时文件传递过来的关键字信息。因此对goodspider.py进行以下修改:

1
2
3
4
5
temp = open('tempgoods.temp', 'r')
good = temp.read()
temp.close()
# 天猫搜索该商品第一页
url = "https://list.tmall.com/search_product.htm?q=" + good + "&type=p&vmarket=&spm=875.7931836%2FA.a2227oh.d100&from=mallfp..pc_1_searchbutton"

2、爬取结果显示

爬取结果通过TreeView控件显示。控件实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 结果csv表格
tree = ttk.Treeview(root, show="headings", columns=('店名', '商品名', '购买链接', '价格', '评分', '月交易量', '评论数'))
# 表格滚动条
ysb = ttk.Scrollbar(root, orient='vertical', command=tree.yview)
xsb = ttk.Scrollbar(root, orient='horizontal', command=tree.xview)
tree.configure(yscroll=ysb.set, xscroll=xsb.set)
tree.column('店名', width=100, anchor='center')
tree.column('商品名', width=250, anchor='center')
tree.column('购买链接', width=125, anchor='center')
tree.column('价格', width=50, anchor='center')
tree.column('评分', width=65, anchor='center')
tree.column('月交易量', width=55, anchor='center')
tree.column('评论数', width=55, anchor='center')
tree.heading('店名', text='店名')
tree.heading('商品名', text='商品名')
tree.heading('购买链接', text='购买链接')
tree.heading('价格', text='价格')
tree.heading('评分', text='评分')
tree.heading('月交易量', text='月交易量')
tree.heading('评论数', text='评论数')
vbar = ttk.Scrollbar(root, orient=VERTICAL, command=tree.yview)
tree.configure(yscrollcommand=vbar.set)
# 初始化表格数据
refreshTree()

3、结果表格的刷新

每次爬取结束获取新的信息后需要对结果表格内容进行更新。而且得到的商品信息需要根据各商品的得分进行从大到小的排序。更新函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def refreshTree():
# 清空
items = tree.get_children()
for item in items:
tree.delete(item)
# 读取csv
if os.path.exists('goods.csv'):
csvfile = file('goods.csv', 'rb')
if csvfile:
lines = []
reader = csv.reader(csvfile)
i = 0
# 转存至数组
for line in reader:
# 不输出第一行
if i > 0:
lines.append(line)
i = i + 1
# 冒泡排序
for j in range(len(lines) - 1, 0, -1):
for k in range(j):
if string.atof(lines[k][4]) < string.atof(lines[k + 1][4]):
lines[k], lines[k + 1] = lines[k + 1], lines[k]
i = 0
for line in lines:
tree.insert('', i, values=(
line[0].decode(sys.getfilesystemencoding()), line[1].decode(sys.getfilesystemencoding()),
line[2].decode(sys.getfilesystemencoding()), line[3].decode(sys.getfilesystemencoding()),
line[4].decode(sys.getfilesystemencoding()), line[5].decode(sys.getfilesystemencoding()),
line[6].decode(sys.getfilesystemencoding())))
i = i + 1

4、点击表格内商品直接跳转至购买界面

TreeView点击监听代码如下:

1
2
3
4
5
6
7
8
9
# 监听Click
tree.bind("<Double-1>", onDBClick)
跳转函数代码如下:
def onDBClick(event):
# 点击跳转天猫
item = tree.selection()[0]
info = tree.item(item, "values")
url = info[2]
webbrowser.open_new(url)

以上就是构建该项目的全部步骤。执行main.py即可实现项目的启动。