=====================================================
关于使用txt搜索功能的说明
=====================================================

关于搜索功能不能用这点困扰我一段时间了，所以我前两天让AI写了一个爬虫脚本，把网站耽美区和言情区所有的txt的链接和书名都抓取下来，大家就可以直接通过全文搜索，找到你想要的txt的下载链接。

因为我是电脑端，所以在一开始给大家说明的时候有很多固化思维，也看到大家版聊反馈的一些问题，没考虑到手机使用的很多差异，感谢很多姐妹在帮忙义务解答问题（爱你们！啾咪！），我就斗胆综合一下大家的反馈写一个整体的说明。

首先，我要声明一下，这不是一个常规方法，我不鼓励任何人写爬虫，用爬虫。通过爬虫爬取任何网站的信息都是有风险的，会给网站带来很大的流量负担！

所以，这个方法是为了解决搜索功能下线的问题，两相其害取其轻，我并不想因为这个方法给网站制造新的问题！

以上，感谢。步骤说明开始：

=====================================================

1. 下载txt文件

- 如果是我上传的文件，名字一般是“站内截止YYYY-MM-DD X点耽美全量txt链接csv文件.txt”或者“站内截止YYYY-MM-DD X点言情全量txt链接csv文件.txt”
- 如果当前页面找不到，可以去“24小时热榜”或者“周热门”里面去找，这个文件下载的人比较多，基本上都会被顶到前排
- 你也可以下载其他姐妹上传的txt文件，内容应该是一样的

内容长这样：
文件名,链接,上传时间,文件大小
那个链接复制打开后显示已失效是怎么回事啊，你们有这个问题吗。本文《电竞方程式》.txt,https://www.asw227.com/file.php?hash=bcf366dc31c27e893129a215299fc265,08-19 21:45,2.43 MB
《他与它》（精校版全本+番外完）求昏君，但只有5岁的最新章节.txt,https://www.asw227.com/file.php?hash=e3a0400f131234a838bf68cd443fba0c,08-19 21:39,2.53 MB


2. 打开与检索

💻【电脑端使用教程（推荐修改后缀为 csv）】
- 修改后缀：找到下载好的 txt 文件，右键点击“重命名”，把文件名末尾的 `.txt` 改成 `.csv`（若系统提示“修改扩展名可能导致文件不可用”，点击“是”确认即可）。
- 打开文件：使用 Excel 或 WPS 打开重命名后的 `.csv` 表格文件。
- 检索方法：
  1. 按快捷键 `Ctrl + F` 打开查找窗口。
  2. 输入想要搜索的书名、作者或关键词，点击“查找下一个”或“查找全部”。
  3. 表格内会清晰列出【文件名】、【链接】、【上传时间】、【文件大小】。复制对应的链接粘贴到浏览器地址栏打开即可下载。

  【注意】修改文件名后缀不是必须，只是会让你在电脑端拥有更好的搜索体验，你在电脑上仍然可以直接打开txt，在txt内ctrl + F 直接搜索

📱【手机端使用教程（不建议改后缀，直接 txt 全文检索）】
- 无需改后缀：手机端保持 `.txt` 后缀即可，直接打开更方便。
- 打开文件：直接使用手机自带的“文本阅读器”、“电子书”或“文件管理器”打开 txt 文件。
- 检索方法：
  1. 点击阅读器/编辑器中的“查找”或“搜索”功能按键。
  2. 输入想要搜索的书名或关键词。
  3. 找到目标条目后，长按选中并复制后面的下载链接，粘贴到手机浏览器中打开即可下载。

  【常见问题】为什么复制链接出来在浏览器打开会提示文件不存在？
  【答】宝宝，很可能是因为你复制的时候多了个逗号，或者少复制了一两个字母，所以导致链接不对哦。建议再去仔细复制一遍呢

  【常见问题】为什么我把后缀改成 .csv 后，用 Excel 打开里面的中文字全是乱码？
  【答】 因为 Excel 的编码识别有点笨。最简单的解决办法是用 WPS 打开（WPS聪明一点，不会乱码）。

如果姐妹只想了解搜索功能怎么用，那你看到这里就可以了，后面的不用看。

=====================================================
关于站内搜索功能的想法
=====================================================

