Exemple #1
0
class Ggzy_neimenggu(scrapy.Spider):
    name = 'ggzy_neimenggu_spider'  # 需要修改成全局唯一的名字
    allowed_domains = ['ggzyjy.nmg.gov.cn']  # 需要修改成爬取网站的域名

    def __init__(self, *args, **kwargs):
        super(Ggzy_neimenggu, self).__init__(*args, **kwargs)

        self.num = 0
        self.site = self.allowed_domains[0]
        self.crawl_mode = CrawlMode.HISTORY

        # 用来判断数据是否已经入库的mongo表对象
        self.mongo_client = None
        self.mongo_col_obj = None

        self.crawl_helper = None

        # 日志相关
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s  %(filename)s : %(levelname)s  %(message)s')

        # Define a RotatingFileHandler
        rf_handler = logging.handlers.RotatingFileHandler(
            filename='./{}.log'.format(self.name),
            mode='a',
            maxBytes=10 * 1024 * 1024,
            backupCount=20,
        )
        formatter = logging.Formatter('%(asctime)s  %(filename)s:%(lineno)s : %(levelname)s  %(message)s')
        rf_handler.setFormatter(formatter)
        rf_handler.setLevel(logging.INFO)

        # Create an root instance
        logging.getLogger().addHandler(rf_handler)

    def init_crawl_helper(self):
        try:
            if 'spider_name' in self.__dict__:
                spider_name = self.spider_name
            else:
                spider_name = self.name

            _settings = self.settings
            self.mongo_client = pymongo.MongoClient(_settings.get('MONGDB_URI'))
            self.crawl_helper = JyCrawlHelper(
                spider_name=spider_name,
                mongo_client=self.mongo_client,
                db_name=_settings.get('MONGDB_DB_NAME'),
                col_name=_settings.get('MONGDB_COLLECTION'))
        except Exception as e:
            logging.exception('Get get_crawl_helper failed')
            self.crawl_helper = None

    def start_requests(self):
        """
        爬虫默认接口,启动方法
        :return:
        """
        # 获取爬取时传过来的参数
        # start_time: 开始时间
        # end_time: 结束时间
        # start_page: 开始页 (优先于start_time)
        # end_page: 结束页 (优先于end_time)
        # stop_item: 连续遇到[stop_item]个重复条目后,退出本次爬取
        # spider_name: 指定的spider_name,如果不指定,使用self.name
        # command example:
        # nohup python3 -m scrapy crawl ggzy_neimenggu_spider -a start_time="2019:01:01" -a end_time="2020:02:25" > /dev/null&
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now" -a start_page="700" -a end_page="1000" -a stop_item="10000"
        assert self.start_time is not None
        assert self.end_time is not None

        self.crawl_mode = CrawlMode.REAL_TIME if str(self.start_time).lower() == 'now' else CrawlMode.HISTORY

        if self.crawl_mode == CrawlMode.HISTORY:
            if (len(self.start_time) != 10 or len(self.end_time) != 10
                    or self.start_time[4] != ':' or self.end_time[4] != ':'):
                logging.error('Bad date format start_time:[{}] end_time:[{}]. Example: 2019:01:01'.format(
                    self.start_time, self.end_time))
                return
        else:
            # 取当天日期
            _dt = datetime.fromtimestamp(time.time())
            self.start_time = _dt.strftime("%Y:%m:%d")
            self.end_time = self.start_time

        # 初始化self.crawl_helper
        self.init_crawl_helper()

        # 主要配置项
        _source_info = {
            # 页面的key,保证唯一
            'page_1': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '招标公告与资格预审公告',
                'notice_type_code': '0101',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'jsgcZbgg',
                'scrollValue': '931',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_2': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '变更/补遗公告',
                'notice_type_code': '0105',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'jsgcGzsx',
                'scrollValue': '886',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_4': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '中标候选人公示',
                'notice_type_code': '0104',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'jsgcZbhxrgs',
                'scrollValue': '871',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_5': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '交易结果公示',
                'notice_type_code': '0104',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'jsgcZbjggs',
                'scrollValue': '871',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_6': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '政府采购',
                'tos_code': '02',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '采购/资格预审公告',
                'notice_type_code': '0201',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'zfcg/cggg',
                'scrollValue': '807',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_7': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '政府采购',
                'tos_code': '02',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '更正公告',
                'notice_type_code': '0204',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'zfcg/gzsx',
                'scrollValue': '871',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_8': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '政府采购',
                'tos_code': '02',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '中标公告',
                'notice_type_code': '0202',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'zfcg/zbjggs',
                'scrollValue': '871',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_9': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '土地矿产',
                'tos_code': '03',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '出让公告',
                'notice_type_code': '0301',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'tdAndKq/toCrggPage',
                'scrollValue': '871',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_10': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '土地矿产',
                'tos_code': '03',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '成交宗地/出让结果公示',
                'notice_type_code': '0302',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'tdAndKq/toCjqrPage',
                'scrollValue': '871',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_11': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '国有产权',
                'tos_code': '05',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '挂牌披露',
                'notice_type_code': '0501',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': 'sw',
                'bid_type': 'cqjy/crgg',
                'scrollValue': '892',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_12': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '国有产权',
                'tos_code': '05',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '交易结果',
                'notice_type_code': '0502',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': 'sw',
                'bid_type': 'cqjy/cjqr',
                'scrollValue': '892',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_13': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '其他',
                'tos_code': '90',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '交易公告',
                'notice_type_code': '9001',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'qtjy/jygg',
                'scrollValue': '',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_14': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '内蒙古自治区公共资源交易网',

                # list页面的base地址
                'base_url': 'http://ggzyjy.nmg.gov.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 3000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//div[@style="min-height: 900px; width: 938px"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[3]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '其他',
                'tos_code': '90',
                'source': '内蒙古自治区公共资源交易网',
                'notice_type': '交易结果',
                'notice_type_code': '9002',
                'site_name': '内蒙古自治区公共资源交易网',
                'area_code': '15',
                'content_code': '1',
                'industryName': '',
                'projectType': '',
                'bid_type': 'qtjy/jyqr',
                'scrollValue': '',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
        }

        logging.info('start crawling...')

        # 轮询每个类别
        for _k, _v in _source_info.items():

            # 填充爬取的基本信息
            self.crawl_helper.init_crawl_info(_k, _v)

            # 假定每个类别有不超过100000个页面
            for _page_num in range(100000):
                # 轮询公告中的不同list页面
                if self.crawl_helper.get_stop_flag(_k):
                    break

                # 根据获得下一页的函数,得到下一页的URL
                _param = {
                    'bid_type': _v['bid_type']
                }
                _request_url = _v['get_next_page_url'](page_index=_page_num + 1, base_url=_v['base_url'], _param=_param)
                # _request = ""
                # 生成request
                if _v["method"] == "post":
                    _payload = {
                        'currentPage': str(_page_num + 1),
                        'industriesTypeCode': '000',
                        'time': '',
                        'scrollValue': _v['scrollValue'],
                        'projectType': _v['projectType'],
                        'bulletinName': '',
                        'area': '',
                        'startTime': '',
                        'endTime': ''
                    }
                    _request = scrapy.FormRequest(url=_request_url, formdata=_payload, callback=_v['callback'])
                else:
                    _request = scrapy.Request(_request_url, callback=_v['callback'])

                # 如果需要js渲染,需要使用下面的函数
                # _request = SplashRequest(_request_url, callback=_v['callback'], args={'wait': 2})

                # 填充必要的参数
                _request.meta['param'] = _v
                _request.meta['crawl_key'] = _k
                _request.meta['page_index'] = _page_num + 1

                yield _request

            # 单个类别的爬取结束
            self.crawl_helper.stop_crawl_info(_k)

        logging.info('stop crawling...')

    def closed(self, reason):
        if self.mongo_client:
            self.mongo_client.close()

        self.crawl_helper.store_crawl_info_2_db(key=None, status='stopped', comment=reason)
        logging.info('Spider[{}] closed, reason:[{}]'.format(self.name, reason))

    @staticmethod
    def get_normal_next_page_url(page_index, base_url, _param):
        return '{}{}'.format(base_url, _param['bid_type'])

    def parse_list_page_common(self, response):
        """
        通用版list页面解析
        必要条件:
        :param response:
        :return:
        """

        assert 'crawl_key' in response.meta
        assert 'page_index' in response.meta
        assert 'param' in response.meta
        assert 'xpath_of_list' in response.meta['param']
        assert 'xpath_of_detail_url' in response.meta['param']
        assert 'item_parse_class' in response.meta['param']

        list_page_content_md5 = hashlib.md5(response.body).hexdigest()
        logging.info('Get page list url, page:[{}], url:[{}], status:[{}], body md5:[{}]'.format(
            response.meta['page_index'],
            response.url,
            response.status,
            list_page_content_md5))

        logging.info('Crawl info: {}'.format(self.crawl_helper.crawl_info))

        crawl_key = response.meta['crawl_key']

        # 更新状态表记录
        self.crawl_helper.store_crawl_info_2_db(crawl_key, 'active')

        if not self.crawl_helper.should_continue_page_parse(response, crawl_key, list_page_content_md5):
            return

        _item_idx = 0
        if response.meta['param']['requests_type'] == "dict":
            _request = response.text.encode('utf-8')
            _response_data = json.loads(response.text)
            # _dict_xpath = response.meta['param']['xpath_of_list'].split("/")
            # if len(_dict_xpath) > 1:
            for _dictn_num in response.meta['param']["xpath_of_list"]:
                _response_data = _response_data[_dictn_num]
            for selector in _response_data:
                _detail_url = ''
                try:
                    _item_idx += 1
                    # _detail_url = response.urljoin(
                    #     selector.xpath(response.meta['param']['xpath_of_detail_url']).extract_first())
                    _detail_url = response.urljoin(
                        selector[response.meta['param']['xpath_of_detail_url']]
                    )
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param']
                    )

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging.info('item is: {}'.format(item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
        else:
            for selector in response.xpath(response.meta['param']['xpath_of_list']):
                # print(selector.xpath('string()').extract_first())
                _detail_url = ''
                try:
                    _item_idx += 1
                    _detail_url = "http://ggzyjy.nmg.gov.cn" + selector.xpath(
                        response.meta['param']['xpath_of_detail_url']).extract_first()
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param']
                    )

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item['content'] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
class BdhzbSpider(scrapy.Spider):
    name = 'bdhzb_spider'  # 需要修改成全局唯一的名字
    allowed_domains = ['bdhzb.cn']  # 需要修改成爬取网站的域名

    def __init__(self, *args, **kwargs):
        super(BdhzbSpider, self).__init__(*args, **kwargs)

        self.num = 0
        self.site = self.allowed_domains[0]
        self.crawl_mode = CrawlMode.HISTORY

        # 用来判断数据是否已经入库的mongo表对象
        self.mongo_client = None
        self.mongo_col_obj = None

        self.crawl_helper = None

        # 日志相关
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s  %(filename)s : %(levelname)s  %(message)s')

        # Define a RotatingFileHandler
        rf_handler = logging.handlers.RotatingFileHandler(
            filename='./{}.log'.format(self.name),
            mode='a',
            maxBytes=10 * 1024 * 1024,
            backupCount=20,
        )
        formatter = logging.Formatter('%(asctime)s  %(filename)s:%(lineno)s : %(levelname)s  %(message)s')
        rf_handler.setFormatter(formatter)
        rf_handler.setLevel(logging.INFO)

        # Create an root instance
        logging.getLogger().addHandler(rf_handler)

    def init_crawl_helper(self):
        try:
            if 'spider_name' in self.__dict__:
                spider_name = self.spider_name
            else:
                spider_name = self.name

            _settings = self.settings
            self.mongo_client = pymongo.MongoClient(_settings.get('MONGDB_URI'))
            self.crawl_helper = JyCrawlHelper(
                spider_name=spider_name,
                mongo_client=self.mongo_client,
                db_name=_settings.get('MONGDB_DB_NAME'),
                col_name=_settings.get('MONGDB_COLLECTION'))
        except Exception as e:
            logging.exception('Get get_crawl_helper failed')
            self.crawl_helper = None

    def start_requests(self):
        """
        爬虫默认接口,启动方法
        :return:
        """
        # 获取爬取时传过来的参数
        # start_time: 开始时间
        # end_time: 结束时间
        # start_page: 开始页 (优先于start_time)
        # end_page: 结束页 (优先于end_time)
        # stop_item: 连续遇到[stop_item]个重复条目后,退出本次爬取
        # spider_name: 指定的spider_name,如果不指定,使用self.name
        # command example:
        # python3 -m scrapy crawl bdhzb_spider -a start_time="2019:01:01" -a end_time="2020:03:02"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now" -a start_page="700" -a end_page="1000" -a stop_item="10000"
        assert self.start_time is not None
        assert self.end_time is not None

        self.crawl_mode = CrawlMode.REAL_TIME if str(self.start_time).lower() == 'now' else CrawlMode.HISTORY

        if self.crawl_mode == CrawlMode.HISTORY:
            if (len(self.start_time) != 10 or len(self.end_time) != 10
                    or self.start_time[4] != ':' or self.end_time[4] != ':'):
                logging.error('Bad date format start_time:[{}] end_time:[{}]. Example: 2019:01:01'.format(
                    self.start_time, self.end_time))
                return
        else:
            # 取当天日期
            _dt = datetime.fromtimestamp(time.time())
            self.start_time = _dt.strftime("%Y:%m:%d")
            self.end_time = self.start_time

        # 初始化self.crawl_helper
        self.init_crawl_helper()

        # 主要配置项
        _source_info = {
            # 页面的key,保证唯一
            'gkzb_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '公开招标',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '公开招标',
                'source': '北大荒招标',
                'bid_sort': '003001',
                'industry': '',
                'tos_code': '01',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },

            'zfcg_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '政府采购',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '政府采购',
                'source': '北大荒招标',
                'bid_sort': '003007',
                'industry': '',
                'tos_code': '02',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,

            },

            'fgkzb_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '非公开招标',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'source': '北大荒招标',
                'bid_sort': '003002',
                'industry': '非公开招标',
                'tos_code': '01',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,

            },

            'cqjy_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '产权交易',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'source': '北大荒招标',
                'bid_sort': '003002',
                'industry': '产权交易',
                'tos_code': '01',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,

            },

            'tdjy_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '土地交易',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'source': '北大荒招标',
                'bid_sort': '003004',
                'industry': '土地交易',
                'tos_code': '01',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,

            },

            'slgc_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '水利工程',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'source': '北大荒招标',
                'bid_sort': '003005',
                'industry': '水利工程',
                'tos_code': '01',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,

            },

            'jtgc_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '交通工程',

                # list页面的base地址
                'base_url': 'http://bdhzb.cn/jyxx/',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 7000000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//ul[@id="wb-data-item"]/li',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'source': '北大荒招标',
                'bid_sort': '003006',
                'industry': '交通工程',
                'tos_code': '01',
                'content_code': 1,
                'site_name': '北大荒电子招标网',
                'area_code': '670000',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,

            },

        }

        logging.info('start crawling...')

        # 轮询每个类别
        for _k, _v in _source_info.items():

            # 仅爬取指定类型
            if 'type' in self.__dict__ and self.type != _k:
                continue

            # 填充爬取的基本信息
            self.crawl_helper.init_crawl_info(_k, _v)

            # 假定每个类别有不超过100000个页面
            for _page_num in range(1000000):
                # 如果有start_page项,忽略之前的页面
                if 'start_page' in self.__dict__ and _page_num < int(self.start_page) - 1:
                    continue

                # 如果有end_page项
                if 'end_page' in self.__dict__ and _page_num > int(self.end_page) + 1:
                    break

                # 轮询公告中的不同list页面
                if self.crawl_helper.get_stop_flag(_k):
                    break

                # 根据获得下一页的函数,得到下一页的URL
                _ext_param = {
                    'time_type': _v['time_type'],
                    'bid_sort': _v['bid_sort'],
                    'start_time': self.start_time,
                    'end_time': self.end_time,
                }
                _request_url = _v['get_next_page_url'](page_index=_page_num, base_url=_v['base_url'],
                                                       ext_param=_ext_param)

                # 生成request
                _request = scrapy.Request(_request_url, callback=_v['callback'])
                # 使用proxy
                # _request.meta['proxy'] = JyScrapyUtil.get_http_proxy()
                # _request.meta['max_retry_times'] = 5
                # logging.info('Use proxy[] to generate scrapy request'.format(_request.meta['proxy']))
                # 如果需要js渲染,需要使用下面的函数
                # _request = SplashRequest(_request_url, callback=_v['callback'], args={'wait': 2})

                # 填充必要的参数
                _request.meta['param'] = _v
                _request.meta['crawl_key'] = _k
                _request.meta['page_index'] = _page_num + 1

                yield _request

            # 单个类别的爬取结束
            self.crawl_helper.stop_crawl_info(_k)

        logging.info('stop crawling...')

    def closed(self, reason):
        if self.mongo_client:
            self.mongo_client.close()

        self.crawl_helper.store_crawl_info_2_db(key=None, status='stopped', comment=reason)
        logging.info('Spider[{}] closed, reason:[{}]'.format(self.name, reason))

    @staticmethod
    def get_normal_next_page_url(page_index, base_url, ext_param):

        if page_index == 0:
            return '{}{}/secondPage.html'.format(base_url, ext_param['bid_sort'])
        else:
            return '{}{}/{}.html'.format(base_url, ext_param['bid_sort'], page_index + 1)

    def parse_list_page_common(self, response):
        """
        通用版list页面解析
        必要条件:
        :param response:
        :return:
        """

        assert 'crawl_key' in response.meta
        assert 'page_index' in response.meta
        assert 'param' in response.meta
        assert 'xpath_of_list' in response.meta['param']
        assert 'xpath_of_detail_url' in response.meta['param']
        assert 'item_parse_class' in response.meta['param']

        list_page_content_md5 = hashlib.md5(response.body).hexdigest()
        logging.info('Get page list url, page:[{}], url:[{}], status:[{}], body md5:[{}]'.format(
            response.meta['page_index'],
            response.url,
            response.status,
            list_page_content_md5))

        logging.info('Crawl info: {}'.format(self.crawl_helper.crawl_info))

        crawl_key = response.meta['crawl_key']

        # 更新状态表记录
        self.crawl_helper.store_crawl_info_2_db(crawl_key, 'active')

        if not self.crawl_helper.should_continue_page_parse(response, crawl_key, list_page_content_md5):
            return

        _item_idx = 0
        for selector in response.xpath(response.meta['param']['xpath_of_list']):
            _detail_url = ''
            try:
                _item_idx += 1
                _detail_url = response.urljoin(
                    selector.xpath(response.meta['param']['xpath_of_detail_url']).extract_first())
                _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                # 检查记录是否已在库中,并做相应的跳出动作
                loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                if loop_break:
                    return
                if item_break:
                    continue

                # 生成并返回爬取item
                item_parser = response.meta['param']['item_parse_class'](selector)
                item = item_parser.get_common_raw_item(
                    _id=_unq_id,
                    detail_url=_detail_url,
                    site=self.site,
                    ext_param=response.meta['param']
                )

                # 随机休眠
                time.sleep(random.randint(50, 100) / 1000.0)

                # 更新数据库中爬取数量
                self.crawl_helper.increase_total_item_num(crawl_key)

                logging_item = item.copy()
                logging_item["content"] = ""
                logging.info('item is: {}'.format(logging_item))
                yield item

            except Exception as e:
                logging.exception('Handle [{}] failed'.format(_detail_url))
