- 项目要求:
内容完整程度
可用性(可操作、易操作、美观)
- 确定项目目标:
分析某二手手机交易论坛上的帖子,从中得出其用户行为的描述,为用户进行分类,输出洞察报告。
- 从互联网获取数据:
使用python构建一个定向爬虫,爬取太平洋电脑网的二手手机交易论坛http://itbbs.pconline.com.cn/es/f240027.html的数据。
首先观察网页,
可以看到网页中有很多有价值的信息,包括分类信息,标题,用户,时间等,因此我们确信通过对该网页的爬取可以获得我们想要的数据。
下面开始分析网页源码,经过分析我们可以发现每个帖子的信息都在如下的结构中:
每个帖子的信息都在class=’cf’的<li>标签中,下面关注我们需要爬取的信息:用户uid在class=’area-author’的<div>标签的data-uid属性中,分类信息在class=’tit-sub’的<a>标签的string中,标题在class=’tit-toe’的<a>标签的string中,时间在class=’time’的<span>标签的string中。
经过分析,我们已经可以构建爬虫了,就像平常一样使用request库和beautifulsoup4库即可,下面是爬虫部分源码的截图,完整源码见附录1。
目标网页论坛像大部分论坛一样是分页的,某一页只显示特定数量的帖子,因此需要爬取多页数据,但是由于目标网页内部逻辑,当爬取页数过多会出现很多重复数据,经过试验发现爬取小于100页时数据没有重复,因此我们爬取100页的数据,爬取结果有1032条数据。数据的形态如下:
- 数据清洗/整理:
首先观察发现,该网站中大于95%的帖子是用来出售手机的,因此放弃对买/卖/换的分类,全部认为是出售二手手机。
由于数据是以自然语言的形式存在的,需要对数据进行清洗和整理,判断出所交易手机的品牌。
首先我们使用到了python的第三方库——jieba库,来进行中文的分词。再对分出的词进行词频统计,统计的部分结果如下:
在这里我们采用了‘符合关键词——打标签’的方法,把所有词中能够反映出手机品牌的,按词频从高到低在python字典中对应到手机品牌,整理出的关键词结果如下:
然后我们根据人们发帖写标题的规律——重要的信息写在前面,把分类与标题拼成一个字符串,分词后对每个词寻找对应的品牌信息,若有对应则直接打上标签。
与此同时,整个帖子中都不存在关键词的,最后的标签结果会是None,这类数据直接删除,这样很多人成交后会把帖子改成《已出》等标题的无效信息就被清洗掉了。
清洗整理过后的数据再加上用户uid和时间信息,即完成了数据的清洗和整理,整理后的数据形态如下所示:
数据清洗和整理的源代码见附录2.
- 描述统计&洞察结论:
有了上面的数据,我们已经可以各厂商在该论坛占比的基本情况进行统计了,统计结果如下:
可见苹果手机在二手市场中占有绝对优势的份额达到了2/3;另人欣喜的是,国产手机华为、小米分列二、三位,而如魅族,vivo等国产手机也占有一定的份额,国产手机总共占有约1/4的份额,而当年的手机巨头三星、诺基亚等正逐步没落。
- 选择变量&算法:
因为要研究的是这些用户与二手交易相关的行为,因此选择变量为各品牌手机拥有台数。
我们的目标是将用户分群,因此选择聚类,方法选择最简单的 K-means 算法。
- 设定参数 & 加载算法:
我们对每个用户的手机台数进行统计,得到的部分结果如下:
统计的源码见附录3.
然后就可以用k-means算法进行分类了,经过多次测试,发现当聚类数k=3时,分类结果的可解释性良好,分类结果如下(聚类源码见附录4)。
我们可以根据这张表对不同类别的人进行解读了,测试通过!
- 报告撰写:
聚类模型结果可归为下图:
可见该论坛中大部分还是普通用户,人均交易约为1次,大致就是把不用的手机放到论坛上换点小钱的那种。
平台中的苹果商人数量很多,有110人,占比达到16.9%,由于人数较多导致人均交易次数被分流而很有限,只有3.4次。于此同时我们可以发现二手苹果商人对苹果手机执着程度很高,其他品牌的手机人均拥有数量均不到0.1台,应该是钟情于苹果手机的高利润了。
安卓商人这一类人数很少,仅有6人,占比仅仅1%,但是人均交易次数高达8.2次,可见由于人数较少,交易的机会也较大。其中二手三星手机交易比重最大,达3.5次,或许是因为三星作为外国企业在国内的利润较高。于此同时我们可以发现安卓商人可谓是爱好广泛,苹果、华为、小米手机人均拥有1台以上,不像二手苹果商人那样钟情于苹果市场。
以上为对用户的聚类结果,可见由于二手安卓商人人数较少,交易次数较多,在该论坛中倒卖二手安卓机的商机还是很大的。
最后附上词云图片(制作图片的源码见附录5):
- 附录:
附录1:
#爬取数据
import requests
from bs4 import BeautifulSoup
allUniv=[]
def getHTML(url):
try:
r=requests.get(url)
r.raise_for_status()
r.encoding='gb2312'
return r.text
except:
return ''
def getInfoList(soup):
lils=soup.find_all('li',{'class':'cf'})
ils=[]
for li in lils:
title=li.find('a',{'class':'tit-sub'}).string.replace('[','').replace(']','')
content=li.find('a',{'class':'tit toe'}).string
time=li.find('span',{'class':'time'}).string
uid=li.find('div',{'class':'area-author'}).attrs['data-uid']
ils.append([title,content,time,uid])
return ils
def getOnePageInfo(url):
html=getHTML(url)
soup=BeautifulSoup(html,'html.parser')
ils=getInfoList(soup)
return ils
def rowFmt(row):
f_row=[]
for e in row:
t=e.replace(',',',')
f_row.append(t)
return f_row
def main(num):
info=getOnePageInfo('http://itbbs.pconline.com.cn/es/f240027.html')
for i in range(2,num):
info.extend(getOnePageInfo('http://itbbs.pconline.com.cn/es/f240027_'+str(i)+'.html'))
fw=open('info__'+str(num)+'.csv','w')
for row in info:
f_row=rowFmt(row)
try:
fw.write(','.join(f_row)+'\n')
except:
print(row)
fw.close()
print('pages:',num,len(info),'finished')
main(100)
附录2:
#清洗,整理数据
import jieba
#[用户,品牌]
rep={'苹果':'Apple','国行':'Apple','6':'Apple','x':'Apple',
'iphone':'Apple','7p':'Apple',
'三星':'SAMSUNG','小米':'小米',
'8':'Apple','8p':'Apple','7':'Apple','港版':'Apple',
'华为':'华为',
'6sp':'Apple',
'荣耀':'华为','s8':'SAMSUNG','note8':'SAMSUNG',
'iphone6':'Apple','6s':'Apple','iphone7':'Apple','xs':'Apple'
,'魅族':'魅族','vivo':'vivo',
'5s':'Apple','5':'Apple',
'7plus':'Apple','iphone6s':'Apple','iphonex':'Apple',
'魅蓝':'魅族','note10':'SAMSUNG','note9':'SAMSUNG',
'x23':'vivo','s9':'SAMSUNG',
'8plus':'Apple',
'mix2s':'小米',
'iphone7p':'Apple',
'oppo':'oppo',
'6p':'Apple',
'红米':'小米',
'iphone4':'Apple',
'mate9':'华为',
'iphone7plus':'Apple',
'诺基亚':'Nokia',
'粪':'Apple',
'note6':'SAMSUNG','x20a':'vivo','p20':'华为','mate10pro':'华为',
'mate10':'华为','p9':'华为','s7edge':'SAMSUNG',
'mix2':'小米',
'xsm':'Apple','xsmax':'Apple',
'mx5':'魅族',
'7p128':'Apple',
'note3':'SAMSUNG','r15':'oppo',
'i7p':'Apple','i7':'Apple','6sp128g':'Apple','pius':'Apple',
'pro6s':'魅族','mate8':'华为','r9':'oppo','6x':'小米','4x':'小米',
'p10plus':'华为',
'r17':'oppo','oppor11':'oppo','oppor11s':'oppo','oppaa73':'oppo',
'8p64g':'Apple','iphone5s':'Apple',
'10pro':'华为','mate9pro':'华为',
'iphone8p':'Apple','ip6':'Apple','i6':'Apple',
'iphone6sp':'Apple','iphone5':'Apple','iphone8':'Apple',
'vivonex':'vivo',
'6s128g':'Apple','6sp5.5':'Apple','nexs':'vivo'}
def listWords(filename):
fo=open(filename,'r')
words=[]
counts={}
for line in fo:
txt=line.split(',')[1]
t=txt.lower()
word=jieba.lcut(t)
words.extend(word)
for word in words:
counts[word]=counts.get(word,0)+1
lst=sorted(counts.items(),key=lambda x:x[1],reverse=True)
count=0
for l in lst:
count+=1
print(l)
print(count)
def getBrand(txt):
words=jieba.lcut(txt)
for word in words:
if rep.get(word,0)!=0:
return rep[word]
return 0
def getInfo(filename):
ils=[]
fo=open(filename,'r')
count=0
for line in fo:
line=line.replace('\n','')
content=line.split(',')
txt=content[0]+content[1].lower()
time=content[2]
uid=content[3]
brand=getBrand(txt)
if brand!=0:
ils.append([uid,brand,time])
count+=1
print(count)
return ils
def main(filename):
ils=getInfo(filename)
fw=open('clr_'+filename,'w')
for row in ils:
fw.write(','.join(row)+'\n')
fw.close()
main('info__100.csv')
附录3:
#统计用户信息
#[apple,sunsung,huawei,oppo,vivo,meizu,mi,nokia]
def getBrandCount(filename):
brandCount={}
fo=open(filename,'r')
for line in fo:
line=line.replace('\n','')
content=line.split(',')
brandCount[content[1]]=brandCount.get(content[1],0)+1
print(brandCount)
return brandCount
def getUserCount(filename):
userCount={}
fo=open(filename,'r')
for line in fo:
line=line.replace('\n','')
content=line.split(',')
uid=content[0]
brand=content[1]
value=userCount.get(uid,
{'Apple':0,'SAMSUNG':0,'华为':0,'oppo':0,
'vivo':0,'魅族':0,'小米':0,'Nokia':0})
value[brand]+=1
userCount[uid]=value
return userCount
def main(filename):
uc=getUserCount(filename)
fw=open('uc_'+filename,'w')
count=0
for item in uc.items():
ls=[item[0]]
values=list(item[1].values())
val=[]
for v in values:
val.append(str(v))
ls.extend(val)
fw.write(','.join(ls)+'\n')
count+=1
fw.close()
print(count,'finished')
main('clr_info__100.csv')
附录4:
#用户聚类
from numpy import *
def loadData(filename):
data=[]
fo=open(filename)
for line in fo:
line=line.replace('\n','')
l1=line.split(',')
ls=[]
for i in range(1,9):
ls.append(float(l1[i]))
#print(ls)
data.append(ls)
fo.close()
return data
def dist(va,vb):#calc distance
return sqrt(sum(power((va-vb),2)))
def randCents(dataSet,k):#random centers
n=shape(dataSet)[1]
cents=mat(zeros((k,n)))
for i in range(n):
mini=min(dataSet[:,i])
maxi=max(dataSet[:,i])
rangei=float(maxi-mini)
cents[:,i]=mini+rangei*random.rand(k,1)
return cents
def kMeans(dataSet,k,getDist=dist,getCents=randCents):
m=shape(dataSet)[0]
cluster=mat(zeros((m,2)))
cents=getCents(dataSet,k)
flag=True
while flag:
flag=False
for i in range(m):
minDist=inf
minIdx=-1
for j in range(k):
dist=getDist(cents[j,:],dataSet[i,:])
if dist<minDist:
minDist=dist
minIdx=j
if cluster[i,0]!=minIdx:
flag=True
cluster[i,:]=minIdx,minDist**2
#print(cents)
for cent in range(k):
pts=[]
for data in range(m):
if cluster[data,0]==cent:
pts.append(dataSet[data,:])
#pts=mat(pts)
cents[cent,:]=mean(pts,axis=0)
return cents,cluster
def writeMat(m,filename):
fw=open(filename,'w')
for i in range(shape(m)[0]):
row=[]
for j in range(shape(m)[1]):
row.append(str(m[i,j]))
fw.write(','.join(row)+'\n')
fw.close()
def main(filename,k):
data=loadData(filename)
dataSet=mat(data)
cents0,cluster0=kMeans(dataSet,k)
print(cents0)
writeMat(cents0,str(k)+'cents__'+filename)
writeMat(cluster0,str(k)+'cluster__'+filename)
main('uc_clr_info__100.csv',3)
附录5:
#作图
import numpy as np
import matplotlib.pyplot as plt
from wordcloud import WordCloud
import PIL
import jieba
def drawPie(filename):
fo=open(filename)
fracs={}
explode=[0.03,0.1,0.1,0.1,0.1,0.1,0.1,0.1]
plt.axes(aspect=1)
count=0
for line in fo:
line=line.replace('\n','')
ls=line.split(',')
fracs[ls[1]]=fracs.get(ls[1],0)+1
count+=1
print(fracs)
val=fracs.values()
key=list(fracs.keys())
for i in range(len(key)):
if key[i]=='华为':
key[i]='HUAWEI'
continue
elif key[i]=='魅族':
key[i]='MEIZU'
continue
elif key[i]=='小米':
key[i]='MI'
continue
plt.pie(x=val,labels=key,explode=explode,autopct='%3.1f %%',shadow=False,labeldistance=1.1)
plt.show()
print(count)
for i in fracs.items():
print(i[0],i[1]/1032*100)
def wordcloudplot(txt):
path = r'C:\Windows\Fonts\FZSTK.TTF'
alice_mask = np.array(PIL.Image.open('bg.jpg'))
wordcloud = WordCloud(font_path=path,
background_color="white",
margin=5, width=1800, height=800, mask=alice_mask, max_words=80, max_font_size=60,
random_state=42).generate(txt)
wordcloud.to_file('dora.jpg')
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
def main():
fo=open('info__100.csv')
ls=[]
count=0
for line in fo:
txt=line.split(',')[1]
t=txt.lower()
word=jieba.lcut(t)
for w in word:
if len(w)>1:
ls.append(w)
print(len(ls))
words=' '.join(ls)
#print(words)
wordcloudplot(words)
main()