我自己有开发经验，在我的理解中，搜索其实是个很不好做的功能，如果算法写不好，每次搜索给服务器带来的负担都很大（比爬虫还大！）

当前的网站嘛……php程序，本来就不怎么优化（能用就不错了），在搜索负担大的情况下，将功能暂时下线其实是个正常的处理方法，不然服务器会三天两头挂掉（除非出钱去升级服务器性能，但如果我是站长我不会这么干，因为划不来）

以上都是个人猜测，正是基于这个猜测，我才弄了一个爬虫出来（因为我自己想找文，搜索是我的命根子！）

通过txt来搜索其实是把堆在服务器的搜索运算分流到了我们每个人自己的手机或者电脑里面，对于我们用户来说肯定会麻烦很多

而且这种方法会带来问题，txt链接文件是需要持续更新的，不然一个月之后过去了，txt文件里面没有收录过往一个月新增的txt链接，那搜索功能还是不好用

但我不建议时不时就去更新一下，就像上面我多次说过，能别用爬虫还是不要用，我觉得一周更新个2次就可以，而且更新的频次多了，网站上的无关内容也变多了，不利于大家找文

旧文大家可以通过txt来搜索，新文就麻烦大家翻个两三页看看吧

鉴于我不一定能保持稳定更新，所以我把如何更新的方法写在了下面，有技术力的姐妹可以尝试一下自己弄

弄完记得把文件后缀改成txt才能上传哦

如果我还有空，那我会尽量保证3天左右上传一次文档的，咳咳，周末是必然不会上传的

爱你们(*￣3￣)╭♡

P.S. 其实网站上方有个“重要提示”的红框框，里面有搜索功能。但我自己对比了一下，里面只有完结文，文库跟现在网站里面的不一样，如果想搜本站的txt，还是老老实实用文件来搜索吧
P.S. 爬虫脚本我限制了单任务爬取的最多页数，如果超过1111页会直接退出，也是为了效率着想哈，以防万一抓太多崩掉了，不过网站要到这个页数还要好久呢，暂时也不用担心这个问题

=====================================================
小白教程：如何使用“自动更新下载器”获取最新链接
=====================================================

姐妹们好！如果你拿到了一份叫 `incremental_scraper.py` 的文件，却完全不知道怎么用，不要慌！
按照这篇教程一步步做，不需要懂任何一行代码，电脑就可以自动帮你把最新的资源链接整理成csv文件！

---

【第一步：给电脑安装 Python 环境】
这个脚本是用 Python 写的，所以电脑里必须有 Python 才能运行

🍎 如果你是苹果 Mac 系统：
1. Mac 电脑通常自带 Python，你可以先跳过安装，直接看下方的【如何确认安装成功】。如果没有，请去 https://www.python.org/downloads/mac-osx/ 下载 macOS 安装包并一直点击“下一步”安装。

🪟 如果你是 Windows 系统：
1. 浏览器打开官网：https://www.python.org/downloads/
2. 点击那个大大的黄色按钮 "Download Python 3.xx" 下载安装包。
3. 双击下载好的安装包。
   ★★★ 最重要的一步 ★★★
   在弹出来的安装界面【最底端】，有一个叫 `Add Python 3.xx to PATH` 的选项，一定要打上勾（打上勾！打上勾！）。
4. 勾选后，点击 `Install Now`，等它装完点击 Close 就可以了。

✅ 【如何确认自己的 Python 已经安装成功？】
- Windows 系统：按下键盘上的 `Win + R` 键，输入 `cmd` 后回车。在黑框里输入 `python --version` 并回车。如果出现 `Python 3.x.x` 的字样，说明安装成功！
- Mac 系统：按下 `Command + 空格` 键，搜索 `Terminal`（或“终端”）并打开。在里面输入 `python3 --version` 并回车。如果出现 `Python 3.x.x` 的字样，说明安装成功！

---

【第二步：准备一个工作环境】
1. 在你的电脑桌面（或者其他你喜欢的地方）新建一个文件夹，随便起个名字，比如叫“资源更新站”。
2. 把你拿到的 `incremental_scraper.py` 文件，放进这个文件夹里。
3. 如果你手里之前已经有了旧的表格文件（以 .csv 结尾，比如以前爬的几万条数据的表格），也请把它们原封不动地**一起放进这个文件夹**。
   （如果没有旧表格也没关系，程序会自动帮你从头开始下载，只是第一次会花点时间。）

