176 lines
5.9 KiB
PHP
176 lines
5.9 KiB
PHP
<?php
|
|
require_once __DIR__ . '/UnifiedScrmData.class.php';
|
|
|
|
/**
|
|
* 数据爬虫类
|
|
* 用于处理需要先获取 Token 授权,再读取接口数据的场景
|
|
*/
|
|
class Xinghe
|
|
{
|
|
private $cookieFile;
|
|
private $ch; // cURL 句柄
|
|
private $pageUrl;
|
|
private $account;
|
|
private $password;
|
|
private $unifiedData;
|
|
private $baseUrl;
|
|
|
|
/**
|
|
* 构造函数:初始化 cURL 和 Cookie 文件
|
|
*/
|
|
public function __construct($pageUrl, $username, $password)
|
|
{
|
|
$this->pageUrl = $pageUrl;
|
|
$this->username = $username;
|
|
$this->password = $password;
|
|
|
|
$parsedUrl = parse_url($pageUrl);
|
|
$scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
|
|
$host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
|
|
$baseUrl = $scheme . $host;
|
|
$this->baseUrl = $baseUrl;
|
|
|
|
$this->unifiedData = new UnifiedScrmData();
|
|
}
|
|
|
|
public function run()
|
|
{
|
|
// 1. 创建临时 Cookie 文件
|
|
$this->cookieFile = tempnam(sys_get_temp_dir(), 'spider_cookie_');
|
|
if ($this->cookieFile === false) {
|
|
throw new \RuntimeException("无法在系统临时目录创建 Cookie 文件");
|
|
}
|
|
|
|
// 2. 初始化 cURL 并设置全局参数
|
|
$this->ch = curl_init();
|
|
|
|
$apiUrl = '/share/share/api_yinliu_count.html?page=1&limit=10&id=&class_id=&is_repet=1&start_time=&end_time=';
|
|
|
|
$defaultOptions = [
|
|
CURLOPT_RETURNTRANSFER => true, // 将结果返回为字符串
|
|
CURLOPT_HEADER => false, // 不输出响应头
|
|
CURLOPT_TIMEOUT => 15, // 设置超时时间(秒)
|
|
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
|
// 核心优化:同时指定读写 Cookie 为同一个文件,cURL 会自动维护会话
|
|
CURLOPT_COOKIEJAR => $this->cookieFile,
|
|
CURLOPT_COOKIEFILE => $this->cookieFile,
|
|
];
|
|
|
|
curl_setopt_array($this->ch, $defaultOptions);
|
|
|
|
$this->authenticate();
|
|
$first_page_data = $this->fetchApiData($apiUrl); // 首页数据
|
|
$this->unifiedData->total = $first_page_data['count']; // 总在线人数
|
|
$this->unifiedData->todayNewCount = $first_page_data['totalRow']['day_sum']; // 今日新增在线人数
|
|
|
|
$total_pages = ceil($this->unifiedData->total/10); // 总页数
|
|
$allListPagesData = [$first_page_data['data']]; // 初始化列表容器,装入第一页数据
|
|
for ($page = 2; $page <= $total_pages; $page++) {
|
|
$apiUrl = '/share/share/api_yinliu_count.html?page=' . $page . '&limit=10&id=&class_id=&is_repet=1&start_time=&end_time=';
|
|
|
|
$page_data = $this->fetchApiData($apiUrl);
|
|
|
|
$allListPagesData[] = $page_data['data'];
|
|
}
|
|
|
|
return $this->parseToUnifiedData($first_page_data, $allListPagesData);
|
|
}
|
|
|
|
// 清爽至极的数据清洗:详情是详情,列表是列表
|
|
protected function parseToUnifiedData($detailData, $allListPagesData)
|
|
{
|
|
$unifiedData = $this->unifiedData;
|
|
|
|
// 循环合并了所有页数的 List 数组
|
|
foreach ($allListPagesData as $records) {
|
|
foreach ($records as $item) {
|
|
if(!empty($item['user'])) {
|
|
$number = $item['user'] ?? null;
|
|
$isOnline = (isset($item['online']) && $item['online'] == 1);
|
|
|
|
$unifiedData->addNumber($number, $isOnline, $item['day_sum']);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $unifiedData;
|
|
}
|
|
|
|
/**
|
|
* 析构函数:释放资源,清理垃圾
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
// 关闭 cURL 会话
|
|
if (is_resource($this->ch) || $this->ch instanceof \CurlHandle) {
|
|
curl_close($this->ch);
|
|
}
|
|
|
|
// 删除临时 Cookie 文件
|
|
if (file_exists($this->cookieFile)) {
|
|
unlink($this->cookieFile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 第一步:访问授权页面,建立会话
|
|
*
|
|
* @param string $authUrl 包含 token 的授权地址
|
|
* @return bool 授权请求是否成功
|
|
* @throws \Exception
|
|
*/
|
|
public function authenticate()
|
|
{
|
|
$this->sendRequest($this->pageUrl);
|
|
return true; // 如果没有抛出异常,则认为请求成功
|
|
}
|
|
|
|
/**
|
|
* 第二步:请求 API 接口获取数据
|
|
*
|
|
* @param string $apiUrl 接口数据地址
|
|
* @return array|null 解析后的数组数据,如果解析失败返回 null
|
|
* @throws \Exception
|
|
*/
|
|
public function fetchApiData($apiUrl)
|
|
{
|
|
$response = $this->sendRequest($this->baseUrl . $apiUrl);
|
|
|
|
// 尝试解析 JSON 数据
|
|
$data = json_decode($response, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
throw new \RuntimeException("JSON 解析失败: " . json_last_error_msg() . "。原始响应: " . $response);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* 发送 cURL 请求的底层私有方法
|
|
*
|
|
* @param string $url 目标地址
|
|
* @return string 服务器响应内容
|
|
* @throws \Exception
|
|
*/
|
|
private function sendRequest($url)
|
|
{
|
|
curl_setopt($this->ch, CURLOPT_URL, $url);
|
|
|
|
$response = curl_exec($this->ch);
|
|
|
|
// 检查是否有网络或 cURL 底层错误
|
|
if ($response === false) {
|
|
$error = curl_error($this->ch);
|
|
throw new \RuntimeException("请求失败 [{$url}]: {$error}");
|
|
}
|
|
|
|
// 检查 HTTP 状态码
|
|
$httpCode = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
|
|
if ($httpCode >= 400) {
|
|
throw new \RuntimeException("HTTP 请求异常 [{$url}],状态码: {$httpCode}");
|
|
}
|
|
|
|
return (string)$response;
|
|
}
|
|
} |