Python 크롤러는 미국 과학 연구 사이트에서 데이터를 크롤링 및 다운로드합니다.

목차

미션 소개

과제 해결 아이디어 및 경험 획득

특정 단계

요약하다

                        나는 당신의 관심을 기대하는 Zheng Yin입니다


안녕하세요 여러분, 저는 Zheng Yin입니다. 오늘은 상대적으로 형사적인 파충류 사례를 알려드리겠습니다.

Python 크롤러는 미국 과학 연구 사이트에서 데이터를 크롤링 및 다운로드합니다.

무료로 팔로우하고 지원하기가 쉽지 않습니다

미션 소개

  • 대상 웹사이트: https://app.powerbigov.us/view?r=eyJrIjoiYWEx…

  • 대상 데이터: 2009-2013년의 표 형식 데이터를 다운로드하고 CSV 파일로 저장

      대상 웹사이트는 아름다운나라의 과학 연구 데이터와 PowerBI에서 구현한 웹페이지 데이터로 Ctrl+C로 복사할 수 없으므로 크롤링하여 도움을 요청합니다.


과제 해결 아이디어 및 경험 획득

  먼저 작업을 두 부분으로 분해할 수 있습니다. 하나는 웹 사이트에서 로컬로 데이터를 크롤링하는 것이고 다른 하나는 데이터를 구문 분석하고 CSV 파일을 출력하는 것입니다 .

  • 데이터 부분 크롤링:

    1. 웹 페이지를 구문 분석하고 데이터의 비동기 로딩을 위한 실제 요청 주소와 매개변수를 찾습니다.

    2. 모든 데이터를 가져오는 크롤러 코드 작성

  • 데이터 부분 구문 분석

  이것이 이 작업의 가장 큰 어려움입니다.난이도는 반환된 데이터 목록에서 요소의 수가 고정된 숫자가 아니라 이전 행과 다른 값일 뿐이며 어떤 열이 다른지, 어떤 열이 동일하게 "R" 값을 사용하는 것은 정상적인 솔루션이 R 관계를 구문 분석하고 구문 분석을 완료하는 함수를 찾기 위해 JS를 역순으로 사용하는 것임을 나타냅니다 . 그러나 웹 페이지의 JS는 매우 복잡하고 많은 함수 이름이 축약형이기 때문에 읽기가 매우 어렵고 역성공한 사례가 없습니다.

  이 문제를 해결하기 위해 1차 버전을 수동으로 조회하고 요약해서 1차 버전을 완성했는데 뜻밖에도 이 공유 글을 쓰다가 갑자기 아이디어가 뜨고 데이터를 요청하는 방식이 바뀌고 분석 단계를 건너뛰었습니다. R 관계:

  • 옵션 1: 일반 요청에 따라 R 관계를 사용하여 데이터 구문 분석

  2009년부터 2013년까지 필요한 데이터 중 전체 데이터를 다운받아 분석한 결과, 최소값 0, 최대값 4083의 총 124개의 R 관계가 존재한다. 이 124개의 관계를 수동으로 조회하면 사전이 된다. 분석을 완료하도록 했습니다. 요약된 관계는 다음과 같습니다(5시간 동안 수동 쿼리, 피곤함).

  • 옵션 2: 시간을 공간으로 교환하고, R 관계를 파싱하는 어려움
      을 우회하여 한 번에 한 행의 데이터만 요청 데이터를 검토하다가 갑자기 깨달았다. 요청된 데이터의 첫 번째 행은 완전해야 함 한 번에 하나의 데이터 행만 요청하면 이전 행과 같은 상황이 발생할 가능성이 없으며 이 경우 R 관계를 구문 분석하는 어려움을 우회할 수 있습니다. 테스트 후 솔루션이 가능하지만 다음 문제를 고려해야 합니다.

    1. 멀티 쓰레드 가속을 켜서 시간을 단축시키되 멀티 쓰레드를 켜도 12년에 12개 쓰레드만 열릴 수 있고, 가장 많은 줄을 가진 연도는 약 20,000줄로 크롤러가 필요로 한다. 약 5~6시간 동안 실행

    2. 중단점은 프로그램이 비정상적으로 중단된 후 처음부터 시작할 필요가 없도록 계속 상승합니다.