---

【第三步：如何运行？（两种超简单的方法）】

方法一：使用 AI 助手（强烈推荐，适合零基础）
如果你平时在用 WorkBuddy、Trae、豆包（电脑端的工作任务模式）或者其它带代码运行功能的 AI 软件：
1. 打开你的 AI 软件。
2. 用软件打开你刚才新建的“资源更新站”文件夹。
3. 直接给 AI 发这样一句话：
   “我是一个小白，请帮我配置一下 Python 的依赖环境（aiohttp, beautifulsoup4, pandas），然后帮我运行这里的 incremental_scraper.py 脚本。”
4. 接下来你什么都不用管，AI 会全自动帮你安装必需的插件，并帮你跑起来，你只要看着它工作就行啦！

方法二：手动运行（如果你没装上述 AI 软件）

🪟 Windows 系统的操作：
1. 打开你新建的“资源更新站”文件夹。
2. 在文件夹顶部的“路径地址栏”（就是显示文件夹位置的地方），把里面的字删掉，输入 `cmd`，然后按回车。此时会弹出一个黑色的框框。
3. 在黑框里复制粘贴下面这行命令，然后按回车（这是在给脚本安装必要的翻译插件）：
   pip install aiohttp beautifulsoup4 pandas
4. 等屏幕上的字刷完停下来后，再复制粘贴下面这行命令，按回车（这是正式启动脚本）：
   python incremental_scraper.py

🍎 Mac 苹果系统的操作：
1. 找到你新建的“资源更新站”文件夹。
2. 在这个文件夹上点击右键，选择“服务” -> “新建当前位置的终端窗口”（或者打开终端 App，输入 `cd ` 后把文件夹拖进去按回车）。
3. 在终端里复制粘贴下面这行命令，按回车：
   pip3 install aiohttp beautifulsoup4 pandas
   
【提示】 如果你在输入这行命令后，屏幕出现了一大段包含 error 甚至 externally-managed-environment 的红字报错，不要慌！这是较新的苹果系统增加了安全限制。建议你直接放弃“方法二”，退回去使用“方法一：使用 AI 助手”，AI 老师会自动帮你绕过这个限制

4. 等屏幕上的字停下来后，复制粘贴这行命令，按回车：
   python3 incremental_scraper.py


5. 成功！你现在就可以看着屏幕上的日志不断跳动了。

---

【我该怎么判断成功了？】
- 如果文件夹里有旧的 csv 表格：程序运行会非常非常快！几秒钟后，它会提示“增量抓取完成，提前停止”，这说明它只下载了最近几天更新的内容，并且合并到了你的旧表格里。
- 如果文件夹里是空的：程序会勤勤恳恳地从第1页爬到最后1页，请耐心等待它跑完（可能需要几分钟），跑完后你的文件夹里会多出两个 `.csv` 的表格文件。

只要把生成的 `.csv` 表格用 Excel 或者 WPS 打开，你就能看到整理得整整齐齐的文件名、链接、时间和大小啦！
有任何不懂的，把所有内容都丢给D老师或者豆老师，让它们来给你答疑解惑，它们超耐心的！

=====================================================
小白教程：如何获取 incremental_scraper.py 文件？

1. ctrl + c 复制====分割线====后面的所有内容
2. 新建一个txt文件，把已复制的内容粘贴进去
3. 保存并关闭txt文件，“xxx.txt”文件改名为“incremental_scraper.py”（注意前后不要有空格或多余的标点）

（你把txt文件后缀改成py后缀的时候，系统会提示你文件可能不可用，没关系，点击确认！）
=====================================================
=================incremental_scraper.py==============
========================分割线=======================

import asyncio
import aiohttp
from bs4 import BeautifulSoup
import pandas as pd
import logging
import time
import os
import glob
from urllib.parse import urlparse
import re

# 设置日志格式，方便在终端看到运行进度和提示信息
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

CONCURRENCY_LIMIT = 5 # 异步请求的并发数限制（同时最多发起5个请求）
BATCH_SIZE = 5        # 每次增量探查的页面批次大小（每批抓取5页）