Exemple #3
0
class Jy_guangdongSpider(scrapy.Spider):
    name = 'jy_guangdong_spider'  # 需要修改成全局唯一的名字
    allowed_domains = ['zbtb.gd.gov.cn']  # 需要修改成爬取网站的域名

    def __init__(self, *args, **kwargs):
        super(Jy_guangdongSpider, self).__init__(*args, **kwargs)

        self.num = 0
        self.site = self.allowed_domains[0]
        self.crawl_mode = CrawlMode.HISTORY

        # 用来判断数据是否已经入库的mongo表对象
        self.mongo_client = None
        self.mongo_col_obj = None

        self.crawl_helper = None

        # 日志相关
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s  %(filename)s : %(levelname)s  %(message)s')

        # Define a RotatingFileHandler
        rf_handler = logging.handlers.RotatingFileHandler(
            filename='./{}.log'.format(self.name),
            mode='a',
            maxBytes=10 * 1024 * 1024,
            backupCount=20,
        )
        formatter = logging.Formatter('%(asctime)s  %(filename)s:%(lineno)s : %(levelname)s  %(message)s')
        rf_handler.setFormatter(formatter)
        rf_handler.setLevel(logging.INFO)

        # Create an root instance
        logging.getLogger().addHandler(rf_handler)

    def init_crawl_helper(self):
        try:
            if 'spider_name' in self.__dict__:
                spider_name = self.spider_name
            else:
                spider_name = self.name

            _settings = self.settings
            self.mongo_client = pymongo.MongoClient(_settings.get('MONGDB_URI'))
            self.crawl_helper = JyCrawlHelper(
                spider_name=spider_name,
                mongo_client=self.mongo_client,
                db_name=_settings.get('MONGDB_DB_NAME'),
                col_name=_settings.get('MONGDB_COLLECTION'))
        except Exception as e:
            logging.exception('Get get_crawl_helper failed')
            self.crawl_helper = None

    def start_requests(self):
        """
        爬虫默认接口,启动方法
        :return:
        """
        # 获取爬取时传过来的参数
        # start_time: 开始时间
        # end_time: 结束时间
        # start_page: 开始页 (优先于start_time)
        # end_page: 结束页 (优先于end_time)
        # stop_item: 连续遇到[stop_item]个重复条目后,退出本次爬取
        # spider_name: 指定的spider_name,如果不指定,使用self.name
        # command example:
        # python3 -m scrapy crawl jy_guangdong_spider -a start_time="2019:01:01" -a end_time="2020:03:26"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now" -a start_page="700" -a end_page="1000" -a stop_item="10000"
        assert self.start_time is not None
        assert self.end_time is not None

        self.crawl_mode = CrawlMode.REAL_TIME if str(self.start_time).lower() == 'now' else CrawlMode.HISTORY

        if self.crawl_mode == CrawlMode.HISTORY:
            if (len(self.start_time) != 10 or len(self.end_time) != 10
                    or self.start_time[4] != ':' or self.end_time[4] != ':'):
                logging.error('Bad date format start_time:[{}] end_time:[{}]. Example: 2019:01:01'.format(
                    self.start_time, self.end_time))
                return
        else:
            # 取当天日期
            _dt = datetime.fromtimestamp(time.time())
            self.start_time = _dt.strftime("%Y:%m:%d")
            self.end_time = self.start_time

        # 初始化self.crawl_helper
        self.init_crawl_helper()

        # 主要配置项
        _source_info = {
            # 页面的key,保证唯一
            'page_1': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZgysgg',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "资格预审公告",
                'notice_type_code': "0101",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zgysgg',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_2': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZgyswj',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "资格预审文件",
                'notice_type_code': "0104",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zgyswj',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_3': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZgyssqwj',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "资格预审申请报告",
                'notice_type_code': "0101",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zgyssqwj',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_4': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZgscbg',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "资格预审报告",
                'notice_type_code': "0101",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zgscbg',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_5': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZbgg',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "招标公告",
                'notice_type_code': "0101",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zbgg',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_6': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZbwj',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "招标文件",
                'notice_type_code': "0101",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zbwj',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },

            'page_7': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listTbwj',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "投标文件",
                'notice_type_code': "0101",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'tbwj',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_8': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listPbbg',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "评标报告",
                'notice_type_code': "0104",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'pbbg',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_9': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZbhxrgs',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "中标候选人公示",
                'notice_type_code': "0104",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zbhxrgs',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_10': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '广东省招标投标监管网',

                # list页面的base地址
                'base_url': 'http://zbtb.gd.gov.cn/bid/listZbjg',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 700,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['data'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': 'id',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '广东省招标投标监管网',
                'site_name': '广东省招标投标监管网',
                'notice_type': "中标结果",
                'notice_type_code': "0104",
                'area_code': '670000',
                'content_code': '1',
                'industryName': '',
                'type': 'zbjg',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },

        }

        logging.info('start crawling...')

        # 轮询每个类别
        for _k, _v in _source_info.items():

            # 填充爬取的基本信息
            self.crawl_helper.init_crawl_info(_k, _v)

            # 假定每个类别有不超过100000个页面
            for _page_num in range(100000):
                # 轮询公告中的不同list页面
                if self.crawl_helper.get_stop_flag(_k):
                    break

                # 根据获得下一页的函数,得到下一页的URL
                _request_url = _v['get_next_page_url'](page_index=_page_num, base_url=_v['base_url'])

                # 生成request
                if _v["method"] == "post":
                    payload = {
                        'draw': '4',
                        'columns[0][data]': 'id',
                        'columns[0][name]': '',
                        'columns[0][searchable]': 'true',
                        'columns[0][orderable]': 'false',
                        'columns[0][search][value]': '',
                        'columns[0][search][regex]': 'false',
                        'start': '60',
                        'length': '20',
                        'search[value]': '',
                        'search[regex]': 'false',
                        'page': '4',
                        'type': 'zgysgg',
                        'xmmc': '',
                        'rows': '20'
                    }
                    _request = scrapy.FormRequest(url=_request_url, formdata=payload, callback=_v['callback'])
                else:
                    _request = scrapy.Request(_request_url, callback=_v['callback'])

                # 如果需要js渲染,需要使用下面的函数
                # _request = SplashRequest(_request_url, callback=_v['callback'], args={'wait': 2})

                # 填充必要的参数
                _request.meta['param'] = _v
                _request.meta['crawl_key'] = _k
                _request.meta['page_index'] = _page_num + 1

                yield _request

            # 单个类别的爬取结束
            self.crawl_helper.stop_crawl_info(_k)

        logging.info('stop crawling...')

    def closed(self, reason):
        if self.mongo_client:
            self.mongo_client.close()

        self.crawl_helper.store_crawl_info_2_db(key=None, status='stopped', comment=reason)
        logging.info('Spider[{}] closed, reason:[{}]'.format(self.name, reason))

    @staticmethod
    def get_normal_next_page_url(page_index, base_url):
        return '{}'.format(base_url)

    def parse_list_page_common(self, response):
        """
        通用版list页面解析
        必要条件:
        :param response:
        :return:
        """

        assert 'crawl_key' in response.meta
        assert 'page_index' in response.meta
        assert 'param' in response.meta
        assert 'xpath_of_list' in response.meta['param']
        assert 'xpath_of_detail_url' in response.meta['param']
        assert 'item_parse_class' in response.meta['param']

        list_page_content_md5 = hashlib.md5(response.body).hexdigest()
        logging.info('Get page list url, page:[{}], url:[{}], status:[{}], body md5:[{}]'.format(
            response.meta['page_index'],
            response.url,
            response.status,
            list_page_content_md5))

        logging.info('Crawl info: {}'.format(self.crawl_helper.crawl_info))

        crawl_key = response.meta['crawl_key']

        # 更新状态表记录
        self.crawl_helper.store_crawl_info_2_db(crawl_key, 'active')

        if not self.crawl_helper.should_continue_page_parse(response, crawl_key, list_page_content_md5):
            return

        _item_idx = 0
        if response.meta['param']['requests_type'] == "dict":
            _request = response.text.encode('utf-8')
            _response_data = json.loads(response.text)
            for _dictn_num in response.meta['param']["xpath_of_list"]:
                _response_data = _response_data[_dictn_num]
            for selector in _response_data:
                _detail_url = ''
                try:
                    _item_idx += 1
                    _detail_url = 'http://zbtb.gd.gov.cn/bid/' + 'detailZgysgg?id='+str(selector["id"])
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param']
                    )

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item["content"] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
        else:
            for _selector_num, selector in enumerate(response.xpath(response.meta['param']['xpath_of_list'])):
                _detail_url = ''
                try:
                    _item_idx += 1
                    _url_id = selector.xpath(response.meta['param']['xpath_of_detail_url']).extract_first()
                    _url_id = _url_id.split("'")[1].replace('\\r\\n', '')
                    _detail_url = 'http://ec.ccccltd.cn/PMS/biddetail.shtml?id=' + str(_url_id)

                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param']
                    )

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item["content"] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item
                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
