首页 / 资讯中心 / 文章详情

Scrapy新闻爬虫工程实战:反爬对抗、动态评论与时间标准化

Scrapy新闻爬虫工程实战:反爬对抗、动态评论与时间标准化 ★ FEATURED ARTICLE
简介这是一份基于Scrapy框架开发的Python新闻爬虫实战项目面向具备基础Python和Web开发能力的学习者与开发者用于解决多源主流媒体网易、搜狐、凤凰、澎湃新闻及评论数据的自动化采集、结构化提取与周期性更新问题。资源包共26个文件以18个Python脚本为核心涵盖spiders网站解析逻辑、pipelines数据清洗与存储、settings配置管理、debug运行入口等完整模块另含3个XML配置文件IDE环境支持、2个PYC编译文件及README.md等辅助文档整体仅26KB轻量易读。已有495人学习下载项目结构清晰体现Scrapy标准流程从Scheduler调度、Downloader下载、Spider解析到Item Pipeline持久化附带定时更新设计说明与多站点调试脚本如debug_163.py、debug_pengpai.py可直接复用核心爬取逻辑、快速适配新站点或扩展评论情感分析等下游任务。1. 这不是“一键爬全网”的玩具一个真实跑过10万页新闻的Scrapy工程专治反爬升级、动态评论加载和多源时间对齐你搜“Python新闻爬虫”十有八九点开的是三行代码requests.get()的Demo——它连搜狐首页都进不去更别说网易新闻页底那层用Ajax异步加载的500条评论。而这个NewsSpider-master项目是某高校实验室为支撑舆情分析课题实际部署过的工程它真正在生产环境里连续跑满7天稳定抓取网易、搜狐、凤凰、澎湃四站共12.7万页新闻正文嵌套评论日均更新耗时控制在22小时内。它不承诺“全自动无脑运行”但把Scrapy框架里最硬的三块骨头——反爬对抗策略封装、评论异步接口逆向解析、多源发布时间标准化处理——全拆开塞进了spiders/和pipelines.py里。适合两类人一是正被搜狐User-Agent轮换卡住、被澎湃JSON评论接口绕晕的中级Python开发者二是需要交付可复现、可审计、能过答辩的课程设计或毕设的同学。它不是教你怎么写第一个爬虫而是告诉你当页面结构每天变、评论藏在XHR里、发布时间格式五花八门时Scrapy该怎么扛住。2. 从debug_xxx.py切入四站爬虫的启动逻辑与核心参数配置这个项目没用scrapy crawl命令直接跑而是用一组debug_xxx.py作为入口脚本——这不是偷懒是为调试留出明确断点。每个debug文件本质是Scrapy CrawlerProcess的封装它绕过了scrapy.cfg的全局配置让单站调试互不干扰。我们以debug_163.py网易为例看它如何精准控制爬取行为。2.1 debug_163.py网易爬虫的启动器与关键参数注入# debug_163.py from scrapy.crawler import CrawlerProcess from scrapyspider.spiders.netease_spider import NeteaseSpider import scrapy if __name__ __main__: process CrawlerProcess({ USER_AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36, DOWNLOAD_DELAY: 1.5, RANDOMIZE_DOWNLOAD_DELAY: True, CONCURRENT_REQUESTS: 2, COOKIES_ENABLED: True, RETRY_TIMES: 3, RETRY_HTTP_CODES: [500, 502, 503, 504, 408, 429], ITEM_PIPELINES: { scrapyspider.pipelines.CsvPipeline: 300, }, FEEDS: { output/netease_news.csv: { format: csv, overwrite: True, fields: [title, content, comments, publish_time, url, source] } } }) process.crawl(NeteaseSpider, start_urls[https://news.163.com/]) process.start()这段代码的关键不在语法而在参数选择背后的工程权衡DOWNLOAD_DELAY: 1.5和CONCURRENT_REQUESTS: 2是硬性组合——网易对并发极其敏感设成3个请求并发5分钟内必触发429 Too Many Requests而1.5秒延迟配合随机化能有效模拟人类浏览节奏这是某开发者在日志里反复比对response.status后定下的阈值。RETRY_HTTP_CODES显式加入429因为网易的反爬返回码不走常规5xx而是直接429不加这条爬虫遇到限流就直接放弃不会重试。FEEDS配置而非FEED_URI是因为Scrapy 2.6已弃用旧方式这里强制指定fields顺序确保CSV列头与后续数据分析脚本完全对齐避免因字段错位导致pandas读取报错。提示start_urls传入的是频道页如https://news.163.com/不是文章页。真正的URL发现由NeteaseSpider.parse()中的response.css(a[href*/article/]::attr(href)).getall()完成——它只提取带/article/路径的链接过滤掉广告和视频页这是网易页面结构的稳定锚点。2.2 spiders目录结构四站分离的Spider类与共用基类项目将四站爬虫严格隔离在spiders/下netease_spider.py→NeteaseSpidersohu_spider.py→SohuSpiderifeng_spider.py→IfengSpiderpengpai_spider.py→PengpaiSpider但它们并非完全独立。所有Spider都继承自scrapyspider/spiders/base_spider.py中的BaseNewsSpider类该基类统一实现了三件事URL去重中间件注册在__init__中调用self.crawler.engine.slot.scheduler.enqueue_request前先检查request.url是否已在Redis缓存中需自行部署Redis并修改settings.py时间标准化钩子提供parse_publish_time(self, raw_time: str) - datetime抽象方法各子类必须实现例如凤凰的时间字符串是2023-08-15 14:22:33而澎湃是2023-08-15T14:22:3308:00基类不处理具体格式只保证返回datetime对象评论加载等待机制定义wait_for_comments(self, response)方法内部调用scrapy_splash或playwright需额外安装等待Ajax加载完成——但注意本项目默认关闭此功能仅在debug_pengpai.py中启用因为澎湃的评论接口必须等DOM渲染后才能触发XHR请求。2.3 settings.py反爬对抗的底层开关与中间件链settings.py是整个项目的反爬中枢关键配置如下配置项值作用说明ROBOTSTXT_OBEYFalse网易/搜狐robots.txt禁止爬取/news/路径必须关闭否则Scheduler直接丢弃所有URLDOWNLOADER_MIDDLEWARES{scrapyspider.middlewares.RandomUserAgentMiddleware: 543, scrapyspider.middlewares.ProxyMiddleware: 550}RandomUserAgentMiddleware每请求轮换UA列表内置23个主流浏览器UAProxyMiddleware是空壳预留代理接入点当前未启用避免新手误配无效代理导致全站失败SPIDER_MIDDLEWARES{scrapyspider.middlewares.DuplicateUrlFilterMiddleware: 100}在URL入队前查重依赖scrapy-redis若未部署Redis则降级为内存字典去重仅限单机HTTPCACHE_ENABLEDTrue启用本地HTTP缓存路径为httpcache/调试时设为True可避免重复请求上线前必须关掉特别注意RandomUserAgentMiddleware的实现逻辑它不简单随机选UA而是根据request.url域名匹配UA池——访问news.163.com时优先用Chrome UA访问www.sohu.com时倾向用Firefox UA这是为应对不同站点对UA的校验强度差异。源码在middlewares.py第42行UA池硬编码在类属性中修改需谨慎。3. pipelines.py深度解析从原始HTML到结构化CSV的四道工序pipelines.py是数据落地的核心流水线它不只做“保存”而是承担清洗、补全、标准化三大任务。本项目定义了CsvPipeline主流程和CommentPipeline评论专项二者通过ITEM_PIPELINES字典的执行顺序串联。3.1 CsvPipeline标题、内容、时间、来源的四步清洗# pipelines.py import re from datetime import datetime from scrapy.exceptions import DropItem class CsvPipeline: def process_item(self, item, spider): # 步骤1标题强清洗——移除【】、[]、等分隔符及前后空格 if item.get(title): item[title] re.sub(r[【\[\]\|\s], , item[title]).strip() if len(item[title]) 4: # 标题少于4字视为无效如“首页”、“更多” raise DropItem(fTitle too short: {item[title]}) # 步骤2内容清洗——删除广告段落、版权声明、重复换行 if item.get(content): content item[content] # 删除含“广告”、“推广”、“商务合作”的整段 content re.sub(r(?m)^.*?(广告|推广|商务合作|免责声明).*?$\n?, , content) # 合并连续空白行 content re.sub(r\n\s*\n, \n\n, content) item[content] content.strip() # 步骤3时间标准化——调用Spider实现的parse_publish_time if item.get(raw_publish_time) and hasattr(spider, parse_publish_time): try: item[publish_time] spider.parse_publish_time(item[raw_publish_time]) except Exception as e: # 时间解析失败则用当前时间打上标记便于后期排查 item[publish_time] datetime.now() item[time_parse_error] str(e) # 步骤4来源补全——若item无source字段从URL推导 if not item.get(source): domain_map { news.163.com: 网易新闻, www.sohu.com: 搜狐新闻, news.ifeng.com: 凤凰新闻, www.thepaper.cn: 澎湃新闻 } for domain, name in domain_map.items(): if domain in item.get(url, ): item[source] name break return item这段代码的精妙在于防御性编程标题清洗用re.sub(r[【\[\]\|\s], , ...)而非strip()因为网易标题常含【快讯】前缀搜狐标题爱用[图]不剔除会污染NLP分析内容清洗的(?m)标志启用多行模式确保^和$匹配每行首尾精准删除广告段落时间解析失败不中断流程而是记录错误原因到time_parse_error字段方便后期用grep time_parse_error output/*.csv批量定位问题URL来源补全用域名映射而非硬编码当新增站点如腾讯新闻时只需扩展domain_map字典无需改逻辑。3.2 CommentPipeline评论的异步加载与结构化解析澎湃和凤凰的评论不直接在HTML里需调用独立API。CommentPipeline负责这一步# pipelines.py import json import requests from urllib.parse import urljoin class CommentPipeline: def process_item(self, item, spider): if not item.get(comments) and item.get(comment_api_url): # 澎湃评论API示例https://www.thepaper.cn/contentapi/comment/list?contid123456size100 try: headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36} response requests.get(item[comment_api_url], headersheaders, timeout10) if response.status_code 200: data response.json() # 澎湃API返回data[data][list]每条评论含content,user_name,time comments [] for cmt in data.get(data, {}).get(list, []): comments.append({ content: cmt.get(content, ), user_name: cmt.get(user_name, ), time: cmt.get(time, ) }) item[comments] comments else: item[comments] [] except Exception as e: item[comments] [] item[comment_fetch_error] str(e) return item关键点timeout10是血泪经验——澎湃API偶尔响应超20秒不设超时会导致整个pipeline阻塞item[comment_api_url]由Spider在parse_article()中提取例如澎湃文章页的script标签里含window.contId123456Spider用re.search(rcontId(\d), response.text)捕获后拼接API URL评论数据存为字典列表而非字符串方便后续用json.dumps(item[comments])转存CSV或直接导入MongoDB。3.3 csv_process.py离线数据二次加工的救命脚本csv_process.py不是pipeline而是独立工具脚本解决上线后才发现的脏数据问题# csv_process.py import pandas as pd import sys def clean_csv(input_file, output_file): df pd.read_csv(input_file, on_bad_linesskip) # 修复因评论含逗号导致的CSV列错位 df df.dropna(subset[title, content]) # 删除空标题或空内容的行 df[publish_time] pd.to_datetime(df[publish_time], errorscoerce) df df.dropna(subset[publish_time]) # 删除时间无法解析的行 df.to_csv(output_file, indexFalse, encodingutf-8-sig) print(fCleaned {len(df)} rows to {output_file}) if __name__ __main__: if len(sys.argv) ! 3: print(Usage: python csv_process.py input.csv output.csv) sys.exit(1) clean_csv(sys.argv[1], sys.argv[2])为什么需要它因为Scrapy的CSV导出在遇到含换行符的评论时会破坏格式on_bad_linesskip跳过损坏行再用dropna剔除明显异常数据。某次实测原始netease_news.csv有12.7万行经此脚本清洗后剩11.9万行丢失的8000行全是评论字段含\n且未被双引号包裹的脏数据——这是Scrapy CSV exporter的固有缺陷非项目Bug。4. 避坑指南四站爬虫高频翻车现场与血泪解决方案爬取四大新闻站绝非复制粘贴就能跑通。以下是某开发者在7天压测中记录的真实踩坑清单每一条都对应一个git commit回滚点。4.1 现象搜狐爬虫启动后立即报twisted.internet.error.TimeoutError日志显示DNS lookup failed原因搜狐使用CDN加速其DNS解析受地域影响极大。在华东服务器上解析www.sohu.com正常在华北服务器上却超时。根本原因是scrapy默认DNS resolver未设置超时底层twisted卡死。解决在settings.py中添加DNS_TIMEOUT 10 TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor并确保系统/etc/resolv.conf中DNS服务器为223.5.5.5阿里DNS禁用本地systemd-resolved。4.2 现象凤凰新闻爬取到的publish_time全是1970-01-01 08:00:00原因凤凰页面中发布时间藏在meta propertyarticle:published_time content2023-08-15T14:22:3308:00但Spider代码误用了response.css(meta[namepubdate]::attr(content)).get()而凤凰根本没有namepubdate的meta标签。解决在ifeng_spider.py的parse_article()中将时间提取逻辑改为publish_time response.css(meta[propertyarticle:published_time]::attr(content)).get() if not publish_time: # 降级方案从正文第一段找“发布时间2023年08月15日” text response.css(.article-content p::text).get() if text: m re.search(r发布时间[:]\s*(\d{4}年\d{1,2}月\d{1,2}日), text) if m: publish_time m.group(1).replace(年,-).replace(月,-).replace(日,)4.3 现象澎湃评论API返回{code:403,msg:Forbidden}但浏览器F12能正常调用原因澎湃API校验Referer头且要求Referer必须是文章页URL如https://www.thepaper.cn/newsDetail_forward_1234567而Scrapy默认Referer是https://www.thepaper.cn/。解决在pengpai_spider.py的parse_article()中构造评论请求时显式设置yield scrapy.Request( urlcomment_api_url, headers{Referer: response.url}, callbackself.parse_comments, meta{article_url: response.url} )4.4 现象debug_pengpai.py运行时抛ModuleNotFoundError: No module named scrapy_splash但pip install scrapy-splash后仍报错原因scrapy-splash需配套运行Splash服务Docker容器仅装Python包无效。项目README.md未说明此依赖。解决安装Docker运行docker run -p 8050:8050 scrapinghub/splash在settings.py中添加SPLASH_URL http://localhost:8050 DUPEFILTER_CLASS scrapy_splash.SplashAwareDupeFilter HTTPCACHE_STORAGE scrapy_splash.SplashAwareFSCacheStorage4.5 现象多线程运行debug_*.py时CSV文件内容混杂网易数据写进了搜狐的CSV原因FEEDS配置中output/netease_news.csv是相对路径当多个debug脚本并发执行时工作目录冲突导致文件句柄错乱。Scrapy的FEEDS机制非线程安全。解决强制指定绝对路径并为每个脚本生成唯一文件名# debug_163.py中 import os output_path os.path.abspath(foutput/netease_news_{int(time.time())}.csv) FEEDS: {output_path: {format: csv, ...}}5. 定时更新与数据验证用cronshell脚本构建每日自动流水线项目需求明确要求“每个新闻网页及其评论能在1天内更新”这意味着不能只靠手动运行python debug_*.py。我们用Linux cronshell脚本构建轻量级调度系统不引入Airflow等重型组件。5.1 调度脚本run_daily.sh四站串行执行与失败熔断#!/bin/bash # run_daily.sh LOG_DIR/var/log/newsspider mkdir -p $LOG_DIR DATE$(date %Y%m%d_%H%M%S) # 定义站点执行顺序与超时阈值单位秒 declare -A SITES SITES[netease]1800 # 网易30分钟 SITES[sohu]2400 # 搜狐40分钟 SITES[ifeng]2100 # 凤凰35分钟 SITES[pengpai]3600 # 澎湃60分钟含Splash渲染 echo Daily crawl started at $(date) $LOG_DIR/crawl.log for site in ${!SITES[]}; do echo Starting $site crawl... $LOG_DIR/crawl.log timeout ${SITES[$site]} python /path/to/NewsSpider-master/debug_${site}.py 21 | tee $LOG_DIR/${site}_${DATE}.log EXIT_CODE${PIPESTATUS[0]} if [ $EXIT_CODE -eq 0 ]; then echo $site crawl succeeded $LOG_DIR/crawl.log # 清理旧日志保留7天 find $LOG_DIR -name ${site}_*.log -mtime 7 -delete else echo $site crawl FAILED with code $EXIT_CODE $LOG_DIR/crawl.log # 熔断停止后续站点发告警此处简化为写文件 touch $LOG_DIR/ALERT_${site}_${DATE} exit $EXIT_CODE fi done echo Daily crawl finished at $(date) $LOG_DIR/crawl.log关键设计timeout命令硬性限制每站执行时长防止单站卡死拖垮全局PIPESTATUS[0]获取timeout的退出码非Python进程码timeout超时返回124正常结束返回0熔断机制任一站失败即exit不继续执行后续站点避免脏数据污染日志按站点时间戳分离方便grep -r 429 /var/log/newsspider/快速定位反爬问题。5.2 数据验证脚本validate_data.py用SQL思维校验10万页数据质量光跑完不等于数据可用。validate_data.py用pandas模拟SQL查询验证核心指标# validate_data.py import pandas as pd import glob import sys def validate_all_csv(): csv_files glob.glob(output/*.csv) if not csv_files: print(No CSV files found in output/) return False all_dfs [] for f in csv_files: try: df pd.read_csv(f, on_bad_linesskip) # 强制转换时间列为datetime无效值转NaT df[publish_time] pd.to_datetime(df[publish_time], errorscoerce) all_dfs.append(df) except Exception as e: print(fFailed to read {f}: {e}) if not all_dfs: return False full_df pd.concat(all_dfs, ignore_indexTrue) # 验证1总行数 10万 total_rows len(full_df) print(fTotal rows: {total_rows}) if total_rows 100000: print(❌ FAIL: Total rows 100,000) return False # 验证2各站数据分布合理网易应最多澎湃应最少 source_count full_df[source].value_counts() print(fSource distribution:\n{source_count}) if source_count.get(网易新闻, 0) 30000 or source_count.get(澎湃新闻, 0) 5000: print(❌ FAIL: Source distribution unbalanced) return False # 验证3时间字段有效率 95% valid_time_ratio full_df[publish_time].notna().mean() print(fPublish time valid ratio: {valid_time_ratio:.2%}) if valid_time_ratio 0.95: print(❌ FAIL: Publish time valid ratio 95%) return False print(✅ PASS: All validations passed) return True if __name__ __main__: success validate_all_csv() sys.exit(0 if success else 1)此脚本被集成到run_daily.sh末尾# run_daily.sh末尾追加 echo Running data validation... $LOG_DIR/crawl.log python /path/to/NewsSpider-master/validate_data.py $LOG_DIR/crawl.log 21 VALIDATION_CODE$? if [ $VALIDATION_CODE -ne 0 ]; then echo Validation FAILED $LOG_DIR/crawl.log exit $VALIDATION_CODE fi5.3 cron配置精确到小时的每日调度将脚本加入crontab确保每天凌晨2:30执行避开网站流量高峰# 编辑 crontab $ crontab -e # 添加一行 30 2 * * * cd /path/to/NewsSpider-master ./run_daily.sh /var/log/newsspider/cron.log 21注意cd命令必须显式指定工作目录否则debug_*.py中的相对路径output/会指向/root/而非项目目录追加日志避免覆盖21捕获stderr。从那以后我每次部署新爬虫都强制走一遍run_daily.sh的完整流程先手动执行验证日志输出再chmod x再crontab -e最后用tail -f /var/log/newsspider/cron.log盯住首次运行。漏掉任何一环第二天早上打开邮箱看到的不是数据报表而是满屏的429和TimeoutError。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站