content = $sContent; $oModelArticleCache->group_code = $sGroupCode; $oModelArticleCache->created_at = time(); $r = $oModelArticleCache->save(); if (!$r) { return $oResult->fail("写入缓存失败"); } $oModelArticleCacheTime = ModelArticleCacheTime::first(); $oModelArticleCacheTime->time = time(); $r = $oModelArticleCacheTime->save(); if (!$r) { return $oResult->fail("更新cacheTime失败"); } return $sGroupCode."写入一条缓存"; } public function saveToSchema(): Flow { $oFlow = Flow::start("save"); $oArticleCache = ModelArticleCache::where("status", 1)->get(); if ($oArticleCache->isEmpty()) { return $oFlow->done("没有要处理的数据"); } // 第一步,格式化通用tgSchema $aDataMix["data"] = []; foreach ($oArticleCache as $oArticleCacheRow) { if (!isset($sFromPaichu)) { // 等于只是第一次each会赋值 $sFromPaichu = $oArticleCacheRow->group_code; } if ($oArticleCacheRow->group_code !== $sFromPaichu) { continue; // 实现一次只save一种group_code } $aContent = json_decode($oArticleCacheRow->content, true); $aReturn = $this->_schemaByCache_common($aContent, $oArticleCacheRow->group_code); foreach ($aReturn as $aData) { $aDataMix["data"][] = $aData; } $sGroupCode = $oArticleCacheRow->group_code; // 这里等于直接提取后一个group_code,不算完美,但没大问题 } // 第二步,进一步格式化为对应form的Schema $method = '_schemaWithFrom_' . $oArticleCacheRow->group_code; if (method_exists($this, $method)) { $aDataMix = $this->$method($aDataMix, $sGroupCode); } else { // 没有对应直接不处理 } $y = date("Y"); $sYqian = substr($y, 1, 2); $sYhou = substr($y, 3, 1); if ($oArticleCacheRow->code) { $sArticleCode = $oArticleCacheRow->code; } else { $sArticleCode = $sGroupCode."-".$sYqian.date("mdHis").$sYhou; } $oArticleSchema = ModelArticleSchema::where("code", $sArticleCode)->first(); if (!$oArticleSchema) { $oArticleSchema = new ModelArticleSchema(); } $oArticleSchema->schema = json_encode($aDataMix["data"]); $oArticleSchema->code = $sArticleCode; $oArticleSchema->group_code = $sGroupCode; $oArticleSchema->name = $aDataMix["name"] ?? "none"; $oArticleSchema->save(); ModelArticleCache::where("status", 1)->where("group_code", $sFromPaichu)->update([ "status" => 0, // 上线改0 "code" => $sArticleCode, ]); ServiceArticleCmd::startWithO($oArticleSchema)->syncOpen()->save("new"); return $oFlow->done(); } private function _schemaByCache_common($aContent, $sGroupCode) { $aDataMix = []; if (isset($aContent["message"]["document"]["mime_type"])) { // 包含文档 $sDocumentType = $aContent["message"]["document"]["mime_type"]; $aDataTmp = []; // new data init $aDataTmp["type"] = "str"; $aDataTmp["name"] = "str"; $aDataTmp["content"]["base"] = ""; if (strpos($sDocumentType, "image") !== false) { // 包含图片 $sDocumentName = $aContent["message"]["document"]["file_name"]; // img data init $aDataTmp["type"] = "img"; $aDataTmp["name"] = "img_cesu"; $aDataTmp["content"]["base"] = $sDocumentName; $aDataTmp["content"]["img"] = [ "title" => $sDocumentName, "base" => '', "thumb" => '', ]; $aCacheImgId = [ "base" => $aContent["message"]["document"]["file_id"], "thumb" => $aContent["message"]["document"]["thumb"]["file_id"], ]; $sBotToken = ModelTelegramKey::botTokenByCode($sGroupCode); $sPathUpload = $_ENV["dir_base"]."public/upload/"; if (!is_dir($sPathUpload)) { mkdir($sPathUpload, 0777, true); } foreach ($aCacheImgId as $k => $aImgIdRow) { // 第一步:获取 file_path $getFileUrl = "https://api.telegram.org/bot".$sBotToken."/getFile?file_id=".$aImgIdRow; $response = file_get_contents($getFileUrl); $data = json_decode($response, true); if (!$data['ok']) { $aDataTmp["content"]["img"][$k] = "获取图片途径失败"; continue; } $filePath = $data['result']['file_path']; // 第二步:下载文件内容 $downloadUrl = "https://api.telegram.org/file/bot".$sBotToken."/".$filePath; $imageData = file_get_contents($downloadUrl); if ($imageData === false) { $aDataTmp["content"]["img"][$k] = "下载文件失败"; continue; } // 第三步:保存到服务器 // $savePath = 'upload/'; // if (!is_dir($savePath)) { // mkdir($savePath, 0777, true); // } $fileName = basename($filePath); $fileName = uniqid($sGroupCode.'-'.$k.'-'.time().'-').$fileName; // 加前缀并提高熵值 file_put_contents($sPathUpload . $fileName, $imageData); $aDataTmp["content"]["img"][$k] = $_ENV["APP_URL"].'/upload/'.$fileName;; } } else { // 包含图片 end $aDataTmp["content"]["base"] = "未处理类型{$sDocumentType}"; } $aDataMix[] = $aDataTmp; } // 包含文档 end // 包含文本 if (isset($aContent["message"]["caption"])) { $sStr = $aContent["message"]["caption"]; $aEntities = $aContent["message"]["caption_entities"] ?? []; foreach ($aEntities as $aEntitiesRow) { $iEntitiesOffset = $aEntitiesRow["offset"] ?? 0; $iEntitiesLen = $aEntitiesRow["length"] ?? 0; $sEntitiesType = $aEntitiesRow["type"] ?? ""; if ($sGroupCode == "ffq_tg_a") { // 按说都改替换,缝补就这样 if ($sEntitiesType == "text_link") { $sEntitiesUrl = $aEntitiesRow["url"] ?? ""; $sLinkText = $this->tg_replace($sStr, "", $iEntitiesOffset, $iEntitiesLen, true); $sLinkHtml = "".$sLinkText.""; $sStr = $this->tg_replace($sStr, $sLinkHtml, $iEntitiesOffset, $iEntitiesLen, false); } } } $aDataTmp = []; $aDataTmp["type"] = "str"; $aDataTmp["name"] = "str"; $aDataTmp["content"]["base"] = $sStr; $aDataMix[] = $aDataTmp; } if (count($aDataMix) == 0) { $aDataTmp = []; $aDataTmp["type"] = "loss"; $aDataTmp["name"] = "loss"; $aDataTmp["content"]["base"] = "提取失败:".json_encode($aContent); $aDataMix[] = $aDataTmp; } return $aDataMix; } private function _schemaWithFrom_ffq_tg_a($aDataMix, $sGroupCode) { $aDataMixNew = []; $aDataMixNew["name"] = ''; $aDataMixNew["title"] = ''; $aDataMixNew["data"] = []; foreach ($aDataMix["data"] as $aDataMixRow) { $sContent = $aDataMixRow["content"]["base"] ?? "⚠️ 没有content?"; $sType = $aDataMixRow['type'] ?? "str"; if ($sType == 'str') { $sRowName = 'str'; // $sContent = str_replace("
", " ", $sContent); $aContent = $this->ffq_tg_str_fetch($sContent); $sArticleName = $aContent["name"] ?? ''; $sStrTiqu = $aContent["tiqu"] ?? ''; $sStrBaoliu = $aContent["baoliu"] ?? ''; $sStrBaoliu = str_replace("翻翻墙", "{{sAuthorName}}", $sStrBaoliu); $sStrBaoliu = str_replace("😀", "{{sIconText}}", $sStrBaoliu); $sStrBaoliu = str_replace("📊", "{{sIconText}}", $sStrBaoliu); if (!$sStrTiqu) { $aDataMixNew["data"][] = [ "type" => "str", "name" => "tag_tiqu", "content" => [ "base" => $sStrTiqu, ], ]; } if ($sArticleName) { $aDataMixNew["name"] = $sArticleName; $aDataMixNew["data"][] = [ "type" => "str", "name" => "schema_name", "content" => [ "base" => $sArticleName, ], ]; } $sStrBaoliu = nl2br($sStrBaoliu); $sStrBaoliu = str_replace(array("\r", "\n"), '', $sStrBaoliu); $tmp = "

"; if (preg_match('/机场自介(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) { $sStr = $matches[1]; $aDataMixNew["data"][] = [ "type" => "str", "name" => "jichangzijie", "content" => [ "title" => "机场自介", "base" => $sStr ], ]; } if (preg_match('/链接导航(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) { $sStr = $matches[1]; $aDataMixNew["data"][] = [ "type" => "str", "name" => "lianjiedaohang", "content" => [ "title" => "链接导航", "base" => $sStr, ], ]; } if (preg_match('/主观点评(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) { $sStr = $matches[1]; $aDataMixNew["data"][] = [ "type" => "str", "name" => "zhuguandianping", "content" => [ "title" => "{{sAuthorName}}主观点评", "base" => $sStr ], ]; } if (preg_match('/主观评分(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) { $sStr = $matches[1]; $aDataMixNew["data"][] = [ "type" => "str", "name" => "zhuguanpingfen", "content" => [ "title" => "{{sAuthorName}}主观评分", "base" => $sStr ], ]; } // if (preg_match('/提醒您(.*?)'.$tmp.'/', $sStrBaoliu, $matches)) { // $sStr = $matches[1]; // $aDataMixNew["data"][] = [ // "type" => "str", // "name" => "tixingnin", // "content" => [ // "title" => "提醒", // "base" => $sStr // ], // ]; // } $aDataMixNew["data"][] = [ "type" => "str", "name" => "tixingnin", "content" => [ "title" => "提醒", "base" => "任何机场都有跑路可能,建议仅月付,以降低风险。(不针对任何机场)", ], ]; //// 本地不爬,上线取消注释 preg_match('/论坛[::]\s*(https?:\/\/[^\s]*?\.html)/u', $sStrBaoliu, $matches); if (isset($matches[1])) { $sUrl = $matches[1]; $aReturn = $this->getTaocan_ffq_tg($sUrl); if (Flow::isFailResult($aReturn)) { $tmp = "⚠️ 套餐table获取失败?"; } $tmp = $aReturn["sData"]; $aDataTmp = []; $aDataTmp["type"] = "str"; $aDataTmp["name"] = "table_taocan"; $aDataTmp["content"]["base"] = $tmp; $aDataMixNew["data"][] = $aDataTmp; } if (str_contains($sStrBaoliu, "评分")) { $sRowName = "str_jieshao"; } $aDataMixNew["data"][] = [ "type" => "str", "name" => $sRowName, "content" => [ "base" => $sStrBaoliu, ], ]; } else if ($sType == 'img') { // $aDataMixNew["images"][] = $aDataMixRow; $aDataMixNew["data"][] = $aDataMixRow; } else { $aDataMixNew["data"][] = $aDataMixRow; } } return $aDataMixNew; } private function ffq_tg_str_fetch($str) { $arr = [ "tiqu" => '', "baoliu" => $str, "name" => '', ]; if (preg_match('/#([^📊]+)📊/u', $str, $matches)) { $arr["tiqu"] = $matches[1]; // 提取内容(不含 # 和 📊) $arr["baoliu"] = preg_replace('/#([^📊]+)📊/u', '', $str); // 去掉提取部分 $tmp = explode("#", $arr["tiqu"]); $arr["name"] = $tmp[0] ?? ''; $arr["name"] = trim($arr["name"]); } return $arr; } private function getTaocan_ffq_tg($url) { $oResult = Flow::make("getTaocan_ffq_tg"); // 初始化 cURL $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 10, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/115.0 Safari/537.36', ]); $html = curl_exec($ch); curl_close($ch); if ($html === false) { return $oResult->step("html=false")->sData("⚠️ 无法读取网页内容")->done(); } // 解析 HTML libxml_use_internal_errors(true); $dom = new \DOMDocument(); $dom->loadHTML($html); libxml_clear_errors(); $xpath = new \DOMXPath($dom); // 找到第一个 class 为 markdown-body 的 div $markdownDiv = $xpath->query('//div[contains(@class, "markdown-body")]')->item(0); if (!$markdownDiv) { return $oResult->step("div loss")->sData("⚠️ 未找到 class 为 'markdown-body' 的 div")->done(); } // 找到该 div 中的第一个 table $table = null; foreach ($markdownDiv->getElementsByTagName('table') as $t) { $table = $t; break; } if (!$table) { return $oResult->step("table loss")->sData("⚠️ 未找到 table 标签")->done(); } $sHtml = $dom->saveHTML($table); return $oResult->sData($sHtml)->done(); } function tg_replace($text, $repl, $offset, $length, $onlyReturn = false) { // 1. 统一转为 UTF-16LE 字节流,对齐 TG 的索引逻辑 $u16 = mb_convert_encoding($text, 'UTF-16LE', 'UTF-8'); // 2. 锁定靶区(TG offset * 2 字节) if ($onlyReturn) { // 模式 A:侦察目标 $target = substr($u16, $offset * 2, $length * 2); return mb_convert_encoding($target, 'UTF-8', 'UTF-16LE'); } // 模式 B:定点清除 $pre = substr($u16, 0, $offset * 2); $suf = substr($u16, ($offset + $length) * 2); $replU16 = mb_convert_encoding($repl, 'UTF-16LE', 'UTF-8'); return mb_convert_encoding($pre . $replU16 . $suf, 'UTF-8', 'UTF-16LE'); } }