优化!1000元的Python副业单子,爬取下载美国科研网站数据

蚂蚁学Python

共 6230字,需浏览 13分钟

 · 2022-06-02

前情提要

此前已经结单的美国科研数据爬虫(详情见1000元的Python副业单子,爬取下载美国科研网站数据)虽然完结了,但是还是存在一些缺陷:

  1. 方案一爬取效率高,但是需要解析R关系,而R关系是手工整理的,且只整理到了2013年以前数据,2014年起的数据就无法解析了;
  2. 方案二是每次请求一行数据,虽然绕过了解析R关系,能够达成目标,但其最大的缺陷是运行时间太长了,爬虫部分整整运行了近6个小时,向服务器发送了近18万次请求。

以上缺陷是否可以进行优化呢?
本着精益求精的态度,花点时间将项目进行一下优化,看看能做到哪一步,Let's go!!!

优化方案思路

将上述方案一和方案二进行结合,在爬取全量数据时使用方案一,在解析R关系是使用方案二,那么就能大大的提高运行效率,具体如下:

  1. 已知批量获取的数据,从第2行开始,若与上一行数据相同,则返回的数据中将不包括相同数据,取而代之的是一个R关系的参数,此外,还有一个Ø参数,代表该行数据中,有部分列本身内容为空值;那么要解析批量数据,只需要推导出R与Ø两个参数所对应的规则;
  2. 推导规则只需要取得不同的R与Ø两个参数组合数据的行样例,以及其上一行数据,即可进行推导;
  3. 那么处理步骤应为: 1正常请求所有数据 -> 2汇总数据,取得R与Ø两个参数组合对应的样例行 -> 3以单行请求的方式获取样例行 -> 4解析样例行,推导出R与Ø参数组合与列的关系,制作成字典 -> 5正常解析第1步骤取得的所有数据行。

具体步骤

  1. 爬取全量数据

使用此前已经完成的crawl_page.py爬虫,nrows设置为20000行,即可一次请求完成该步骤,如下图:

设置每次爬取20000行数据
并发完成爬取,总用时仅4秒
  1. 汇总分析已爬取的数据(使用jupyter notebook进行探索)

    obj1 = ParseData()  # 创建方案一解析对象
    # 使用方案1解析对象中的汇总功能,将爬取到的数据合并成一个dataframe
    data = obj1.make_single_excel()  
    # R关系与Ø关系用0填充空值,并修改为整数形式
    data['R'] = data['R'].fillna(0).astype(int)
    data['Ø'] = data['Ø'].fillna(0).astype(int)
    # 添加年份
    data['year'] = data['year'].astype(int)
    data.head()
    运行截图
    # 删除不需要的列
    data = data.drop(columns=['S''C''filename''part'])
    # 按关系组合去重,保留第一条不重复的数据
    data = data.drop_duplicates(['R''Ø'], keep='first')
    # 列重新排序
    data = data[['R''Ø''year']]
    # 筛选去除0, 0 关系,该关系无需查询
    data = data.loc[~((data['R'] == 0) & (data['Ø'] == 0))]
    data
    近18万行数据中,存在的关系组合共154种
    • 整理获取不重复的关系组合
    • 首先汇总已经爬取到的全量数据,代码如下:
  2. 要解析上述154种R和Ø的组合关系,那么需要这154行数据,以及其上一行数据,共308行数据进行比对解析,因此问题转变为如何取得这308行数据。

前期进行页面分析时,可知每个请求的第1行都是完整的数据,因此,只要定位到这154行的上一行位置,然后再请求2次单行数据即可获得需要的数据。因此修改了部分PageSpider代码,满足该需求:

class PageSpiderv2(PageSpider):
    """
    继承并修改PageSpider对象的部分功能
    """

    def make_params(self, year: int = None, nrows: int = None, key: list = None) -> dict:
        """
        制作请求体中的参数
        :param year: 修改Post参数中的年份
        :param nrows: 修改Post参数中的count
        :param key: 下一页的关键字RestartTokens,默认空,第一次请求时无需传入该参数
        :return: dict
        """

        params = self.params.copy()
        if key:
            params['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Binding']['DataReduction'][
                'Primary']['Window']['RestartTokens'] = key
        if year:
            self.params['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Query']['Where'][0][
                'Condition']['In']['Values'][0][0]['Literal']['Value'] = f'{year}L'
        if nrows:
            self.params['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Binding']['DataReduction'][
            'Primary']['Window']['Count'] = nrows
        return params

    def crawl_page(self, year: int, nrows: int, key: list = None):
        """
        按照传入的参数单独爬取数据
        :param year: 需要爬取的数据的年份
        :param nrows: 需要爬取的数据的count值
        :param key: 下一页的关键字RestartTokens,默认空,第一次请求时无需传入该参数
        :return: None
        """

        while True:  # 创建死循环,直至爬取的结果是200时返回response
            try:
                res = requests.post(url=self.url, headers=self.headers, json=self.make_params(year, nrows, key),
                    timeout=self.timeout)
            except Exception as e:  # 其他异常,打印一下异常信息
                print(f'{self.year} Error: {e}')
                time.sleep(5)  # 休息5秒后再次请求
                continue  # 跳过后续步骤
            if res.status_code == 200:
                return res
            else:
                time.sleep(5)