Exemple #4
0
class Jy_shanxi_Spider(scrapy.Spider):
    name = 'jy_shanxi_spider'  # 需要修改成全局唯一的名字
    allowed_domains = ['www.ccgp-guizhou.gov.cn']  # 需要修改成爬取网站的域名

    def __init__(self, *args, **kwargs):
        super(Jy_shanxi_Spider, self).__init__(*args, **kwargs)

        self.num = 0
        self.site = self.allowed_domains[0]
        self.crawl_mode = CrawlMode.HISTORY

        # 用来判断数据是否已经入库的mongo表对象
        self.mongo_client = None
        self.mongo_col_obj = None

        self.crawl_helper = None

        # 日志相关
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s  %(filename)s : %(levelname)s  %(message)s')

        # Define a RotatingFileHandler
        rf_handler = logging.handlers.RotatingFileHandler(
            filename='./{}.log'.format(self.name),
            mode='a',
            maxBytes=10 * 1024 * 1024,
            backupCount=20,
        )
        formatter = logging.Formatter('%(asctime)s  %(filename)s:%(lineno)s : %(levelname)s  %(message)s')
        rf_handler.setFormatter(formatter)
        rf_handler.setLevel(logging.INFO)

        # Create an root instance
        logging.getLogger().addHandler(rf_handler)

    def init_crawl_helper(self):
        try:
            if 'spider_name' in self.__dict__:
                spider_name = self.spider_name
            else:
                spider_name = self.name

            _settings = self.settings
            self.mongo_client = pymongo.MongoClient(_settings.get('MONGDB_URI'))
            self.crawl_helper = JyCrawlHelper(
                spider_name=spider_name,
                mongo_client=self.mongo_client,
                db_name=_settings.get('MONGDB_DB_NAME'),
                col_name=_settings.get('MONGDB_COLLECTION'))
        except Exception as e:
            logging.exception('Get get_crawl_helper failed')
            self.crawl_helper = None

    def start_requests(self):
        """
        爬虫默认接口,启动方法
        :return:
        """
        # 获取爬取时传过来的参数
        # start_time: 开始时间
        # end_time: 结束时间
        # start_page: 开始页 (优先于start_time)
        # end_page: 结束页 (优先于end_time)
        # stop_item: 连续遇到[stop_item]个重复条目后,退出本次爬取
        # spider_name: 指定的spider_name,如果不指定,使用self.name
        # command example:
        # nohup python3 -m scrapy crawl jy_shanxi_spider -a start_time="2019:01:01" -a end_time="2020:02:25" > /dev/null&
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now" -a start_page="700" -a end_page="1000" -a stop_item="10000"
        assert self.start_time is not None
        assert self.end_time is not None

        self.crawl_mode = CrawlMode.REAL_TIME if str(self.start_time).lower() == 'now' else CrawlMode.HISTORY

        if self.crawl_mode == CrawlMode.HISTORY:
            if (len(self.start_time) != 10 or len(self.end_time) != 10
                    or self.start_time[4] != ':' or self.end_time[4] != ':'):
                logging.error('Bad date format start_time:[{}] end_time:[{}]. Example: 2019:01:01'.format(
                    self.start_time, self.end_time))
                return
        else:
            # 取当天日期
            _dt = datetime.fromtimestamp(time.time())
            self.start_time = _dt.strftime("%Y:%m:%d")
            self.end_time = self.start_time

        # 初始化self.crawl_helper
        self.init_crawl_helper()

        # 主要配置项
        _source_info = {
            # 页面的key,保证唯一
            'page_1': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '山西省招标投标公共服务平台',

                # list页面的base地址
                'base_url': 'http://www.sxbid.com.cn/f/list-6796f0c147374f85a50199b38ecb0af6.html',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//table[@class="download_table"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[2]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '山西省招标投标公共服务平台',
                'notice_type': '招标公告',
                'notice_type_code': '0101',
                'site_name': '山西省招标投标公共服务平台',
                'area_code': '14',
                'content_code': '1',
                'industryName': '',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },

            'page_2': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '山西省招标投标公共服务平台',

                # list页面的base地址
                'base_url': 'http://www.sxbid.com.cn/f/list-54f5e594f4314654aadf09f7c9ae28bf.html',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//table[@class="download_table"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[2]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '山西省招标投标公共服务平台',
                'notice_type': '中标候选人公示',
                'notice_type_code': '0104',
                'site_name': '山西省招标投标公共服务平台',
                'area_code': '14',
                'content_code': '1',
                'industryName': '',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_3': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name': '山西省招标投标公共服务平台',

                # list页面的base地址
                'base_url': 'http://www.sxbid.com.cn/f/list-d4bfee46e6ed452d82588dc17207a34b.html',

                # list页面的call_back处理函数
                'callback': self.parse_list_page_common,

                'method': "post",

                'requests_type': "html",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url': self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num': 1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num': 500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': '//table[@class="download_table"]//tr',

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url': './td[2]/a/@href',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class': BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos': '工程建设',
                'tos_code': '01',
                'source': '山西省招标投标公共服务平台',
                'notice_type': '中标结果公示',
                'notice_type_code': '0104',
                'site_name': '山西省招标投标公共服务平台',
                'area_code': '14',
                'content_code': '1',
                'industryName': '',
                'time_type': 6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },

        }

        logging.info('start crawling...')

        # 轮询每个类别
        for _k, _v in _source_info.items():

            # 填充爬取的基本信息
            self.crawl_helper.init_crawl_info(_k, _v)

            # 假定每个类别有不超过100000个页面
            for _page_num in range(100000):
                # 轮询公告中的不同list页面
                if self.crawl_helper.get_stop_flag(_k):
                    break

                # 根据获得下一页的函数,得到下一页的URL
                _request_url = _v['get_next_page_url'](page_index=_page_num, base_url=_v['base_url'])
                # _request = ""
                # 生成request
                if _v["method"] == "post":
                    _payload = {
                        'form_random_token': '5616376850485434166',
                        'pageNo': str(_page_num + 1),
                        'pageSize': '15',
                        'accordToLaw': '1',
                        'resourceType': '1',
                        'title': '',
                        'publishTimeRange': '',
                    }
                    _request = scrapy.FormRequest(url=_request_url, formdata=_payload, callback=_v['callback'])
                else:
                    _request = scrapy.Request(_request_url, callback=_v['callback'])

                # 如果需要js渲染,需要使用下面的函数
                # _request = SplashRequest(_request_url, callback=_v['callback'], args={'wait': 2})

                # 填充必要的参数
                _request.meta['param'] = _v
                _request.meta['crawl_key'] = _k
                _request.meta['page_index'] = _page_num + 1

                yield _request

            # 单个类别的爬取结束
            self.crawl_helper.stop_crawl_info(_k)

        logging.info('stop crawling...')

    def closed(self, reason):
        if self.mongo_client:
            self.mongo_client.close()

        self.crawl_helper.store_crawl_info_2_db(key=None, status='stopped', comment=reason)
        logging.info('Spider[{}] closed, reason:[{}]'.format(self.name, reason))

    @staticmethod
    def get_normal_next_page_url(page_index, base_url):
        return '{}'.format(base_url)

    def parse_list_page_common(self, response):
        """
        通用版list页面解析
        必要条件:
        :param response:
        :return:
        """

        assert 'crawl_key' in response.meta
        assert 'page_index' in response.meta
        assert 'param' in response.meta
        assert 'xpath_of_list' in response.meta['param']
        assert 'xpath_of_detail_url' in response.meta['param']
        assert 'item_parse_class' in response.meta['param']

        list_page_content_md5 = hashlib.md5(response.body).hexdigest()
        logging.info('Get page list url, page:[{}], url:[{}], status:[{}], body md5:[{}]'.format(
            response.meta['page_index'],
            response.url,
            response.status,
            list_page_content_md5))

        logging.info('Crawl info: {}'.format(self.crawl_helper.crawl_info))

        crawl_key = response.meta['crawl_key']

        # 更新状态表记录
        self.crawl_helper.store_crawl_info_2_db(crawl_key, 'active')

        if not self.crawl_helper.should_continue_page_parse(response, crawl_key, list_page_content_md5):
            return

        _item_idx = 0
        if response.meta['param']['requests_type'] == "dict":
            _request = response.text.encode('utf-8')
            _response_data = json.loads(response.text)
            # _dict_xpath = response.meta['param']['xpath_of_list'].split("/")
            # if len(_dict_xpath) > 1:
            for _dictn_num in response.meta['param']["xpath_of_list"]:
                _response_data = _response_data[_dictn_num]
            for selector in _response_data:
                _detail_url = ''
                try:
                    _item_idx += 1
                    # _detail_url = response.urljoin(
                    #     selector.xpath(response.meta['param']['xpath_of_detail_url']).extract_first())
                    _detail_url = response.urljoin(
                        selector[response.meta['param']['xpath_of_detail_url']]
                    )
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param']
                    )

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item["content"] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
        else:
            for selector in response.xpath(response.meta['param']['xpath_of_list']):
                _detail_url = ''
                try:
                    _item_idx += 1
                    _detail_url = response.urljoin(
                        selector.xpath(response.meta['param']['xpath_of_detail_url']).extract_first())
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param']
                    )

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item["content"] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
Exemple #5
0
class Jy_jilinSpider(scrapy.Spider):
    name = 'jy_jilin_spider'  # 需要修改成全局唯一的名字
    allowed_domains = ['www.jl.gov.cn']  # 需要修改成爬取网站的域名

    def __init__(self, *args, **kwargs):
        super(Jy_jilinSpider, self).__init__(*args, **kwargs)

        self.num = 0
        self.site = self.allowed_domains[0]
        self.crawl_mode = CrawlMode.HISTORY

        # 用来判断数据是否已经入库的mongo表对象
        self.mongo_client = None
        self.mongo_col_obj = None

        self.crawl_helper = None

        # 日志相关
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s  %(filename)s : %(levelname)s  %(message)s')

        # Define a RotatingFileHandler
        rf_handler = logging.handlers.RotatingFileHandler(
            filename='./{}.log'.format(self.name),
            mode='a',
            maxBytes=10 * 1024 * 1024,
            backupCount=20,
        )
        formatter = logging.Formatter(
            '%(asctime)s  %(filename)s:%(lineno)s : %(levelname)s  %(message)s'
        )
        rf_handler.setFormatter(formatter)
        rf_handler.setLevel(logging.INFO)

        # Create an root instance
        logging.getLogger().addHandler(rf_handler)

    def init_crawl_helper(self):
        try:
            if 'spider_name' in self.__dict__:
                spider_name = self.spider_name
            else:
                spider_name = self.name

            _settings = self.settings
            self.mongo_client = pymongo.MongoClient(
                _settings.get('MONGDB_URI'))
            self.crawl_helper = JyCrawlHelper(
                spider_name=spider_name,
                mongo_client=self.mongo_client,
                db_name=_settings.get('MONGDB_DB_NAME'),
                col_name=_settings.get('MONGDB_COLLECTION'))
        except Exception as e:
            logging.exception('Get get_crawl_helper failed')
            self.crawl_helper = None

    def start_requests(self):
        """
        爬虫默认接口,启动方法
        :return:
        """
        # 获取爬取时传过来的参数
        # start_time: 开始时间
        # end_time: 结束时间
        # start_page: 开始页 (优先于start_time)
        # end_page: 结束页 (优先于end_time)
        # stop_item: 连续遇到[stop_item]个重复条目后,退出本次爬取
        # spider_name: 指定的spider_name,如果不指定,使用self.name
        # command example:
        #       python3 -m scrapy crawl jy_jilin_spider -a start_time="2019:01:01" -a end_time="2020:02:26"
        # nohup python3 -m scrapy crawl jy_jilin_spider -a start_time="2019:01:01" -a end_time="2020:02:25" > /dev/null&
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now" -a start_page="700" -a end_page="1000" -a stop_item="10000"
        assert self.start_time is not None
        assert self.end_time is not None

        self.crawl_mode = CrawlMode.REAL_TIME if str(
            self.start_time).lower() == 'now' else CrawlMode.HISTORY

        if self.crawl_mode == CrawlMode.HISTORY:
            if (len(self.start_time) != 10 or len(self.end_time) != 10
                    or self.start_time[4] != ':' or self.end_time[4] != ':'):
                logging.error(
                    'Bad date format start_time:[{}] end_time:[{}]. Example: 2019:01:01'
                    .format(self.start_time, self.end_time))
                return
        else:
            # 取当天日期
            _dt = datetime.fromtimestamp(time.time())
            self.start_time = _dt.strftime("%Y:%m:%d")
            self.end_time = self.start_time

        # 初始化self.crawl_helper
        self.init_crawl_helper()

        # 主要配置项
        _source_info = {
            # 页面的key,保证唯一
            'page_1': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '政府采购',
                'tos_code':
                '02',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "采购公告",
                'notice_type_code':
                '0201',
                'area_code':
                '32',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '工程建设',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='政府采购' and iType='采购公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_2': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '政府采购',
                'tos_code':
                '02',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "变更公告",
                'notice_type_code':
                '0204',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '工程建设',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='政府采购' and iType='变更公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_3': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '政府采购',
                'tos_code':
                '02',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "中标公告",
                'notice_type_code':
                '0202',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '工程建设',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='政府采购'  and iType='中标公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_4': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '政府采购',
                'tos_code':
                '02',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "合同公示",
                'notice_type_code':
                '0202',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '工程建设',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='政府采购'  and iType='合同公示'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_5': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '政府采购',
                'tos_code':
                '02',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "单一来源公示",
                'notice_type_code':
                '0201',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '工程建设',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='政府采购' and iType='单一来源论证公示'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_6': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '工程建设',
                'tos_code':
                '01',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "招标公告",
                'notice_type_code':
                '0101',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='工程建设'   and iType='招标公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_7': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '工程建设',
                'tos_code':
                '01',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "变更公告",
                'notice_type_code':
                '0104',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='工程建设'   and iType='变更公告工程'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_8': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '工程建设',
                'tos_code':
                '01',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "中标候选人公示",
                'notice_type_code':
                '0104',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='工程建设'   and iType='中标候选人公示'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_9': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '工程建设',
                'tos_code':
                '01',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "中标结果公示",
                'notice_type_code':
                '0104',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='工程建设'   and iType='中标结果公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_10': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '土地使用权',
                'tos_code':
                '03',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "出让公告",
                'notice_type_code':
                '0301',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='土地使用权'    and iType='出让公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_11': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '土地使用权',
                'tos_code':
                '03',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "成交宗地",
                'notice_type_code':
                '0302',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='土地使用权'    and iType='成交宗地'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_12': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '国有产权',
                'tos_code':
                '05',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "挂牌披露",
                'notice_type_code':
                '0501',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='国有产权'  and iType='挂牌披露'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_13': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '国有产权',
                'tos_code':
                '05',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "交易结果",
                'notice_type_code':
                '0502',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='国有产权'  and iType='交易结果'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_14': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '股权产权交易',
                'tos_code':
                '90',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "挂牌披露信息",
                'notice_type_code':
                '9001',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='股权类产权交易'  and iType='挂牌披露信息'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_15': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '股权产权交易',
                'tos_code':
                '90',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "交易结果信息",
                'notice_type_code':
                '9002',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='股权类产权交易'  and iType='交易结果信息'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
            'page_16': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '吉林省公共资源交易公共服务平台',

                # list页面的base地址
                'base_url':
                'http://was.jl.gov.cn/was5/web/search?',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,
                'method':
                "get",
                'requests_type':
                "dict",

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                1000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list': ['datas'],

                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                'docpuburl',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'tos':
                '药械采购',
                'tos_code':
                '90',
                'source':
                '吉林省公共资源交易公共服务平台',
                'notice_type':
                "交易结果信息",
                'notice_type_code':
                '9002',
                'area_code':
                '22',
                'content_code':
                '1',
                'site_name':
                '吉林省公共资源交易公共服务平台',
                'industry':
                '',
                'channelid':
                '237687',
                'searchword':
                "gtitle<>'' and gtitle<>'null' and tType='药械采购'  and iType='采购公告'",
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
            },
        }

        logging.info('start crawling...')

        # 轮询每个类别
        for _k, _v in _source_info.items():

            # 填充爬取的基本信息
            self.crawl_helper.init_crawl_info(_k, _v)

            # 假定每个类别有不超过100000个页面
            for _page_num in range(100000):
                # 轮询公告中的不同list页面
                if self.crawl_helper.get_stop_flag(_k):
                    break

                # 根据获得下一页的函数,得到下一页的URL
                _ext_param = {
                    'channelid': _v['channelid'],
                    'searchword': _v['searchword'],
                    'start_time': self.start_time,
                    'end_time': self.end_time,
                }
                _request_url = _v['get_next_page_url'](page_index=_page_num,
                                                       base_url=_v['base_url'],
                                                       ext_param=_ext_param)
                # _request = ""
                # 生成request
                if _v["method"] == "post":
                    payload = {}
                    _request = scrapy.FormRequest(url=_request_url,
                                                  body=payload,
                                                  callback=_v['callback'])
                else:
                    _request = scrapy.Request(_request_url,
                                              callback=_v['callback'])

                # 如果需要js渲染,需要使用下面的函数
                # _request = SplashRequest(_request_url, callback=_v['callback'], args={'wait': 2})

                # 填充必要的参数
                _request.meta['param'] = _v
                _request.meta['crawl_key'] = _k
                _request.meta['page_index'] = _page_num + 1

                yield _request

            # 单个类别的爬取结束
            self.crawl_helper.stop_crawl_info(_k)

        logging.info('stop crawling...')

    def closed(self, reason):
        if self.mongo_client:
            self.mongo_client.close()

        self.crawl_helper.store_crawl_info_2_db(key=None,
                                                status='stopped',
                                                comment=reason)
        logging.info('Spider[{}] closed, reason:[{}]'.format(
            self.name, reason))

    @staticmethod
    def get_normal_next_page_url(page_index, base_url, ext_param):
        _param = {
            'channelid': ext_param['channelid'],
            'page': page_index + 1,
            'prepage': '17',
            'searchword': '',
            'callback': '',
            'callback': 'result',
            '_': '',
        }
        return '{}{}'.format(base_url, urlencode(_param, quote_via=quote_plus))

    def parse_list_page_common(self, response):
        """
        通用版list页面解析
        必要条件:
        :param response:
        :return:
        """

        assert 'crawl_key' in response.meta
        assert 'page_index' in response.meta
        assert 'param' in response.meta
        assert 'xpath_of_list' in response.meta['param']
        assert 'xpath_of_detail_url' in response.meta['param']
        assert 'item_parse_class' in response.meta['param']

        list_page_content_md5 = hashlib.md5(response.body).hexdigest()
        logging.info(
            'Get page list url, page:[{}], url:[{}], status:[{}], body md5:[{}]'
            .format(response.meta['page_index'], response.url, response.status,
                    list_page_content_md5))

        logging.info('Crawl info: {}'.format(self.crawl_helper.crawl_info))

        crawl_key = response.meta['crawl_key']

        # 更新状态表记录
        self.crawl_helper.store_crawl_info_2_db(crawl_key, 'active')

        if not self.crawl_helper.should_continue_page_parse(
                response, crawl_key, list_page_content_md5):
            return

        _item_idx = 0
        if response.meta['param']['requests_type'] == "dict":
            _request = response.text.encode('utf-8')
            print(isinstance(response.text, str))

            _response_dict = response.text.replace('result(',
                                                   '').replace(');', '')

            _response_data = json.loads(_response_dict)
            # _dict_xpath = response.meta['param']['xpath_of_list'].split("/")
            # if len(_dict_xpath) > 1:
            for _dictn_num in response.meta['param']["xpath_of_list"]:
                _response_data = _response_data[_dictn_num]
            for selector in _response_data:
                _detail_url = ''
                try:
                    _item_idx += 1
                    _detail_url = selector['docpuburl']
                    # _detail_url = response.urljoin(
                    #     selector[response.meta['param']['xpath_of_detail_url']]
                    # )
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(
                        crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(
                        crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](
                        selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param'])

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item["content"] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
        else:
            for selector in response.xpath(
                    response.meta['param']['xpath_of_list']):
                _detail_url = ''
                try:
                    _item_idx += 1
                    _detail_url = response.urljoin(
                        selector.xpath(
                            response.meta['param']
                            ['xpath_of_detail_url']).extract_first())
                    _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                    logging.info('Parse item, [{}]-[{}/{}]'.format(
                        crawl_key, _item_idx, response.meta['page_index']))

                    # 检查记录是否已在库中,并做相应的跳出动作
                    loop_break, item_break = self.crawl_helper.should_continue_item_parse(
                        crawl_key, _unq_id)
                    if loop_break:
                        return
                    if item_break:
                        continue

                    # 生成并返回爬取item
                    item_parser = response.meta['param']['item_parse_class'](
                        selector)
                    item = item_parser.get_common_raw_item(
                        _id=_unq_id,
                        detail_url=_detail_url,
                        site=self.site,
                        ext_param=response.meta['param'])

                    # 随机休眠
                    time.sleep(random.randint(50, 100) / 1000.0)

                    # 更新数据库中爬取数量
                    self.crawl_helper.increase_total_item_num(crawl_key)

                    logging_item = item.copy()
                    logging_item["content"] = ""
                    logging.info('item is: {}'.format(logging_item))
                    yield item

                except Exception as e:
                    logging.exception('Handle [{}] failed'.format(_detail_url))
Exemple #6
0
class GjdwDzswSpider(scrapy.Spider):
    name = 'gjdw_dzsw_spider'  # 需要修改成全局唯一的名字
    allowed_domains = ['ecp.sgcc.com.cn']  # 需要修改成爬取网站的域名

    def __init__(self, *args, **kwargs):
        super(GjdwDzswSpider, self).__init__(*args, **kwargs)

        self.num = 0
        self.site = self.allowed_domains[0]
        self.crawl_mode = CrawlMode.HISTORY

        # 用来判断数据是否已经入库的mongo表对象
        self.mongo_client = None
        self.mongo_col_obj = None

        self.crawl_helper = None

        # 日志相关
        logging.basicConfig(
            level=logging.DEBUG,
            format='%(asctime)s  %(filename)s : %(levelname)s  %(message)s')

        # Define a RotatingFileHandler
        rf_handler = logging.handlers.RotatingFileHandler(
            filename='./{}.log'.format(self.name),
            mode='a',
            maxBytes=10 * 1024 * 1024,
            backupCount=20,
        )
        formatter = logging.Formatter(
            '%(asctime)s  %(filename)s:%(lineno)s : %(levelname)s  %(message)s'
        )
        rf_handler.setFormatter(formatter)
        rf_handler.setLevel(logging.INFO)

        # Create an root instance
        logging.getLogger().addHandler(rf_handler)

    def init_crawl_helper(self):
        try:
            if 'spider_name' in self.__dict__:
                spider_name = self.spider_name
            else:
                spider_name = self.name

            _settings = self.settings
            self.mongo_client = pymongo.MongoClient(
                _settings.get('MONGDB_URI'))
            self.crawl_helper = JyCrawlHelper(
                spider_name=spider_name,
                mongo_client=self.mongo_client,
                db_name=_settings.get('MONGDB_DB_NAME'),
                col_name=_settings.get('MONGDB_COLLECTION'))
        except Exception as e:
            logging.exception('Get get_crawl_helper failed')
            self.crawl_helper = None

    def start_requests(self):
        """
        爬虫默认接口,启动方法
        :return:
        """
        # 获取爬取时传过来的参数
        # start_time: 开始时间
        # end_time: 结束时间
        # start_page: 开始页 (优先于start_time)
        # end_page: 结束页 (优先于end_time)
        # stop_item: 连续遇到[stop_item]个重复条目后,退出本次爬取
        # spider_name: 指定的spider_name,如果不指定,使用self.name
        # command example:
        # python3 -m scrapy crawl gjdw_dzsw_spider -a start_time="2019:01:01" -a end_time="2020:03:02"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now"
        # py -3 -m scrapy crawl base_spider -a start_time="now" -a end_time="now" -a start_page="700" -a end_page="1000" -a stop_item="10000"
        assert self.start_time is not None
        assert self.end_time is not None

        self.crawl_mode = CrawlMode.REAL_TIME if str(
            self.start_time).lower() == 'now' else CrawlMode.HISTORY

        if self.crawl_mode == CrawlMode.HISTORY:
            if (len(self.start_time) != 10 or len(self.end_time) != 10
                    or self.start_time[4] != ':' or self.end_time[4] != ':'):
                logging.error(
                    'Bad date format start_time:[{}] end_time:[{}]. Example: 2019:01:01'
                    .format(self.start_time, self.end_time))
                return
        else:
            # 取当天日期
            _dt = datetime.fromtimestamp(time.time())
            self.start_time = _dt.strftime("%Y:%m:%d")
            self.end_time = self.start_time

        # 初始化self.crawl_helper
        self.init_crawl_helper()

        # 主要配置项
        _source_info = {
            # 页面的key,保证唯一
            'zbgg_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '国家电网公司商务平台',

                # list页面的base地址
                'base_url':
                'http://ecp.sgcc.com.cn/topic_project_list.jsp?columnName=topic10',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                50000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list':
                '//table[@class="font02"]/tr',
                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                './td[3]/a/@onclick',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'notice_type':
                '招标公告',
                'notice_type_code':
                '0101',
                'tos':
                '工程建设',
                'tos_code':
                '01',
                'source':
                '国家电网公司商务平台',
                'site_name':
                '国家电网公司商务平台',
                'area_code':
                '670000',
                'content_code':
                '1',
                'industryName':
                '',
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
                'column_code1':
                '',
                'column_code2':
                '',
                'connect_type':
                'project',
            },
            'zbhxr_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '国家电网公司商务平台',

                # list页面的base地址
                'base_url':
                'http://ecp.sgcc.com.cn/topic_news_list.jsp?columnName=topic22',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                50000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list':
                '//ul[@class="font02"]/li',
                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                './div[1]/a/@onclick',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'notice_type':
                '中标候选人',
                'notice_type_code':
                '0104',
                'tos_code':
                '01',
                'tos':
                '工程建设',
                'source':
                '国家电网公司商务平台',
                'site_name':
                '国家电网公司商务平台',
                'area_code':
                '670000',
                'content_code':
                '1',
                'industryName':
                '',
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
                'column_code1':
                '014001003',
                'column_code2':
                '014002009',
                'connect_type':
                'news',
            },
            'zbjg_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '国家电网公司商务平台',

                # list页面的base地址
                'base_url':
                'http://ecp.sgcc.com.cn/topic_news_list.jsp?columnName=topic23',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                50000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list':
                '//ul[@class="font02"]/li',
                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                './div[1]/a/@onclick',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'notice_type':
                '中标结果',
                'notice_type_code':
                '0104',
                'tos_code':
                '01',
                'tos':
                '工程建设',
                'source':
                '国家电网公司商务平台',
                'site_name':
                '国家电网公司商务平台',
                'area_code':
                '670000',
                'content_code':
                '1',
                'industryName':
                '',
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
                'column_code1':
                '014001007',
                'column_code2':
                '014002003',
                'connect_type':
                'news',
            },
            'fjgg_search': {
                # 通常会被填充在'source'字段里,有时也可以放在'tos'
                'name':
                '国家电网公司商务平台',

                # list页面的base地址
                'base_url':
                'http://ecp.sgcc.com.cn/topic_news_list.jsp?columnName=topic24',

                # list页面的call_back处理函数
                'callback':
                self.parse_list_page_common,

                # 得到下一页url的函数,返回值一定是一个url
                'get_next_page_url':
                self.get_normal_next_page_url,

                # 网站中该页面的最大页数,(可选配置,仅为优化程序执行效率,可不填)
                'stop_page_num':
                50000,

                # 连续遇到[stop_dup_item_num]个重复条目后,停止本次抓取
                # 提示:在程序运行初始阶段,此值可以设的较大,以便爬取所有的历史记录
                'stop_dup_item_num':
                500000 if self.crawl_mode == CrawlMode.HISTORY else 60,

                # list页面中,获得条目列表的xpath
                'xpath_of_list':
                '//ul[@class="font02"]/li',
                # 获得每一个条目链接地址的xpath
                'xpath_of_detail_url':
                './div[1]/a/@onclick',

                # 对每一个条目进行解析,返回CommonRawItem的类,需要实现
                'item_parse_class':
                BaseItemCommonParser,

                # 其它信息,可以辅助生成CommonRawItem的字段
                # 参考函数parse_list_page_common() 中 item_parser.get_common_raw_item()代码
                'notice_type':
                '否决公告',
                'notice_type_code':
                '0101',
                'tos':
                '工程建设',
                'tos_code':
                '01',
                'source':
                '国家电网公司商务平台',
                'site_name':
                '国家电网公司商务平台',
                'area_code':
                '670000',
                'content_code':
                '1',
                'industryName':
                '',
                'time_type':
                6 if self.crawl_mode == CrawlMode.HISTORY else 0,
                'column_code1':
                '014001005',
                'column_code2':
                '014002006',
                'connect_type':
                'news',
            },
        }

        logging.info('start crawling...')

        # 轮询每个类别
        for _k, _v in _source_info.items():

            # 填充爬取的基本信息
            self.crawl_helper.init_crawl_info(_k, _v)

            # 假定每个类别有不超过100000个页面
            for _page_num in range(100):
                # 轮询公告中的不同list页面
                if self.crawl_helper.get_stop_flag(_k):
                    break

                # 根据获得下一页的函数,得到下一页的URL
                _ext_param = {
                    'column_code1': _v['column_code1'],
                    'column_code2': _v['column_code2'],
                }
                _request_url = _v['get_next_page_url'](
                    page_index=_page_num,
                    base_url=_v['base_url'],
                    connect_type=_v['connect_type'],
                    ext_param=_ext_param)

                # 生成request
                _request = scrapy.Request(_request_url,
                                          callback=_v['callback'])
                # 如果需要js渲染,需要使用下面的函数
                # _request = SplashRequest(_request_url, callback=_v['callback'], args={'wait': 2})

                # 填充必要的参数
                _request.meta['param'] = _v
                _request.meta['crawl_key'] = _k
                _request.meta['page_index'] = _page_num + 1

                yield _request

            # 单个类别的爬取结束
            self.crawl_helper.stop_crawl_info(_k)

        logging.info('stop crawling...')

    def closed(self, reason):
        if self.mongo_client:
            self.mongo_client.close()

        self.crawl_helper.store_crawl_info_2_db(key=None,
                                                status='stopped',
                                                comment=reason)
        logging.info('Spider[{}] closed, reason:[{}]'.format(
            self.name, reason))

    @staticmethod
    def get_normal_next_page_url(page_index, base_url, connect_type,
                                 ext_param):
        """

        :param page_index:
        :param base_url:
        :param connect_type:  连接类型 project 项目 news 新闻
        :return:
        """
        _param = {
            'site': 'global',
            'company_id': '',
            'status': '',
            'project_name': '',
            'pageNo': page_index + 1,
        }
        _param1 = {
            'site': 'global',
            'company_id': '',
            'column_code1': ext_param['column_code1'],
            'column_code2': ext_param['column_code2'],
            'pageNo': page_index + 1,
        }
        if connect_type == 'project':
            return '{}&{}'.format(base_url,
                                  urlencode(_param, quote_via=quote_plus))
        else:
            return '{}&{}'.format(base_url,
                                  urlencode(_param1, quote_via=quote_plus))

    def parse_list_page_common(self, response):
        """
        通用版list页面解析
        必要条件:
        :param response:
        :return:
        """
        assert 'crawl_key' in response.meta
        assert 'page_index' in response.meta
        assert 'param' in response.meta
        assert 'xpath_of_list' in response.meta['param']
        assert 'xpath_of_detail_url' in response.meta['param']
        assert 'item_parse_class' in response.meta['param']

        list_page_content_md5 = hashlib.md5(response.body).hexdigest()
        logging.info(
            'Get page list url, page:[{}], url:[{}], status:[{}], body md5:[{}]'
            .format(response.meta['page_index'], response.url, response.status,
                    list_page_content_md5))

        logging.info('Crawl info: {}'.format(self.crawl_helper.crawl_info))

        crawl_key = response.meta['crawl_key']

        # 更新状态表记录
        # self.crawl_helper.store_crawl_info_2_db(crawl_key, 'active')

        if not self.crawl_helper.should_continue_page_parse(
                response, crawl_key, list_page_content_md5):
            return

        _item_idx = 0

        for selector in response.xpath(
                response.meta['param']['xpath_of_list']):

            _detail_url = ''
            try:
                # url 标签
                onc = selector.xpath(
                    response.meta['param']['xpath_of_detail_url'])
                # logging.info(onc.extract_first())
                if len(onc) > 0:

                    lis = onc.extract_first().replace('showProjectDetail', '').replace('showNewsDetail', '') \
                        .replace('(', '').replace('\'', '').replace(', ', ',').replace(')', '').replace(';',
                                                                                                        '').rstrip().lstrip()
                    detail_param = lis.split(',')
                    # logging.info(lis.split(','))
                    if response.meta['param']['connect_type'] == 'project':
                        peoject_default = 'http://ecp.sgcc.com.cn/html/project/{}/{}.html'
                        _detail_url = peoject_default.format(
                            detail_param[0], detail_param[1])
                    else:
                        news_default = 'http://ecp.sgcc.com.cn/html/news/{}/{}.html'
                        _detail_url = news_default.format(
                            detail_param[0], detail_param[1])
                else:
                    continue
                _item_idx += 1
                _unq_id = JyScrapyUtil.get_unique_id(_detail_url)

                logging.info('Parse item, [{}]-[{}/{}]'.format(
                    crawl_key, _item_idx, response.meta['page_index']))

                # 检查记录是否已在库中,并做相应的跳出动作
                # loop_break, item_break = self.crawl_helper.should_continue_item_parse(crawl_key, _unq_id)
                # if loop_break:
                #     return
                # if item_break:
                #     continue

                # 生成并返回爬取item
                item_parser = response.meta['param']['item_parse_class'](
                    selector)
                item = item_parser.get_common_raw_item(
                    _id=_unq_id,
                    detail_url=_detail_url,
                    site=self.site,
                    ext_param=response.meta['param'])

                # 随机休眠
                time.sleep(random.randint(50, 100) / 1000.0)

                # 更新数据库中爬取数量
                # self.crawl_helper.increase_total_item_num(crawl_key)

                logging.info('item is: {}'.format(item))
                yield item

            except Exception as e:
                logging.exception('Handle [{}] failed'.format(_detail_url))