|
@@ -0,0 +1,416 @@
|
|
|
+import io
|
|
|
+import time
|
|
|
+
|
|
|
+import pandas as pd
|
|
|
+from PIL import Image
|
|
|
+from lxml.html import fromstring, tostring
|
|
|
+from selenium import webdriver
|
|
|
+from selenium.webdriver import ActionChains
|
|
|
+from selenium.webdriver import Chrome
|
|
|
+from selenium.webdriver.common.by import By
|
|
|
+from selenium.webdriver.support import expected_conditions as EC
|
|
|
+from selenium.webdriver.support.wait import WebDriverWait
|
|
|
+
|
|
|
+from chaojiying import Chaojiying_Client
|
|
|
+from utils.databases import mongo_table
|
|
|
+from utils.log import logger
|
|
|
+
|
|
|
+'''MongoDB'''
|
|
|
+company_tab = mongo_table('national', 'company')
|
|
|
+
|
|
|
+'''验证码服务'''
|
|
|
+chaojiying = Chaojiying_Client('ddddjy', 'ddddjy2021', '929622')
|
|
|
+
|
|
|
+'''企业资质'''
|
|
|
+COMPANY_QUALITY_MAPS = {
|
|
|
+ '资质类别': 'quality_type',
|
|
|
+ '资质证书号': 'quality_no',
|
|
|
+ '资质名称': 'quality_name',
|
|
|
+ '发证日期': 'fzrq',
|
|
|
+ '发证有效期': 'fzyxq',
|
|
|
+ '发证机关': 'fzjg',
|
|
|
+}
|
|
|
+'''不良行为'''
|
|
|
+BAD_BEHAVIOR_MAPS = {
|
|
|
+ '诚信记录主体及编号': 'integrity_no',
|
|
|
+ '决定内容': 'decide_content',
|
|
|
+ '实施部门': 'ssbm',
|
|
|
+ '决定日期与有效期': 'execution_date',
|
|
|
+}
|
|
|
+'''黑名单记录'''
|
|
|
+BLACK_LIST_MAPS = {
|
|
|
+ '黑名单记录主体及编号': 'black_list_no',
|
|
|
+ '黑名单认定依据': 'black_list_rdyj',
|
|
|
+ '认定部门': 'rdbm',
|
|
|
+ '决定日期与有效期': 'execution_date',
|
|
|
+}
|
|
|
+'''失信联合惩戒记录'''
|
|
|
+PUNISH_MAPS = {
|
|
|
+ '失信记录编号': 'punish_no',
|
|
|
+ '失信联合惩戒记录主体': 'punish_subject',
|
|
|
+ '法人姓名': 'legal_person',
|
|
|
+ '列入名单事由': 'reason',
|
|
|
+ '认定部门': 'rdbm',
|
|
|
+ '列入日期': 'join_date',
|
|
|
+}
|
|
|
+
|
|
|
+CRAWL_SITE = 'http://jzsc.mohurd.gov.cn/data/company'
|
|
|
+
|
|
|
+
|
|
|
+def html2element(html):
|
|
|
+ return fromstring(html)
|
|
|
+
|
|
|
+
|
|
|
+def element2html(lxml_element):
|
|
|
+ return tostring(lxml_element, encoding='utf-8').decode()
|
|
|
+
|
|
|
+
|
|
|
+def display_prompt_popup(html):
|
|
|
+ _element = html2element(html)
|
|
|
+ node = _element.xpath('//div[@class="el-dialog__wrapper"]')[0]
|
|
|
+ _popup_style = node.attrib.get('style')
|
|
|
+ if _popup_style is not None:
|
|
|
+ _styles = str(_popup_style).split(';')
|
|
|
+ res = list(filter(lambda x: len(x) > 0, _styles))[-1].strip().lower()
|
|
|
+ if res == 'display: none':
|
|
|
+ '''无提示弹框'''
|
|
|
+ return False
|
|
|
+ '''有提示弹框'''
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def display_geetest_panel(html):
|
|
|
+ _element = html2element(html)
|
|
|
+ node = _element.xpath('//div[@class="geetest_panel_next"]')
|
|
|
+ if len(node) == 0:
|
|
|
+ '''无验证码'''
|
|
|
+ return False
|
|
|
+ _geetest_panel = node[0]
|
|
|
+ geetest_style = _geetest_panel.attrib.get('style')
|
|
|
+ if geetest_style is not None and geetest_style == 'display: block;':
|
|
|
+ '''有验证码'''
|
|
|
+ return True
|
|
|
+ else:
|
|
|
+ '''无验证码'''
|
|
|
+ return False
|
|
|
+
|
|
|
+
|
|
|
+def prompt_popup(driver: Chrome):
|
|
|
+ while True:
|
|
|
+ if not display_prompt_popup(driver.page_source):
|
|
|
+ break
|
|
|
+ driver.find_element_by_xpath('//div[@class="el-dialog captchaDilaog"]/div[3]/div/button[1]').click()
|
|
|
+ time.sleep(2)
|
|
|
+
|
|
|
+
|
|
|
+def geetest_panel(driver: Chrome, save_img_to_local=False):
|
|
|
+ pic_id = None
|
|
|
+ while True:
|
|
|
+ if not display_geetest_panel(driver.page_source):
|
|
|
+ break
|
|
|
+
|
|
|
+ if pic_id is not None:
|
|
|
+ '''打码平台失败'''
|
|
|
+ captcha_result = chaojiying.ReportError(pic_id)
|
|
|
+ pic_id = None
|
|
|
+ logger.info(captcha_result)
|
|
|
+
|
|
|
+ '''获取验证图片对象'''
|
|
|
+ wait = WebDriverWait(driver, 60, 0.5)
|
|
|
+ locator = (By.CLASS_NAME, 'geetest_panel_next')
|
|
|
+ touclick_element = wait.until(EC.presence_of_element_located(locator))
|
|
|
+
|
|
|
+ '''获取网页截图'''
|
|
|
+ element_png = touclick_element.screenshot_as_png
|
|
|
+ screenshot = Image.open(io.BytesIO(element_png))
|
|
|
+
|
|
|
+ '''修改截图尺寸;超级鹰:推荐宽不超过460px,高不超过310px'''
|
|
|
+ # reim = screenshot.resize((306, 310))
|
|
|
+ # reim = screenshot.resize((307, 300))
|
|
|
+ reim = screenshot.resize((310, 300))
|
|
|
+
|
|
|
+ '''获取验证码图片'''
|
|
|
+ bytes_array = io.BytesIO()
|
|
|
+ reim.save(bytes_array, format='PNG')
|
|
|
+
|
|
|
+ '''保存验证码到本地'''
|
|
|
+ if save_img_to_local:
|
|
|
+ touclick_element.screenshot('captcha.png')
|
|
|
+ with open('ele.png', 'wb') as wp:
|
|
|
+ wp.write(bytes_array.getvalue())
|
|
|
+
|
|
|
+ '''识别验证码'''
|
|
|
+ captcha_result = chaojiying.PostPic(bytes_array.getvalue(), 9004)
|
|
|
+ logger.info(captcha_result)
|
|
|
+ pic_id = captcha_result['pic_id']
|
|
|
+
|
|
|
+ '''解析识别结果'''
|
|
|
+ groups = captcha_result.get('pic_str').split('|')
|
|
|
+ locations = [[int(number) for number in group.split(',')] for group in groups]
|
|
|
+
|
|
|
+ '''点击验证图片'''
|
|
|
+ for location in locations:
|
|
|
+ # logger.info(location)
|
|
|
+ ActionChains(driver).move_to_element_with_offset(
|
|
|
+ touclick_element,
|
|
|
+ location[0] + 10,
|
|
|
+ location[1] + 47
|
|
|
+ ).click().perform()
|
|
|
+ time.sleep(1)
|
|
|
+
|
|
|
+ '''保存点击之后的图片'''
|
|
|
+ if save_img_to_local:
|
|
|
+ touclick_element.screenshot('touclick_img.png')
|
|
|
+
|
|
|
+ '''提交验证码'''
|
|
|
+ locator = (By.CLASS_NAME, 'geetest_commit')
|
|
|
+ commit_element = wait.until(EC.presence_of_element_located(locator))
|
|
|
+ ActionChains(driver).click(commit_element).perform()
|
|
|
+ time.sleep(5)
|
|
|
+
|
|
|
+
|
|
|
+def check_page(driver: Chrome, **kwargs):
|
|
|
+ """检查页面"""
|
|
|
+ prompt_popup(driver)
|
|
|
+ geetest_panel(driver, save_img_to_local=kwargs.get('save_img_to_local'))
|
|
|
+
|
|
|
+
|
|
|
+def click(driver: Chrome, button, allow_check_page=False, wait_time=1):
|
|
|
+ driver.execute_script("arguments[0].click();", button)
|
|
|
+ time.sleep(wait_time)
|
|
|
+ if allow_check_page:
|
|
|
+ check_page(driver)
|
|
|
+
|
|
|
+
|
|
|
+def next_page(driver: Chrome):
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ node = element.xpath('//button[@class="btn-next"]')[0]
|
|
|
+ attrib = node.attrib.get('disabled')
|
|
|
+ if attrib is not None and attrib == 'disabled':
|
|
|
+ '''最大页码'''
|
|
|
+ return False
|
|
|
+ else:
|
|
|
+ '''继续翻页'''
|
|
|
+ button = driver.find_element_by_class_name('btn-next')
|
|
|
+ click(driver, button)
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def current_page(html):
|
|
|
+ element = html2element(html)
|
|
|
+ nodes = element.xpath('//ul[@class="el-pager"]/li')
|
|
|
+ for node in nodes:
|
|
|
+ if node.attrib.get('class') == 'number active':
|
|
|
+ return node.text
|
|
|
+
|
|
|
+
|
|
|
+def extract_content(html):
|
|
|
+ """抽取页面结构化数据"""
|
|
|
+ results = []
|
|
|
+ '''字段映射表'''
|
|
|
+ _maps = {
|
|
|
+ **COMPANY_QUALITY_MAPS,
|
|
|
+ **BAD_BEHAVIOR_MAPS,
|
|
|
+ **BLACK_LIST_MAPS,
|
|
|
+ **PUNISH_MAPS,
|
|
|
+ }
|
|
|
+ '''转化成dataframe'''
|
|
|
+ dfs = pd.read_html(html)
|
|
|
+ if len(dfs) == 2:
|
|
|
+ columns = list(dfs[0].columns.array)
|
|
|
+ values = dfs[1].values
|
|
|
+ '''合并内容'''
|
|
|
+ panel_container = [dict(zip(columns, val)) for val in values]
|
|
|
+ '''转换字段'''
|
|
|
+ for item in panel_container:
|
|
|
+ _item = {}
|
|
|
+ for key, val in item.items():
|
|
|
+ if key in _maps:
|
|
|
+ _item[_maps[key]] = val
|
|
|
+ results.append(_item)
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+def click_query(driver: Chrome):
|
|
|
+ """查询按钮"""
|
|
|
+ button = driver.find_element_by_class_name("ssButton")
|
|
|
+ click(driver, button)
|
|
|
+ time.sleep(1)
|
|
|
+
|
|
|
+
|
|
|
+def select_qualify_category(driver: Chrome, records):
|
|
|
+ span_elements = driver.find_elements(by=By.XPATH, value='//div[@class="labelInPut labelInPutRadio"]/span')
|
|
|
+ for span_element in span_elements:
|
|
|
+ qualify_category = span_element.text
|
|
|
+ if qualify_category not in records:
|
|
|
+ logger.info(f'>>资质类别:{qualify_category} <<')
|
|
|
+ click(driver, span_element)
|
|
|
+ click_query(driver)
|
|
|
+ records.append(span_element.text)
|
|
|
+ return False
|
|
|
+ else:
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def crawl_spider(driver: Chrome, handler):
|
|
|
+ """采集爬虫"""
|
|
|
+ exception_count = 0
|
|
|
+ td_elements = driver.find_elements(By.XPATH, value='//table[@class="el-table__body"]//tr/td[3]')
|
|
|
+ for td_element in td_elements:
|
|
|
+ if exception_count > 3:
|
|
|
+ '''数据异常,停止采集'''
|
|
|
+ return False
|
|
|
+ button = td_element.find_element_by_class_name("link")
|
|
|
+ click(driver, button, wait_time=2)
|
|
|
+ title = td_element.text
|
|
|
+ for current_handler in driver.window_handles:
|
|
|
+ if current_handler == handler:
|
|
|
+ continue
|
|
|
+ '''切换到弹出页面'''
|
|
|
+ driver.switch_to.window(current_handler)
|
|
|
+ current_url = driver.current_url
|
|
|
+ '''首次进入详情页,检查页面弹框和验证码面板'''
|
|
|
+ check_page(driver)
|
|
|
+ company = {}
|
|
|
+
|
|
|
+ '''企业基础数据'''
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ nodes = element.xpath('//div[@class="detaile-header__info--table"]')
|
|
|
+ for node in nodes:
|
|
|
+ credit_no = "".join(node.xpath('./div[1]/div[1]/div[2]/text()')).strip()
|
|
|
+ legal_person = "".join(node.xpath('./div[1]/div[2]/div[2]/text()')).strip()
|
|
|
+ company_type = "".join(node.xpath('./div[2]/div[1]/div[2]/text()')).strip()
|
|
|
+ address = "".join(node.xpath('./div[2]/div[2]/div[2]/text()')).strip()
|
|
|
+ business_address = "".join(node.xpath('./div[3]/div[1]/div[2]/text()')).strip()
|
|
|
+ company = {
|
|
|
+ 'credit_no': credit_no, # 统一社会信用代码
|
|
|
+ 'legal_person': legal_person, # 企业法定代表人
|
|
|
+ 'company_type': company_type, # 企业登记注册类型
|
|
|
+ 'address': address, # 企业注册属地
|
|
|
+ 'business_address': business_address, # 企业经营地址
|
|
|
+ }
|
|
|
+ # logger.info(item)
|
|
|
+
|
|
|
+ '''企业资质'''
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ node = element.xpath('//div[@class="panel-container"]')[0]
|
|
|
+ company_quality_html = element2html(node)
|
|
|
+ company_quality = extract_content(company_quality_html)
|
|
|
+ company['company_quality'] = company_quality
|
|
|
+ company['company_quality_html'] = {'html': company_quality_html}
|
|
|
+
|
|
|
+ '''注册人员'''
|
|
|
+ company_staff = driver.find_element_by_id("tab-companyStaff")
|
|
|
+ click(driver, company_staff, allow_check_page=True)
|
|
|
+ registrar = []
|
|
|
+ reg_buttons = driver.find_elements(by=By.XPATH, value='//div[contains(@id, "tab-")]/span')
|
|
|
+ for btn in reg_buttons:
|
|
|
+ '''点击分类'''
|
|
|
+ driver.execute_script("arguments[0].click();", btn)
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ nodes = element.xpath('//div[@class="el-table__body-wrapper is-scrolling-none"]/table//tr')
|
|
|
+ for node in nodes:
|
|
|
+ name = "".join(node.xpath('./td[2]//span/text()')).strip()
|
|
|
+ id_no = "".join(node.xpath('./td[3]/div/text()')).strip()
|
|
|
+ reg_type = "".join(node.xpath('./td[4]/div/text()')).strip()
|
|
|
+ reg_no = "".join(node.xpath('./td[5]/div/text()')).strip()
|
|
|
+ reg_major = "".join(node.xpath('./td[6]/div/text()')).strip()
|
|
|
+ registrar.append({
|
|
|
+ 'name': name, # 姓名
|
|
|
+ 'id_no': id_no, # 身份证号
|
|
|
+ 'reg_type': reg_type, # 注册类别
|
|
|
+ 'reg_no': reg_no, # 注册号(执业印章号)
|
|
|
+ 'reg_major': reg_major, # 注册专业
|
|
|
+ })
|
|
|
+ company['company_staff'] = registrar
|
|
|
+
|
|
|
+ '''不良行为'''
|
|
|
+ bad_behavior = driver.find_element_by_id('tab-badBehavior')
|
|
|
+ click(driver, bad_behavior, allow_check_page=True)
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ node = element.xpath('//div[@aria-labelledby="tab-badBehavior"]/div')[0]
|
|
|
+ bad_behavior_html = element2html(node)
|
|
|
+ bad_behaviors = extract_content(company_quality_html)
|
|
|
+ company['bad_behavior'] = bad_behaviors
|
|
|
+ company['bad_behavior_html'] = {'html': bad_behavior_html}
|
|
|
+
|
|
|
+ '''黑名单记录'''
|
|
|
+ black_list = driver.find_element_by_id('tab-blackList')
|
|
|
+ click(driver, black_list, allow_check_page=True)
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ node = element.xpath('//div[@id="pane-blackList"]/div')[0]
|
|
|
+ black_list_html = element2html(node)
|
|
|
+ black_list_array = extract_content(company_quality_html)
|
|
|
+ company['black_list'] = black_list_array
|
|
|
+ company['black_list_html'] = {'html': black_list_html}
|
|
|
+
|
|
|
+ '''失信联合惩戒记录'''
|
|
|
+ punish = driver.find_element_by_id('tab-punishLog')
|
|
|
+ click(driver, punish, allow_check_page=True)
|
|
|
+ element = html2element(driver.page_source)
|
|
|
+ node = element.xpath('//div[@id="pane-punishLog"]/div')[0]
|
|
|
+ punish_html = element2html(node)
|
|
|
+ punish_array = extract_content(company_quality_html)
|
|
|
+ company['punish'] = punish_array
|
|
|
+ company['punish_html'] = {'html': punish_html}
|
|
|
+
|
|
|
+ '''保存企业数据'''
|
|
|
+ if len(company['credit_no']) > 0:
|
|
|
+ company_tab.insert_one(company)
|
|
|
+ logger.info(f'>>> {title} - {current_url} - 采集成功 - 保存入库')
|
|
|
+ else:
|
|
|
+ exception_count += 1 # 页面无企业数据
|
|
|
+ logger.info(f'>>> {title} - {current_url} - 采集失败 - 无社会信用代码')
|
|
|
+
|
|
|
+ '''关闭详情页标签'''
|
|
|
+ driver.close()
|
|
|
+ '''返回列表页'''
|
|
|
+ driver.switch_to.window(handler)
|
|
|
+ else:
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def downloader(driver: Chrome, handler):
|
|
|
+ while True:
|
|
|
+ logger.info(f">>> 第{current_page(driver.page_source)}页<<<")
|
|
|
+ allow_crawl = crawl_spider(driver, handler)
|
|
|
+ '''是否继续采集'''
|
|
|
+ if not allow_crawl:
|
|
|
+ logger.info("网站数据异常,终止采集")
|
|
|
+ return False
|
|
|
+ '''翻页'''
|
|
|
+ if not next_page(driver):
|
|
|
+ logger.info('采集结束')
|
|
|
+ break
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def start(enable_remote_driver=False):
|
|
|
+ options = webdriver.ChromeOptions()
|
|
|
+ if enable_remote_driver:
|
|
|
+ options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
|
|
|
+ options.add_argument("--disable-gpu")
|
|
|
+ chrome_driver = webdriver.Chrome(options=options)
|
|
|
+ main_handler = chrome_driver.current_window_handle # 获取操作句柄
|
|
|
+ chrome_driver.get(CRAWL_SITE)
|
|
|
+ time.sleep(3)
|
|
|
+ '''采集记录'''
|
|
|
+ records = ['全部', '勘察企业', '监理企业', '设计与施工一体化企业', '建筑业企业']
|
|
|
+ # records = ['全部']
|
|
|
+ while True:
|
|
|
+ '''选择资质类别'''
|
|
|
+ crawl_finished = select_qualify_category(chrome_driver, records)
|
|
|
+ if crawl_finished:
|
|
|
+ logger.info('任务结束')
|
|
|
+ break
|
|
|
+ '''下载数据'''
|
|
|
+ _continue = downloader(chrome_driver, main_handler)
|
|
|
+ if not _continue:
|
|
|
+ break
|
|
|
+
|
|
|
+ if not enable_remote_driver:
|
|
|
+ chrome_driver.quit()
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ start(enable_remote_driver=True)
|