20241319 2024-2025-2 《Python程序设计》实验四报告

20241319 2024-2025-2 《Python程序设计》实验四报告

课程:《Python程序设计》
班级: 2413
姓名: 吴辰曦
学号:20241319
实验教师:王志强
实验日期:2025年3月26日
必修/选修: 公选课

1.实验内容

Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
课代表和各小组负责人收集作业(源代码、视频、综合实践报告)

Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等

2.实验过程及结果

(1)选题:由于我喜欢看电影解说,所以我想的是制作一个爬取豆瓣top 的爬虫
(3)过程:原本只是想做一个由requests库和Beautifulsoup制作的简单爬虫,但似乎豆瓣的反爬机制限制了我的访问,于是我对我的代码进行了第一次升级——selenium库,而经过一系列调试后,可以爬取一些内容,但仅限于电影名称、上映时间和评分,于是代码又经过了一次升级——Scrapy库,终于,现在能完成大部分我所想要的内容了,但仍有些小问题存在
最终代码

点击查看代码
# -*- coding: utf-8 -*-
import scrapy
import re
import time
import random
import pandas as pd
import json
import logging
from scrapy.crawler import CrawlerProcess


class DoubanMovieItem(scrapy.Item):
    # 基础信息
    rank = scrapy.Field()  # 排名
    title = scrapy.Field()  # 电影名称
    rating = scrapy.Field()  # 评分
    year = scrapy.Field()  # 上映年份
    director = scrapy.Field()  # 导演
    actors = scrapy.Field()  # 主演
    quote = scrapy.Field()  # 经典台词

    # 扩展信息
    genres = scrapy.Field()  # 类型
    duration = scrapy.Field()  # 片长
    region = scrapy.Field()  # 制片国家/地区
    language = scrapy.Field()  # 语言
    imdb_link = scrapy.Field()  # IMDb链接
    synopsis = scrapy.Field()  # 剧情简介
    rating_count = scrapy.Field()  # 评价人数
    crawl_time = scrapy.Field()  # 爬取时间


class DoubanTop250Spider(scrapy.Spider):
    name = 'douban_top250'
    allowed_domains = ['movie.douban.com']
    start_urls = ['https://movie.douban.com/top250']

    # 自定义设置(反爬优化)
    custom_settings = {
        'DOWNLOAD_DELAY': 2.5,  # 请求延迟(秒)
        'CONCURRENT_REQUESTS': 3,  # 并发请求数
        'RETRY_TIMES': 3,  # 失败重试次数
        'COOKIES_ENABLED': False,  # 禁用Cookies
        'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
        'FEED_EXPORT_ENCODING': 'utf-8',
        'LOG_LEVEL': 'INFO',
        'DEFAULT_REQUEST_HEADERS': {  # 完整请求头
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Referer': 'https://movie.douban.com/top250',
            'Connection': 'keep-alive'
        }
    }

    def start_requests(self):
        """生成分页请求"""
        for page in range(0, 250, 25):
            url = f'https://movie.douban.com/top250?start={page}'
            yield scrapy.Request(
                url=url,
                callback=self.parse,
                meta={'delay': 1.5 + 2 * random.random()}  # 随机延迟(1.5-3.5秒)
            )

    def parse(self, response):
        """解析列表页"""
        for movie in response.css('.item'):
            try:
                item = DoubanMovieItem()

                # 基础字段提取
                item['rank'] = movie.css('.pic em::text').get()
                item['title'] = movie.css('.title::text').get()
                item['rating'] = movie.css('.rating_num::text').get()
                item['rating_count'] = movie.css('.star span:last-child::text').re_first(r'(\d+)人评价')

                # 增强版导演/演员解析
                info_text = ''.join(movie.css('.bd p::text').getall())

                # 导演信息(带安全检测)
                if '导演:' in info_text:
                    director_match = re.search(r'导演:\s*(.*?)(?:\s*主演:|$)', info_text)
                    item['director'] = director_match.group(1).strip() if director_match else ""
                else:
                    item['director'] = ""

                # 演员信息(带安全检测)
                if '主演:' in info_text:
                    actors_match = re.search(r'主演:\s*(.*?)(?:\s*类型:|$)', info_text)
                    item['actors'] = actors_match.group(1).strip() if actors_match else ""
                else:
                    item['actors'] = ""

                # 年份信息
                year_match = re.search(r'(\d{4})', info_text)
                item['year'] = year_match.group(1) if year_match else ""

                # 经典台词
                item['quote'] = movie.css('.inq::text').get() or ""

                # 详情页链接
                detail_url = movie.css('.hd a::attr(href)').get()

                # 发起详情页请求
                yield response.follow(
                    url=detail_url,
                    callback=self.parse_detail,
                    meta={'item': item},
                    priority=10
                )

            except Exception as e:
                self.logger.error(f"列表页解析异常: {str(e)}")

    def parse_detail(self, response):
        """解析详情页"""
        item = response.meta['item']

        try:
            # 类型和时长
            item['genres'] = '/'.join(response.css('span[property="v:genre"]::text').getall())
            item['duration'] = response.css('span[property="v:runtime"]::attr(content)').get()

            # 从info区块提取多语言字段
            info_html = response.css('#info').get() or ""

            # 国家/地区(三重匹配方案)
            region_match = re.search(r'制片国家/地区:</span>(.*?)<br', info_html) or \
                           re.search(r'地区:</span>(.*?)<br', info_html) or \
                           re.search(r'国家:</span>(.*?)<br', info_html)
            item['region'] = region_match.group(1).strip() if region_match else ""

            # 语言(双重匹配方案)
            lang_match = re.search(r'语言:</span>(.*?)<br', info_html) or \
                         re.search(r'语言:</span>(.*?)$', info_html)
            item['language'] = lang_match.group(1).strip() if lang_match else ""

            # IMDb链接
            item['imdb_link'] = response.xpath('//a[contains(@href, "imdb.com")]/@href').get()

            # 剧情简介
            synopsis = response.css('span[property="v:summary"]::text').getall()
            item['synopsis'] = ''.join(synopsis).strip() if synopsis else ""

            # 爬取时间戳
            item['crawl_time'] = time.strftime("%Y-%m-%d %H:%M:%S")

            yield item

        except Exception as e:
            self.logger.error(f"详情页解析异常: {str(e)}")
            # 返回已解析的基础信息
            item['genres'] = ""
            item['duration'] = ""
            item['region'] = ""
            item['language'] = ""
            item['imdb_link'] = ""
            item['synopsis'] = ""
            yield item