def find_target_csv(base_url):
    """
    遍历当前目录下所有的 csv 文件，
    通过读取前几行的“链接”字段，自动寻找与当前正在抓取的网站相匹配的存量数据表。
    """
    csv_files = glob.glob('*.csv')
    for file in csv_files:
        try:
            # 只读取前5行来快速试探，节省内存和时间
            df_head = pd.read_csv(file, nrows=5)
            if '链接' in df_head.columns:
                first_link = df_head['链接'].iloc[0]
                # 如果这个表格里的第一个链接是以当前正在抓取的网址开头的，说明找对文件了
                if str(first_link).startswith(base_url):
                    return file
        except Exception as e:
            continue
    return None

async def fetch_page(session, base_url, page_num, semaphore):
    """
    异步获取指定页码的 HTML 内容。
    """
    url = f"{base_url}/index.php?page={page_num}"
    async with semaphore:
        try:
            async with session.get(url, timeout=15) as response:
                if response.status == 200:
                    html = await response.text()
                    return page_num, html
                else:
                    logging.warning(f"第 {page_num} 页返回了异常状态码: {response.status}")
                    return page_num, None
        except Exception as e:
            logging.error(f"抓取第 {page_num} 页时出错: {e}")
            return page_num, None

def parse_html(html, base_url, page_num):
    """
    使用 BeautifulSoup 解析 HTML，提取文件名、链接、上传时间和文件大小。
    """
    data = []
    if not html:
        return data
        
    soup = BeautifulSoup(html, "html.parser")
    # 找到所有包含文件信息的卡片 div
    cards = soup.find_all('div', class_='file-card')
    
    for card in cards:
        try:
            # 获取链接所在的 a 标签
            a_tag = card.find('a', class_='file-card-link')
            if not a_tag:
                continue
                
            href = a_tag.get('href')
            # 如果是相对路径，则补全为绝对网址路径
            link = base_url + href if href.startswith('/') else href
            
            # 提取文件名
            name_div = card.find('div', class_='file-name')
            name = name_div.text.strip() if name_div else "N/A"
            
            # 提取详细信息（大小和时间）
            details = card.find('div', class_='file-details')
            size, upload_time = "N/A", "N/A"
            if details:
                spans = details.find_all('span', class_='detail-item', recursive=False)
                if len(spans) >= 3:
                    # 去除多余的图标字符（📦 和 🕒）并清除前后空格
                    size = spans[1].text.replace('📦', '').strip()
                    upload_time = spans[2].text.replace('🕒', '').strip()
                    
            data.append({
                '文件名': name,
                '链接': link,
                '上传时间': upload_time,
                '文件大小': size
            })
        except Exception as e:
            logging.error(f"解析第 {page_num} 页的某个文件卡片时出错: {e}")
            
    return data

async def get_max_pages(session, base_url):
    """动态请求第一页并获取网站的最大页码"""
    try:
        async with session.get(base_url, timeout=15) as response:
            if response.status == 200:
                html = await response.text()
                soup = BeautifulSoup(html, "html.parser")
                last_page_a = soup.find('a', string='尾页')
                if last_page_a:
                    href = last_page_a.get('href', '')
                    match = re.search(r'page=(\d+)', href)
                    if match:
                        return int(match.group(1))
            return 9999 # 默认安全极大值
    except Exception as e:
        logging.error(f"获取 {base_url} 最大页码失败: {e}")
        return 9999

