|
@@ -1,206 +1,287 @@
|
|
|
from time import sleep
|
|
|
+from typing import List, Optional
|
|
|
|
|
|
from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
|
|
from selenium.webdriver.common.by import By
|
|
|
from selenium.webdriver.support import expected_conditions as ec
|
|
|
-from selenium.webdriver.support.wait import WebDriverWait
|
|
|
|
|
|
import utils
|
|
|
from adapters.data_collection_adapter_interface import IDataCollectionAdapter
|
|
|
-from stores.data_store_interface import IDataStore
|
|
|
|
|
|
|
|
|
class ChinabiddingDataCollectionAdapter(IDataCollectionAdapter):
|
|
|
- """
|
|
|
- 中国招标网数据采集适配器
|
|
|
- """
|
|
|
-
|
|
|
- def __init__(self, url: str, store: IDataStore = None):
|
|
|
- self._url = url
|
|
|
- self._store = store
|
|
|
- self._driver = None
|
|
|
- self._keyword = None
|
|
|
- self._adapter_type = "chinabidding"
|
|
|
- self._next_count = 0
|
|
|
+ """中国采购与招标网数据采集适配器"""
|
|
|
+
|
|
|
+ def __init__(self, url: str):
|
|
|
+ """初始化适配器
|
|
|
+
|
|
|
+ Args:
|
|
|
+ url: 目标网站URL
|
|
|
+ """
|
|
|
+ super().__init__(url, "chinabidding", "全部")
|
|
|
|
|
|
def login(self, username: str, password: str) -> None:
|
|
|
+ """登录网站
|
|
|
+
|
|
|
+ Args:
|
|
|
+ username: 用户名
|
|
|
+ password: 密码
|
|
|
+ """
|
|
|
try:
|
|
|
+ # 点击登录按钮
|
|
|
login_el = self.driver.find_element(
|
|
|
By.XPATH, "//div[@id='loginRight']/a[@class='login']"
|
|
|
)
|
|
|
login_el.click()
|
|
|
- wait = WebDriverWait(self.driver, 10, 1)
|
|
|
- wait.until(ec.presence_of_element_located((By.ID, "userpass")))
|
|
|
- # if not self._wait_until(
|
|
|
- # ec.presence_of_element_located((By.ID, "userpass"))
|
|
|
- # ):
|
|
|
- # raise TimeoutException(f"id='userpass' 元素没有找到")
|
|
|
+
|
|
|
+ # 等待登录框加载
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.ID, "userpass")),
|
|
|
+ timeout=10,
|
|
|
+ message="登录框加载超时",
|
|
|
+ )
|
|
|
+
|
|
|
+ # 输入用户名密码
|
|
|
un_el = self.driver.find_element(By.ID, "username")
|
|
|
un_el.send_keys(username)
|
|
|
pass_el = self.driver.find_element(By.ID, "userpass")
|
|
|
pass_el.send_keys(password)
|
|
|
+
|
|
|
+ # 点击登录
|
|
|
login_btn = self.driver.find_element(By.ID, "login-button")
|
|
|
login_btn.click()
|
|
|
- wait.until(ec.presence_of_element_located((By.ID, "site-content")))
|
|
|
- # if not self._wait_until(ec.presence_of_element_located((By.ID, "site-content"))):
|
|
|
- # raise TimeoutException(f"id='site-content' 元素没有找到")
|
|
|
+
|
|
|
+ # 等待登录成功
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.ID, "site-content")),
|
|
|
+ message="登录成功页面加载超时",
|
|
|
+ )
|
|
|
+ self.logger.info("登录成功")
|
|
|
+
|
|
|
except TimeoutException as e:
|
|
|
- raise Exception(f"登录失败 [{self._adapter_type}] [超时]: {e}")
|
|
|
+ raise Exception(f"登录超时: {e}")
|
|
|
except NoSuchElementException as e:
|
|
|
- raise Exception(f"登录失败 [{self._adapter_type}] [找不到元素]: {e}")
|
|
|
+ raise Exception(f"页面元素未找到: {e}")
|
|
|
|
|
|
- def _collect(self, keyword: str):
|
|
|
- items = self._search_by_type(keyword, 0)
|
|
|
- self._process_list(items, 0)
|
|
|
- sleep(2)
|
|
|
- items = self._search_by_type(keyword, 1)
|
|
|
- self._process_list(items, 1)
|
|
|
- if utils.get_config_bool(self.batch_save_key):
|
|
|
- self.store.save_collect_data(True)
|
|
|
+ def _collect(self, keyword: str) -> None:
|
|
|
+ """执行数据采集
|
|
|
|
|
|
- def _search_by_type(self, keyword: str, data_type):
|
|
|
+ Args:
|
|
|
+ keyword: 单个搜索关键词
|
|
|
+ """
|
|
|
try:
|
|
|
- self.driver.get(self._url)
|
|
|
- if data_type == 0:
|
|
|
- utils.get_logger().info(f"开始采集 招标公告")
|
|
|
- el = self.driver.find_element(
|
|
|
- By.XPATH, "//div[@id='z-b-g-g']/h2/a[@class='more']"
|
|
|
- )
|
|
|
- else:
|
|
|
- utils.get_logger().info(f"开始采集 中标结果公告")
|
|
|
- el = self.driver.find_element(
|
|
|
- By.XPATH, "//div[@id='z-b-jg-gg']/h2/a[@class='more']"
|
|
|
- )
|
|
|
- el.click()
|
|
|
- if not self._wait_until(ec.number_of_windows_to_be(2)):
|
|
|
- return []
|
|
|
- self.driver.close()
|
|
|
- self.driver.switch_to.window(self.driver.window_handles[0])
|
|
|
- return self._search(keyword)
|
|
|
- except TimeoutException as e:
|
|
|
- raise Exception(f"搜索失败 [{self._adapter_type}] [超时]: {e}")
|
|
|
- except NoSuchElementException as e:
|
|
|
- raise Exception(f"搜索失败 [{self._adapter_type}] [找不到元素]: {e}")
|
|
|
|
|
|
- def _search(self, keyword: str) -> list:
|
|
|
- if not self._wait_until(
|
|
|
- ec.presence_of_element_located((By.ID, "searchBidProjForm"))
|
|
|
- ):
|
|
|
- return []
|
|
|
+ self.logger.info(f"开始采集关键词: {keyword}, 时间范围: {self._search_txt}")
|
|
|
+
|
|
|
+ # 采集招标公告
|
|
|
+ self.logger.info("开始采集招标公告")
|
|
|
+ items = self._search_by_type(keyword, 0)
|
|
|
+ self._process_list(items, 0)
|
|
|
+ sleep(2)
|
|
|
+
|
|
|
+ # 采集中标公告
|
|
|
+ self.logger.info("开始采集中标公告")
|
|
|
+ items = self._search_by_type(keyword, 1)
|
|
|
+ self._process_list(items, 1)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"采集失败: {e}")
|
|
|
+ raise
|
|
|
+
|
|
|
+ def _search_by_type(self, keyword: str, data_type: int) -> List:
|
|
|
+ """根据类型搜索数据
|
|
|
+
|
|
|
+ Args:
|
|
|
+ keyword: 搜索关键词
|
|
|
+ data_type: 数据类型(0:招标,1:中标)
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ List: 搜索结果列表
|
|
|
+ """
|
|
|
+ # 打开首页
|
|
|
+ self.driver.get(self.url)
|
|
|
+
|
|
|
+ # 选择公告类型
|
|
|
+ if data_type == 0:
|
|
|
+ el = self.driver.find_element(
|
|
|
+ By.XPATH, "//div[@id='z-b-g-g']/h2/a[@class='more']"
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ el = self.driver.find_element(
|
|
|
+ By.XPATH, "//div[@id='z-b-jg-gg']/h2/a[@class='more']"
|
|
|
+ )
|
|
|
+ el.click()
|
|
|
+
|
|
|
+ # 切换窗口
|
|
|
+ self._wait_for(ec.number_of_windows_to_be(2), message="新窗口打开超时")
|
|
|
+
|
|
|
+ self.driver.close()
|
|
|
+ self.driver.switch_to.window(self.driver.window_handles[0])
|
|
|
+
|
|
|
+ # 执行搜索
|
|
|
+ return self._search(keyword)
|
|
|
+
|
|
|
+ def _search(self, keyword: str) -> List:
|
|
|
+ """执行搜索"""
|
|
|
+ # 等待搜索框加载
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.ID, "searchBidProjForm")),
|
|
|
+ message="搜索框加载超时",
|
|
|
+ )
|
|
|
+
|
|
|
+ # 输入关键词
|
|
|
search_el = self.driver.find_element(
|
|
|
By.XPATH, "//form[@id='searchBidProjForm']/ul/li/input[@id='fullText']"
|
|
|
)
|
|
|
search_el.clear()
|
|
|
search_el.send_keys(keyword)
|
|
|
+
|
|
|
+ # 点击搜索
|
|
|
search_btn = self.driver.find_element(
|
|
|
By.XPATH, "//form[@id='searchBidProjForm']/ul/li/button"
|
|
|
)
|
|
|
search_btn.click()
|
|
|
+
|
|
|
+ # 等待结果加载
|
|
|
self._next_count = 0
|
|
|
- if not self._wait_until(
|
|
|
- ec.presence_of_element_located((By.ID, "site-content"))
|
|
|
- ):
|
|
|
- return []
|
|
|
- default_search_txt = "全部"
|
|
|
- search_txt = utils.get_config_value(self.search_day_key, default_search_txt)
|
|
|
- utils.get_logger().debug(f"搜索日期条件: {search_txt}")
|
|
|
- if search_txt != default_search_txt:
|
|
|
- last_el = self.driver.find_element(By.LINK_TEXT, search_txt)
|
|
|
- sleep(1)
|
|
|
- last_el.click()
|
|
|
- if not self._wait_until(
|
|
|
- ec.presence_of_element_located((By.ID, "site-content"))
|
|
|
- ):
|
|
|
- return []
|
|
|
- else:
|
|
|
- sleep(1)
|
|
|
- try:
|
|
|
- a_links = self.driver.find_elements(
|
|
|
- By.XPATH, "//form[@id='pagerSubmitForm']/a"
|
|
|
- )
|
|
|
- count = len(a_links)
|
|
|
- if count > 1:
|
|
|
- count = count - 1
|
|
|
- utils.get_logger().debug(f"共查询到 {count} 页,每页 10 条")
|
|
|
- except Exception as e:
|
|
|
- utils.get_logger().error(f"搜索失败[尝试查询页数]: {e}")
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.ID, "site-content")),
|
|
|
+ message="搜索结果加载超时",
|
|
|
+ )
|
|
|
+
|
|
|
+ # 设置时间范围
|
|
|
+ self._set_search_date()
|
|
|
+
|
|
|
+ # 获取结果列表
|
|
|
items = self.driver.find_elements(By.XPATH, "//ul[@class='as-pager-body']/li/a")
|
|
|
return items
|
|
|
|
|
|
- def _process_list(self, items: list, data_type) -> list:
|
|
|
+ def _set_search_date(self) -> None:
|
|
|
+ """设置搜索时间范围"""
|
|
|
+ try:
|
|
|
+ if self._search_txt != self._default_search_txt:
|
|
|
+ last_el = self.driver.find_element(By.LINK_TEXT, self._search_txt)
|
|
|
+ sleep(1)
|
|
|
+ last_el.click()
|
|
|
+
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.ID, "site-content")),
|
|
|
+ message="设置时间范围后页面加载超时",
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ sleep(1)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ self.logger.error(f"设置时间范围失败: {e}")
|
|
|
+
|
|
|
+ def _process_list(self, items: List, data_type: int) -> None:
|
|
|
+ """处理数据列表
|
|
|
+
|
|
|
+ Args:
|
|
|
+ items: 数据列表
|
|
|
+ data_type: 数据类型(0:招标,1:中标)
|
|
|
+ """
|
|
|
if not items:
|
|
|
- return []
|
|
|
+ return
|
|
|
+
|
|
|
+ # 处理当前页
|
|
|
for item in items:
|
|
|
self._process_item(item, data_type)
|
|
|
sleep(2)
|
|
|
+
|
|
|
+ # 处理下一页
|
|
|
next_items = self._next_page()
|
|
|
- return self._process_list(next_items, data_type)
|
|
|
+ if next_items:
|
|
|
+ self._process_list(next_items, data_type)
|
|
|
+
|
|
|
+ def _next_page(self) -> Optional[List]:
|
|
|
+ """获取下一页数据
|
|
|
|
|
|
- def _next_page(self) -> list:
|
|
|
+ Returns:
|
|
|
+ List: 下一页数据列表
|
|
|
+ """
|
|
|
try:
|
|
|
+ # 查找下一页按钮
|
|
|
try:
|
|
|
btn = self.driver.find_element(
|
|
|
By.XPATH, "//form[@id='pagerSubmitForm']/a[@class='next']"
|
|
|
)
|
|
|
except NoSuchElementException:
|
|
|
- utils.get_logger().debug(f"翻页结束 [{self._adapter_type}]")
|
|
|
- return []
|
|
|
+ self.logger.debug("已到最后一页")
|
|
|
+ return None
|
|
|
+
|
|
|
+ # 点击下一页
|
|
|
btn.click()
|
|
|
self._next_count += 1
|
|
|
- utils.get_logger().debug(
|
|
|
- f"下一页[{self._next_count+1}]: {self.driver.current_url}"
|
|
|
+ self.logger.debug(f"下一页[{self._next_count+1}]")
|
|
|
+
|
|
|
+ # 等待页面加载
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.ID, "site-content")),
|
|
|
+ message="下一页加载超时",
|
|
|
)
|
|
|
- if not self._wait_until(
|
|
|
- ec.presence_of_element_located((By.ID, "site-content"))
|
|
|
- ):
|
|
|
- return []
|
|
|
+
|
|
|
+ # 获取数据列表
|
|
|
items = self.driver.find_elements(
|
|
|
By.XPATH, "//ul[@class='as-pager-body']/li/a"
|
|
|
)
|
|
|
return items
|
|
|
+
|
|
|
except NoSuchElementException as e:
|
|
|
- raise Exception(f"翻页失败 [{self._adapter_type}] [找不到元素]: {e}")
|
|
|
- except TimeoutException as e:
|
|
|
- raise Exception(f"翻页失败 [{self._adapter_type}] [超时]: {e}")
|
|
|
+ raise Exception(f"页面元素未找到: {e}")
|
|
|
|
|
|
- def _process_item(self, item, data_type):
|
|
|
+ def _process_item(self, item, data_type: int) -> None:
|
|
|
+ """处理单条数据
|
|
|
+
|
|
|
+ Args:
|
|
|
+ item: 数据项
|
|
|
+ data_type: 数据类型(0:招标,1:中标)
|
|
|
+ """
|
|
|
main_handle = self.driver.current_window_handle
|
|
|
close = True
|
|
|
+
|
|
|
try:
|
|
|
+ # 检查URL是否已采集
|
|
|
url = item.get_attribute("href")
|
|
|
if self._check_is_collect_by_url(url):
|
|
|
close = False
|
|
|
return
|
|
|
+
|
|
|
+ # 打开详情页
|
|
|
item.click()
|
|
|
- if not self._wait_until(ec.number_of_windows_to_be(2)):
|
|
|
- return
|
|
|
+ self._wait_for(ec.number_of_windows_to_be(2), message="新窗口打开超时")
|
|
|
+
|
|
|
+ # 切换窗口
|
|
|
handles = self.driver.window_handles
|
|
|
for handle in handles:
|
|
|
if handle != main_handle:
|
|
|
self.driver.switch_to.window(handle)
|
|
|
break
|
|
|
+
|
|
|
+ # 获取URL
|
|
|
url = self.driver.current_url
|
|
|
- utils.get_logger().debug(f"跳转详情")
|
|
|
- if not self._wait_until(
|
|
|
- ec.presence_of_element_located((By.CLASS_NAME, "content"))
|
|
|
- ):
|
|
|
- return
|
|
|
+ self.logger.debug(f"打开详情页: {url}")
|
|
|
+
|
|
|
+ # 等待内容加载
|
|
|
+ self._wait_for(
|
|
|
+ ec.presence_of_element_located((By.CLASS_NAME, "content")),
|
|
|
+ message="详情页加载超时",
|
|
|
+ )
|
|
|
+
|
|
|
+ # 获取内容
|
|
|
content = self.driver.find_element(By.CLASS_NAME, "content").text
|
|
|
+
|
|
|
+ # 检查关键词并保存
|
|
|
if self._check_content(content):
|
|
|
self._save_db(url, content, data_type)
|
|
|
else:
|
|
|
self._save_db(url, content, data_type, is_invalid=True)
|
|
|
|
|
|
except TimeoutException as e:
|
|
|
- utils.get_logger().error(
|
|
|
- f"采集发生异常 [{self._adapter_type}] Timeout: {self.driver.current_url}。Exception: {e}"
|
|
|
- )
|
|
|
- # raise Exception(f"采集失败 [超时]: {e}")
|
|
|
+ self.logger.error(f"处理数据超时: {e}")
|
|
|
except NoSuchElementException as e:
|
|
|
- utils.get_logger().error(
|
|
|
- f"采集发生异常 [{self._adapter_type}] NoSuchElement: {self.driver.current_url}。Exception: {e}"
|
|
|
- )
|
|
|
- raise Exception(f"采集失败 [{self._adapter_type}] [找不到元素]: {e}")
|
|
|
+ self.logger.error(f"页面元素未找到: {e}")
|
|
|
+ raise
|
|
|
finally:
|
|
|
if close:
|
|
|
sleep(2)
|