翼度科技»论坛 编程开发 python 查看内容

【开源】2024最新python豆瓣电影数据爬虫+可视化分析项目

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
项目介绍

【开源】项目基于python+pandas+flask+mysql等技术实现豆瓣电影数据获取及可视化分析展示,觉得有用的朋友可以来个一键三连,感谢!!!
项目演示

项目截图


  • 首页

  • 列表页

  • 爬虫演示

项目地址

https://github.com/mudfish/python-douban-view
项目结构


核心模块

电影爬虫
  1. """
  2. 异步并发爬虫
  3. """
  4. # 本次运行获取的最大页数
  5. MAX_PAGES = 5
  6. # 进度控制文件
  7. PAGE_PROGRESS_FILE = "page_progress.json"
  8. # 电影类型
  9. MOVIE_TYPES = ["剧情", "喜剧", "动作", "爱情", "科幻", "动画"]
  10. # CSV文件名
  11. CSV_NAME = "movie_data.csv"
  12. # CSV头
  13. CSV_HEADS = [
  14.     "id",
  15.     "movie_id",
  16.     "title",
  17.     "year",
  18.     "directors",
  19.     "casts",
  20.     "rating",
  21.     "cover",
  22.     "country",
  23.     "summary",
  24.     "types",
  25.     "lang",
  26.     "release_date",
  27.     "time",
  28.     "url",
  29. ]
  30. # 上映日期匹配正则,剔除非数字和-
  31. RELEASE_DATE_REMOVE_RE = r"[^0-9-]"
  32. engine = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/db_douban")
  33. def get_id():
  34.     return str(random.randint(1, 100000000)) + str(time.time()).split(".")[1].strip()
  35. class Spider:
  36.     def __init__(self):
  37.         self.movie_page_url = "https://m.douban.com/rexxar/api/v2/movie/recommend?"
  38.         self.movie_detail_url = "https://movie.douban.com/subject/{}/"
  39.         self.headers = {
  40.             "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
  41.             "Referer": "https://movie.douban.com/explore",
  42.         }
  43.         self.movie_types = MOVIE_TYPES
  44.         self.page_progress = {}
  45.         # 需要抓取的页面数
  46.         self.total_pages = 0
  47.         self.completed_pages = 0
  48.         self.global_progress_bar = None
  49.     def init(self):
  50.         # 每次跑之前,先删除之前的csv文件
  51.         if os.path.exists(CSV_NAME):
  52.             os.remove(CSV_NAME)
  53.         with open(CSV_NAME, "w", newline="", encoding="utf-8") as writer_f:
  54.             writer = csv.writer(writer_f)
  55.             writer.writerow(CSV_HEADS)
  56.     def load_page_progress(self):
  57.         if os.path.exists(PAGE_PROGRESS_FILE):
  58.             with open(PAGE_PROGRESS_FILE, "r", encoding="utf-8") as f:
  59.                 # 判断文件内容是否为空
  60.                 if os.stat(PAGE_PROGRESS_FILE).st_size == 0:
  61.                     # 初始化页面进度
  62.                     print("初始化页面进度")
  63.                     self.page_progress = {}
  64.                     self.save_page_progress()
  65.                 else:
  66.                     self.page_progress = json.load(f)
  67.     def save_page_progress(self):
  68.         with open(PAGE_PROGRESS_FILE, "w", encoding="utf-8") as f:
  69.             json.dump(self.page_progress, f, ensure_ascii=False)
  70.     async def get_movie_pages(self, session, type_name):
  71.         start_page = self.page_progress.get(type_name, 1)
  72.         if start_page <= MAX_PAGES:
  73.             for page in range(start_page, MAX_PAGES + 1):
  74.                 # print(f'{type_name}第{page}页:')
  75.                 start_time = time.time()
  76.                 params = {"start": (page - 1) * 20, "count": 10, "tags": type_name}
  77.                 try:
  78.                     async with session.get(
  79.                         self.movie_page_url, headers=self.headers, params=params
  80.                     ) as resp:
  81.                         resp.raise_for_status()
  82.                         respJson = await resp.json()
  83.                         movie_list = respJson["items"]
  84.                         for i, m in enumerate(movie_list):
  85.                             if m["type"] == "movie":
  86.                                 await self.process_movie(session, m)
  87.                                 # progress_bar.update(round(1/len(movie_list)))
  88.                         self.page_progress[type_name] = page + 1
  89.                         # 记录进度
  90.                         self.save_page_progress()
  91.                         # 刷新全局进度
  92.                         self.update_global_progress()
  93.                 except Exception as e:
  94.                     print(f"处理:{type_name}第{page}页失败: {e}")
  95.                     traceback.print_exc()
  96.                     continue
  97.     async def process_movie(self, session, movie):
  98.         movie_data = []
  99.         movie_data.append(get_id())
  100.         movie_data.append(movie["id"])
  101.         movie_data.append(movie["title"])
  102.         movie_data.append(movie["year"])
  103.         async with session.get(
  104.             self.movie_detail_url.format(movie["id"]), headers=self.headers
  105.         ) as resp:
  106.             resp.raise_for_status()
  107.             html_text = await resp.text()
  108.         path = etree.HTML(html_text)
  109.         # 导演
  110.         movie_data.append(",".join(path.xpath('//a[@rel="v:directedBy"]/text()')))
  111.         # 主演
  112.         movie_data.append(",".join(path.xpath('//a[@rel="v:starring"]/text()')))
  113.         # 评分
  114.         movie_data.append(path.xpath('//strong[@property="v:average"]/text()')[0])
  115.         # 封面
  116.         movie_data.append(path.xpath('//img[@rel="v:image"]/@src')[0])
  117.         # 国家
  118.         movie_data.append(
  119.             path.xpath(
  120.                 '//span[contains(text(),"制片国家")]/following-sibling::br[1]/preceding-sibling::text()[1]'
  121.             )[0].replace(" / ", ",")
  122.         )
  123.         # 摘要
  124.         movie_data.append(path.xpath('//span[@property="v:summary"]/text()')[0].strip())
  125.         # 类型
  126.         movie_data.append(
  127.             ",".join(path.xpath('//div[@id="info"]/span[@property="v:genre"]/text()'))
  128.         )
  129.         # 语言
  130.         movie_data.append(
  131.             path.xpath(
  132.                 '//span[contains(text(),"语言")]/following-sibling::br[1]/preceding-sibling::text()[1]'
  133.             )[0]
  134.         )
  135.         # 上映日期
  136.         movie_data.append(
  137.             re.sub(
  138.                 RELEASE_DATE_REMOVE_RE,
  139.                 "",
  140.                 path.xpath('//span[@property="v:initialReleaseDate"]/text()')[0][:10],
  141.             )
  142.         )
  143.         # 时长(空处理)
  144.         # print(movie["id"])
  145.         movie_time = path.xpath('//span[@property="v:runtime"]/text()')
  146.         if len(movie_time) > 0:
  147.             movie_data.append(movie_time[0])
  148.         else:
  149.             movie_data.append("")
  150.         # url
  151.         movie_data.append(self.movie_detail_url.format(movie["id"]))
  152.         self.save_to_csv(movie_data)
  153.     def save_to_csv(self, row):
  154.         with open(CSV_NAME, "a", newline="", encoding="utf-8") as f:
  155.             writer = csv.writer(f)
  156.             writer.writerow(row)
  157.     def clean_csv(self):
  158.         print("===========清理数据============")
  159.         df = pd.read_csv(CSV_NAME, encoding="utf-8")
  160.         df.drop_duplicates(subset=["movie_id"], keep="first", inplace=True)
  161.         print("存储到数据库...")
  162.         df.to_sql("tb_movie", con=engine, index=False, if_exists="append")
  163.         print("清理重复数据...")
  164.         engine.connect().execute(
  165.             text(
  166.                 "delete t1 from tb_movie t1 inner join (select min(id) as id,movie_id from tb_movie group by movie_id having count(*) > 1) t2 on t1.movie_id=t2.movie_id where t1.id>t2.id"
  167.             )
  168.         )
  169.     def update_global_progress(self):
  170.         self.completed_pages += 1
  171.         # print(self.completed_pages)
  172.         self.global_progress_bar.update(1)
  173.         self.global_progress_bar.refresh()
  174.     async def run(self):
  175.         self.init()
  176.         self.load_page_progress()
  177.         # self.total_pages = MAX_PAGES*len(MOVIE_TYPES) - sum(self.page_progress.get(type_name, 1) for type_name in MOVIE_TYPES)
  178.         for type_name in MOVIE_TYPES:
  179.             if MAX_PAGES > self.page_progress.get(type_name, 1):
  180.                 self.total_pages += MAX_PAGES + 1 - self.page_progress.get(type_name, 1)
  181.         print(self.total_pages)
  182.         if self.total_pages > 0:
  183.             self.global_progress_bar = tqdm(
  184.                 total=self.total_pages, desc="progress", unit="page", colour="GREEN"
  185.             )
  186.             async with aiohttp.ClientSession() as session:
  187.                 tasks = [
  188.                     self.get_movie_pages(session, type_name)
  189.                     for type_name in self.movie_types
  190.                 ]
  191.                 await asyncio.gather(*tasks)
  192.             # 请求结束后,清空页面进度
  193.             # self.page_progress = {}
  194.             # self.save_page_progress()
  195.             self.global_progress_bar.close()
  196.             self.clean_csv()
  197. if __name__ == "__main__":
  198.     loop = asyncio.get_event_loop()
  199.     spider = Spider()
  200.     loop.run_until_complete(spider.run())
