python爬虫入门练习——爬取北航课程中心课件资源

目标是课程中心7195条课程信息下的所有资源~:

1 前言

首先声明我现在还只是个萌新,某一天忽然想学一下爬虫,然后去百度随便找了个教程然后看了开头一点点,于是想随便找个项目练习一下,所以就写了个简单的爬取课件资源的py程序。

为什么只看了一点点呢,因为实在看不下去啊,当时的心理状态:“他在讲什么,这软件干嘛的,这库咋用的,为什么他知道这么多啊……”然后想算了找本书学学可能更好吧,于是我翻开了目录:

幸好我先看视频配置好了环境,这些东西都还有印象,要是先看书的话简直就是新手劝退。果然学一个东西之前还是要进行一些取舍,根据自己的目的来,看要达到一个什么水平,之后分配适当的时间,能不用就不学,毕竟不是自己专业相关的东西,纯属娱乐,能迅速达到自己的目标就行了,虽然有些功利但毕竟光陰矢の如し。

首先是几个惯例性问题:

1.1 What

啥是爬虫啊?百度告诉我们爬虫就是请求网站并提取数据自动化程序,我之前平常玩玩手游,又菜又不想花太多时间,经常用按键精灵写一些简单脚本完成重复性的动作,当然其中也涉及到很多循环判断。Web spider也是个类似的东西,能够免去许多重复性的工作,所以其实技术含量倒没啥,而且大佬们都把轮子给造好了,我们只需要伸手就能用;真正要下点功夫的也就是和一些反爬虫斗智斗勇,有时候也容易走偏甚至触犯法律。

1.2 Why

为什么要用爬虫,或者说什么时候用;当然是你需要大量数据的时候,这些数据可以是各种类型的,比如文本可以用来进行一些大数据分析、用来给AI学习、用来抢票…….来看看知乎大神们的回答:利用爬虫技术能做到哪些很酷很有趣很有用的事情?肯定比我想得到的用法要多。

1.3 Where

JAVA | PYTHON | PHP | C# | C/C++……这么多语言,随便哪个都能拿来写爬虫,要我推荐当然是pyhton啊,毕竟人家是IEEE Spectrum 2018 编程语言 Top 1,所以我也是从pyhton爬虫开始学(别的都用的太少不熟悉- -),如果你没学过,正好我推荐密歇根大学Charles Severance教授的一系列python课程,什么听说你被墙了网站打不开,没关系我都下好了打包送你ofek。然后我用的是python3,软件推荐pyCharm,谁用谁知道。

2 爬之前需要了解的

真正动手开始写才知道写博客真的挺花时间的。

2.1 网页基础

学爬虫的一个好处就是你可以顺带了解到几乎每天都会接触的网页的一些知识,从而带来许多方便之处;比如某天登录一个网址它默认帮你填充了密码但是你忘了具体是什么想看一下,你只需要看一眼开发者工具里浏览器发出的请求就可以了。(我是觉得比打开保存的密码表单一个个找要快,当然你也许有更好的管理密码的工具)

这段对我来说真的不好写,毕竟是个外行人,崔老师的博客和书里都有很详细的讲解,我就简单讲下我的认识吧:

  • 打开你的浏览器开发者工具,Elements下那块一堆尖括号让人难受的字符串就是html——HyperText Markup Language,它包含许许多多的标签,很多时候在这里面就藏了我们想要的东西(当然这只是最幸运的情况),接下来的工作就是如何让计算机能识别并且提出这些东西出来。

  • 点击Network项,会发现一排东西,XHR、JS、CSS、Img……有些时候没那么幸运,html里空空如也,这说明该网站使用的是别的加载方式如ajax等;这时不妨去XHR里瞧一瞧说不定就发现了呢,JS(JavaScript)有很多都是写好的函数用来控制网页的行为,我是还没仔细研究过这块;CSS主要用来美化网页布局如排版、字体等;img就如其名,简单易懂。

  • 点击Application项,在storage找到Cookies,这是个非常重要的东西,网站用它辨别你的身份,所以爬虫常用它进行伪装,让对方认为是一个人在操作模拟器点击浏览网页;当然这只是第一步很简单的伪装。

  • 点进一个请求,看到右边有Requests HeadersResponse Headers等等,这又是一个非非非常重要的一项,里面清清楚楚的记载了浏览器是如何发送请求并且网站端是如何响应的,所以如果你的爬虫不按它的格式发请求还妄想访问它,等待你的只会是一系列的奇葩状态码

2.2 Python相关库

  • 先说点别的,好像有种叫做可视化爬虫的东西,就算不怎么懂编程都能爬些数据,我没接触过,不过也是一种选择吧。
  • requests库,用来发送请求,python内置的urllib也能用,不过好像这个更方便。(自行安装)
  • Beautiful Soup,用来解析html的库,官网有点意思,十分出色,好像还有几个如XPath、Pyquery,功能都差不多,先只学一个以后不够用再说。
  • 还有如re、os这些库,用来进行字符串匹配和文件操作的,也很常用。
  • 数据储存方面,我现在还用不到MongoDB、MySQL这些东西,所以相关库就暂且不提。