async def incremental_scrape(base_url, prefix=""):
    """
    主增量抓取逻辑：
    1. 自动定位旧 CSV 文件，建立“已知链接库”。
    2. 按批次（每批5页）往下抓，一旦遇到已知链接就停止。
    3. 合并新旧数据，全局去重后覆盖保存。
    """
    logging.info(f"=== 开始对 {base_url} 进行增量抓取 ===")
    start_time = time.time()
    
    # 1. 尝试寻找存量的 CSV 文件并加载已有链接
    target_csv = find_target_csv(base_url)
    existing_links = set()
    df_existing = None
    
    if target_csv:
        logging.info(f"成功找到存量数据文件: {target_csv}")
        df_existing = pd.read_csv(target_csv)
        if '链接' in df_existing.columns:
            # 将旧文件里所有的链接提取出来，存入一个集合（Set）里，用于后续的高速查重
            existing_links = set(df_existing['链接'].dropna().astype(str))
        logging.info(f"共加载了 {len(existing_links)} 条已知链接。")
    else:
        # 如果当前目录下没找到对应的 CSV，说明是第一次运行，我们自动生成一个默认文件名
        domain = urlparse(base_url).netloc
        target_csv = f"{prefix}{domain.replace('www.', '').replace('.', '_')}_files.csv"
        logging.info(f"未找到存量文件，本次将进行全量抓取，并将新建文件: {target_csv}")
    
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
    }
    
    all_new_data = []
    semaphore = asyncio.Semaphore(CONCURRENCY_LIMIT)
    connector = aiohttp.TCPConnector(limit=CONCURRENCY_LIMIT)
    
    current_page = 1
    stop_scraping = False
    
    async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
        # 动态获取最大页码
        max_pages = await get_max_pages(session, base_url)
        logging.info(f"动态识别到 {base_url} 的最大页数为: {max_pages}")
        
        if max_pages > 1111:
            logging.error(f"最大页数 {max_pages} 超过了安全限制 1111 页，脚本直接退出，拒绝执行！")
            return
        
        # 只要还没超出最大页数，且没有触发停止条件，就继续抓
        while current_page <= max_pages and not stop_scraping:
            # 准备要抓取的一小批页码（比如第 1 到 5 页）
            end_page = min(current_page + BATCH_SIZE - 1, max_pages)
            logging.info(f"正在抓取批次: 第 {current_page} 页 到 第 {end_page} 页...")
            
            tasks = [fetch_page(session, base_url, p, semaphore) for p in range(current_page, end_page + 1)]
            
            batch_data = []
            for future in asyncio.as_completed(tasks):
                page_num, html = await future
                if html:
                    page_data = parse_html(html, base_url, page_num)
                    batch_data.extend(page_data)
            
            # 检查这刚刚抓下来的一批数据中，是否有链接已经在我们的“已知链接库”里了
            overlap_found = False
            for item in batch_data:
                if item['链接'] in existing_links:
                    overlap_found = True
                    break
            
            all_new_data.extend(batch_data)
            
            # 如果发现了重复链接，说明我们已经“摸”到了上次抓取的旧数据，可以直接叫停了
            if overlap_found:
                logging.info(f"在批次 {current_page}-{end_page} 中发现了历史已存链接，增量抓取完成，提前停止！")
                stop_scraping = True
            else:
                # 否则继续往下推一批页码
                current_page = end_page + 1

    logging.info(f"本次运行共新抓取到了 {len(all_new_data)} 条数据（含边界重复数据）。")
    
    # 2. 合并新旧数据，并进行精确去重
    df_new = pd.DataFrame(all_new_data)
    
    if df_existing is not None and not df_existing.empty:
        # 把新抓到的数据和旧数据拼接到一起
        df_combined = pd.concat([df_new, df_existing], ignore_index=True)
    else:
        df_combined = df_new
        
    if not df_combined.empty:
        initial_count = len(df_combined)
        # 按照“链接”进行全局去重，只保留第一条（因为新数据排在前面，所以保留的是最新抓取的）
        df_combined.drop_duplicates(subset=['链接'], keep='first', inplace=True)
        final_count = len(df_combined)
        logging.info(f"去重清理: 共移除了 {initial_count - final_count} 条重复数据。")
        
        # 将最新的干净数据覆盖保存到目标 CSV 文件中
        df_combined.to_csv(target_csv, index=False, encoding='utf-8-sig')
        logging.info(f"数据已成功保存至 {target_csv}. 总耗时: {time.time() - start_time:.2f} 秒")
    else:
        logging.info("本次没有获取到任何需要保存的数据。")

async def main():
    # 定义需要增量抓取的网站列表和默认文件名前缀
    sites = [
        {"url": "https://www.asw227.com", "prefix": "danmei_"},
        {"url": "https://www.asw7777.com", "prefix": "yanqing_"}
    ]
    for site in sites:
        await incremental_scrape(site["url"], site.get("prefix", ""))

if __name__ == "__main__":
    asyncio.run(main())
