Python筛选志愿深圳活动(异步加载爬虫实战)

引入

事情是这样的,最近我和我同学两人要去做志愿活动,希望能一天内做两个,一个在上午,一个下午,并且日期要在18号

所以筛选条件就是:

①时间为上午(或下午)

②日期为 2024.8.18

③15岁的我们要能参加,也就是说允许做义工的年龄包含15岁

④可招募人数 >= 2人

写程序时是13号, 当时距离18号还有很久,首页基本上都是最近几天的活动,所以一个一个翻显然不现实,因此,打算写一个程序帮我完成

网站分析

初步分析网页

进入志愿深圳查询志愿项目页面,发现每个项目右下角都有一串日期,这个可以帮助进行初步筛选。往下滑动网页,发现会加载出更多的活动,说明网站使用了异步加载技术,那么我们就无法通过直接请求网页,然后用xpath定位的方式实现筛选了,而是要抓取并分析其请求的数据。一般来说,这些数据的格式为JSON,所以如果能抓取到,那么分析就易如反掌了

抓取数据

按F12调出开发者工具,选择网络(某些浏览器中叫Network,edge则是一个图标),选择XHR,最后往下滑动网页

发现多了一条请求,点进去看看

发现这个请求返回了20个活动,以及活动的一些信息

分析URL

接下来使用Postman分析该请求的请求URL(也可以不用Postman,直接瞪眼法+手动删减URL再用浏览器尝试请求也行)

 发现它有15个参数,但是有8个是空的。把这8个参数删除,再请求一次,看看会不会影响结果

可以发现并没有影响

再删,发现_sw1、lat、lng的存在与否也对结果没有影响

接下来就要分析其他参数的作用了。参数_c、_a应该是查询活动的固定参数,不用管,剩下的_p、_ps对结果有影响,前者是页数,后者是每页的活动数。所以我们的程序就可以将_ps固定,将_p不断+1,实现翻页的效果

所以最终URL就被缩减为:https://www.sva.org.cn/Default.aspx?_c=ProgramSearch&_a=GetPrograms&_p={p}&_ps={ps}

分析数据

将_p设为1,_ps设为2(方便分析),将链接复制到浏览器访问,并复制返回结果

粘贴到JSON解析工具(方便分析)

可以发现,返回结果是一个大字典,其中包含5个键,我们要找的项目就在Programs里面。而Programs的值是一个列表,每个列表又包含若干个字典(字典个数取决于参数_ps的值),每个字典中有其所对应项目的一些信息

但是这里面的信息并不完整,我们只能从中找到活动日期这一我们需要的信息,至于年龄限制这些全都找不到

所以我们继续分析网页

进一步分析网页

回到志愿活动查询页面,随便找一个活动点击进去,能看到详细信息,所以我们需要在这个网页内进行数据抓取和分析

抓取数据

与上文相同,此处不再赘述

最终得到URL为https://www.sva.org.cn/Default.aspx?_c=Program&_a=GetProfile&ProgramID=f2da36200f38a8b2669767f63ab70291

分析URL

参数_c、_a老朋友了,不管。新出现了一个参数ProgramID看着就很重要,显然它是数据库定位到指定活动的凭据。而ProgramID也是初步分析网页后可得的,它是键 ProID 的值。所以我们的程序只需要在初步分析时记下ProID,即可访问活动的详细信息

分析数据

同上,直接复制链接到浏览器,并粘贴到解析工具分析,结果如下

发现返回是一个大字典,有三个键,我们要找的详细信息在Program键中,Program键的值也是一个字典,这个字典中:

RegAgeMin 表示 最低年龄 (int)

RegAgeMax 表示 最大年龄 (int)

PositionsAvailable 表示 报名总人数 (int)

PositionsOccupied 表示 已报名人数 (int)

StartTime 中含有 活动开始时间 (str)

EndTime 中含有 活动结束时间 (str)

获得这些信息后,就可以运用 if 进行判断,筛选出符合条件的活动

Python实现

import

import requests

常量定义

根据筛选条件,定义以下常量

ACTIVITY_NUM = 12  # 需要找到的符合条件的活动数量
TARGET_TIME = '2024-08-18'  # 目标日期
AVAILABLE_POSITIONS = 2  # 可招募人数
AGE = 15  # 年龄限制
START_TIME = 6  # 时间限制
END_TIME = 12  # 将开始时间设为6,结束时间设为12,代表上午
PS = 1000  # 请求返回的活动数(待会详细解释)

变量定义

p = 0  # 页码
activities = 0  # 已检索活动数
activity_links = []  # 筛选出来的活动的页面链接

初步筛选(筛出满足日期条件的活动)

def date():
    global activities
    url = f"https://www.sva.org.cn/Default.aspx?_c=ProgramSearch&_a=GetPrograms&_p={p}&_ps={PS}"
    response = requests.get(url)
    programs = response.json().get("Programs")

    for _ in programs:
        activities += 1
        if TARGET_TIME in _.get('StartDate'):  # 因为StartDate内容为 xxxx-xx-xxTxx:xx:xx 所以要用 in 而不是 == 
            if details(_.get('ProID')):
                return True

根据详细信息筛选(年龄,时间,人数等)

