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))
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))
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))
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))
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))