dd/app/Services/Cron/ShadowRocketCrawl.php
2026-08-12 14:00:47 +08:00

275 lines
8.5 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Services\Cron;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Pool;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Symfony\Component\DomCrawler\Crawler;
use App\Models\ShadowRocketIds;
use Illuminate\Support\Carbon;
use App\Services\TomTool\Telegram\Slave as TeleSlave;
final class ShadowRocketCrawl
{
private const ARRAY_TARGET_URLS = [
'https://proxygo.org/freeappleid/',
'https://proxygo.org/1-freeappleid/',
];
/**
* 执行多页面并发账号爬取任务并合并去重
*
* @return array
*/
public static function run()
{
echo "/n/n ShadowRocketCrawl::run";
try {
$aResponses = Http::pool(fn (Pool $oPool) => array_map(
fn (string $sUrl) => $oPool->as($sUrl)
->withHeaders([
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'zh-CN,zh;q=0.9,en;q=0.8',
])
->withOptions([
'curl' => [
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
],
])
->connectTimeout(10)
->timeout(30)
->retry(3, 1000)
->get($sUrl),
self::ARRAY_TARGET_URLS
));
} catch (\Throwable $oException) {
TeleSlave::warn("ShadowRocketCrawl:并发请求执行异常:" . $oException->getMessage());
return [];
}
$aAllAccountList = [];
foreach (self::ARRAY_TARGET_URLS as $sUrl) {
$oResponse = $aResponses[$sUrl] ?? null;
if ($oResponse instanceof \Throwable) {
TeleSlave::warn("ShadowRocketCrawl:请求网络异常:" . $sUrl . ",错误: " . $oResponse->getMessage());
continue;
}
if (!$oResponse || $oResponse->failed()) {
$iStatusCode = $oResponse ? $oResponse->status() : 0;
TeleSlave::warn("ShadowRocketCrawl:请求页面失败:" . $sUrl . ",状态码: " . $iStatusCode);
continue;
}
$sPageContent = $oResponse->body();
if (empty(trim($sPageContent))) {
TeleSlave::warn("ShadowRocketCrawl:请求页面内容为空:" . $sUrl);
continue;
}
$aParsedAccounts = self::parseHtmlToAccounts($sPageContent);
$aAllAccountList = array_merge($aAllAccountList, $aParsedAccounts);
}
$arr = self::deduplicateAccounts($aAllAccountList);
$json = json_encode($arr);
$sTodayDate = date('Y-m-d');
$oShadowRocketRecord = ShadowRocketIds::whereDate('created_at', $sTodayDate)->first()
?? new ShadowRocketIds();
if (empty($json)) {
$json = "[]";
}
$oShadowRocketRecord->arr = $json;
$oShadowRocketRecord->save();
echo "\n ok";
}
/**
* 按邮箱对爬取的账号进行去重
*
* @param array $aAccountList
* @return array
*/
private static function deduplicateAccounts(array $aAccountList): array
{
$aUniqueAccounts = [];
foreach ($aAccountList as $aAccount) {
$sEmail = $aAccount['sEmail'] ?? '';
if (!empty($sEmail) && !isset($aUniqueAccounts[$sEmail])) {
$aUniqueAccounts[$sEmail] = $aAccount;
}
}
return array_values($aUniqueAccounts);
} // 好像因为这里成为0
/**
* 解析 HTML 并提取账号列表(包含地区与检测时间)
*
* @param string $sHtmlContent
* @return array
*/
private static function parseHtmlToAccounts(string $sHtmlContent): array
{
$aAccountList = [];
$oCrawler = new Crawler($sHtmlContent);
$oCrawler->filter('.posts-item')->each(function (Crawler $oNode) use (&$aAccountList) {
$sEmail = self::extractEmailFromNode($oNode);
$sPassword = self::extractPasswordFromNode($oNode);
$sRegion = self::extractRegionFromNode($oNode);
$sCheckTime = self::extractCheckTimeFromNode($oNode);
if (!empty($sEmail) && !empty($sPassword)) {
$aAccountList[] = [
'sEmail' => $sEmail,
'sPassword' => $sPassword,
'sRegion' => $sRegion,
'sCheckTime' => $sCheckTime,
];
}
});
return $aAccountList;
}
/**
* 从单个卡片节点中精准提取完整 Email
*
* @param Crawler $oNode
* @return string
*/
private static function extractEmailFromNode(Crawler $oNode): string
{
$oFullLinkNode = $oNode->filter('.item-tags a.external');
if ($oFullLinkNode->count() > 0) {
$sHref = $oFullLinkNode->attr('href') ?? '';
if (str_contains($sHref, '#')) {
$aParts = explode('#', $sHref);
$sDecoded = self::decodeCloudflareEmail($aParts[1] ?? '');
if (self::isValidEmail($sDecoded)) {
return $sDecoded;
}
}
}
$oTagEmailNode = $oNode->filter('.item-tags [data-cfemail]');
if ($oTagEmailNode->count() > 0) {
$sHex = $oTagEmailNode->attr('data-cfemail') ?? '';
$sDecoded = self::decodeCloudflareEmail($sHex);
if (self::isValidEmail($sDecoded)) {
return $sDecoded;
}
}
$oAllCfNodes = $oNode->filter('[data-cfemail]');
foreach ($oAllCfNodes as $oDomElement) {
$sHex = $oDomElement->getAttribute('data-cfemail');
$sDecoded = self::decodeCloudflareEmail($sHex);
if (self::isValidEmail($sDecoded)) {
return $sDecoded;
}
}
return '';
}
/**
* 从单个卡片节点中提取密码
*
* @param Crawler $oNode
* @return string
*/
private static function extractPasswordFromNode(Crawler $oNode): string
{
$oBtnNodes = $oNode->filter('button[copy]');
foreach ($oBtnNodes as $oDomElement) {
$sCopyVal = trim($oDomElement->getAttribute('copy'));
if (!empty($sCopyVal)) {
return $sCopyVal;
}
}
return '';
}
/**
* 从单个卡片节点中提取账号地区(如:美区、日本、中国大陆)
*
* @param Crawler $oNode
* @return string
*/
private static function extractRegionFromNode(Crawler $oNode): string
{
$oSpans = $oNode->filter('h5 span');
if ($oSpans->count() >= 2) {
$sRawText = trim($oSpans->eq(1)->text(''));
return trim(str_replace(['【', '】'], '', $sRawText));
}
return '';
}
/**
* 从单个卡片节点中提取检测时间
*
* @param Crawler $oNode
* @return string
*/
private static function extractCheckTimeFromNode(Crawler $oNode): string
{
$oTimeNode = $oNode->filter('.text-muted');
if ($oTimeNode->count() > 0) {
$sRawText = trim($oTimeNode->text(''));
return trim(str_replace('检测时间:', '', $sRawText));
}
return '';
}
/**
* 解密 Cloudflare XOR 加密邮箱
*
* @param string $sEncodedHex
* @return string
*/
private static function decodeCloudflareEmail(string $sEncodedHex): string
{
if (empty($sEncodedHex) || strlen($sEncodedHex) < 4 || !ctype_xdigit($sEncodedHex)) {
return '';
}
$iKey = hexdec(substr($sEncodedHex, 0, 2));
$sDecodedEmail = '';
$iLength = strlen($sEncodedHex);
for ($iIndex = 2; $iIndex < $iLength; $iIndex += 2) {
$sDecodedEmail .= chr(hexdec(substr($sEncodedHex, $iIndex, 2)) ^ $iKey);
}
return trim($sDecodedEmail);
}
/**
* 校验邮箱合法性
*
* @param string $sEmail
* @return bool
*/
private static function isValidEmail(string $sEmail): bool
{
return !empty($sEmail) && filter_var($sEmail, FILTER_VALIDATE_EMAIL) !== false;
}
}