平常人可以漂亮到什么程度?教你爬取知乎大神们的回答一探究竟!

大家好,今天才哥带大家看看知乎这个高达14.3万关注,2.6亿浏览,回答数超过1.27万的问题《平常人可以漂亮到什么程度?》。

开不开心

最近呢,可能是因为写了几篇关于爬虫获取美女照片的文章的缘故?总是收到知乎推送这个话题,由于关注才哥颜值得到蹭蹭上涨,现在终于敢点开这个问题,然后一探究竟啦!

平常人可以漂亮到什么程度?

注:全部代码、照片等数据,公众号后台回复“知乎”即可获取!

1. 预览

我们只将赞同数前100的回答中的照片进行对比,通过face++颜值评分接口获取的分值进行排序。

注:face++颜值评分不代表本人观点哈,感觉很多不是那个味道,哈哈。

以下是赞同数前三甲的答案中照片颜值评分最高的:

rankcountscorepic
15703288.976
24655187.644
32705089.54

以下是颜值评分前三甲:

rankscorepic
194.01
291.4
391.133

2. 数据采集

知乎的数据采集需要登录账号后才能看到,登录账号,点开目标网址:https://www.zhihu.com/question/50426133

2.1. 分析网页接口

常规流程:

  1. F12打开开发者模式—>Network—>XHR
  2. 下滑加载更多;
  3. 观测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值。

copy 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数据,嗯。

preview

最下面的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,&lt;svg xmlns=&#39;http://www.w3.org/2000/svg&#39; width=&#39;960&#39; height=&#39;1280&#39;&gt;&lt;/svg&gt;" 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处理后的结果预览:

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,成了大热门。

year

绘图代码:

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)

散点图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值