复制代码
电影可视化

接口代码
  1. from flask import Flask, render_template, request, redirect, url_for, session
  2. from utils import db_query
  3. app = Flask(__name__)
  4. app.secret_key = "mysessionkey"
  5. # 统一请求拦截
  6. @app.before_request
  7. def before_request():
  8.     # 利用正则匹配,如果/static开头和/login, /logout,/register的请求,则不拦截;其他的判断是否已登录
  9.     if (
  10.         request.path.startswith("/static")
  11.         or request.path == "/login"
  12.         or request.path == "/logout"
  13.         or request.path == "/register"
  14.     ):
  15.         return
  16.     # 如果没有登录,则跳转到登录页面
  17.     if not session.get("login_username"):
  18.         return redirect(url_for("login"))
  19. # 首页
  20. @app.route("/")
  21. def index():
  22.     # 获取电影统计数据
  23.     movie_stats = db_query.fetch_movie_statistics()
  24.     # 获取电影分类统计
  25.     movie_type_distribution = db_query.fetch_movie_type_distribution()
  26.     # 获取电影评分统计
  27.     movie_rating_distribution = db_query.fetch_movie_rating_distribution()
  28.     print(movie_rating_distribution)
  29.     return render_template(
  30.         "index.html",
  31.         login_username=session.get("login_username"),
  32.         movie_stats=movie_stats,
  33.         movie_type_distribution=movie_type_distribution,
  34.         movie_rating_distribution=movie_rating_distribution,
  35.     )
  36. # 登录
  37. @app.route("/login", methods=["GET", "POST"])
  38. def login():
  39.     if request.method == "POST":
  40.         req_params = dict(request.form)
  41.         # 判断用户名密码是否正确
  42.         sql = "SELECT * FROM `tb_user` WHERE `username` = %s AND `password` = %s"
  43.         params = (req_params["username"], req_params["password"])
  44.         if len(db_query.query(sql, params)) > 0:
  45.             # 存储session
  46.             session["login_username"] = req_params["username"]
  47.             return redirect(url_for("index"))
  48.         else:
  49.             return render_template(
  50.                 "error.html",
  51.                 error="用户名或密码错误",
  52.             )
  53.     elif request.method == "GET":
  54.         return render_template("login.html")
  55. # 退出
  56. @app.route("/logout")
  57. def logout():
  58.     session.pop("login_username", None)
  59.     return redirect(url_for("index"))
  60. # 注册
  61. @app.route("/register", methods=["GET", "POST"])
  62. def register():
  63.     if request.method == "POST":
  64.         req_params = dict(request.form)
  65.         if req_params["password"] == req_params["password_confirm"]:
  66.             # 判断是否已存在该用户名
  67.             sql = "SELECT * FROM `tb_user` WHERE `username` = %s"
  68.             params = (req_params["username"],)
  69.             result = db_query.query(sql, params)
  70.             if len(result) > 0:
  71.                 return render_template(
  72.                     "error.html",
  73.                     error="用户名已存在",
  74.                 )
  75.             sql = "INSERT INTO `tb_user` (`username`, `password`) VALUES (%s, %s)"
  76.             params = (
  77.                 req_params["username"],
  78.                 req_params["password"],
  79.             )
  80.             db_query.query(sql, params, db_query.QueryType.NO_SELECT)
  81.             return redirect(url_for("login"))
  82.         else:
  83.             return render_template(
  84.                 "error.html",
  85.                 error="两次密码输入不一致",
  86.             )
  87.     elif request.method == "GET":
  88.         return render_template("register.html")
  89. @app.route("/list")
  90. def movie_list():
  91.     # 查询数据库获取电影列表
  92.     movies = db_query.fetch_movie_list()  # 假设此函数返回一个包含电影信息的列表
  93.     # 渲染并返回list.html,同时传递movies数据
  94.     return render_template(
  95.         "list.html", login_username=session.get("login_username"), movies=movies
  96.     )
  97. @app.errorhandler(404)
  98. def page_not_found(error):
  99.     return render_template("404.html"), 404
  100. @app.errorhandler(500)
  101. def system_error(error):
  102.     return render_template("500.html"), 500
  103. if __name__ == "__main__":
  104.     # 静态文件缓存自动刷新
  105.     app.jinja_env.auto_reload = True
  106.     app.run(host="127.0.0.1", port=8002, debug=True)
