Python零基础,该如何掌握爬虫技术

学习任何新事物,都需要先搭建起学习框架。通常我们会从「基础知识」、「案例实操」两个板块切入,构建自己的学习闭环。


一、关于「基础知识」

对于想快速了解知识关键要点的新手,以下是比较推荐的学习素材:

MOOC课程:Python语言程序设计Python网络爬虫与信息提取

参考书籍:《利用Python进行数据分析》《Python编程:从入门到实践》


二、关于「案例实操」

验证自己是否真正理解知识并能应用的最佳方式,就是通过案例完成实践操作。

本文通过一个实操案例,给大家进行讲解。

案例:「作为爬虫新手,如何利用Python爬取NBA球员数据」


1、搭建工作环境

传统方式:对于Win用户,需要自己在本地搭建运行环境。

准备工作会是:

搭建Python3.6环境

  • 用Anaconda

安装Package

  • Requests库 

  • BeautifulSoup4(bs4)库 

  • Numpy和Pandas库(用于数据存储和清洗) 

说明:以上工具,除Python环*,皆为Python的第三方库。Windows环境下通过cmd的pip install命令安装。对于Mac用户,虽没那么复杂,但同样会需要花费时间与精力。


大家会发现,按以上步骤搭建工作环境,是个特别费时费力的事儿,P2、P3的兼容性问题也很突出。那么有没有更好的方法呢?答案是,有的。


科赛网,针对数据工作者,打造了K-Lab在线数据分析协作平台。它是基于Jupyter Notebook提供的数据分析服务,同时涵盖Python3、Python2、R三种主流编程语言,内置100+数据分析工具包。可以很好的解决大家数据工程问题,关于这点就非常适合新手入门。

(说明:以下正文内涉及的完整代码,均可直接登录科赛网(kesci.com)查看,并利用「Fork」键功能进行再编辑,生成自己的数据分析作品)


2、搭建工作流 


1)选取目标官网:NBA中文数据网站

Python零基础,该如何掌握爬虫技术


2)导入库

#引入主要的爬取工具:import requests
from bs4 import BeautifulSoup#以下是数据的清洗和存储所需的辅助工具import re
import numpy as np
import pandas as pd


完成导入后,获取每个球员各个赛季单场比赛的基础数据(CSV文档格式):

Python零基础,该如何掌握爬虫技术


3)找到页面,批量爬取

样例:LeBron James的详情页,该网站数据源相对规整,URL设置易找规律,方便抓取。

Python零基础,该如何掌握爬虫技术

说明:为了实现自动批量抓取,爬取时一般需要让爬虫先爬取列表页,再根据列表页的索引链接爬取相应的详情页。但在该项目并不需要如此,从链接1链接2可以看出其中规律。


将需要关注的参数用粗体表示:

  • page:表示数据的页码,每页显示20条数据。

  • GameType:season表示常规赛,playoff表示季后赛。

  • Player id:网站自定义的球员编号。

爬取时,我们只需要改动这些数据就能实现全站资源的爬取,非常适合新手。


4)解析页面

推荐在Chrome浏览器的开发者模式下解析页面,它能快速定位到所需内容。

图中蓝标显示的Table标签便是需要抓取的表格。

Python零基础,该如何掌握爬虫技术

关于table标签:

table标签有两个子标签thead、tbody。

  • thead子标签:存储表头信息,包含一个tr子标签(表头只有一行)。tr又包含多个th子标签,每个th存储一格表头信息。

  • tbody子标签:存储表格主题,包含多个tr子标签。tr又包含多个td子标签,每个td存储一格表格信息。


5)将表格内容搬运到本地

首先,通过fillframe函数,将网页上的表格转换成一个pandas的DataFrame表格对象。

def fillframe(tbody,index):   
#这里只使用tbody标签,表头通过index传入,index是一个list
    frame=pd.DataFrame()    if tbody:
        all_tr=tbody.find_all('tr')        for tr in all_tr:
            dic={}
            all_td=tr.find_all('td')
            i=-1            for td in all_td:                if i==-1:    
#可以发现,网页表格中每行的第一格都是空的,所以我们需要将其跳过。
                    i+=1                    continue
                else:
                    dic[index[i]]=td.string
                i+=1
            frame=pd.concat([frame,pd.DataFrame(dic,index=[0])],ignore_index=True)        return frame


