吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1863|回复: 31
收起左侧

[Python 原创] [Py]单线程小说爬取器

[复制链接]
LuLuWuWei1120 发表于 2026-2-21 12:05
自制小说网站爬取器,单线程,所以速度较慢,但是胜在稳定可靠。将带有“第一章”“第一节”等字样的网址输入即可爬取,最好是目录页。
供学习参考,若用于其余用途与我无关(゚3゚)~♪

[Python] 纯文本查看 复制代码
import requests
from bs4 import BeautifulSoup
import re
import difflib
from urllib.parse import urlparse, urljoin

# 请求头,模拟浏览器访问
headers = {
    "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"
}

# 辅助函数:检测两段内容是否高度相似,用于重复内容校验
def is_content_similar(content1, content2, threshold=0.95):
    # 去除空白符,避免换行、空格干扰对比结果
    clean_content1 = re.sub(r'\s+', '', content1)
    clean_content2 = re.sub(r'\s+', '', content2)
    
    # 文本长度差异超过10%,直接判定为不相似,提升检测效率
    max_length = max(len(clean_content1), len(clean_content2), 1)
    if abs(len(clean_content1) - len(clean_content2)) / max_length > 0.1:
        return False
    
    # 计算文本相似度,超过阈值则判定为内容一致
    similarity = difflib.SequenceMatcher(None, clean_content1, clean_content2).ratio()
    return similarity >= threshold

# 按小说标准格式提取章节标题,彻底避免抓取网站名
def get_chapter_title(page_text):
    # 匹配通用小说章节格式:第+数字/中文数字+章/节/回/页+可选标题内容
    title_pattern = re.compile(r"第[0-9一二三四五六七八九十百千]+[章节回页]\s*.*", re.I)
    match_result = title_pattern.search(page_text)
    if match_result:
        return match_result.group().strip()
    return "未知章节"

# 获取单章的标题、正文、下一章链接
def get_chapter(url, base_url):
    try:
        resp = requests.get(url, headers=headers, timeout=10)
        resp.encoding = resp.apparent_encoding or "utf-8"
        soup = BeautifulSoup(resp.text, "html.parser")
        page_full_text = soup.get_text()

        # 提取章节标题
        chapter_title = get_chapter_title(page_full_text)

        # 抓取正文内容
        content = ""
        content_div = soup.find("div", id=re.compile("content|chapter|text", re.I))
        if content_div:
            content = content_div.get_text(separator="\n", strip=True)
            content = re.sub(r"\n+", "\n\n", content)

        # 查找下一章链接
        next_url = None
        next_keywords = ["下一章", "下一节", "下一页", "下页", "后页", "→", ">>", "》"]
        for word in next_keywords:
            next_a = soup.find("a", string=lambda s: s and word in str(s).strip())
            if next_a:
                next_url = urljoin(base_url, next_a["href"])
                break

        return chapter_title, content, next_url
    except Exception as e:
        print(f"章节抓取异常:{str(e)}")
        return "异常章节", "", None

# 从目录页查找第一章的入口链接
def find_first_chapter(soup, base_url):
    first_keywords = ["第一章", "第一节", "楔子", "序", "前言"]
    # 优先匹配明确的开头章节
    for word in first_keywords:
        for a in soup.find_all("a"):
            tag_text = a.get_text(strip=True)
            if tag_text and word in tag_text:
                return urljoin(base_url, a["href"])
    # 兜底匹配第N章通用格式
    backup_pattern = re.compile(r"第[0-9一二三四五六七八九十]+[章节]", re.I)
    for a in soup.find_all("a"):
        if backup_pattern.search(a.get_text(strip=True)):
            return urljoin(base_url, a["href"])
    return None