class DataPipeline:
    """数据存储管道"""

    def __init__(self):
        self.items = []

    def process_item(self, item, spider):
        spider.logger.info(f"已收集: {item.get('title', '未知电影')}")
        self.items.append(dict(item))
        return item

    def close_spider(self, spider):
        """爬虫结束时保存数据"""
        import os
        from datetime import datetime

        if not self.items:
            spider.logger.error("未收集到任何数据,跳过文件生成")
            return

        try:
            # 创建专用输出目录
            output_dir = "douban_top250_data"
            os.makedirs(output_dir, exist_ok=True)

            # 验证目录写入权限
            test_file = os.path.join(output_dir, "write_test.txt")
            with open(test_file, "w", encoding="utf-8") as f:
                f.write("权限测试 - 成功")
            os.remove(test_file)

            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            spider.logger.info(f"准备保存{len(self.items)}条数据")

            # 保存为Excel
            excel_file = os.path.join(output_dir, f"豆瓣TOP250_{timestamp}.xlsx")
            df = pd.DataFrame(self.items)
            df.to_excel(excel_file, index=False, encoding='utf-8-sig')
            spider.logger.info(f"Excel文件已生成: {excel_file}")

            # 保存为JSON
            json_file = os.path.join(output_dir, f"豆瓣TOP250_{timestamp}.json")
            with open(json_file, 'w', encoding='utf-8') as f:
                json.dump(self.items, f, ensure_ascii=False, indent=2)
            spider.logger.info(f"JSON文件已生成: {json_file}")

            # 保存为TXT
            txt_file = os.path.join(output_dir, f"豆瓣TOP250_{timestamp}.txt")
            with open(txt_file, 'w', encoding='utf-8') as f:
                for idx, item in enumerate(self.items):
                    f.write(f"=== 电影#{idx + 1} ===\n")
                    f.write(f"排名: {item.get('rank', '')}\n")
                    f.write(f"标题: {item.get('title', '')}\n")
                    f.write(f"评分: {item.get('rating', '')} ({item.get('rating_count', '0')}人评价)\n")
                    f.write(f"年份: {item.get('year', '')}\n")
                    f.write(f"导演: {item.get('director', '')}\n")
                    f.write(f"主演: {item.get('actors', '')}\n")
                    f.write(f"类型: {item.get('genres', '')}\n")
                    f.write(f"地区: {item.get('region', '')}\n")
                    f.write(f"IMDb: {item.get('imdb_link', '')}\n")
                    f.write(f"名言: {item.get('quote', '')}\n")
                    f.write(f"简介: {item.get('synopsis', '')[:100]}...\n")
                    f.write("-" * 60 + "\n\n")
            spider.logger.info(f"TXT文件已生成: {txt_file}")

            spider.logger.info(f"数据保存完成!共保存{len(self.items)}条记录")

        except Exception as e:
            spider.logger.error(f"‼文件保存失败: {str(e)}")
            # 记录详细错误
            error_log = os.path.join(output_dir, f"error_{timestamp}.log")
            with open(error_log, "w", encoding="utf-8") as err_f:
                err_f.write(f"错误时间: {datetime.now()}\n")
                err_f.write(f"错误信息: {str(e)}\n")
                err_f.write("收集到的数据:\n")
                for item in self.items:
                    err_f.write(f"{item}\n")

# 独立运行脚本
if __name__ == "__main__":
    print("=" * 60)
    print("豆瓣TOP250专业爬虫 v8.0 (终极修复版)")
    print("=" * 60)

    # 配置Scrapy
    process = CrawlerProcess(settings={
        'DOWNLOAD_DELAY': 2.5,
        'CONCURRENT_REQUESTS': 3,
        'RETRY_TIMES': 3,
        'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36',
        'ITEM_PIPELINES': {'__main__.DataPipeline': 300},
        'TELNETCONSOLE_ENABLED': True  # 启用Telnet调试
    })

    process.crawl(DoubanTop250Spider)
    process.start()
该代码仍不能爬取一些项目,但以无伤大雅 以下是运行时的一些图片:

爬取的信息:

3.实验过程中遇到的的问题

(1)在1.0requests和Beautifulsoup版本里,代码爬取不到任何信息
(2)在2.0selenium版里爬取的内容极其有限
(3)3.0Scrapy版本里仍有一些内容爬取失败
解决方案:使用了强大的deepseek祝我一臂之力

课程总结与感悟

最后的实验,让我知道了我在python上的学习了解还很浅薄,在有AI的帮助下,仍耗费了大量的时间才堪堪完成了一个简单的爬虫程序。
最后,也感谢王老师在这学期python公选课带领我入门python这门有趣的语言。
纸上得来终觉浅,绝知此事要躬行!

posted @ 2025-06-10 19:40  20241319吴辰曦  阅读(20)  评论(0)    收藏  举报