其次,用fillindex函数将fillframe函数生成index。

def fillindex(thead):
    index=[]    if thead:
        all_th=thead.tr.find_all('th')
        i=-1        for th in all_th:            if i==-1:
                i+=1                continue
            else:
                index.append(th.string)
            i+=1        return index


经过以上操作,不难发现,网页上表格的表头可能有歧义。比如它将投篮的命中数和三分的命中数两项数据的索引都设置为了命中。为了保险起见,建议手动设置index。

index=['球员','赛季','结果','比分','首发','时间','投篮','命中','出手','三分', '三分命中','三分出手','罚球','罚球命中','罚球出手','篮板','前场','后场','助攻','抢断','盖帽','失误','犯规','得分']


6)开始爬取页面,并提取table

提取过程中,由于每个球员的数据条目数不同,不能确定球员数据的页数。

可以考虑给spider函数设置为“在页面无法正常读取或读取的页面中找不到table标签时返回False值”。

def spider(page,player_id,gametype,index):
    url='http://www.stat-nba.com/query.php?page='+str(page)+'&QueryType=game&GameType='+str(gametype)+'&Player_id='+str(player_id)+'&crtcol=season&order=1'
    r=requests.get(url,timeout=30)    if r.status_code==200:
        demo=r.text
        soup=BeautifulSoup(demo,"html.parser")

        data=soup.find('div',{'class':'stat_box'})        if not data:           #找不到数据表格时退出
            return False

        table=data.find('table')        if table:
            tbody=table.find('tbody')            return fillframe(tbody,index)        else:    #数据表格为空时退出
            return False    else:   #页面读取失败时退出
        return False


其次,用update函数保存更新已爬取好的DataFrame。

def update(frame,path,filename):
    try:  #尝试读取文件filename
        frame0=pd.read_csv(path+'\\'+filename)
    except: #如果文件已经存在,则更新它
        frame.to_csv(path+'\\'+filename,index=False,encoding='utf-8')    else: #否则创建名为filename的文件
        frame0=pd.concat([frame0,frame],ignore_index=True)
        frame0.to_csv(path+'\\'+filename,index=False,encoding='utf-8')


最后,着手设计主函数,来控制循环以及存储数据。

frame_player=pd.DataFrame()
gametype='season'for player_id in range(1862,1863):  
#这里仅爬取一位球员(James)测试,需要正式爬取请改为range(1,4450)
    page=0
    flag=True    while flag:
        result=spider(page,player_id,gametype,index)        if type(result)==bool and not result:   #返回False时
            flag=False            break
        else:   #爬取成功时读取表格
            frame_player=pd.concat([frame_player,result],ignore_index=True)
        page+=1


7)测试阶段:爬取一位球员数据,结果如下:

frame_player.head()

Python零基础,该如何掌握爬虫技术


接下来,就可以使用之前定义的update函数将frame_player保存到本地,也可以在每次while循环结束时运行一次update函数以起到实时更新的效果。

注意:实时更新完以后需执行frame_player=DataFrame()语句,将frame_player清空以避免数据重复。

以上便是完整的爬取数据操作过程。


8)常见问题解答

由于网站自身的编码原因,爬取时可能会遇到所有中文字符都成为乱码,如下:

frame_player.head()

Python零基础,该如何掌握爬虫技术


对该数据集,可以手动输入Index来解决表头乱码。

爬取球员英文姓名解决球员姓名乱码,具体的函数如下:

def getname(player_id):
    r2=requests.get('http://www.stat-nba.com/player/'+str(player_id)+'.html',timeout=30)    if r2.status_code==200:
        demo2=r2.text
        soup2=BeautifulSoup(demo2,"html.parser")
        name_source=soup2.find('div',{'class':"playerinfo"}).find('div',{'class':'name'})        if re.findall('[A-z]* [A-z]*',name_source.contents[0]):
            name=re.findall('[A-z]* [A-z]*',name_source.contents[0])[0]        else:
            name=np.nan    else:
        name=np.nan    return name

PS:爬虫还有很多可以优化的空间,比如控制每次发送请求的时间间隔防止IP被封,爬取球队名称做映射来解决比分中的乱码。