# 主函数
def main():
    index_url = input("请输入小说目录页URL:").strip()
    if not index_url:
        print("请输入有效的URL")
        return

    # 解析网站基础域名
    parsed_url = urlparse(index_url)
    base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"

    # 获取目录页内容
    try:
        index_resp = requests.get(index_url, headers=headers, timeout=10)
        index_resp.encoding = index_resp.apparent_encoding or "utf-8"
        index_soup = BeautifulSoup(index_resp.text, "html.parser")
    except Exception as e:
        print(f"目录页获取失败:{str(e)}")
        return

    # 处理书名,过滤文件名非法字符
    book_title = index_soup.title.get_text(strip=True) if index_soup.title else "小说"
    book_title = re.sub(r'[\\/*?:"<>|]', "", book_title)
    print("书名:", book_title)

    # 找到第一章链接,进入起始页
    current_url = find_first_chapter(index_soup, base_url)
    if not current_url:
        print("未找到第一章入口")
        return

    # 创建保存文件
    filename = f"{book_title}.txt"
    with open(filename, "w", encoding="utf-8") as f:
        f.write(book_title + "\n\n")

    # 初始化检测用变量
    last_chapter_title = ""  # 保存上一章标题,用于重复标题检测
    last_chapter_content = ""  # 保存上一章正文,用于内容一致性检测
    chapter_count = 0  # 章节计数器,用于触发每200章的检测

    print("开始按阅读顺序下载\n")

    # 循环爬取,按下一章顺序推进,保证不乱序
    while current_url:
        chapter_title, content, next_url = get_chapter(current_url, base_url)
        print(f"已下载:{chapter_title}")

        # 写入文件,先做标题重复检测
        with open(filename, "a", encoding="utf-8") as f:
            # 仅当当前章节名与上一章不同时,才写入章节名
            if chapter_title != last_chapter_title:
                f.write(chapter_title + "\n\n")
                
            # 无论标题是否重复,都写入正文内容
            f.write(content + "\n\n\n")

        # 章节计数+1
        chapter_count += 1

        # 每200章触发相邻章节内容一致性检测(如第200&201章、400&401章)
        if chapter_count % 200 == 1 and chapter_count > 1:
            if is_content_similar(last_chapter_content, content):
                print(f"警告:第{chapter_count-1}章与第{chapter_count}章内容高度一致,疑似重复爬取(ctrl+c停止)")

        # 更新上一章的标题和内容,用于下一轮检测
        last_chapter_title = chapter_title
        last_chapter_content = content

        # 推进到下一章
        current_url = next_url

    print("\n全部下载完成")

if __name__ == "__main__":
    main()

免费评分

参与人数 4吾爱币 +8 热心值 +3 收起 理由
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
yd608 + 1 谢谢@Thanks!
jaffa + 1 谢谢@Thanks!
jtjt68 + 1 热心回复!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

qiuyan521 发表于 2026-2-26 09:08
我帮你优化了一下。全局 Session + 自动重试、网络波动、超时不会直接崩。更稳的正文抓取。同时匹配 id 和 class,支持更多网站。自动去广告。
内置广告过滤,正文更干净。
防重复机制增强
连续 3 章重复就自动停止,不会无限死循环。
空章节自动跳过
不会把空白内容写进文件。
防封 IP
可配置爬取间隔,默认 1.5 秒,非常安全。
编码更稳
不乱码、不报错。
下一章匹配更宽松
更容易找到下一章,不会半路断更。
import requests
from bs4 import BeautifulSoup
import re
import difflib
import time
from urllib.parse import urlparse, urljoin
from requests.adapters import HTTPAdapter

# ===================== 配置区(可自行调整)=====================
TIMEOUT = 15          # 请求超时
REQUEST_DELAY = 1.5   # 爬取间隔秒数,防封IP
MAX_RETRY = 3         # 失败重试次数
SIMILAR_THRESHOLD = 0.95  # 内容重复判定阈值
# ===============================================================

headers = {
    "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"
}

# 全局会话(带重试,更稳定)
session = requests.Session()
session.mount('http://', HTTPAdapter(max_retries=MAX_RETRY))
session.mount('https://', HTTPAdapter(max_retries=MAX_RETRY))

# 广告过滤规则
ad_patterns = [
    re.compile(r'请收藏.*?网址.*?', re.I),
    re.compile(r'最新网址.*?', re.I),
    re.compile(r'手机版.*?访问.*?', re.I),
    re.compile(r'广告.*?', re.I),
    re.compile(r'vip会员.*?充值.*?', re.I),
    re.compile(r'『.*?』', re.U),
    re.compile(r'【.*?】', re.U),
]

def clean_ads(text):
    for pat in ad_patterns:
        text = pat.sub('', text)
    return text

# 内容相似度检测
def is_content_similar(content1, content2, threshold=SIMILAR_THRESHOLD):
    clean1 = re.sub(r'\s+', '', content1)
    clean2 = re.sub(r'\s+', '', content2)
    if not clean1 or not clean2:
        return False
    max_len = max(len(clean1), len(clean2))
    if abs(len(clean1)-len(clean2)) / max_len > 0.1:
        return False
    ratio = difflib.SequenceMatcher(None, clean1, clean2).ratio()
    return ratio >= threshold

# 提取章节标题
def get_chapter_title(page_text):
    pattern = re.compile(r"第[0-9一二三四五六七八九十百千]+[章节回页]\s*.*", re.I)
    match = pattern.search(page_text)
    if match:
        return match.group().strip()
    return "未知章节"