复制代码
首页
  1. <!DOCTYPE html>
  2. <html lang="en">
  3.   <head>
  4.     <meta charset="utf-8" />
  5.     <meta http-equiv="X-UA-Compatible"
  6.     content="IE=edge" />
  7.     <meta
  8.       name="viewport"
  9.       content="width=device-width, initial-scale=1, shrink-to-fit=no"
  10.     />
  11.     <meta name="description" content="" />
  12.     <meta name="author" content="" />
  13.     <title>首页</title>
  14.    
  15.     <link
  16.       href="/static/vendor/fontawesome-free/css/all.min.css"
  17.       rel="stylesheet"
  18.       type="text/css"
  19.     />
  20.     <link
  21.       href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
  22.       rel="stylesheet"
  23.     />
  24.    
  25.     <link href="/static/css/sb-admin-2.min.css" rel="stylesheet" />
  26.   </head>
  27.   <body id="page-top">
  28.    
  29.    
  30.       
  31.       <ul
  32.         
  33.         id="accordionSidebar"
  34.       >
  35.         
  36.         <a
  37.          
  38.           target="_blank" href="https://www.cnblogs.com/index.html"
  39.         >
  40.          
  41.             <i ></i>
  42.          
  43.           豆瓣电影可视化
  44.         </a>
  45.         
  46.         <hr  />
  47.         
  48.         <li >
  49.           <a  target="_blank" href="https://www.cnblogs.com/">
  50.             <i ></i>
  51.             首页</a
  52.           >
  53.         </li>
  54.         
  55.         <li >
  56.           <a  target="_blank" href="https://www.cnblogs.com/list">
  57.             <i ></i>
  58.             电影列表</a
  59.           >
  60.         </li>
  61.         
  62.         <hr  />
  63.         
  64.         
  65.           <button  id="sidebarToggle"></button>
  66.         
  67.       </ul>
  68.       
  69.       
  70.       
  71.         
  72.         
  73.          
  74.           <nav
  75.             
  76.           >
  77.             
  78.             <button
  79.               id="sidebarToggleTop"
  80.               
  81.             >
  82.               <i ></i>
  83.             </button>
  84.             
  85.             
  86.             
  87.             <ul >
  88.               
  89.               
  90.               <li >
  91.                 <a
  92.                   
  93.                   target="_blank" href="https://www.cnblogs.com/#"
  94.                   id="userDropdown"
  95.                   role="button"
  96.                   data-toggle="dropdown"
  97.                   aria-haspopup="true"
  98.                   aria-expanded="false"
  99.                 >
  100.                   {{login_username}}</span
  101.                   >
  102.                   <img
  103.                     
  104.                     src="https://www.cnblogs.com/static/img/avatar.png"
  105.                   />
  106.                 </a>
  107.                
  108.                
  109.                   <a
  110.                     
  111.                     href="https://www.cnblogs.com/#"
  112.                     data-toggle="modal"
  113.                     data-target="#logoutModal"
  114.                   >
  115.                     <i
  116.                      
  117.                     ></i>
  118.                     Logout
  119.                   </a>
  120.                
  121.               </li>
  122.             </ul>
  123.           </nav>
  124.          
  125.          
  126.          
  127.             
  128.             
  129.             
  130.             
  131.               
  132.               
  133.                
  134.                   
  135.                     
  136.                      
  137.                         
  138.                           电影总数
  139.                         
  140.                         
  141.                           {{ movie_stats['total_movies'] }}
  142.                         
  143.                      
  144.                      
  145.                         <i ></i>
  146.                      
  147.                     
  148.                   
  149.                
  150.               
  151.               
  152.               
  153.                
  154.                   
  155.                     
  156.                      
  157.                         
  158.                           电影最高评分
  159.                         
  160.                         
  161.                           {{ movie_stats['highest_rating'] }}
  162.                         
  163.                      
  164.                      
  165.                         <i ></i>
  166.                      
  167.                     
  168.                   
  169.                
  170.               
  171.               
  172.               
  173.                
  174.                   
  175.                     
  176.                      
  177.                         
  178.                           出演最多演员
  179.                         
  180.                         
  181.                           
  182.                            
  183.                               {{ movie_stats['most_popular_cast'] }}
  184.                            
  185.                           
  186.                           
  187.                            
  188.                               
  189.                            
  190.                           
  191.                         
  192.                      
  193.                      
  194.                         <i
  195.                           
  196.                         ></i>
  197.                      
  198.                     
  199.                   
  200.                
  201.               
  202.               
  203.               
  204.                
  205.                   
  206.                     
  207.                      
  208.                         
  209.                           制片最多国家
  210.                         
  211.                         
  212.                           {{ movie_stats['most_common_country'] }}
  213.                         
  214.                      
  215.                      
  216.                         <i ></i>
  217.                      
  218.                     
  219.                   
  220.                
  221.               
  222.             
  223.             
  224.             
  225.               
  226.               
  227.                
  228.                   
  229.                   
  230.                     <h6 >
  231.                       电影分类统计
  232.                     </h6>
  233.                   
  234.                   
  235.                   
  236.                     
  237.                     
  238.                   
  239.                
  240.               
  241.               
  242.               
  243.                
  244.                   
  245.                   
  246.                     <h6 >
  247.                       电影评分统计
  248.                     </h6>
  249.                     
  250.                       <a
  251.                         
  252.                         target="_blank" href="https://www.cnblogs.com/#"
  253.                         role="button"
  254.                         id="dropdownMenuLink"
  255.                         data-toggle="dropdown"
  256.                         aria-haspopup="true"
  257.                         aria-expanded="false"
  258.                       >
  259.                         <i
  260.                           
  261.                         ></i>
  262.                       </a>
  263.                      
  264.                         Dropdown Header:
  265.                         <a  target="_blank" href="https://www.cnblogs.com/#">Action</a>
  266.                         <a  target="_blank" href="https://www.cnblogs.com/#">Another action</a>
  267.                         
  268.                         <a  target="_blank" href="https://www.cnblogs.com/#"
  269.                           >Something else here</a
  270.                         >
  271.                      
  272.                     
  273.                   
  274.                   
  275.                   
  276.                     
  277.                   
  278.                
  279.               
  280.             
  281.             
  282.            
  283.          
  284.          
  285.         
  286.         
  287.         
  288.         <footer >
  289.          
  290.             
  291.               @Laoxu Open Source.<a
  292.                   target="_blank"
  293.                   href="https://github.com/mudfish"
  294.                   >Github</a
  295.                 ></span
  296.               >
  297.             
  298.          
  299.         </footer>
  300.         
  301.       
  302.       
  303.    
  304.    
  305.    
  306.     <a  target="_blank" href="https://www.cnblogs.com/#page-top">
  307.       <i ></i>
  308.     </a>
  309.    
  310.    
  311.       
  312.         
  313.          
  314.             <h5  id="exampleModalLabel">Ready to Leave?</h5>
  315.             <button
  316.               
  317.               type="button"
  318.               data-dismiss="modal"
  319.               aria-label="Close"
  320.             >
  321.               ×
  322.             </button>
  323.          
  324.          
  325.          
  326.             <button
  327.               
  328.               type="button"
  329.               data-dismiss="modal"
  330.             >
  331.               Cancel
  332.             </button>
  333.             <a  target="_blank" href="https://www.cnblogs.com/logout">Logout</a>
  334.          
  335.         
  336.       
  337.    
  338.    
  339.    
  340.    
  341.    
  342.    
  343.    
  344.    
  345.    
  346.    
  347.    
  348.    
  349.    
  350.    
  351.    
  352.    
  353.   </body>
  354. </html>
复制代码
来源:https://www.cnblogs.com/wikiman/p/18200446
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

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

x

举报 回复 使用道具