(总算快到正片了…)

3 py爬虫思路及过程—以北航课程中心为例

3.1 明确需求

  1. 获取北航课程中心的所有课程,信息(类别、名称、链接、教师)储存为一个文本文档。
  2. 筛选想下载的课程(抱歉,我全都要),进入链接并判断是否有课件资源,建立文件夹储存课件以及详细的课程信息。

3.2 分析网站

由于我不在北航,校外访问首先要登录北航vpn,首先打开开发者工具刷新网址,可以看到有一个Name是sign_in,明显就是我们要找的;然后,点击登录后(首先你得有个北航课程中心的账号)发现Headers里面多了Form Data这一项,里面记录了你的账号密码和一个authenticity_token以及一些键名,发送请求时肯定是要按照这个格式来;

不过细心的你可能发现每次authenticity_token的值都在变化,你可能会说:这要我怎么请求啊?但北航怎么会难为你呢,更加细心的你在网站的html源代码里一搜便搜到了竟然有这个值,所以你只需要先提取出来再加入你的请求中就行了,多简单。

当你满怀欣喜进入一门课程的界面时,又发现右上角竟然还是没有登录的状态??对这个设定让我当初下课件的时候特别难受,你还得再点一下登录按钮然后会自动跳转进portal界面然后你再返回课程界面按下F5!而且过了一会又会变成需要登录的状态,这个细节在写爬虫的时候也得注意一下,这里不细讲了,毕竟程序员们都在说:


3.3 请求获取解析储存数据

首先看下一共有多少条目课程:

3.3.1 coursesinfo_spider.py

import requests
from urllib.parse import urlencode
from requests.exceptions import ConnectionError
import json
from multiprocessing import Pool
s = requests.Session()        #会话维持
#返回当页的json
def get_one_page(pagenumber):
    data={
        'pageSize': '50',
        'pageNumber': pagenumber,
        'orderParams': '[{"field":"updateDate","dir":"desc"}]',
        'searchParams': '[]',
        'specialParams': '[]'
    }
  loginurl='https://course.e.buaa.edu.cn/opencourse/course/list?'+urlencode(data)
    headers = {
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'Accept-Encoding': 'gzip, deflate, br',
        'Host':'course.e.buaa.edu.cn',
        'Connection': 'keep-alive',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36',
        'Cookie':#这个我可不能告诉你) ,
        'Referer': 'https://course.e.buaa.edu.cn/opencourse/course/list',
        'X-Requested-With': 'XMLHttpRequest'
    }     #我是老老实实按照它的请求写的,当然少写几个也可能没啥问题
    try:
        login=s.post(loginurl, headers=headers)
        print('status code:', login.status_code)
        if login.status_code == 200:
            return login.content.decode("utf-8")
    except ConnectionError:
        return None

#返回generator,当前页上的所有课程
def parse_page(pagenumber):
    text=get_one_page(pagenumber)
    data = json.loads(text)
    for item in data.get('content'):
        yield {
            'Type':item.get('courseType'),
            'Title':item.get('title'),
            'url':"https://course.e.buaa.edu.cn/opencourse/course/detail/"+str(item.get('id')),
            'Teacher':item.get('teachers')[0].get('name')
        }

#以字符串形式返回当前页的课程
def each_page_content(pagenumber):
    content = []
    for item in parse_page(pagenumber):
        content.append(item)
    return content

#主程序
if __name__ == '__main__':
    pool = Pool()    #多线程,这里数据就几千条,其实没必要
    content=list()
    result=pool.map(each_page_content, [i  for i in range(1,2)])
    for j in range(len(result)):
        content=content+result[j]
    pool.close()
    pool.join()
    with open('beihangcourses.txt', 'w', encoding='utf-8') as f:
        for item in content:
            f.write(json.dumps(item, ensure_ascii=False) + '\n')
        f.close()                   #储存为字典形式运行结果

运行结果:


3.3.2 download_courses.py