# 获取单章内容(优化版)
def get_chapter(url, base_url):
    try:
        resp = session.get(url, headers=headers, timeout=TIMEOUT)
        resp.encoding = resp.apparent_encoding or "utf-8"
        soup = BeautifulSoup(resp.text, "html.parser")

        chapter_title = get_chapter_title(soup.get_text())

        # 更鲁棒的正文匹配:id 或 class
        content = ""
        content_div = soup.find(
            "div",
            attrs={"id": re.compile(r"content|chapter|text|booktext|novel", re.I)}
        )
        if not content_div:
            content_div = soup.find(
                "div",
                attrs={"class": re.compile(r"content|chapter|text|booktext|novel", re.I)}
            )

        if content_div:
            content = content_div.get_text(separator="\n", strip=True)
            content = clean_ads(content)
            content = re.sub(r'\n+', '\n\n', content).strip()

        # 找下一章(更宽松匹配)
        next_url = None
        next_words = ["下一章", "下一节", "下一页", "后一页", "下页", "→", ">>", "》", "下"]
        for word in next_words:
            a_list = soup.find_all("a", string=lambda s: s and word in str(s).strip())
            for a_tag in a_list:
                href = a_tag.get("href")
                if href and href.strip():
                    next_url = urljoin(base_url, href)
                    break
            if next_url:
                break

        return chapter_title, content, next_url

    except Exception as e:
        print(f"抓取异常:{str(e)}")
        return "异常章节", "", None

# 找第一章
def find_first_chapter(soup, base_url):
    first_keywords = ["第一章", "第一节", "楔子", "序", "前言", "第1章"]
    for word in first_keywords:
        for a in soup.find_all("a"):
            t = a.get_text(strip=True)
            if t and word in t:
                return urljoin(base_url, a["href"])
    pat = re.compile(r"第[0-9一二三四五六七八九十]+[章节]", re.I)
    for a in soup.find_all("a"):
        if pat.search(a.get_text(strip=True)):
            return urljoin(base_url, a["href"])
    return None

def main():
    index_url = input("请输入小说目录页URL:").strip()
    if not index_url:
        print("URL不能为空")
        return

    parsed = urlparse(index_url)
    base_url = f"{parsed.scheme}://{parsed.netloc}"

    try:
        resp = session.get(index_url, headers=headers, timeout=TIMEOUT)
        resp.encoding = resp.apparent_encoding or "utf-8"
        soup = BeautifulSoup(resp.text, "html.parser")
    except Exception as e:
        print(f"目录页获取失败:{e}")
        return

    # 清理书名
    book_title = soup.title.get_text(strip=True) if soup.title else "小说"
    book_title = re.sub(r'[\\/*?:"<>|]', "", book_title)
    print("书名:", book_title)

    current_url = find_first_chapter(soup, base_url)
    if not current_url:
        print("未找到第一章")
        return

    filename = f"{book_title}.txt"
    with open(filename, "w", encoding="utf-8") as f:
        f.write(book_title + "\n\n")

    last_title = ""
    last_content = ""
    chapter_count = 0
    repeat_stop_count = 0  # 连续重复计数

    print("开始下载...\n")

    while current_url:
        chapter_title, content, next_url = get_chapter(current_url, base_url)

        # 空内容保护
        if not content:
            print(f"【跳过空内容】{chapter_title}")
            current_url = next_url
            time.sleep(REQUEST_DELAY)
            continue

        # 重复内容自动跳过/停止
        if is_content_similar(last_content, content):
            repeat_stop_count += 1
            print(f"&#9888;&#65039;  检测到重复内容,连续重复 {repeat_stop_count}/3")
            if repeat_stop_count >= 3:
                print("&#128721; 连续重复过多,自动停止")
                break
        else:
            repeat_stop_count = 0

        print(f"已下载:{chapter_title}")

        # 写入文件
        with open(filename, "a", encoding="utf-8") as f:
            if chapter_title != last_title:
                f.write(chapter_title + "\n\n")
            f.write(content + "\n\n\n")

        chapter_count += 1
        last_title = chapter_title
        last_content = content
        current_url = next_url

        # 防爬延迟
        time.sleep(REQUEST_DELAY)

    print("\n&#9989; 全部下载完成!")

if __name__ == "__main__":
    main()
jtjt68 发表于 2026-2-21 15:18
leo121oel 发表于 2026-2-21 19:45
m_h 发表于 2026-2-21 19:47
我现在都是 直接浏览器打开网页读给我听
hozy 发表于 2026-2-21 22:37
现在盗版网站很多都是一章分成了几页,这个会翻页吗?
无名 发表于 2026-2-21 22:41
太麻烦了,直接用阅读app
 楼主| LuLuWuWei1120 发表于 2026-2-22 00:02
hozy 发表于 2026-2-21 22:37
现在盗版网站很多都是一章分成了几页,这个会翻页吗?

我本人测试可以翻页
 楼主| LuLuWuWei1120 发表于 2026-2-22 00:03
无名 发表于 2026-2-21 22:41
太麻烦了,直接用阅读app

可针对小众小说,源中无对应资源
 楼主| LuLuWuWei1120 发表于 2026-2-22 00:04
m_h 发表于 2026-2-21 19:47
我现在都是 直接浏览器打开网页读给我听

这个主要下载到本地,无网时,本地txt文件加本地版tts即可离线听
 楼主| LuLuWuWei1120 发表于 2026-2-22 00:05
jtjt68 发表于 2026-2-21 15:18
随便哪个小说网站都行么

理论上来说是的
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-24 18:12

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表