def write_data(data: dict, filename: pl.Path):
    """
    将爬取到的数据写入TXT文件
    :param data: 需要写入的数据
    :param filename: 输出的文件名称
    """

    with open(filename, 'w', encoding='utf-8'as fin:
        fin.write(json.dumps(data))
    return data
# 定义并创建存储爬取到的R和Ø组合关系的文件夹
relation_path = pl.Path('./tmp/relation')
if not relation_path.is_dir():
    relation_path.mkdir()

"""
遍历不重复的R和Ø组合关系,逐一爬取数据,
爬取数据的逻辑是:
1.进行3次请求
2.第1次请求,根据index值取到R和Ø组合关系的上一行数据的RT值,
3.第2、3次请求,获取R和Ø组合关系的上一行与当前行,根据R和Ø组合关系创建文件夹,存储文件
"""

for idx in data.index:
    r, q, year = data.loc[idx]  # 拆包获取r,q,year数据
    # 定义并创建R和Ø组合关系文件夹,文件夹名以R和Ø组合关系命名
    out_path = relation_path / f'{r}_{q}'
    if not out_path.is_dir():
        out_path.mkdir()
    # 第1次爬取,获取定位的RT值
    req = PageSpiderv2(year)
    res = json.loads(req.crawl_page(year, nrows=idx).text)
    key = res['results'][0]['result']['data']['dsr']['DS'][0].get('RT')

    # 第2、3次爬取,获取2行数据用于比对,解析R和Ø组合关系
    for n in range(2):
        res = json.loads(req.crawl_page(year, nrows=2, key=key).text)
        res = write_data(res, out_path / f'{r}_{q}_{n}.txt')
        key = res['results'][0]['result']['data']['dsr']['DS'][0].get('RT')
    print('完成', r, q, year, idx)
运行结果示意
  1. 整理数据推导出R与Ø参数组合与列关系:
# Ø参数的空列数值字典
blank_col_dict = {
        60: ['Manufacturer Full Name''Manufacturer ID''City''State'],
        128: ['Payment Category'],
        2048: ['Number of Events Reflected']
    }
# 创建方案二的解析对象
obj2 = ParseDatav2()
# 初始化定义relation_df,第一行为R和Ø组合为0值,所有列均为1
relation_df = pd.DataFrame(['R''Ø'] + obj2.row.columns.tolist()[:-1])
relation_df[1] = 1
relation_df.set_index(0, inplace=True)
relation_df = relation_df.T
relation_df[['R''Ø']] = [0, 0]
relation_df
运行截图
# 遍历爬取到的R和Ø组合数据文件夹,解析R和Ø组合关系
for r_dir in relation_path.iterdir():
    r_files = list(r_dir.iterdir())
    r, q = r_dir.stem.split('_')
    res0 = obj2.parse_data(r_files[0])
    res1 = obj2.parse_data(r_files[1])
    # 两行数据对比,当不一致时是1,否则是0
    res = res0 != res1
    # 获取Ø关系的空列
    blank_cols = blank_col_dict.get(int(q))
    # 如有空列数据,则将对应列值清除
    if blank_cols:
        res[blank_cols] = False
    res[['R''Ø']] = [r, q]
    res = res[['R''Ø'] + obj2.row.columns.tolist()]
    res = res.drop(columns='idx')
    relation_df = pd.concat([relation_df, res])
# 整理组合关系dataframe
relation_df[['R''Ø']] = relation_df[['R''Ø']].astype(int)
relation_df.sort_values(by=['R''Ø'], inplace=True)
relation_df.set_index(['R''Ø'], inplace=True)
# 输出关系字典
relation_df.to_csv('./cols_dict.csv', encoding='utf-8')
relation_df
运行截图
  1. 使用解析得到R和Ø的组合的关系,解析输出CSV文件

该步骤因相比第一版增加了Ø关系,因此有对ParseData对象的方法进行了部分修改,具体详见代码,运行示例如下:

解析输出文件用时5分钟

总结

本次流程优化主要就是希望提高数据处理的效率,优化后:

  1. 全量数据请求,用时仅秒级,可以忽略不计;
  2. 在获取R和Ø的组合关系上,使用单线程请求,用时8分多钟:

3. 解析输出文件5分多种

总用时,在15分钟以内,与方案二的时间近6个小时相比,效率提升极为明显:

方案二用时近6个小时完成

同时在本次流程优化中并没有使用到太多新的技术,没能JS逆向解析出关系规则,还是没能解析,但是通过灵活使用参数,组合不同方案优势,还是能够极大的提升运行效率。
回过头来看,如果在接单的初期,在尝试JS逆向失败后,要是能够直接想到这个解决方案,那么5个小时的人工查询R关系时间,5个小时的单行爬虫运行时间,可以节约的时间大大的有啊!!!嗯,下次一定要提醒自己,要多打开思路,多尝试不同的方向思考,不要一条道走到黑,也许换个方向就能取得突破。总而言之,方法总比困难多,还是多思考,多尝试,多积累经验吧!!!


扫码下方,购买蚂蚁老师课程

提供答疑服务,和副业渠道

抖音每晚直播间购买,便宜100元优惠!

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报