
1. 项目缘起从手动翻页到自动化抓取作为一个经常需要找点老电影或者冷门资源看看的影迷我过去没少在“电影天堂”这类网站上耗费时间。手动一页一页地翻眼睛都看花了效率极低。后来学了Python第一个想到的实战项目就是把这个过程自动化。今天要聊的就是如何用Python爬虫把电影天堂这类网站的分页数据一网打尽。这不仅仅是写几行代码那么简单它涉及到对网页结构的分析、请求的模拟、数据的解析以及最核心的——如何处理分页逻辑。无论你是想批量获取电影信息建立自己的片库还是学习爬虫分页处理的经典案例这个项目都是一个绝佳的练手机会。整个过程我会结合我踩过的坑和总结的技巧带你一步步实现。2. 环境准备与核心库选型工欲善其事必先利其器。在开始写爬虫之前我们需要搭建好Python环境并选择合适的库。对于新手我强烈建议使用Anaconda来管理Python环境它能避免很多依赖冲突的麻烦。2.1 Python环境与依赖库安装首先确保你安装了Python 3.7或以上版本。打开命令行输入python --version检查。接下来我们需要安装几个核心库requests: 用于发送HTTP请求获取网页源代码。它是目前最流行、最简单的HTTP库。BeautifulSoup4 (bs4): 用于解析HTML或XML文档从复杂的网页结构中提取我们需要的数据就像用一把梳子把乱发理顺。lxml: 这是一个解析器BeautifulSoup可以使用它来加速解析。虽然bs4也自带html.parser但lxml速度更快容错性更好。安装命令非常简单在命令行中执行pip install requests beautifulsoup4 lxml如果速度慢可以使用国内镜像源例如pip install requests beautifulsoup4 lxml -i https://pypi.tuna.tsinghua.edu.cn/simple注意有些教程可能会提到urllib但requests库的API更加人性化代码更简洁对于初学者和日常使用来说是更好的选择。除非有特殊需求否则不建议从urllib开始。2.2 分析目标网站电影天堂的结构在写代码之前我们必须先“侦察”敌情。打开电影天堂的某个分类页例如最新电影观察它的分页规律。URL规律通常分页信息会体现在URL中。比如第一页可能是https://www.dytt89.com/1/第二页是https://www.dytt89.com/2/以此类推。也有的网站使用查询参数如?page1。我们需要通过查看翻页时的URL变化来确定规律。页面结构使用浏览器的“开发者工具”按F12。我们需要找到电影列表的容器所有电影条目被包裹在哪个HTML标签里比如一个div class”co_content8”或者一个ul列表。单个电影信息的结构每个电影条目里如何提取电影名称、下载链接、发布时间等信息。通常电影名称在一个a标签里下载链接在另一个页面的a标签的href属性中。分页导航栏网站是如何展示“上一页”、“下一页”、“页码”的。我们需要从中找出总页数或者判断是否还有下一页的逻辑。这个分析过程是爬虫成功的关键。我建议先用浏览器手动分析清楚甚至可以手动复制几个页面的HTML源码保存下来在离线状态下用BeautifulSoup练习解析这样不会因为频繁请求而被网站暂时限制访问。3. 核心爬取逻辑设计与实现掌握了网站结构我们就可以开始设计爬虫的核心逻辑了。一个健壮的分页爬虫其核心流程可以概括为构造初始请求 - 解析当前页数据 - 寻找并处理下一页链接 - 循环直至结束。3.1 单页数据抓取与解析我们先实现抓取和解析单页电影列表的功能。这里以假设的电影天堂结构为例实际代码需要根据你分析的真实HTML结构进行调整。import requests from bs4 import BeautifulSoup import time def get_one_page(url): 获取单页HTML内容 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() # 如果状态码不是200抛出异常 response.encoding response.apparent_encoding # 自动识别编码 return response.text except requests.RequestException as e: print(f请求页面失败: {url}, 错误: {e}) return None def parse_one_page(html): 解析单页HTML提取电影信息 if not html: return [] soup BeautifulSoup(html, lxml) movie_list [] # 假设电影条目在一个class为‘co_content8’的div下的所有table中 # 实际需要根据网站结构调整选择器 items soup.select(.co_content8 table) for item in items: try: # 提取电影名称假设在第一个a标签内 title_tag item.find(a) title title_tag.text.strip() if title_tag else N/A # 提取详情页链接 detail_link title_tag[href] if title_tag and title_tag.has_attr(href) else N/A # 注意这里可能需要拼接完整的URL # full_detail_link urljoin(base_url, detail_link) # 提取发布时间假设在特定的font或span标签里 date_tag item.find(font, color#8F8C89) publish_date date_tag.text.strip() if date_tag else N/A movie_list.append({ title: title, detail_link: detail_link, publish_date: publish_date }) except Exception as e: print(f解析单个电影条目时出错: {e}) continue # 跳过当前出错条目继续解析下一个 return movie_list实操心得User-Agent头非常重要很多网站会对没有浏览器标识的请求如Python-requests进行屏蔽或返回不同的内容。此外加入try...except异常处理是写出稳定爬虫的必要习惯网络请求和HTML解析都可能因为网站微调而失败不能让一个错误导致整个程序崩溃。3.2 分页逻辑的多种实现策略分页是本次项目的核心。根据网站分页方式的不同我们有几种策略策略一基于URL规律拼接最常见如果页码直接体现在URL中如/page/1/,/page/2/或?page1这是最简单的情况。我们只需要用一个循环不断生成下一页的URL即可。def crawl_by_url_pattern(base_url, total_pages): 通过拼接URL页码进行爬取 all_movies [] for page in range(1, total_pages 1): print(f正在爬取第 {page} 页...) # 根据规律构造URL例如 base_url 是 ‘https://.../page/’ url f{base_url}{page}/ # 或者 url f{base_url}?page{page} html get_one_page(url) movies parse_one_page(html) all_movies.extend(movies) time.sleep(1) # 礼貌性延迟避免对服务器造成压力 return all_movies关键点如何确定total_pages有时可以从第一页的分页导航栏解析出总页数有时则需要我们设置一个较大的上限当爬取到某一页发现没有内容或返回404时停止。策略二解析“下一页”按钮如果网站URL没有明显规律或者我们想写一个更通用的爬虫可以解析当前页面的“下一页”链接。def crawl_by_next_link(start_url): 通过跟踪‘下一页’链接进行爬取 all_movies [] current_url start_url while current_url: print(f正在爬取: {current_url}) html get_one_page(current_url) if not html: break # 解析当前页数据 movies parse_one_page(html) all_movies.extend(movies) # 解析出“下一页”的链接 soup BeautifulSoup(html, lxml) next_link_tag soup.find(a, text下一页) # 根据实际文本找 # 或者通过class/id找例如 next_link_tag soup.select_one(.next-page a) if next_link_tag and next_link_tag.has_attr(href): # 可能需要拼接完整URL # next_url urljoin(current_url, next_link_tag[href]) next_url next_link_tag[href] # 防止死循环检查下一页URL是否和当前页相同或者是否已经访问过 if next_url ! current_url: current_url next_url else: current_url None else: # 没有找到“下一页”链接说明已是最后一页 current_url None time.sleep(1) return all_movies策略三结合两种方法推荐最稳健的方法是先尝试从第一页解析出总页数然后使用URL拼接法进行爬取。如果解析失败再降级使用“下一页”跟踪法。def smart_crawler(start_url): 智能爬取优先尝试解析总页数 all_movies [] # 1. 获取第一页尝试解析总页数 first_page_html get_one_page(start_url) if not first_page_html: return all_movies soup BeautifulSoup(first_page_html, lxml) total_pages parse_total_pages(soup) # 需要实现parse_total_pages函数 if total_pages: # 方法一URL拼接 base_url_pattern deduce_base_pattern(start_url) # 推断URL模式 for page in range(1, total_pages 1): url construct_url(base_url_pattern, page) # 构造URL # ... 爬取逻辑 pass else: # 方法二跟踪下一页 all_movies crawl_by_next_link(start_url) return all_movies踩坑实录分页逻辑中最容易出问题的是“最后一页”的判断。有的网站最后一页的“下一页”按钮是禁用的但a标签还在有的则是直接消失了。用URL拼接法时如果总页数判断错误可能会一直请求不存在的页面。我的经验是在解析函数里增加一个判断如果当前页解析出的电影列表为空或者解析到的关键元素结构与之前差异巨大就认为可能已经到了末页之外应该终止循环。4. 数据存储、反爬应对与工程化考量爬取到数据后我们需要将其保存下来。同时一个完整的爬虫项目还需要考虑反爬虫策略和代码结构。4.1 数据的持久化存储将数据保存在内存列表里不是长久之计。根据数据量和使用场景可以选择以下几种方式1. 保存为CSV文件轻量级易查看import csv def save_to_csv(movie_list, filenamemovies.csv): 将电影列表保存为CSV文件 if not movie_list: print(没有数据可保存) return keys movie_list[0].keys() # 获取字典的键作为表头 with open(filename, w, newline, encodingutf-8-sig) as f: # utf-8-sig解决Excel打开乱码 writer csv.DictWriter(f, fieldnameskeys) writer.writeheader() writer.writerows(movie_list) print(f数据已保存至 {filename})2. 保存为JSON文件保持结构适合传输import json def save_to_json(movie_list, filenamemovies.json): with open(filename, w, encodingutf-8) as f: json.dump(movie_list, f, ensure_asciiFalse, indent4) # indent使格式美观 print(f数据已保存至 {filename})3. 存入数据库大量数据复杂查询对于电影数据关系型数据库如SQLite轻便或MySQL是很好的选择。你需要先设计表结构电影名、详情链接、发布时间等然后使用sqlite3或pymysql库进行插入操作。个人建议对于初学者或数据量不大的项目CSV是最佳选择。它可以用Excel直接打开非常直观。存入数据库是更工程化的做法便于后续的数据分析和查询。4.2 常见反爬策略与应对措施电影天堂这类资源站通常反爬不严但了解这些知识对爬取其他网站至关重要。User-Agent检测我们已经做了使用常见的浏览器UA。请求频率过高这是最容易被封IP的原因。必须在请求间加入随机延迟。import time import random time.sleep(random.uniform(1, 3)) # 随机延迟1-3秒IP封锁如果单个IP请求过于频繁可能会被暂时封锁。解决方案有使用代理IP池从免费的或付费的代理IP服务获取IP轮流使用。requests库使用代理很简单proxies {“http”: “http://10.10.1.10:3128”, “https”: “http://10.10.1.10:1080”}然后在get请求中传入proxiesproxies。降低请求频率这是最经济的方法。动态加载内容如果电影列表是通过JavaScript动态加载的查看网页源代码发现没有数据但浏览器里能看到那么requests获取的HTML就不包含数据。这时需要用到Selenium或Pyppeteer这类工具来模拟浏览器行为或者更高级地找到网站加载数据的真实API接口通过浏览器开发者工具的Network面板查看XHR/Fetch请求。经验技巧对于新手遇到动态加载的网站先从Network面板找API接口。如果能找到直接用requests模拟那个接口请求效率远高于启动一个笨重的浏览器。如果找不到再考虑Selenium。4.3 代码工程化与可维护性当爬虫代码越来越长功能越来越多时好的代码结构能让你后期维护省心百倍。模块化将不同的功能拆分成函数或类。例如可以创建一个MovieSpider类将发送请求、解析页面、处理分页、保存数据作为其方法。配置文件将基础URL、请求头、数据库连接信息、代理IP列表等写入配置文件如config.ini或settings.py避免硬编码在代码中。日志记录使用Python内置的logging模块记录程序运行情况比如成功爬取了多少页哪一页失败了错误原因是什么。这比单纯用print打印专业得多也便于排查问题。错误重试机制网络请求可能因各种原因失败。可以为get_one_page函数添加重试逻辑。from tenacity import retry, stop_after_attempt, wait_random_exponential # 需要安装 tenacity 库: pip install tenacity retry(stopstop_after_attempt(3), waitwait_random_exponential(multiplier1, max10)) def get_one_page_retry(url): # 原有的请求逻辑 return get_one_page(url)这个装饰器会让函数在失败后随机等待一段时间并重试最多重试3次。5. 完整代码示例与实测调试将以上所有部分整合形成一个可运行的完整脚本框架。请注意以下代码中的选择器如.co_content8 table是示例你必须根据电影天堂网站的实际HTML结构进行修改。import requests from bs4 import BeautifulSoup import time import random import csv from urllib.parse import urljoin class DyttSpider: def __init__(self, base_url): self.base_url base_url self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } self.session requests.Session() # 使用Session可以保持一些连接状态提升效率 self.all_movies [] def get_page(self, url): 获取页面包含简单错误处理和延迟 try: # 随机延迟模拟人类操作 delay random.uniform(1, 2.5) time.sleep(delay) resp self.session.get(url, headersself.headers, timeout15) resp.raise_for_status() # 尝试多种编码方式 resp.encoding resp.apparent_encoding or gb2312 or utf-8 # 电影天堂常用gbk编码 return resp.text except Exception as e: print(f[ERROR] 请求 {url} 失败: {e}) return None def parse_page(self, html): 解析单页提取电影信息 soup BeautifulSoup(html, lxml) movies [] # !!! 关键这里的选择器需要你根据实际网站修改 !!! # 例如 items soup.select(#header div div.bd2 div.bd3 div:nth-child(2) div.co_area2 div.co_content8 ul table) items soup.select(.co_content8 table) # 这只是示例 for item in items: try: # 查找电影标题和详情页链接 title_link item.find(a, class_ulink) # 示例根据实际class调整 if title_link: title title_link.get_text().strip() detail_path title_link.get(href) detail_url urljoin(self.base_url, detail_path) if detail_path else else: title, detail_url N/A, # 查找发布日期 date_tag item.find(font, color#8F8C89) pub_date date_tag.get_text().strip() if date_tag else N/A movies.append({ title: title, detail_url: detail_url, publish_date: pub_date }) except AttributeError as e: print(f解析条目时发生属性错误: {e}, 跳过该条目) continue return movies def get_total_pages(self, first_page_html): 从第一页解析总页数示例逻辑需自定义 soup BeautifulSoup(first_page_html, lxml) # 假设分页控件在一个class为‘x’的div里最后一个页码是总页数 # page_select soup.select_one(.x) # if page_select: # last_page page_select.find_all(a)[-2].text # 倒数第二个可能是最后一页 # return int(last_page) # 更常见的是直接看URL模式或“末页”链接 # 这里返回一个示例值或返回None触发下一页跟踪模式 return 10 # 示例假设有10页 def crawl_by_page_num(self, total_pages): 按页码拼接URL爬取 for page in range(1, total_pages 1): print(f[INFO] 正在爬取第 {page} 页...) # 假设URL模式是 base_url ‘/page/’ page_num url f{self.base_url.rstrip(/)}/page/{page}/ if page 1 else self.base_url html self.get_page(url) if not html: print(f[WARN] 第 {page} 页获取失败跳过) continue movies self.parse_page(html) if not movies: print(f[WARN] 第 {page} 页未解析到数据可能已到末页或结构有变停止爬取。) break self.all_movies.extend(movies) print(f[INFO] 第 {page} 页爬取完成共 {len(movies)} 条记录。) def run(self): 主运行函数 print(f[INFO] 开始爬取起始URL: {self.base_url}) # 获取第一页并尝试解析总页数 first_page_html self.get_page(self.base_url) if not first_page_html: print([ERROR] 无法获取起始页程序退出。) return total_pages self.get_total_pages(first_page_html) if total_pages: print(f[INFO] 检测到共 {total_pages} 页开始按页码爬取。) # 先解析第一页数据 first_page_movies self.parse_page(first_page_html) self.all_movies.extend(first_page_movies) # 从第二页开始爬 self.crawl_by_page_num(total_pages) else: print([INFO] 未检测到明确总页数开始跟踪“下一页”链接爬取。) # 这里可以实现 crawl_by_next_link 逻辑 # self.crawl_by_next_link(self.base_url) pass # 保存数据 if self.all_movies: self.save_data() print(f[INFO] 爬取结束共获取 {len(self.all_movies)} 条电影信息。) else: print([INFO] 未爬取到任何数据。) def save_data(self, filenamedytt_movies.csv): 保存数据到CSV with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, detail_url, publish_date]) writer.writeheader() writer.writerows(self.all_movies) print(f[INFO] 数据已保存至 {filename}) if __name__ __main__: # !!! 请替换为电影天堂真实的列表页URL !!! start_url https://www.ygdy8.net/html/gndy/dyzz/index.html # 示例URL可能已失效 spider DyttSpider(start_url) spider.run()实测调试步骤替换URL和选择器将start_url换成电影天堂真实的列表页地址。使用浏览器开发者工具仔细分析电影列表和分页的HTML结构修改parse_page方法中的选择器soup.select(...)和item.find(...)。运行测试先只爬取第一页打印出解析到的数据确认选择器是否正确。调试分页确认第一页没问题后再打开分页爬取逻辑。可以先手动设置一个较小的total_pages如3进行测试。处理编码注意电影天堂这类老牌网站可能使用GBK或GB2312编码。如果发现爬下来的中文是乱码需要调整resp.encoding可以尝试gbk,gb2312,utf-8。应对变化网站结构可能会改版。一个健壮的爬虫应该在解析失败时给出清晰的警告日志而不是默默崩溃。这也是为什么我们要在代码中加入大量try...except和日志打印的原因。最后请务必遵守网站的robots.txt协议合理控制爬取速度不要给目标网站服务器造成过大压力。这个项目更多的是用于学习Python网络爬虫和分页处理技术。