大家好,今天才哥带大家看看知乎这个高达14.3万关注,2.6亿浏览,回答数超过1.27万的问题《平常人可以漂亮到什么程度?》。
最近呢,可能是因为写了几篇关于爬虫获取美女照片的文章的缘故?总是收到知乎推送这个话题,由于关注才哥颜值得到蹭蹭上涨,现在终于敢点开这个问题,然后一探究竟啦!
注:全部代码、照片等数据,公众号后台回复“知乎
”即可获取!
1. 预览
我们只将赞同数前100的回答中的照片进行对比,通过face++颜值评分接口获取的分值进行排序。
注:face++颜值评分不代表本人观点哈,感觉很多不是那个味道,哈哈。
以下是赞同数前三甲的答案中照片颜值评分最高的:
rank | count | score | pic |
---|---|---|---|
1 | 57032 | 88.976 | |
2 | 46551 | 87.644 | |
3 | 27050 | 89.54 |
以下是颜值评分前三甲:
rank | score | pic |
---|---|---|
1 | 94.01 | |
2 | 91.4 | |
3 | 91.133 |
2. 数据采集
知乎的数据采集需要登录账号后才能看到,登录账号,点开目标网址:https://www.zhihu.com/question/50426133
。
2.1. 分析网页接口
常规流程:
F12
打开开发者模式—>Network
—>XHR
;- 下滑加载更多;
- 观测
Name
列变化,找到目标。
通过以上步骤,我们发现以下关键点:
在开发者模式中,我们发现limit
默认为5,通过调试这个值可以发现最大可以设为20,也就是单页20条回答数据。
# 接口url地址
url = 'https://www.zhihu.com/api/v4/questions/50426133/answers?'
# 请求参数
parameters = {
'include': 'data[*].is_normal,admin_closed_comment,reward_info,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,attachment,voteup_count,reshipment_settings,comment_permission,created_time,updated_time,review_info,relevant_info,question,excerpt,is_labeled,paid_info,paid_info_content,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp,is_recognized;data[*].mark_infos[*].url;data[*].author.follower_count,badge[*].topics;data[*].settings.table_of_content.enabled',
'offset': page*20, # 未来的翻页变量在这里
'limit': 20,
'sort_by': 'default',
'platform': 'desktop',
}
我们提到,需要登录获取cookie并传递给请求头才能正常请求到数据,这里可以在开发者模式中的Request Headers中获取到cookie值。
根据以上cookie值构建请求头参数:
# 请求头参数
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
'Cookie':'这里放你复制来的cookie咯'
}
2.2. 基础数据请求
点开Preview
,可以看到接口请求到的数据类型,我们发现是json数据
,嗯。
最下面的totals
是全部回答数量,每个回答的具体内容在data
列表中。在第1步中,我们单页是20个回答数,因此可以构建总页数为 totals//20 +1
。
数据采集代码如下:
import requests
import re
import pandas as pd
import time
from tqdm import tqdm
def get_html(page):
url = 'https://www.zhihu.com/api/v4/questions/50426133/answers?'
parameters = {
'include': 'data[*].is_normal,admin_closed_comment,reward_info,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,attachment,voteup_count,reshipment_settings,comment_permission,created_time,updated_time,review_info,relevant_info,question,excerpt,is_labeled,paid_info,paid_info_content,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp,is_recognized;data[*].mark_infos[*].url;data[*].author.follower_count,badge[*].topics;data[*].settings.table_of_content.enabled',
'offset': page*20,
'limit': 20,
'sort_by': 'default',
'platform': 'desktop',
}
# 请求头参数
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
'Cookie':'这里放你复制来的cookie咯'
}
r = requests.get(url, params=parameters, headers=headers)
return r
# 获取首页数据,用于找回答数
r = get_html(0)
j = r.json()
# 获取全部回答及总页数
totals = j['paging']['totals']
pages = totals//20 + 1
# 获取全部回答详情
df_list = []
for page in tqdm(range(pages)):
# 不限速爬虫会出现403的情况,加了之后发现没问题
time.sleep(0.5)
r = get_html(page)
j = r.json()
data = j['data']
data_df = pd.DataFrame(data)
# 选择需要的数据
df = data_df[['id','type','answer_type','content', 'created_time', 'updated_time','voteup_count', 'comment_count']]
df_list.append(df)
# 合并全部页数据
result = pd.concat(df_list)
回答内容预览:
2.3. 照片数据解析及下载
在请求的基础数据中,我们发现照片数据url地址藏在content中,而content数据格式有点像html文本,当然这里我们用re正则表达式
进行解析哈。
html = '<p>是我见过最好看的地勤小姐姐。</p><p>在重庆机场。</p><figure><noscript><img src="https://pic1.zhimg.com/50/v2-3c6b1af3ed81a111115b60f9336ba4fd_hd.jpg?source=1940ef5c" data-rawwidth="960" data-rawheight="1280" class="origin_image zh-lightbox-thumb" width="960" data-original="https://pic1.zhimg.com/v2-3c6b1af3ed81a111115b60f9336ba4fd_r.jpg?source=1940ef5c"/></noscript><img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='960' height='1280'></svg>" data-rawwidth="960" data-rawheight="1280" class="origin_image zh-lightbox-thumb lazy" width="960" data-original="https://pic1.zhimg.com/v2-3c6b1af3ed81a111115b60f9336ba4fd_r.jpg?source=1940ef5c" data-actualsrc="https://pic1.zhimg.com/50/v2-3c6b1af3ed81a111115b60f9336ba4fd_hd.jpg?source=1940ef5c"/></figure>'
# 照片地址列表
img_urls = re.findall(r'<img src="(https:.*?)\?',html)
由于一共1.27万个回答,照片数更不知道有多少,我这里就不全部下载了,仅下载赞同数前100的回答里的照片啦。
# 按照赞同数降序
result.sort_values(by='voteup_count',ascending=False,inplace=True)
ids = result['id'].to_list()
htmls = result['content'].to_list()
m = n = 0
# for i , j in result[['id','content']].values: # 亦可这样
# 爬取前100个回答中出现的照片
scores = []
for id_, html in zip(ids[:100], htmls[:100]):
m = m+1
img_urls = re.findall(r'<img src="(https:.*?)\?',html)
for num,img_url in enumerate(img_urls):
time.sleep(0.12)
n = n + 1
print(m, num+1, img_url)
# 下载照片
r_img = requests.get(img_url, headers=headers)
img = r_img.content
name = f'{m}-{id_}-{num+1}.jpg'
# 写入本地
with open(name,'wb') as f:
f.write(img)
print(f'共 {n}张 照片已经下载')
照片下载结果预览:
3. 性别分类及颜值评分
由于问题是《平常人可以漂亮到什么程度?》并没有指定性别,不过我们在爬取过程中发现绝大多数还是女性照片。为了更好的批量区分,我们可以在f调用ace++颜值评分接口时也带上性别即可。
在第2步中,我们其实已经知道了照片的url地址,这里只需要循环传入改地址即可。
# 定义获取评分的函数
def ksfaceScore(pic_url):
url = 'https://api-cn.faceplusplus.com/facepp/v3/detect'
APIKey = '你的key'
APISecret = '你的secret'
data = {"api_key":APIKey,
"api_secret":APISecret,
"image_url":pic_url,
"return_attributes":"gender,age,beauty"
}
res = requests.post(url,data = data)
dic_ = eval(res.text)
try:
gender = dic_['faces'][0]['attributes']['gender']['value']
age = dic_['faces'][0]['attributes']['age']['value']
beauty_w = dic_['faces'][0]['attributes']['beauty']['female_score']
beauty_m = dic_['faces'][0]['attributes']['beauty']['male_score']
except :
gender=age=beauty_w=beauty_m = -1
return gender,age,beauty_w,beauty_m
# result = pd.read_csv(r'知乎问题-平常人可以漂亮到什么程度.csv',index_col=0)
result.sort_values(by='voteup_count',ascending=False,inplace=True)
ids = result['id'].to_list()
htmls = result['content'].to_list()
n = 0
m = 0
# for i , j in result[['id','content']].values:
# 爬取前100个回答中出现的照片
scores = []
for id_, html in zip(ids[:100], htmls[:100]):
m = m+1
img_urls = re.findall(r'<img src="(https:.*?)\?',html)
for num,img_url in enumerate(img_urls):
time.sleep(0.12)
score = {}
n = n + 1
print(m, num+1, img_url)
# 获取颜值评分
gender,age,beauty_w,beauty_m = ksfaceScore(img_url)
score['gender'] = gender
score['age'] = age
score['beauty_w'] = beauty_w
score['beauty_m'] = beauty_m
score['name'] = id_
score['url'] = img_url
scores.append(score)
print(f'共 {n}张 照片已经完成颜值评分')
df_score = pd.DataFrame(scores)
性别分类及颜值评分结果预览如下:
由于每个回答里存在多张照片,因为直接获取的结果里对同一个回答也是存在多条结果。
4. 数据处理
为了对每条回答指定唯一一条颜值评分数据,这里简单粗暴的采用取最大值的形式。
另外,为了和回答数据进行合并,我们需要通过唯一id
的形式,而在颜值评分数据里对应的是字典name
,其包含rank、id和page(指同一个回答里第page张照片),需要进行分列。
# 分列
df_score[['rank','id','page']]=df_score.name.str.split('-',2,expand=True)
# 分列后对id字典进行格式转化(转为为int)
df_score.id = df_score.id.astype('int')
# 按照 beauty_m 男性视角颜值评分 由大到小排序
df_score.sort_values(by='beauty_m',ascending=False,inplace=True)
# 按照 id分组,取第一个值也就是排序结果下的最大值
df_score = df_score.groupby('id').first().reset_index()
df_score处理后的结果预览:
合并:
top = pd.merge(df_score,result,how='left')
top = top[['id', 'gender', 'age', 'beauty_w', 'beauty_m', 'name', 'url', 'rank',
'page', 'type', 'voteup_count', 'comment_count']]
预览:
赞同数前三:
top.sort_values('voteup_count',ascending=False).head(3)
https://www.zhihu.com/question/50426133/answer/663160403
https://www.zhihu.com/question/50426133/answer/429332291
https://www.zhihu.com/question/50426133/answer/384888962
颜值评分前三:
top.sort_values('beauty_m',ascending=False).head(3)
https://www.zhihu.com/question/50426133/answer/417622373
https://www.zhihu.com/question/50426133/answer/490096144
https://www.zhihu.com/question/50426133/answer/487442393
补充一点,在数据采集时 关于 发布时间和更新时间两个字段是时间戳格式,可以通过以下方法转化为 我们所处时区的 时间:
# 时间戳改为日期时间格式
result['created_time'] = result['created_time'].apply(lambda x: pd.Timestamp(x, unit="s",tz='Asia/Shanghai'))
result['updated_time'] = result['updated_time'].apply(lambda x: pd.Timestamp(x, unit="s",tz='Asia/Shanghai'))
5. 数据统计与可视化
这部分仅针对全部回答基础数据进行简单的数据统计和可视化
5.1. 回答数时间分布
该问题是2016年9月7日被首次提出,不过直到2017年12月份才开始逐步热门起来,而在2018年该问题回答总数接近7000,成了大热门。
绘图代码:
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = ['Microsoft YaHei'] #设置全局默认字体 为 幼圆
plt.rcParams['axes.unicode_minus'] = False # 解决中文字体下负号显示问题
plt.rcParams["axes.labelsize"] = 16 # 设置全局轴标签字典大小
import seaborn as sns
sns.set_style("darkgrid",{"font.family":['Microsoft YaHei', 'SimHei']}) #seaborn绘图的字体设置
plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
def plot_year(data,year):
df = data[data['year']==year]
plt.bar(df.month, df.回答数, color='lightpink')
plt.title(year)
for a, b in zip(df.month,df.回答数):
plt.text(a, b+0.1, b, ha='center', va='bottom')
plt.figure(figsize=(12,18), dpi=80)
plt.figure(1)
ax1 = plt.subplot(511)
plot_year(data,2016)
ax2 = plt.subplot(512)
plot_year(data,2017)
ax3 = plt.subplot(513)
plot_year(data,2018)
ax4 = plt.subplot(514)
plot_year(data,2019)
ax5 = plt.subplot(515)
plot_year(data,2020)
5.2. 赞同数与评论数关系
基本上赞同数和评论数正相关啦,简单做散点图如下:
df1 = df[(df['comment_count']>0)&(df['voteup_count']>0)]
plt.figure(figsize=(15,8))
plt.scatter(df1.comment_count,df1.voteup_count)