鱼C论坛

 找回密码
 立即注册
查看: 43|回复: 0

[技术交流] 写了个笔趣阁爬书的小程序

[复制链接]
发表于 昨天 15:09 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
如题。笔趣阁的服务器应该挺多,我这个是“https://www.22biqu.com/”,为了把书爬下来放手机里离线看准备的,每个章节1个TXT,用的时候合并到一起,传手机上就可以了。
  1. """
  2. 笔趣阁爬书小程序
  3. python>=3.8
  4. 依赖: pip install playwright tqdm
  5.      playwright install        # 仅需第一次
  6. Author:shadowmage
  7. """
  8. import os
  9. import re
  10. import time
  11. from urllib.parse import urljoin
  12. from playwright.sync_api import sync_playwright

  13. SAVE_DIR  = "output"

  14. # ---------- 工具 ----------
  15. def sanitize_filename(name):
  16.     return re.sub(r'[\\/*?:"<>|]', "_", name)

  17. def get_last_page(base_url):
  18.     with sync_playwright() as p:
  19.         browser = p.chromium.launch(headless=True)
  20.         ctx = browser.new_context(ignore_https_errors=True)
  21.         page = ctx.new_page()
  22.         page.goto(base_url, timeout=60000)
  23.         last_option = page.locator("#indexselect option:last-child").get_attribute("value")
  24.         browser.close()
  25.         return int(last_option.split("/")[-2])

  26. def get_chapter_links(base_url, last_page):
  27.     links = []
  28.     with sync_playwright() as p:
  29.         browser = p.chromium.launch(headless=True)
  30.         ctx = browser.new_context(ignore_https_errors=True)
  31.         page = ctx.new_page()
  32.         for i in range(1, last_page + 1):
  33.             url = f"{base_url.rstrip('/')}/{i}/"
  34.             print(f"正在获取目录页:{url}")
  35.             page.goto(url, timeout=60000)

  36.             # 只拿第二个 section-box 里的章节
  37.             section = page.locator(".section-box").nth(1)
  38.             section.wait_for()
  39.             lis = section.locator("li").all()

  40.             for li in lis:
  41.                 a = li.locator("a")
  42.                 href = a.get_attribute("href")
  43.                 title = a.inner_text()
  44.                 full_url = urljoin("https://www.22biqu.com", href)
  45.                 links.append((title, full_url))
  46.         browser.close()
  47.     return links

  48. def get_chapter_content(url):
  49.     with sync_playwright() as p:
  50.         browser = p.chromium.launch(headless=True)
  51.         ctx = browser.new_context(ignore_https_errors=True)
  52.         page = ctx.new_page()
  53.         page.goto(url, timeout=60000)
  54.         page.wait_for_selector("#content")
  55.         title = page.locator("h1.title").inner_text()
  56.         content = page.locator("#content").inner_text()
  57.         browser.close()
  58.         return title, content

  59. # ---------- 保存txt ----------
  60. def get_output_dir(base_url: str) -> str:
  61.     """
  62.     根据小说首页返回专属输出目录路径:
  63.     1. 先打开页面抓取书名
  64.     2. 生成合法文件夹名
  65.     3. 确保目录存在并返回绝对路径
  66.     """
  67.     with sync_playwright() as p:
  68.         browser = p.chromium.launch(headless=True)
  69.         ctx = browser.new_context(ignore_https_errors=True)
  70.         page = ctx.new_page()
  71.         page.goto(base_url, timeout=60000)
  72.         # 站点如把书名放在 h1,可按实际改选择器
  73.         book_name = page.locator("h1:not(.logo)").inner_text().strip()
  74.         browser.close()

  75.     safe_name = re.sub(r'[\\/:*?"<>|]', "", book_name) or "book"
  76.     out_dir = os.path.join(SAVE_DIR, safe_name)
  77.     os.makedirs(out_dir, exist_ok=True)
  78.     return out_dir

  79. # ---------- 补漏逻辑 ----------
  80. def repair_mode():
  81.     if not os.path.exists("failed.txt"):
  82.         return []                       # 无失败记录,直接回主流程
  83.     with open("failed.txt", "r", encoding="utf-8") as f:
  84.         tasks = [l.strip().split("\t") for l in f if l.strip()]
  85.     if not tasks:
  86.         return []
  87.     print(f"检测到 failed.txt,共 {len(tasks)} 章需要补漏。")
  88.     still_failed = []
  89.     for idx, (title, url) in enumerate(tasks, 1):
  90.         try:
  91.             print(f"[{idx}/{len(tasks)}] 补漏:{title}")
  92.             chapter_title, content = get_chapter_content(url)
  93.             filename = f"{sanitize_filename(chapter_title)}.txt"
  94.             with open(os.path.join("novel_output", filename), "w", encoding="utf-8") as f:
  95.                 f.write(f"{chapter_title}\n\n{content}")
  96.             time.sleep(15)
  97.         except Exception as e:
  98.             print(f"仍失败:{title},{e}")
  99.             still_failed.append((title, url))
  100.     # 重写失败文件
  101.     if still_failed:
  102.         with open("failed.txt", "w", encoding="utf-8") as f:
  103.             for title, url in still_failed:
  104.                 f.write(f"{title}\t{url}\n")
  105.         print(f"还有 {len(still_failed)} 章未成功,可再次运行本脚本补漏。")
  106.         return still_failed
  107.     else:
  108.         os.remove("failed.txt")
  109.         print("补漏完成,failed.txt 已清除。")
  110.         return []

  111. # ---------- 主流程 ----------
  112. def main():
  113.     base_url = input("请输入小说首页地址:").strip()
  114.     if not base_url.startswith("http"):
  115.         print("请输入完整的网址,以 http 或 https 开头")
  116.         return
  117.     base_url = base_url.rstrip("/") + "/"

  118.     # 1. 先进入补漏模式(如果有 failed.txt)
  119.     failed = repair_mode()
  120.     if failed:
  121.         print("请先处理完失败章节再执行完整下载。")
  122.         return

  123.     # 2. 正常流程:获取全部目录
  124.     last_page = get_last_page(base_url)
  125.     print(f"检测到目录共 {last_page} 页")
  126.     chapters = get_chapter_links(base_url, last_page)
  127.     print(f"共获取到 {len(chapters)} 个章节")
  128.     out_dir = get_output_dir(base_url)

  129.     failed = []
  130.     for idx, (title, url) in enumerate(chapters, 1):
  131.         try:
  132.             print(f"[{idx}/{len(chapters)}] 正在下载:{title}")
  133.             chapter_title, content = get_chapter_content(url)
  134.             filename = f"{idx:03}_{sanitize_filename(chapter_title)}.txt"
  135.             with open(os.path.join(out_dir, filename), "w", encoding="utf-8") as f:
  136.                 f.write(f"{chapter_title}\n\n{content}")
  137.             time.sleep(15)
  138.         except Exception as e:
  139.             print(f"下载失败:{title},{e}")
  140.             failed.append((title, url))

  141.     # 3. 把失败记录落盘
  142.     if failed:
  143.         with open("failed.txt", "w", encoding="utf-8") as f:
  144.             for title, url in failed:
  145.                 f.write(f"{title}\t{url}\n")
  146.         print(f"共有 {len(failed)} 章下载失败,已写入 failed.txt,下次运行本脚本将自动补漏。")
  147.     else:
  148.         print("全部下载完成!")

  149. if __name__ == "__main__":
  150.     main()
复制代码
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-9-8 12:45

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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