<think>我们注意到在代码的最后部分,有一个`while True`循环,但是在这个循环之前,代码中有一个`try`块,但对应的`except`块在循环内部,这会导致语法错误。
此外,代码在用户输入部分(选择下载方式)还没有完成,我们需要重构这部分代码。
根据错误信息:`SyntaxError: expected 'except' or 'finally' block`,这是因为`try`块没有对应的`except`或`finally`块。
观察代码,在`for chapter_id in chapter_id_list:`循环内部有一个`try`,然后在循环内部有一个`except`,这是正确的。
但是,在循环之后,代码直接进入了一个`while True`循环,并且在这个循环之前没有`try`,所以错误可能是由于代码结构混乱导致的。
让我们重新组织代码:
1. 首先,我们爬取所有章节的链接,并存储章节ID列表(chapter_id_list)以及构建一个快捷字典(Kuaijie)用于用户选择下载。
2. 然后,我们提供一个菜单让用户选择下载方式:单章下载、部分下载、整本下载。
3. 根据用户的选择,执行相应的下载任务。
但是,原代码在爬取章节内容时,已经尝试下载了整本书,然后又在后面提供了菜单,这显然是不合理的。
因此,我们需要将代码分为两部分:
第一部分:获取书籍的基本信息(书名、所有章节的链接和标题)
第二部分:根据用户选择下载章节
修改计划:
1. 先获取整本书的章节ID列表,并构建一个字典(Kuaijie)将序号映射到章节标题,同时构建一个字典(BIaoTi_url)将标题映射到URL。
2. 然后,我们进入一个循环,显示菜单,让用户选择下载方式,直到用户选择退出。
注意:在获取章节标题时,如果某个章节标题获取失败,我们将其记录到CuoWu列表中,并跳过,然后继续。在后续的下载中,用户可以选择重新下载这些失败的章节。
由于原代码在获取章节标题时已经尝试下载了内容,但我们希望先获取所有章节的标题,然后再根据用户选择下载内容,所以我们需要将下载内容的代码封装成一个函数。
重构步骤:
1. 定义一个函数`download_chapter(chapter_url, title, valid_name)`,用于下载单个章节并保存到文件。
2. 首先,我们只获取章节ID和标题,不下载内容(除非用户选择整本下载,我们才遍历所有章节ID进行下载)。
3. 构建章节标题和URL的映射,以及序号和标题的映射(用于用户选择)。
但是,原代码在遍历章节ID时已经尝试下载了,这可能会造成不必要的下载(如果用户只需要部分章节)。因此,我们修改为:
- 第一次请求:只获取章节的标题,构建映射关系,不下载内容。
- 然后根据用户选择,再决定下载哪些章节。
这样,我们可以避免重复请求(因为之前已经请求过一次获取标题),但是为了减少请求次数,我们可以将第一次请求获取的标题和内容缓存下来(如果用户选择整本下载,那么这些内容可以直接使用)。但是,如果用户只选择部分章节,我们可能还需要重新请求(因为第一次请求只获取了标题,没有内容?不,第一次请求我们同时获取了标题和内容,但是原代码在第一次请求时已经下载了内容并保存了文件)。这不符合按需下载的原则。
因此,调整:
第一次循环:只获取每个章节的标题,不下载内容。这样我们可以快速构建章节列表。
然后,根据用户选择,再下载指定章节的内容。
但是,获取标题需要请求每个章节的页面,这样相当于每个章节至少请求两次(一次获取标题,一次获取内容)。为了减少请求,我们可以在第一次请求时,将获取的内容缓存起来,这样在下载时就不需要再次请求。
但是,考虑到内存问题,如果章节非常多,缓存所有章节内容可能不现实。因此,我们可以选择:
方案A:第一次只获取标题,不缓存内容,然后下载时再请求一次(两次请求)。
方案B:第一次请求时,同时获取标题和内容,并缓存内容,然后根据用户选择将内容写入文件(一次请求,但内存占用大)。
由于章节内容可能比较大,我们选择方案A:第一次只获取标题(如果请求页面时同时获取了内容,我们也可以选择不缓存,因为用户可能不需要下载整本)。
修改后的流程:
1. 获取书籍基本信息(书名、章节ID列表)。
2. 遍历章节ID,获取每个章节的标题(同时也会得到内容,但我们不保存内容,只记录标题和URL),并构建映射字典。
如果标题获取失败,记录错误章节URL。
3. 显示菜单,让用户选择下载方式。
选项1:单章下载(输入序号)
选项2:部分下载(输入起始序号和结束序号)
选项3:整本下载
选项0:退出
4. 根据用户选择,执行下载任务(调用下载函数,此时再请求章节内容)。
注意:在获取标题时,我们可能已经请求过章节页面,为了减少服务器压力,我们可以在第一次请求时,将章节内容缓存到内存或文件中,但这里为了简化,我们选择在下载时重新请求(因为用户可能不会下载所有章节)。
但是,原代码在获取标题时已经将内容写入了文件,这不符合我们的新流程。因此,我们需要修改这部分代码:在第一次遍历章节ID时,只获取标题,不写入文件。
修改代码:
1. 移除第一次遍历时的下载写入操作。
2. 将下载操作放入下载函数中。
由于时间关系,我们只重构关键部分。
另外,原代码中有一个`while True`循环用于用户交互,我们将其放在获取章节列表之后。
具体步骤:
步骤1:获取小说名称和章节ID列表。
步骤2:遍历章节ID,获取每个章节的标题,并构建映射(Kuaijie: 序号->标题,BIaoTi_url: 标题->URL)。
步骤3:显示章节序号和标题的映射(Kuaijie)供用户参考。
步骤4:进入用户交互循环,提供下载选项。
注意:在获取标题时,如果失败,我们记录到CuoWu列表,并在后续提供重新下载这些失败章节的选项。
由于代码较长,我们只给出修改后的关键部分。
由于原代码有部分未完成(比如单章下载和部分下载),我们将补充完整。
重构后的代码结构如下:
```python
# ...(前面的代码,包括change函数、headers、cookies等)
# 输入小说ID
XS_id = input('id:')
link = 'https://fanqienovel.com/page/' + XS_id
# 获取目录页,解析书名和章节ID列表
# 创建文件夹
# 遍历章节ID列表,获取每个章节的标题(和URL)
chapter_infos = [] # 存储章节信息:标题、URL
failed_chapters = [] # 存储失败的章节ID(或URL)
for chapter_id in chapter_id_list:
url = f'https://fanqienovel.com/reader/{chapter_id}'
try:
# 请求章节页面
# 解析标题
# 记录标题和URL
# 构建Kuaijie和BIaoTi_url
except Exception as e:
# 记录失败
failed_chapters.append(url)
# 显示菜单
while True:
# 打印菜单选项
choice = input("请选择下载方式: [1]单章 [2]部分 [3]整本 [0]退出: ")
if choice == '0':
break
elif choice == '1':
# 输入章节序号
# 调用下载函数下载该章节
elif choice == '2':
# 输入起始和结束序号
# 循环下载这些章节
elif choice == '3':
# 下载所有章节
else:
print("无效选择")
# 另外,可以提供重新下载失败章节的选项
# 下载函数
def download_chapter(url, title, folder):
# 请求章节页面,获取内容
# 解析内容
# 保存到文件
```
由于时间限制,我们无法完全重写代码,但我们可以根据这个结构调整。
但是,原代码中已经有一个遍历章节ID的循环,我们将其改为只获取标题,不下载内容。然后,将下载内容的代码封装成函数,在用户选择下载时调用。
注意:原代码在获取标题时已经请求了一次页面,在下载时又要请求一次,这样每个章节请求两次。如果用户选择整本下载,那么相当于每个章节请求两次。为了优化,我们可以在第一次获取标题时,将响应内容缓存起来(例如存入一个字典),然后在下载时直接使用缓存的内容。但是,如果缓存所有章节的HTML,内存可能占用较大。因此,我们可以选择缓存,也可以选择再请求一次(根据实际情况)。
这里我们选择再请求一次,因为用户可能只下载部分章节。
修改后的代码示例(只给出关键部分):</think>你的代码中存在几个语法错误和逻辑问题。主要问题是在`try-except`块后面直接跟了`while True`循环,导致语法错误。此外,用户输入处理部分也不完整。让我帮你修复并优化代码:
```python
import requests
import parsel
import re
import os
import time
import signal
from collections import OrderedDict
def change(content):
"""解密字体加密"""
word_data = {58344: 'd', 58345: '在', 58346: '主', 58347: '特', 58348: '家', 58349: '军', 58350: '然', 58351: '表',
58352: '场', 58353: '4', 58354: '要', 58355: '只', 58356: 'v', 58357: '和', 58359: '6', 58360: '别',
58361: '还', 58362: 'g', 58363: '现', 58364: '儿', 58365: '岁', 58368: '此', 58369: '象', 58370: '月',
58371: '3', 58372: '出', 58373: '战', 58374: '工', 58375: '相', 58376: 'o', 58377: '男', 58378: '直',
58379: '失', 58380: '世', 58381: 'f', 58382: '都', 58383: '平', 58384: '文', 58385: '什', 58386: 'v',
58387: 'o', 58388: '将', 58389: '真', 58390: 't', 58391: '那', 58392: '当', 58394: '会', 58395: '立',
58396: '些', 58397: 'u', 58398: '是', 58399: '十', 58400: '张', 58401: '学', 58402: '气', 58403: '大',
58404: '爱', 58405: '两', 58406: '命', 58407: '全', 58408: '后', 58409: '东', 58410: '性', 58411: '通',
58412: '被', 58413: '1', 58414: '它', 58415: '乐', 58416: '接', 58417: '而', 58418: '感', 58419: '车',
58420: '山', 58421: '公', 58422: '了', 58423: '常', 58424: '以', 58425: '何', 58426: '可', 58427: '话',
58428: '先', 58429: 'p', 58430: 'i', 58431: '4', 58432: '轻', 58433: 'm', 58434: '士', 58435: 'w',
58436: '着', 58437: '变', 58438: '尔', 58439: '快', 58440: 'l', 58441: '个', 58442: '说', 58443: '少',
58444: '色', 58445: '里', 58446: '安', 58447: '花', 58448: '远', 58449: '7', 58450: '难', 58451: '师',
58452: '放', 58453: 't', 58454: '报', 58455: '认', 58456: '面', 58457: '道', 58458: 's', 58460: '克',
58461: '地', 58462: '度', 58463: 'l', 58464: '好', 58465: '机', 58466: 'u', 58467: '民', 58468: '写',
58469: '把', 58470: '万', 58471: '同', 58472: '水', 58473: '新', 58474: '没', 58475: '书', 58476: '电',
58477: '吃', 58478: '像', 58479: '斯', 58480: '5', 58481: '为', 58482: 'y', 58483: '白', 58484: '几',
58485: '日', 58486: '教', 58487: '看', 58488: '但', 58489: '第', 58490: '加', 58491: '候', 58492: '作',
58493: '上', 58494: '拉', 58495: '住', 58496: '有', 58497: '法', 58498: 'r', 58499: '事', 58500: '应',
58501: '位', 58502: '利', 58503: '你', 58504: '声', 58505: '身', 58506: '国', 58507: '问', 58508: '马',
58509: '女', 58510: '他', 58511: 'y', 58512: '比', 58513: '父', 58514: 'x', 58515: 'a', 58516: 'h',
58517: 'n', 58518: 's', 58519: 'x', 58520: '边', 58521: '美', 58522: '对', 58523: '所', 58524: '金',
58525: '活', 58526: '回', 58527: '意', 58528: '到', 58529: 'z', 58530: '从', 58531: 'j', 58532: '知',
58533: '又', 58534: '内', 58535: '因', 58536: '点', 58537: 'q', 58539: '定', 58540: '8', 58541: 'R',
58542: 'b', 58543: '正', 58544: '或', 58545: '夫', 58546: '向', 58547: '德', 58548: '听', 58549: '更',
58551: '得', 58552: '告', 58553: '并', 58554: '本', 58555: 'q', 58556: '过', 58557: '记', 58558: 'l',
58559: '让', 58560: '打', 58561: 'f', 58562: '人', 58563: '就', 58564: '者', 58565: '去', 58566: '原',
58567: '满', 58568: '体', 58569: '做', 58570: '经', 58571: 'K', 58572: '走', 58573: '如', 58574: '孩',
58575: 'c', 58576: 'g', 58577: '给', 58578: '使', 58579: '物', 58581: '最', 58582: '笑', 58583: '部',
58585: '员', 58586: '等', 58587: '受', 58588: 'k', 58589: '行', 58591: '条', 58592: '果', 58593: '动',
58594: '光', 58595: '门', 58596: '头', 58597: '见', 58598: '往', 58599: '自', 58600: '解', 58601: '成',
58602: '处', 58603: '天', 58604: '能', 58605: '于', 58606: '名', 58607: '其', 58608: '发', 58609: '总',
58610: '母', 58611: '的', 58612: '死', 58613: '手', 58614: '入', 58615: '路', 58616: '进', 58617: '心',
58618: '来', 58619: 'h', 58620: '时', 58621: '力', 58622: '多', 58623: '开', 58624: '已', 58625: '许',
58626: 'd', 58627: '至', 58628: '由', 58629: '很', 58630: '界', 58631: 'n', 58632: '小', 58633: '与',
58634: 'z', 58635: '想', 58636: '代', 58637: '么', 58638: '分', 58639: '生', 58640: '口', 58641: '再',
58642: '妈', 58643: '望', 58644: '次', 58645: '西', 58646: '风', 58647: '种', 58648: '带', 58649: 'j',
58651: '实', 58652: '情', 58653: '才', 58654: '这', 58656: 'e', 58657: '我', 58658: '神', 58659: '格',
58660: '长', 58661: '觉', 58662: '间', 58663: '年', 58664: '眼', 58665: '无', 58666: '不', 58667: '亲',
58668: '关', 58669: '结', 58670: '0', 58671: '友', 58672: '信', 58673: '下', 58674: '却', 58675: '重',
58676: '己', 58677: '老', 58678: '2', 58679: '音', 58680: '字', 58681: 'm', 58682: '呢', 58683: '明',
58684: '之', 58685: '前', 58686: '高', 58687: 'p', 58688: 'b', 58689: '目', 58690: '太', 58691: 'e',
58692: '9', 58693: '起', 58694: '棱', 58695: '她', 58696: '也', 58697: 'w', 58698: '用', 58699: '方',
58700: '子', 58701: '英', 58702: '每', 58703: '理', 58704: '便', 58705: '四', 58706: '数', 58707: '期',
58708: '中', 58709: 'c', 58710: '外', 58711: '样', 58712: 'a', 58713: '海', 58714: '们', 58715: '任',
58538: '三', 58590: '一'}
result = ''
for i in content:
code = ord(i)
if code in word_data:
result += word_data[code]
else:
result += i
return result
def download_chapter(url, title, folder, headers, cookies_jar):
"""下载单个章节并保存到文件"""
try:
response = requests.get(url, headers=headers, cookies=cookies_jar, timeout=10)
response.raise_for_status()
html = response.text
# 解析正文内容
selector = parsel.Selector(html)
content_list = selector.css('.muye-reader-content p::text').getall()
content = '\n\n'.join(content_list)
new_content = change(content)
# 清理标题非法字符
valid_title = re.sub(r'[\\/:*?"<>|]', '', title)
file_path = os.path.join(folder, f'{valid_title}.txt')
# 写入章节文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(title + '\n\n')
f.write(new_content)
print(f"✅ 已保存章节: {valid_title}")
return True
except Exception as e:
print(f"❌ 章节下载失败: {str(e)}")
return False
def input_with_timeout(prompt, timeout, default):
"""带超时的输入函数"""
def timeout_handler(signum, frame):
raise TimeoutError()
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout)
try:
user_input = input(prompt)
signal.alarm(0) # 取消定时器
return user_input if user_input else default
except TimeoutError:
print(f"\n超时未输入,自动赋值为: {default}")
return default
except:
signal.alarm(0)
return default
def main():
# 请求头
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36"
}
cookies = ('x-web-secsdk-uid=6caa94ab-afcf-4fd4-afbb-8bc8452fe9be; Hm_lvt_2667d29c8e792e6fa9182c20a3013175=1753504097;'
' HMACCOUNT=3521586B2CBB60FD; s_v_web_id=verify_mdjr2cq7_0Awh10ms_xmxg_4RxP_ACcS_6L26t2CAuVN7;'
' csrf_session_id=42d699e530338ed5c27470544ca7f4d1; novel_web_id=7531242700978275891; gfkadpd=2503,36144;'
' serial_uuid=7531242700978275891; serial_webid=7531242700978275891; passport_csrf_token=456e18769bc01214ea4b162181d52c57; '
'passport_csrf_token_default=456e18769bc01214ea4b162181d52c57;'
' passport_mfa_token=Ci%2BRmTcFLTjXAtuCAzxfHTr5xzh%2FeJAq9pCuk%2FlI65%2BK0XFl43i3nA5U27bvl3phfBpKCjwAAAAAAAAAAAAAT0cRRwFSohWydrXo5%2B%2FUfenqaq2QfGYbBk2u8N3i7t0vf%2B%2Bba1Iwc%2FGAe5KLLYMdq50Q0eD3DRj2sdFsIAIiAQMlxlhx; d_ticket=40365a26b04ae00fd40c24bf84c595e569134; odin_tt=64775b0e5c16e19ea1dcfc0bc2803975e7fe1fa238f7079fad44eb74d422a92b13922f254dfe242c1a10e398110f4a81; n_mh=Q_Sm8BYjtPoAOYv_SaH5P9NQDPqDZ7xoQRgI2GvcfEw; sid_guard=4b4ce70ae45606de9ccf894f86577bf9%7C1753504126%7C5184000%7CWed%2C+24-Sep-2025+04%3A28%3A46+GMT; uid_tt=9828be745c0647d8029cb6544523a987; uid_tt_ss=9828be745c0647d8029cb6544523a987; sid_tt=4b4ce70ae45606de9ccf894f86577bf9; sessionid=4b4ce70ae45606de9ccf894f86577bf9; sessionid_ss=4b4ce70ae45606de9ccf894f86577bf9; session_tlb_tag=sttt%7C11%7CS0znCuRWBt6cz4lPhld7-f________-7F1LcICKBvVj_qj8slUjAFUXFSpOSZ6rZJkWDYtmHBVs%3D; is_staff_user=false; sid_ucp_v1=1.0.0-KGM1OTZmODRmM2NhMjEyNDE4ZjA1YzIzZjhlZDRjOWU0Yjg3ZWY4MTkKHQj_svrjugIQ_rKRxAYYxxMgDDC8gbzSBTgCQO8HGgJobCIgNGI0Y2U3MGFlNDU2MDZkZTljY2Y4OTRmODY1NzdiZjk; ssid_ucp_v1=1.0.0-KGM1OTZmODRmM2NhMjEyNDE4ZjA1YzIzZjhlZDRjOWU0Yjg3ZWY4MTkKHQj_svrjugIQ_rKRxAYYxxMgDDC8gbzSBTgCQO8HGgJobCIgNGI0Y2U3MGFlNDU2MDZkZTljY2Y4OTRmODY1NzdiZjk; Hm_lpvt_2667d29c8e792e6fa9182c20a3013175=1753505316; ttwid=1%7CYbkOCAUHGSmJvwiQZl87vIuNuIeLtgcPuvxpcdV-nME%7C1753505317%7Ca23a7bc2cffe611918008995cea7fdf595616ea6cfe9a04215858502f0d6a522')
cookies_jar = requests.cookies.RequestsCookieJar()
for cookie in cookies.split(';'):
key, value = cookie.split('=', 1)
cookies_jar.set(key.strip(), value.strip())
XS_id = input('请输入小说ID: ')
# 目标小说目录页链接
link = 'https://fanqienovel.com/page/' + XS_id
link_response = requests.get(url=link, headers=headers, cookies=cookies_jar)
link_data = link_response.text
# 提取小说名称
name = re.findall('"bookName":"(.*?)",', link_data)[0]
valid_name = re.sub(r'[\\/:*?"<>|]', '', name)
os.makedirs(valid_name, exist_ok=True)
# 提取章节ID列表
chapter_id_list = re.findall('<a href="/reader/(\\d+)" class="chapter-item-title"', link_data)[1:]
if not chapter_id_list:
print("未找到章节列表,请检查小说ID是否正确")
return
print(f"找到 {len(chapter_id_list)} 个章节")
# 获取章节信息
chapter_info = OrderedDict() # 有序字典保存章节信息
failed_chapters = [] # 保存失败的章节
for idx, chapter_id in enumerate(chapter_id_list, 1):
url = f'https://fanqienovel.com/reader/{chapter_id}'
try:
response = requests.get(url, headers=headers, cookies=cookies_jar, timeout=10)
response.raise_for_status()
html = response.text
# 解析章节标题
selector = parsel.Selector(html)
title = selector.css('.muye-reader-title::text').get()
if not title:
print(f"⚠️ 章节 {chapter_id} 未找到标题")
title = f"未知章节_{idx}"
# 清理标题非法字符
valid_title = re.sub(r'[\\/:*?"<>|]', '', title)
chapter_info[idx] = (valid_title, url)
print(f"获取章节信息: {idx}. {valid_title}")
except Exception as e:
print(f"❌ 章节信息获取失败: {str(e)}")
failed_chapters.append((idx, chapter_id))
time.sleep(2)
# 用户交互菜单
while True:
print("\n===== 小说下载菜单 =====")
print("1. 下载单个章节")
print("2. 下载多个章节(范围)")
print("3. 下载整本小说")
print("4. 重新下载失败的章节")
print("5. 退出程序")
choice = input("请选择操作 (1-5): ")
if choice == '1': # 下载单个章节
chapter_num = input("请输入章节编号: ")
try:
chapter_num = int(chapter_num)
if chapter_num in chapter_info:
title, url = chapter_info[chapter_num]
download_chapter(url, title, valid_name, headers, cookies_jar)
else:
print("无效的章节编号")
except ValueError:
print("请输入有效的数字")
elif choice == '2': # 下载范围章节
start = input("请输入起始章节编号: ")
end = input("请输入结束章节编号: ")
try:
start = int(start)
end = int(end)
if 1 <= start <= end <= len(chapter_info):
for i in range(start, end + 1):
title, url = chapter_info[i]
download_chapter(url, title, valid_name, headers, cookies_jar)
time.sleep(1) # 避免频繁请求
else:
print("无效的范围")
except ValueError:
print("请输入有效的数字")
elif choice == '3': # 下载整本小说
print(f"开始下载整本小说《{valid_name}》,共 {len(chapter_info)} 章...")
for idx, (title, url) in chapter_info.items():
download_chapter(url, title, valid_name, headers, cookies_jar)
time.sleep(1) # 避免频繁请求
elif choice == '4': # 重新下载失败的章节
if not failed_chapters:
print("没有失败的章节")
continue
print("重新下载失败的章节:")
for idx, chapter_id in failed_chapters:
url = f'https://fanqienovel.com/reader/{chapter_id}'
title = f"章节_{idx}" # 使用默认标题
success = download_chapter(url, title, valid_name, headers, cookies_jar)
if success:
# 如果成功,更新章节信息
chapter_info[idx] = (title, url)
failed_chapters.remove((idx, chapter_id))
time.sleep(2)
elif choice == '5': # 退出
print("程序已退出")
break
else:
print("无效的选择,请重新输入")
if __name__ == "__main__":
main()
```
### 代码解释:
1. **重构代码结构**:
- 将代码组织为函数式结构,提高可读性和可维护性
- 使用`main()`函数作为程序入口
- 使用`OrderedDict`有序字典保存章节信息,保持章节顺序
2. **主要函数**:
- `download_chapter()`: 下载单个章节并保存到文件
- `input_with_timeout()`: 带超时的输入函数
- `main()`: 程序主逻辑
3. **错误处理改进**:
- 使用`try-except`捕获所有可能的异常
- 记录失败的章节以便重新下载
- 添加超时处理防止程序卡死
4. **用户交互优化**:
- 提供清晰的菜单界面
- 支持多种下载模式:单章、范围、整本
- 支持重新下载失败的章节
5. **其他改进**:
- 添加章节编号显示
- 优化错误提示信息
- 添加请求间隔避免被封禁
- 使用有序字典保持章节顺序
这个修复版本解决了语法错误问题,优化了用户交互体验,并增强了程序的健壮性。用户可以通过菜单选择不同的下载方式,程序会记录失败的章节并提供重新下载的机会。