특정 단계

  1. 타겟 사이트 분석

  첫 번째 단계는 물론 대상 웹사이트를 분석하고 데이터에 대한 올바른 요청 주소를 찾는 것입니다. 이것은 매우 쉽습니다. Chrome의 개발자 모드를 열고 스크롤 막대를 아래로 드래그하고 실제 주소인 새 요청을 확인합니다. 결과로 직접 이동:

그런 다음 POST 요청의 매개변수를 살펴봅니다.

요청 매개변수

헤더를 다시 보니 안티클라이밍이 없어서 놀랐습니다! 등반 금지! 등반 금지! 음, Pretty Country의 웹사이트는 분위기입니다.

  • 분석 요약:
    # 完整参数就略过,关键参数以下三项:
    # 1.筛选年份的参数,示例:
    param['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Query']['Where'][0]['Condition']['In']['Values'][0][0]['Literal']['Value'] = '2009L'
    # 2.请求下一批数据(请求首批数据时无需传入该参数),示例:
    param['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Binding']['DataReduction']['Primary']['Window']['RestartTokens'] = [["'Augusto E Caballero-Robles'","'Physician'","'159984'","'Daiichi Sankyo, Inc.'","'CC0131'","'Basking Ridge'","'NJ'","'Compensation for Bona Fide Services'","2009L","'4753'"]]
    # 注:以上"RestartTokens"的值在前一批数据的response中,为上一批数据的返回字典值,示例res['results'][0]['result']['data']['dsr']['DS'][0]['RT']
    # 3.请求页面的行数(浏览器访问默认是500行/页,但爬虫访问的话...你懂的),示例:
    param['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Binding']['DataReduction']['Primary']['Window']['Count'] = 500
    # 参数还有很多,例如排序的参数ordby,各种筛选项等
    
    • 데이터 요청 URL:https://wabi-us-gov-virginia-api.analysis.usgovcloudapi.net/public/reports/querydata?synchronous=true

    • POST의 주요 매개변수:

  1. 크롤러의 주요 단계 및 코드

    • 모든 데이터를 얻기 위해 while True 무한 루프가 사용되며 각 요청의 반환 값에 "RT" 키워드가 있으면 POST 매개 변수가 수정되고 "RT" 키워드가 없을 때까지 다음 요청이 시작됩니다. 모든 데이터 크롤링이 종료됨을 의미하는 반환 값(자세한 내용은 코드 참조)

    • 요청 과정에서 예외를 잡아야 하고 예외의 종류에 따라 다음 단계가 결정된다 웹사이트에는 안티클라이밍이 없고 타임아웃이나 접속 오류의 예외만 있기 때문에 요청을 다시 시작하기만 하면 되며, 따라서 중단점을 고려하지 않고 크롤링을 계속할 필요가 없습니다.

    • 위의 단계에서 자세한 코드는 다음과 같습니다.

    • 웹사이트의 크롤링 방지 기능이 없습니다. 정확한 경로와 매개변수를 찾은 후 크롤러 코드의 구현은 비교적 간단합니다. 게시 요청을 직접 시작할 수 있습니다. 코드는 PageSpider 클래스를 통해 구현됩니다(상세 코드 첨부).

    • 중단점 재개에서 프로세스가 해결되고 데이터의 각 행이 TXT 파일에 저장되고 파일 이름이 연도와 행 수를 기록하고 먼저 크롤링된 레코드를 읽고 마지막 요청의 결과를 찾은 다음 그런 다음 후속 요청을 시작합니다.

    • """
      爬取页面数据的爬虫
      """
      import pathlib as pl
      import requests
      import json
      import time
      import threading
      import urllib3
      
      
      def get_cost_time(start: time.time, end: time.time = None):
        """
        计算间隔时长的方法
        :param start: 起始时间
        :param end: 结束时间,默认为空,按最新时间计算
        :return: 时分秒格式
        """
        if not end:
            end = time.time()
        cost = end - start
        days = int(cost / 86400)
        hours = int(cost % 86400 / 3600)
        mins = int(cost % 3600 / 60)
        secs = round(cost % 60, 4)
        text = ''
        if days:
            text = f'{text}{days}天'
        if hours:
            text = f'{text}{hours}小时'
        if mins:
            text = f'{text}{mins}分钟'
        if secs:
            text = f'{text}{secs}秒'
        return text
      
      
      class PageSpider:
        def __init__(self, year: int, nrows: int = 500, timeout: int = 30):
            """
            初始化爬虫的参数
            :param year: 下载数据的年份,默认空,不筛选年份,取得全量数据
            :param nrows: 每次请求获取的数据行数,默认500,最大30000(服务器自动限制,超过无效)
            :param timeout: 超时等待时长
            """
            self.year = year if year else 'all'
            self.timeout = timeout
            # 请求数据的地址
            self.url = 'https://wabi-us-gov-virginia-api.analysis.usgovcloudapi.net/public/reports/querydata?synchronous=true'
            # 请求头
            self.headers = {
                # 太长省略,自行在浏览器中复制
            }
            # 默认参数
            self.params = {
                # 太长省略,自行在浏览器中复制
            }
            # 修改默认参数中的每次请求的行数
            self.params['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Binding']['DataReduction'][
                'Primary']['Window']['Count'] = nrows
            # 修改默认参数中请求的年份
            if self.year != 'all':
                self.params['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Query']['Where'][0][
                    'Condition']['In']['Values'][0][0]['Literal']['Value'] = f'{year}L'
      
        @classmethod
        def read_json(cls, file_path: pl.Path):
            with open(file_path, 'r', encoding='utf-8') as fin:
                res = json.loads(fin.read())
            return res
      
        def get_idx_and_rt(self):
            """
            获取已经爬取过的信息,最大的idx以及请求下一页的参数
            """
            single = True
            tmp_path = pl.Path('./tmp/')
            if not tmp_path.is_dir():
                tmp_path.mkdir()
            files = list(tmp_path.glob(f'{self.year}_part*.txt'))
            if files:
                idx = max([int(filename.stem.replace(f'{self.year}_part', '')) for filename in files])
                res = self.read_json(tmp_path / f'{self.year}_part{idx}.txt')
                key = res['results'][0]['result']['data']['dsr']['DS'][0].get('RT')
                if not key:
                    single = False
            else:
                idx = 0
                key = None
            return idx, key, single
      
        def make_params(self, key: list = None) -> dict:
            """
            制作请求体中的参数
            :param key: 下一页的关键字RestartTokens,默认空,第一次请求时无需传入该参数
            :return: dict
            """
            params = self.params.copy()
            if key:
                params['queries'][0]['Query']['Commands'][0]['SemanticQueryDataShapeCommand']['Binding']['DataReduction'][
                    'Primary']['Window']['RestartTokens'] = key
            return params
      
        def crawl_pages(self, idx: int = 1, key: list = None):
            """
            爬取页面并输出TXT文件的方法,
            :param idx: 爬取的索引值,默认为1,在每行爬取时,代表行数
            :param key: 下一页的关键字RestartTokens,默认空,第一次请求时无需传入该参数
            :return: None
            """
            start = time.time()
            while True:  # 创建死循环爬取直至结束
                try:
                    res = requests.post(url=self.url, headers=self.headers, json=self.make_params(key),
                                        timeout=self.timeout)
                except (
                        requests.exceptions.ConnectTimeout,
                        requests.exceptions.ConnectionError,
                        urllib3.exceptions.ConnectionError,
                        urllib3.exceptions.ConnectTimeoutError
                ):  # 捕获超时异常 或 连接异常
                    print(f'{self.year}_part{idx}: timeout, wait 5 seconds retry')
                    time.sleep(5)  # 休息5秒后再次请求
                    continue  # 跳过后续步骤
                except Exception as e:  # 其他异常,打印一下异常信息
                    print(f'{self.year}_part{idx} Error: {e}')
                    time.sleep(5)  # 休息5秒后再次请求
                    continue  # 跳过后续步骤
                if res.status_code == 200:
                    with open(f'./tmp/{self.year}_part{idx}.txt', 'w', encoding='utf-8') as fout:
                        fout.write(res.text)
                    if idx % 100 == 0:
                        print(f'{self.year}的第{idx}行数据写入完成,已用时: {get_cost_time(start)}')
                    key = json.loads(res.text)['results'][0]['result']['data']['dsr']['DS'][0].get('RT', None)
                    if not key:  # 如果没有RT值,说明已经全部爬取完毕了,打印一下信息退出
                        print(f'{self.year} completed max_idx is {idx}')
                        return
                    idx += 1
                else:  # 打印一下信息重新请求
                    print(f'{self.year}_part{idx} not 200,check please', res.text)
                    continue
      
      
      def mul_crawl(year: int, nrows: int = 2):
        """
        多线程爬取的方法,注按行爬取
        :param year: 需要爬取的年份
        :param nrows: 每份爬取的行数,若每次仅爬取1行数据,nrows参数需要为2,才会有下一行,否则都是第一行
        """
        # 定义爬虫对象
        spider = PageSpider(year, nrows=nrows)
        # 获取爬取对象已爬取的idx,key和是否完成爬取的信号single
        idx, key, single = spider.get_idx_and_rt()
        if not single:
            print(f'{year}年的共{idx}行数据已经全部下载,无需爬取')
            return
        print(f'{year}年的爬虫任务启动, 从{idx+1}行开始爬取')
        spider.crawl_pages(idx+1, key)  # 特别注意,已经爬取了idx行,重启时,下一行需要+1,否则重启后,会覆盖一行数据
      
      
      if __name__ == '__main__':
        pools = []
        for y in range(2009, 2021):
            pool = threading.Thread(
                target=mul_crawl, args=(y, 2), name=f'{y}_thread'  # 按行爬取,nrows参数需要为2
            )
            pool.start()
            pools.append(pool)
        for pool in pools:
            pool.join()
        print('任务全部完成')

