You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
ehentai-go/1step.go

421 lines
9.6 KiB

package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"fyne.io/fyne/v2/widget"
"golang.org/x/net/html"
"golang.org/x/sync/semaphore"
)
// -------------------- 全局变量 --------------------
var (
illegalChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1F]`)
imgLinkRegex = regexp.MustCompile(`<a href="(.*?)"`)
)
// -------------------- 数据结构 --------------------
type GalleryResult struct {
URL string
OK bool
Error error
}
type CrawlStats struct {
Total int
Success int
Failed int
Skipped int
mu sync.Mutex
}
// -------------------- 工具函数 --------------------
func cleanFolderName(title string) string {
// 移除非法字符
clean := illegalChars.ReplaceAllString(title, "_")
// 移除空格和下划线
clean = strings.ReplaceAll(clean, " ", "")
clean = strings.ReplaceAll(clean, "_", "")
clean = strings.TrimSpace(clean)
if clean == "" {
return "gallery"
}
return clean
}
func loadTargets() ([]string, error) {
file, err := os.Open(TargetsFile)
if err != nil {
if os.IsNotExist(err) {
// 创建空文件
file, err := os.Create(TargetsFile)
if err != nil {
return nil, fmt.Errorf("创建目标文件失败: %v", err)
}
file.Close()
return nil, fmt.Errorf("目标文件不存在,已自动创建,请先填写URL")
}
return nil, err
}
defer file.Close()
var targets []string
seen := make(map[string]bool)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" && !seen[line] {
targets = append(targets, line)
seen[line] = true
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(targets) == 0 {
return nil, fmt.Errorf("目标文件为空,请先填写URL")
}
return targets, nil
}
func loadFailedUrl() ([]string, error) {
data, err := os.ReadFile(FailedRecordUrl)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
var failed []string
err = json.Unmarshal(data, &failed)
if err != nil {
return nil, err
}
return failed, nil
}
func saveFailedUrl(keys []string) error {
data, err := json.MarshalIndent(keys, "", " ")
if err != nil {
return err
}
return os.WriteFile(FailedRecordUrl, data, 0644)
}
func ensureDownloadsDir() error {
return os.MkdirAll(DownloadsDir, 0755)
}
func fetchPage(client *http.Client, url string) (string, error) {
var lastErr error
for attempt := 1; attempt <= RetryPerPage; attempt++ {
resp, err := client.Get(url)
if err != nil {
lastErr = err
log.Printf("[%d/%d] 请求失败 %s -> %v", attempt, RetryPerPage, url, err)
time.Sleep(time.Duration(1<<uint(attempt)) * time.Second)
continue
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
log.Printf("[%d/%d] 请求失败 %s -> %s", attempt, RetryPerPage, url, resp.Status)
time.Sleep(time.Duration(1<<uint(attempt)) * time.Second)
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastErr = err
log.Printf("[%d/%d] 读取响应失败 %s -> %v", attempt, RetryPerPage, url, err)
time.Sleep(time.Duration(1<<uint(attempt)) * time.Second)
continue
}
return string(body), nil
}
return "", lastErr
}
// -------------------- HTML 解析 --------------------
func extractTitle(htmlContent string) string {
doc, err := html.Parse(strings.NewReader(htmlContent))
if err != nil {
return "gallery"
}
var title string
var findTitle func(*html.Node)
findTitle = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "title" {
if n.FirstChild != nil {
title = n.FirstChild.Data
return
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
findTitle(c)
}
}
findTitle(doc)
if title == "" {
return "gallery"
}
return title
}
func extractImageLinks(htmlContent string) []string {
var links []string
matches := imgLinkRegex.FindAllStringSubmatch(htmlContent, -1)
for _, match := range matches {
if len(match) > 1 {
links = append(links, match[1])
}
}
return links
}
// -------------------- 画廊爬取 --------------------
func crawlSingleGallery(client *http.Client, sem *semaphore.Weighted, galleryURL string, stats *CrawlStats) GalleryResult {
// 获取信号量
ctx := context.Background()
if err := sem.Acquire(ctx, 1); err != nil {
return GalleryResult{URL: galleryURL, OK: false, Error: err}
}
defer sem.Release(1)
// 解析基础URL和key
baseURL := strings.TrimRight(galleryURL, "/")
parsed, err := url.Parse(baseURL)
if err != nil {
return GalleryResult{URL: galleryURL, OK: false, Error: err}
}
pathParts := strings.Split(parsed.Path, "/")
key := pathParts[len(pathParts)-1]
jsonName := key + ".json"
var folderPath string
jsonData := make(map[string]string)
imgCount := 1
lastPage := false
for page := 0; page < MaxPage && !lastPage; page++ {
pageURL := baseURL
if page > 0 {
pageURL = fmt.Sprintf("%s?p=%d", baseURL, page)
}
htmlContent, err := fetchPage(client, pageURL)
if err != nil {
log.Printf("获取页面失败 %s: %v", pageURL, err)
continue
}
// 提取标题和创建文件夹
title := extractTitle(htmlContent)
cleanTitle := cleanFolderName(title)
folderPath = filepath.Join(DownloadsDir, cleanTitle)
if err := os.MkdirAll(folderPath, 0755); err != nil {
return GalleryResult{URL: galleryURL, OK: false, Error: err}
}
// 检查JSON文件是否已存在
jsonPath := filepath.Join(folderPath, jsonName)
if _, err := os.Stat(jsonPath); err == nil {
stats.mu.Lock()
stats.Skipped++
stats.mu.Unlock()
log.Printf("%s 已存在,跳过", jsonName)
return GalleryResult{URL: galleryURL, OK: true}
}
log.Printf("当前页码:%d %s", page+1, pageURL)
// 提取图片链接
links := extractImageLinks(htmlContent)
if len(links) == 0 {
log.Printf("本页无图片入口,视为最后一页")
lastPage = true
continue
}
// 处理图片链接
for _, link := range links {
// 检查是否重复(简单的重复检测)
isDuplicate := false
for _, existingLink := range jsonData {
if existingLink == link {
isDuplicate = true
lastPage = true
break
}
}
if isDuplicate {
break
}
jsonData[fmt.Sprintf("%04d", imgCount)] = link
imgCount++
}
}
// 保存JSON文件
if len(jsonData) > 0 {
jsonPath := filepath.Join(folderPath, jsonName)
data, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return GalleryResult{URL: galleryURL, OK: false, Error: err}
}
if err := os.WriteFile(jsonPath, data, 0644); err != nil {
return GalleryResult{URL: galleryURL, OK: false, Error: err}
}
log.Printf("保存成功 -> %s (%d 张)", jsonPath, len(jsonData))
stats.mu.Lock()
stats.Success++
stats.mu.Unlock()
return GalleryResult{URL: galleryURL, OK: true}
} else {
log.Printf("%s 未解析到任何图片链接", key)
stats.mu.Lock()
stats.Failed++
stats.mu.Unlock()
return GalleryResult{URL: galleryURL, OK: false, Error: fmt.Errorf("未解析到图片链接")}
}
}
// -------------------- 主流程 --------------------
func UrlDownloader(ip, port string, output *widget.Entry) {
log.SetFlags(log.LstdFlags | log.Lshortfile)
// 确保下载目录存在
if err := ensureDownloadsDir(); err != nil {
log.Fatalf("创建下载目录失败: %v", err)
}
// 加载目标URL
targets, err := loadTargets()
if err != nil {
log.Fatal(err)
}
// 加载失败记录
failed, err := loadFailedUrl()
if err != nil {
log.Printf("加载失败记录失败: %v", err)
failed = []string{}
}
// 合并URL列表(去重)
allURLs := make([]string, 0)
seen := make(map[string]bool)
// 优先添加失败记录
if len(failed) > 0 {
log.Printf("优先重试上次失败画廊: %d 个", len(failed))
for _, url := range failed {
if !seen[url] {
allURLs = append(allURLs, url)
seen[url] = true
}
}
}
// 添加新目标
for _, url := range targets {
if !seen[url] {
allURLs = append(allURLs, url)
seen[url] = true
}
}
if len(allURLs) == 0 {
log.Println("没有需要处理的URL")
return
}
log.Printf("开始处理 %d 个画廊", len(allURLs))
// 创建HTTP客户端
proxy := ip + port
client := createHTTPClient(proxy)
// 创建信号量控制并发
sem := semaphore.NewWeighted(int64(Concurrency))
stats := &CrawlStats{Total: len(allURLs)}
// 使用WaitGroup等待所有任务完成
var wg sync.WaitGroup
results := make(chan GalleryResult, len(allURLs))
// 启动所有爬取任务
for _, galleryURL := range allURLs {
wg.Add(1)
go func(url string) {
defer wg.Done()
result := crawlSingleGallery(client, sem, url, stats)
results <- result
}(galleryURL)
}
// 等待所有任务完成
wg.Wait()
close(results)
// 收集失败结果
var newFailed []string
for result := range results {
if !result.OK {
newFailed = append(newFailed, result.URL)
log.Printf("画廊处理失败 %s: %v", result.URL, result.Error)
}
}
// 处理失败记录
if len(newFailed) > 0 {
if err := saveFailedUrl(newFailed); err != nil {
log.Printf("保存失败记录失败: %v", err)
} else {
log.Printf("本轮仍有 %d 个画廊失败,已写入 %s", len(newFailed), FailedRecordUrl)
}
} else {
// 删除失败记录文件
if err := os.Remove(FailedRecordUrl); err != nil && !os.IsNotExist(err) {
log.Printf("删除失败记录文件失败: %v", err)
} else {
log.Println("全部画廊抓取完成!")
}
}
// 输出统计信息
log.Printf("统计信息: 总计=%d, 成功=%d, 失败=%d, 跳过=%d",
stats.Total, stats.Success, stats.Failed, stats.Skipped)
}