def details(proid):
    url = f"https://www.sva.org.cn/Default.aspx?_c=Program&_a=GetProfile&ProgramID={proid}"
    response = requests.get(url)
    program = response.json().get("Program")

    min_age, max_age = program.get("RegAgeMin"), program.get("RegAgeMax")
    if (max_age and min_age) is None:
        min_age = 0
        max_age = 100
    elif max_age is None:
        max_age = 100

    available_positions = program.get("PositionsAvailable") - program.get("PositionsOccupied")

    start_time, end_time = int(program.get("StartTime")[11:13]), int(program.get("EndTime")[11:13])

    if min_age <= AGE <= max_age and available_positions >= AVAILABLE_POSITIONS and START_TIME <= start_time <= end_time <= END_TIME:
        activity_page_url = f"https://www.sva.org.cn/Default.aspx?_c=Program&ProgramID={proid}"
        activity_links.append(activity_page_url)
        print(f'\r进度:{len(activity_links)} / {ACTIVITY_NUM}', end='')
        if len(activity_links) == ACTIVITY_NUM:
            return True

解释一下:

①为什么对 min_age 和 max_age 进行处理?

有些活动限制年龄是xx岁以上,或者直接没有年龄限制。这两种情况都会造成 min_age 或 max_age 为None,如果不处理,将会在下面的 if 判断中报错:

TypeError: '<=' not supported between instances of 'NoneType' and 'int'

另外,必须先判断 (max_age and min_age) is None,如果先判断了 max_age 就会漏情况

②为什么要重新拼接一次 activity_page_url ?

因为前面的URL是请求数据库的URL,和活动界面的URL不一样

③print前面的 \r 是干嘛的

加入 \r 的目的是将光标移到最前方,使得下次输出能覆盖这次输出的内容,后面的 end=''是为了防止换行

结合一下

while 1:
    p += 1
    if activities >= 10000:
        ans = input('\n查询活动量已超10000,是否继续?(按enter继续,按a结束查询)')
        if ans == 'a':
            break
    if date():
        break

print(f'\n共检索{activities}个活动,其中,符合条件的活动链接为')

for i in activity_links:
    print(i)

加了一个功能,在检索的活动大于10000个时,询问用户是否继续

另外,可以选择 import webbrowser 实现自动打开所有活动链接的功能

小修小补

有一些活动左上角标注了“排期”

点进去后发现会有多个活动

此时需要重新执行一遍初步筛选

针对这种情况,我优化了一下程序

不解释了,直接上程序

import requests

ACTIVITY_NUM = 10
TARGET_TIME = '2024-08-18'
AVAILABLE_POSITIONS = 2
AGE = 15
START_TIME = 6
END_TIME = 12
PS = 1000

p = 0
activities = 0
activity_links = []

def details(proid):
    url = f'https://www.sva.org.cn/Default.aspx?_c=Program&_a=GetProfile&ProgramID={proid}'
    response = requests.get(url)
    program = response.json().get("Program")

    min_age, max_age = program.get("RegAgeMin"), program.get("RegAgeMax")
    if (max_age and min_age) is None:
        min_age = 0
        max_age = 100
    elif max_age is None:
        max_age = 100

    available_positions = program.get("PositionsAvailable") - program.get("PositionsOccupied")

    start_time, end_time = int(program.get("StartTime")[11:13]), int(program.get("EndTime")[11:13])

    if min_age <= AGE <= max_age and available_positions >= AVAILABLE_POSITIONS and START_TIME <= start_time <= end_time <= END_TIME:
        activity_page_url = f"https://www.sva.org.cn/Default.aspx?_c=Program&ProgramID={proid}"
        activity_links.append(activity_page_url)
        print(f'\r进度:{len(activity_links)} / {ACTIVITY_NUM}', end='')
        if len(activity_links) == ACTIVITY_NUM:
            return True

def date(program):
    global activities
    activities += 1
    if TARGET_TIME in program.get('StartDate'):
        if details(program.get('ProID')):
            return True

def search(url):
    response = requests.get(url)
    programs = response.json().get("Programs")

    for program in programs:
        if program.get('ScheduleType') != 1:
            url_ = f"https://www.sva.org.cn/Default.aspx?_c=ProgramSearch&_a=GetPrograms&_p=1&_ps=1000&MasterProgramID={program.get('ProID')}"
            if search(url_):
                return True
        if date(program):
            return True
    return False

print(f'\r进度:{len(activity_links)} / {ACTIVITY_NUM}', end='')

while 1:
    p += 1
    if activities >= 10000:
        ans = input('\n查询活动量已超10000,是否继续?(按enter继续,按a结束查询)')
        if ans == 'a':
            break
    if search(f"https://www.sva.org.cn/Default.aspx?_c=ProgramSearch&_a=GetPrograms&_p={p}&_ps={PS}"):
        break

print(f'\n共检索{activities}个活动,其中,符合条件的活动链接为')

for i in activity_links:
    print(i)

selenium法

其实最开始写这个程序我是用的selenium,主要是这个网站用了异步加载,如果不想去分析各种参数和URL,那用selenium是最省心的方法,不过实际操作下来用selenium的难度远超本文介绍的方法,而且难度高就算了,检索速度还慢,等待时间又久,甚至很容易报错,所以就尝试了一下老老实实的去分析,发现效果出奇的好,其他条件相同的情况下,request法得出结果的时间相比selenium缩短了约95%

当然selenium肯定也有它的使用场景,比如一些查成绩的网站,header中带有加密后的数据,这种基本上就只会使用selenium了。不过在本项目中,因为志愿深圳并不复杂,也没有啥反爬措施(甚至可以不用伪造header直接爬),所以用request的方法

结尾

异步加载在现代网站上经常使用,但是一般不难分析,稍微花点时间就能找到请求URL,进而分析参数,返回数据。有些难爬的网站还要求伪造header,绕开反爬措施,这也是Python爬虫的有趣之处

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值