코드 실행 예시:

시간을 공간으로 교환하고 R 관계형 구문 분석을 우회하여 한 번에 한 줄만 요청합니다.

  1. 분석 데이터

    • 옵션 1

  데이터 파싱의 어려운 부분은 R 간의 관계를 찾는 것인데, 이 부분은 수동 쿼리로 해결됩니다. 바로 코드로 이동해 보겠습니다.

class ParseData:
    """
    解析数据的对象
    """

    def __init__(self, file_path: pl.Path = None):
        """
        初始化对象
        :param file_path: TXT数据存放的路径,默认自身目录下的tmp文件夹
        """
        self.file_path = pl.Path('./tmp') if not file_path else file_path
        self.files = list(self.file_path.glob('2*.txt'))
        self.cols_dict = None
        self.colname_dict = {
            'D0': 'License Type',
            'D1': 'License Number',
            'D2': 'Manufacturer Full Name',
            'D3': 'Manufacturer ID',
            'D4': 'City',
            'D5': 'State',
            'D6': 'Full Name',
            'D7': 'Payment Category',
            'D8': 'Covered Recipient ID'
        }
        self.colname_dict_T = {v: k for k, v in self.colname_dict.items()}

    def make_excels(self):
        """
        将每个数据文件单独转换为excel数据表用于分析每份数据
        :return:
        """
        for file in self.files:
            with open(file, 'r') as fin:
                res = json.loads(fin.read())
            dfx = pd.DataFrame(res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'])
            dfx['filename'] = file.stem
            dfx[['year', 'part']] = dfx['filename'].str.split('_', expand=True)
            dfx['C_count'] = dfx['C'].map(len)
            writer = pd.ExcelWriter(self.file_path / f'{file.stem}.xlsx')
            dfx.to_excel(writer, sheet_name='data')
            for k, v in res['results'][0]['result']['data']['dsr']['DS'][0]['ValueDicts'].items():
                dfx = pd.Series(v).to_frame()
                dfx.to_excel(writer, sheet_name=k)
            writer.save()
        print('所有数据均已转为Excel')

    def make_single_excel(self):
        """
        将所有数据生成一份excel文件,不包含字典
        :return:
        """
        # 合并成整个文件
        df = pd.DataFrame()
        for file in self.files:
            with open(file, 'r') as fin:
                res = json.loads(fin.read())
            dfx = pd.DataFrame(res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'])
            dfx['filename'] = file.stem
            dfx[['year', 'part']] = dfx['filename'].str.split('_', expand=True)
            dfx['C_count'] = dfx['C'].map(len)
            df = pd.concat([df, dfx])
        return df

    def get_cols_dict(self):
        """
        读取列关系的字典
        :return:
        """
        # 读取列字典表
        self.cols_dict = pd.read_excel(self.file_path.parent / 'cols_dict.xlsx')
        self.cols_dict.set_index('R', inplace=True)
        self.cols_dict = self.cols_dict.dropna()
        self.cols_dict.drop(columns=['C_count', ], inplace=True)
        self.cols_dict.columns = [col.split(':')[-1] for col in self.cols_dict.columns]
        self.cols_dict = self.cols_dict.astype('int')

    def make_dataframe(self, filename):
        """
        读取TXT文件,转换成dataframe
        :param filename: 需要转换的文件
        :return: 
        """
        with open(filename, 'r') as fin:
            res = json.loads(fin.read())
        df0 = pd.DataFrame(res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'])
        df0['R'] = df0['R'].fillna(0)
        df0['R'] = df0['R'].map(int)
        values_dict = res['results'][0]['result']['data']['dsr']['DS'][0]['ValueDicts']

        dfx = []
        for idx in df0.index:
            row_value = df0.loc[idx, 'C'].copy()
            cols = self.cols_dict.loc[int(df0.loc[idx, 'R'])].to_dict()
            row = {}
            for col in ['License Type', 'License Number', 'Manufacturer Full Name', 'Manufacturer ID', 'City', 'State',
                        'Full Name', 'Payment Category', 'Disclosure Year', 'Covered Recipient ID', 'Amount of Payment',
                        'Number of Events Reflected']:
                v = cols.get(col)
                if v:
                    value = row_value.pop(0)
                    if col in self.colname_dict.values():
                        if not isinstance(value, str):
                            value_list = values_dict.get(self.colname_dict_T.get(col), [])
                            value = value_list[value]
                    row[col] = value
                else:
                    row[col] = None
            row['R'] = int(df0.loc[idx, 'R'])
            dfx.append(row)

        dfx = pd.DataFrame(dfx)
        dfx = dfx.fillna(method='ffill')
        dfx[['Disclosure Year', 'Number of Events Reflected']] = dfx[
            ['Disclosure Year', 'Number of Events Reflected']].astype('int')
        dfx = dfx[['Covered Recipient ID', 'Full Name', 'License Type', 'License Number', 'Manufacturer ID',
                   'Manufacturer Full Name', 'City',
                   'State', 'Payment Category', 'Amount of Payment', 'Number of Events Reflected', 'Disclosure Year',
                   'R']]
        return dfx

    def parse_data(self, out_name: str = None):
        """
        解析合并数据
        :param out_name: 输出的文件名
        :return: 
        """
        df = pd.DataFrame()
        for n, f in enumerate(self.files):
            dfx = self.make_dataframe(f)
            df = pd.concat([df, dfx])
            print(f'完成第{n + 1}个文件,剩余{len(self.files) - n - 1}个,共{len(self.files)}个')
        df.drop(columns='R').to_csv(self.file_path / f'{out_name}.csv', index=False)
        return df
  • 옵션 II

  솔루션 2를 사용하여 데이터를 처리할 때 사후 데이터를 수행한 후 아직 해결해야 할 두 가지 세부 문제가 있음을 알 수 있습니다.
  첫째, 새로운 키워드 "Ø"가 반환 값에 나타납니다. null. 데이터를 순회한 결과 값이 3개 밖에 없음(60, 128, 2048)임을 알 수 있으므로 col_dict를 수동으로 작성합니다(자세한 내용은 코드 참조). \

class ParseDatav2:
    """
    解析数据的对象第二版,将按行爬取的的json文件,转换成dataframe,增量写入csv文件,
    因每次请求一行,首行数据不存在与上一行相同情形,因此,除个别本身无数据情况,绝大多数均为完整的12列数据,
    """
    def __init__(self):
        """
        初始化
        """
        # 初始化一行的dataframe,
        self.row = pd.DataFrame([
            'Covered Recipient ID', 'Full Name', 'License Type', 'License Number', 'Manufacturer ID',
            'Manufacturer Full Name', 'City', 'State', 'Payment Category', 'Amount of Payment',
            'Number of Events Reflected', 'Disclosure Year'
        ]).set_index(0)
        self.row[0] = None
        self.row = self.row.T
        self.row['idx'] = None
        # 根据 Ø 值的不同选择不同的列,目前仅三种不同的Ø值,注0为默认值,指包含所有列
        self.col_dict = {
            # 完整的12列
            0: ['License Type', 'License Number', 'Manufacturer Full Name', 'Manufacturer ID', 'City', 'State',
                'Full Name', 'Payment Category', 'Disclosure Year', 'Covered Recipient ID', 'Amount of Payment',
                'Number of Events Reflected'],
            # 有4列是空值,分别是 'Manufacturer Full Name', 'Manufacturer ID', 'City', 'State'
            60: ['License Type', 'License Number',
                 'Full Name', 'Payment Category', 'Disclosure Year', 'Covered Recipient ID', 'Amount of Payment',
                 'Number of Events Reflected'],
            # 有1列是空值,是 'Payment Category'
            128: ['License Type', 'License Number', 'Manufacturer Full Name', 'Manufacturer ID', 'City', 'State',
                  'Full Name', 'Disclosure Year', 'Covered Recipient ID', 'Amount of Payment',
                  'Number of Events Reflected'],
            # 有1列是空值,是 'Number of Events Reflected'
            2048: ['License Type', 'License Number', 'Manufacturer Full Name', 'Manufacturer ID', 'City', 'State',
                   'Full Name', 'Payment Category', 'Disclosure Year', 'Covered Recipient ID', 'Amount of Payment'],
        }
        # 列名转换字典
        self.colname_dict = {
            'License Type': 'D0',
            'License Number': 'D1',
            'Manufacturer Full Name': 'D2',
            'Manufacturer ID': 'D3',
            'City': 'D4',
            'State': 'D5',
            'Full Name': 'D6',
            'Payment Category': 'D7',
            'Covered Recipient ID': 'D8'
        }
        # 储存爬取的json文件的路径
        self.data_path = pl.Path('./tmp')
        # 获取json文件的迭代器
        self.files = self.data_path.glob('*.txt')
        # 初始化输出文件的名称及路径
        self.file_name = self.data_path.parent / 'data.csv'

    def create_csv(self):
        """
        先输出一个CSV文件头用于增量写入数据
        :return:
        """
        self.row.drop(0, axis=0).to_csv(self.file_name, index=False)

    def parse_data(self, filename: pl.Path):
        """
        读取按1行数据请求获取的json文件,一行数据
        :param filename: json文件的路径
        :return: None
        """
        row = self.row.copy()  # 复制一行dataframe用于后续修改
        res = PageSpider.read_json(filename)
        # 获取数据中的valuedicts
        valuedicts = res['results'][0]['result']['data']['dsr']['DS'][0]['ValueDicts']
        # 获取数据中每行的数据
        row_values = res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'][0]['C']
        # 获取数据中的'Ø'值(若有),该值代表输出的行中,存在空白部分,用于确定数据列
        cols = ic(self.col_dict.get(
            res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'][0].get('Ø', 0)
        ))
        # 遍历每行数据,修改row这个dataframe的值
        for col, value in zip(cols, row_values):
            ic(col, value)
            colname = self.colname_dict.get(col)  # colname转换,D0~D8
            if colname:  # 如果非空,则需要转换值
                value = valuedicts.get(self.colname_dict.get(col))[0]
            # 修改dataframe数据
            row.loc[0, col] = value
        # 写入索引值
        row['idx'] = int(filename.stem.split('_')[-1].replace('part', ''))
        return row

    def run(self):
        """
        运行写入程序
        """
        self.create_csv()
        for idx, filename in enumerate(self.files):
            row = self.parse_data(filename)
            row.to_csv(self.file_name, mode='a', header=None, index=False)
            print(f'第{idx + 1}个文件{filename.stem}写入表格成功')
        print('全部文件写入完成')

  둘째, 각 데이터 요청 행에 대해 nrows를 2로 설정해야 하며, 이 방법으로 데이터의 마지막 행을 얻을 수 없으므로 마지막으로 반환된 json 데이터에서 데이터의 마지막 행을 구문 분석해야 합니다. 자세한 내용은 LastRow 클래스)

class LastRow:
    """
    获取并写入最后一行数据的类
    由于每次请求一行数据的方式,存在缺陷,无法获取到最后一行数据,
    本方法是对最后一个能够获取的json(倒数第二行)进行解析,取得最后一行数据,
    本方法存在缺陷,即默认最后一行“Amount of Payment”列值一定与倒数第二行不同,
    目前2009年至2020年共12年的数据中,均满足上述条件,没有出错。
    除本方法外,还可以通过逆转排序请求的方式,获取最后一行数据
    """
    def __init__(self):
        """
        初始化
        """
        self.file_path = pl.Path('./tmp')  # 存储爬取json数据的路径
        self.files_df = pd.DataFrame()  # 初始化最后一份请求的dataframe
        # 列名对应的字典
        self.colname_dict = {
            'D0': 'License Type',
            'D1': 'License Number',
            'D2': 'Manufacturer Full Name',
            'D3': 'Manufacturer ID',
            'D4': 'City',
            'D5': 'State',
            'D6': 'Full Name',
            'D7': 'Payment Category',
            'year': 'Disclosure Year',
            'D8': 'Covered Recipient ID',
            'M0': 'Amount of Payment',
            'M1': 'Number of Events Reflected'
        }  
        self.data = pd.DataFrame()  # 初始化最后一行数据data

    def get_last_file(self):
        """
        遍历文件夹,取得最后一份请求的dataframe
        """
        self.files_df = pd.DataFrame(list(self.file_path.glob('*.txt')), columns=['filename'])
        self.files_df[['year', 'idx']] = self.files_df['filename'].map(lambda x: x.stem).str.split('_', expand=True)
        self.files_df['idx'] = self.files_df['idx'].str.replace('part', '')
        self.files_df['idx'] = self.files_df['idx'].astype(int)
        self.files_df.sort_values(by=['year', 'idx'], inplace=True)
        self.files_df = self.files_df.drop_duplicates('year', keep='last')

    def get_last_row(self, ser: pd.Series) -> pd.DataFrame:
        """
        解析文件,获取最后一行的数据
        :param ser: 一行文件信息的series
        """
        # 读取json数据
        res = PageSpider.read_json(ser['filename'])
        # 获取values_dict
        values_dict = res['results'][0]['result']['data']['dsr']['DS'][0]['ValueDicts']
        # 获取文件中的第一行数据
        row_values = res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'][0]['C']
        # 获取文件中的下一行数据,因文件是倒数第二行的数据,因此下一行即为最后一行
        next_row_values = res['results'][0]['result']['data']['dsr']['DS'][0]['PH'][0]['DM0'][1]['C']
        # 初始化Series
        row = pd.Series()
        # 解析数据填充series
        for k, col in self.colname_dict.items():
            value = row_values.pop(0)
            if k.startswith('D'):  # 如果K值是D开头
                values = values_dict[k]
                if len(values) == 2:
                    value = next_row_values.pop(0)
                value = values[-1]
            elif k == 'year':
                pass
            else:
                if next_row_values:
                   value = next_row_values.pop(0)
            row[col] = value
        row['idx'] = ser['idx'] + 1
        row = row.to_frame().T
        return row

    def run(self):
        """
        运行获取最后一行数据的方法
        """
        self.get_last_file()
        for i in self.files_df.index:
            self.data = pd.concat([self.data, self.get_last_row(self.files_df.loc[i])])
        self.data = self.data[[
            'Covered Recipient ID', 'Full Name', 'License Type', 'License Number', 'Manufacturer ID',
            'Manufacturer Full Name', 'City', 'State', 'Payment Category', 'Amount of Payment',
            'Number of Events Reflected', 'Disclosure Year', 'idx'
        ]]
        filename = self.file_path.parent / 'data.csv'
        self.data.to_csv(filename, mode='a', index=False, header=None)
        return self.data
  1. 결과 표시

2010년

2015년

2020년 말

  1. 확장된 사고

  위의 체계 1을 체계 2와 결합하면 다른 R 관계의 모든 행 샘플을 정렬하고 체계 2를 사용하여 소수의 부분 예제를 크롤링한 다음 완전한 R 관계 사전을 파생한 다음 체계 1을 사용하여 크롤링하고 parse 하면 많은 시간을 절약할 수 있습니다. 이 방법은 데이터의 양이 현재의 양을 훨씬 초과할 때 고려할 수 있습니다.


요약하다

  전체 프로젝트를 완료하는 과정에서 나는 어둡고 쿨(1시간 미만의 크롤러의 일부 기능 완료) -> 혼란(JS는 역으로 실패, R 사이의 관계를 요약할 수 없음) -> 불안과 짜증(작업을 완료하지 못하는 것에 대한 걱정, 5시간 이상 수동으로 규칙 쿼리) -> Kaiqiao(검토 과정에서 갑자기 새로운 아이디어 발견) 일련의 프로세스. 최종 결과는 전체 작업을 비교적 순조롭게 완료했으며 가장 큰 느낌은 아이디어의 개발이었습니다. 한 도로가 실패하면 다른 방향으로 문제를 해결할 수 있습니다(참고: 처음부터 count 매개변수 500을 사용했으며, 하지만 점점 늘어나는 요청, 행의 수, 요청한 행의 수를 줄이는 등의 작은 변화가 큰 돌파구를 가져올 것이라고는 생각하지 못했다.)

                        나는 당신의 관심을 기대하는 Zheng Yin입니다

     

추천

출처blog.csdn.net/m0_69043821/article/details/125827925