import requests
from bs4 import BeautifulSoup
import re
import os
class Login(object):                       #模拟登录北航vpn模块
    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36',
            'Host': 'e.buaa.edu.cn'
        }
        self.login_url = 'https://e.buaa.edu.cn/users/sign_in'
        self.session = requests.Session()
        f = open("beihangcourses.txt", "r", encoding='utf-8')  # 读取之前保存的文件
        str = f.read().strip()  # 将txt文件的所有内容读入到字符串str中
        a = str.split('\n')
        self.content = []
        for item in a:
            item = eval(item)
            self.content.append(item)

    def token(self):
        response = self.session.get(self.login_url, headers=self.headers).text
        soup = BeautifulSoup(response, 'lxml')
        token=soup.find(attrs={'name': 'authenticity_token'}).attrs['value']  #获取登录authenticity_token
        return token

    def login(self, email, password):
        post_data = {
            'utf8': '✓',
            'authenticity_token': self.token(),
            'user[login]': email,
            'user[password]': password,
            'commit': '登录 Login',
        }
        response = self.session.post(self.login_url, data=post_data, headers=self.headers)
        print('status_code:',response.status_code)
        if response.status_code == 200:
            print('\n登录北航vpn成功')


    def logincourse(self):       #登录课程中心以下载资源
        self.headers1 = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36',
        }
        self.post_url = 'https://sso-443.e.buaa.edu.cn/login?service=https%3A%2F%2Fcourse.e.buaa.edu.cn%2Fsakai-login-tool%2Fcontainer'
        response1 = self.session.get(self.post_url, headers=self.headers1)


    def view_page(self,page):
        headers={'Cookie': #这个也不能告诉你吧}
        result=self.session.get(self.content[page]['url'], headers=headers).content.decode("utf-8")
        return result

def get_data(page):          #下载某一课程的资源
    code=login.view_page(page)
    soup = BeautifulSoup(code, 'lxml')
    token = soup.find(attrs={'class': 'bd'}).contents
    introduction=re.sub('<[^<]+?>', '', str(token[1])).replace('\n', '').strip()
    dagang=re.sub('<[^<]+?>', '', str(token[3])).replace('\n', '').strip()
    ziyuan=token[5]
    teacher_info=re.sub('<[^<]+?>', '', str(token[7])).replace('\n', '').strip()
    file_name=validateTitle(login.content[page]["Title"])
    folder_path = "E:/course_data/" + file_name + "/"
    if str(ziyuan) == '<div>无</div>':
        folder_path="E:/course_data/" + file_name +"(无课件)"+"/"
    if not os.path.exists(folder_path) :
        os.makedirs(folder_path)
    os.chdir(folder_path)


    with open(file_name+'.txt', 'w+', encoding='utf-8') as f:  # 课程信息写入
        f.write(introduction+'\n\n'+dagang+'\n\n'+teacher_info)
        f.close()
    pattern = re.compile('.*?<a href="(.*?)">(.*?)</a>', re.S)  # re.S: .能匹配换行
    downloadurl = re.findall(pattern, str(ziyuan))
    print(file_name)
    print(downloadurl)
    if not re.search('请先登录后再查看资源!', str(downloadurl))==None:
        login.login(email='#输入你的账号', password='#输入密码')
        login.logincourse()
        code = login.view_page(page)
        soup = BeautifulSoup(code, 'lxml')
        token = soup.find(attrs={'class': 'bd'}).contents
        ziyuan = token[5]
        downloadurl = re.findall(pattern, str(ziyuan))
    for file in downloadurl:
        try:
            r = login.session.get(file[0])
        except:
            continue
        if os.path.exists(file[1])==False:
            try:
                with open(validateTitle(file[1]), "wb") as code:
                    code.write(r.content)
                    print("dowload over:"+file[1])
            except:
                continue


def validateTitle(title):   #  规范奇葩的文件名和课程名!
    rstr = r'[(\s* )(/)(\\)(\:)(\*)(\?)(\")(\<)(\>)(\|)]'  # '/ \ : * ? " < > |'
    new_title = re.sub(rstr, "_", title)  # 替换为下划线
    return new_title


if __name__ == "__main__": #主程序
    login = Login()
    login.login(email='#输入你的账号', password='#输入密码')
    login.logincourse()
    for i in range(#起始下载项,#看你想下载多少):
        get_data(i)
        print('下载完毕')

3.3.3 几点说明

  • requests.Session()用来维持一次登录,这个很重要,不然每次都是相互独立的请求;
  • validateTitle是我后来加的,程序各种报错发现是一些课程名竟然有些./\符号;
  • 我用的是正则表达式,强烈推荐学习,让你的搜索能力一夜猛增;
  • 写了能用我就懒得改了,改是肯定有很多地方能改清晰简洁点的;
  • 判断的地方有待完善,运行太长时有时还是会出现“请先登录后再查看资源”提示

4 总结

学习爬虫中的第一个小练习,也是第一篇有点内容的博客,以后有空会进一步学习下验证码的识别、app的爬取、分布式爬虫等知识,时间有限,下一个练习不知道啥时才能做完- - 博客坚持2周一篇,这几天花的时间多了点。

Author: zcp
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source zcp !
评论
 Previous
日本語で作文——文章を書こう
1 漢字と符号及び原稿用紙の使い方について1.1 表示体系 漢字:  音読み、訓読み $ \begin{cases} 和語:& \text{畳、下駄…} \\ 漢語:& \text{就職、拡大…} \e
Next 
Welcome to CP Zhao's Blog
1 总算把博客建的差不多了!  作为一个非计算机专业的萌新来说,自己搭一个博客真的是一个痛苦的过程,面对一排排的html代码还有json、css文件,简直不知道该干嘛,说实话我现在想改个字体大小都找不到地方,凑
  TOC